Spaces:
Running
Running
Commit ·
45c9f16
1
Parent(s): 86fbe37
Optimize performance: Reduce WebGL, simplify GSAP, fix UI
Browse files
src/components/PortfolioScroll.tsx
CHANGED
|
@@ -72,8 +72,8 @@ export default function PortfolioScroll() {
|
|
| 72 |
});
|
| 73 |
|
| 74 |
const smoothProgress = useSpring(scrollYProgress, {
|
| 75 |
-
stiffness:
|
| 76 |
-
damping:
|
| 77 |
restDelta: 0.001,
|
| 78 |
});
|
| 79 |
|
|
|
|
| 72 |
});
|
| 73 |
|
| 74 |
const smoothProgress = useSpring(scrollYProgress, {
|
| 75 |
+
stiffness: 60,
|
| 76 |
+
damping: 25,
|
| 77 |
restDelta: 0.001,
|
| 78 |
});
|
| 79 |
|
src/components/ServicesSection.tsx
CHANGED
|
@@ -193,51 +193,51 @@ export default function ServicesSection() {
|
|
| 193 |
|
| 194 |
const viewportWidth = window.innerWidth;
|
| 195 |
const viewportCenter = viewportWidth / 2;
|
| 196 |
-
|
| 197 |
// Get card dimensions - measure actual rendered size after layout
|
| 198 |
const cardWidth = cards[0].getBoundingClientRect().width;
|
| 199 |
const gap = viewportWidth >= 768 ? 32 : 24;
|
| 200 |
-
|
| 201 |
// Calculate positions: cards are positioned with paddingLeft = 8vw initially
|
| 202 |
const paddingLeft = viewportWidth * 0.08;
|
| 203 |
-
|
| 204 |
// First card's center position when at initial position (paddingLeft)
|
| 205 |
const firstCardCenterInitial = paddingLeft + (cardWidth / 2);
|
| 206 |
-
|
| 207 |
// Calculate offset needed to center first card initially
|
| 208 |
// We need to shift container left so first card's center aligns with viewport center
|
| 209 |
const initialOffset = firstCardCenterInitial - viewportCenter;
|
| 210 |
-
|
| 211 |
// Last card's left edge position
|
| 212 |
const lastCardLeftEdge = paddingLeft + (numCards - 1) * (cardWidth + gap);
|
| 213 |
// Last card's center position when at initial position
|
| 214 |
const lastCardCenterInitial = lastCardLeftEdge + (cardWidth / 2);
|
| 215 |
-
|
| 216 |
// Calculate how much we need to scroll to center the last card
|
| 217 |
// Scroll amount = difference between last card center and first card center positions
|
| 218 |
const scrollAmount = lastCardCenterInitial - firstCardCenterInitial;
|
| 219 |
-
|
| 220 |
// Each card gets equal time in the scroll sequence
|
| 221 |
-
const scrollDistancePerCard = Math.max(
|
| 222 |
const totalScrollDistance = scrollDistancePerCard * numCards;
|
| 223 |
|
| 224 |
// Setup initial card states with optimized rendering
|
| 225 |
cards.forEach((card, index) => {
|
| 226 |
// Enable hardware acceleration
|
| 227 |
-
gsap.set(card, {
|
| 228 |
-
willChange: "transform, opacity
|
| 229 |
force3D: true,
|
| 230 |
});
|
| 231 |
-
|
| 232 |
if (index === 0) {
|
| 233 |
-
gsap.set(card, { scale: 1, opacity: 1
|
| 234 |
} else {
|
| 235 |
-
gsap.set(card, { scale: 0.
|
| 236 |
}
|
| 237 |
});
|
| 238 |
|
| 239 |
// Set initial position to center first card
|
| 240 |
-
gsap.set(scrollContainer, {
|
| 241 |
x: -initialOffset,
|
| 242 |
willChange: "transform",
|
| 243 |
force3D: true,
|
|
@@ -265,86 +265,68 @@ export default function ServicesSection() {
|
|
| 265 |
fastScrollEnd: true,
|
| 266 |
onUpdate: (self) => {
|
| 267 |
const progress = Math.min(1, Math.max(0, self.progress));
|
| 268 |
-
|
| 269 |
// Update horizontal scroll position (centered)
|
| 270 |
const currentX = -initialOffset - (progress * scrollAmount);
|
| 271 |
gsap.set(scrollContainer, { x: currentX });
|
| 272 |
-
|
| 273 |
// Update card states based on progress
|
| 274 |
// Each card gets 1/numCards of the scroll progress
|
| 275 |
const cardProgress = progress * numCards;
|
| 276 |
const activeCardIndex = Math.min(Math.floor(cardProgress), numCards - 1);
|
| 277 |
const cardLocalProgress = Math.max(0, Math.min(1, cardProgress - activeCardIndex));
|
| 278 |
-
|
| 279 |
-
// Batch DOM updates for better performance
|
| 280 |
cards.forEach((card, index) => {
|
| 281 |
const distance = Math.abs(index - activeCardIndex);
|
| 282 |
-
|
| 283 |
let scale: number;
|
| 284 |
let opacity: number;
|
| 285 |
-
|
| 286 |
-
|
| 287 |
if (distance === 0 && index === activeCardIndex) {
|
| 288 |
-
// Active card - transitioning into focus
|
| 289 |
const focusProgress = Math.min(1, cardLocalProgress * 2);
|
| 290 |
-
scale = 0.
|
| 291 |
-
opacity = 0.
|
| 292 |
-
blur = 3 - (3 * focusProgress);
|
| 293 |
} else if (distance === 1 && index === activeCardIndex + 1) {
|
| 294 |
-
// Next card - coming into view
|
| 295 |
const nextProgress = Math.max(0, Math.min(1, (cardLocalProgress - 1) * 2));
|
| 296 |
-
scale = 0.
|
| 297 |
-
opacity = 0.
|
| 298 |
-
blur = 3 - (3 * nextProgress);
|
| 299 |
} else if (distance === 1 && index === activeCardIndex - 1) {
|
| 300 |
-
// Previous card - fading out
|
| 301 |
const fadeProgress = 1 - Math.max(0, Math.min(1, (cardLocalProgress + 1) * 2));
|
| 302 |
-
scale = 1 - (0.
|
| 303 |
-
opacity = 1 - (0.
|
| 304 |
-
blur = 0 + (3 * fadeProgress);
|
| 305 |
} else {
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
opacity = index < activeCardIndex ? 0.3 : 0.4;
|
| 309 |
-
blur = 3;
|
| 310 |
}
|
| 311 |
-
|
| 312 |
-
// Ensure last card is fully visible at end
|
| 313 |
if (progress >= 0.99 && index === numCards - 1) {
|
| 314 |
scale = 1;
|
| 315 |
opacity = 1;
|
| 316 |
-
blur = 0;
|
| 317 |
}
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
gsap.set(card, {
|
| 321 |
-
scale,
|
| 322 |
-
opacity,
|
| 323 |
-
filter: `blur(${blur}px)`,
|
| 324 |
-
});
|
| 325 |
});
|
| 326 |
},
|
| 327 |
onLeave: () => {
|
| 328 |
-
|
| 329 |
-
gsap.set(cards[numCards - 1], { scale: 1, opacity: 1, filter: "blur(0px)" });
|
| 330 |
gsap.set(scrollContainer, { x: -initialOffset - scrollAmount });
|
| 331 |
},
|
| 332 |
onEnterBack: () => {
|
| 333 |
// Reset to first card centered when scrolling back up
|
| 334 |
-
gsap.set(cards[0], { scale: 1, opacity: 1
|
| 335 |
for (let i = 1; i < numCards; i++) {
|
| 336 |
-
gsap.set(cards[i], { scale: 0.
|
| 337 |
}
|
| 338 |
gsap.set(scrollContainer, { x: -initialOffset });
|
| 339 |
},
|
| 340 |
-
onRefresh:
|
| 341 |
// Ensure proper state after refresh
|
| 342 |
-
const progress =
|
| 343 |
if (progress === 0) {
|
| 344 |
-
gsap.set(cards[0], { scale: 1, opacity: 1
|
| 345 |
gsap.set(scrollContainer, { x: -initialOffset });
|
| 346 |
} else if (progress >= 0.99) {
|
| 347 |
-
gsap.set(cards[numCards - 1], { scale: 1, opacity: 1
|
| 348 |
gsap.set(scrollContainer, { x: -initialOffset - scrollAmount });
|
| 349 |
}
|
| 350 |
},
|
|
@@ -360,9 +342,9 @@ export default function ServicesSection() {
|
|
| 360 |
ScrollTrigger.refresh();
|
| 361 |
}, 300);
|
| 362 |
};
|
| 363 |
-
|
| 364 |
window.addEventListener("resize", handleResize);
|
| 365 |
-
|
| 366 |
// Store cleanup function
|
| 367 |
(sectionRef.current as any)._scrollCleanup = () => {
|
| 368 |
window.removeEventListener("resize", handleResize);
|
|
@@ -398,7 +380,7 @@ export default function ServicesSection() {
|
|
| 398 |
<section
|
| 399 |
id="services"
|
| 400 |
ref={sectionRef}
|
| 401 |
-
className="relative bg-[#050505] overflow-
|
| 402 |
style={{ perspective: "1000px", minHeight: "100vh" }}
|
| 403 |
>
|
| 404 |
{/* Full-screen dark overlay to hide other sections */}
|
|
@@ -409,15 +391,15 @@ export default function ServicesSection() {
|
|
| 409 |
<div className="absolute inset-0 bg-gradient-radial from-blue-500/8 via-purple-500/5 to-transparent blur-3xl animate-pulse" />
|
| 410 |
</div>
|
| 411 |
|
| 412 |
-
{/* Main content - flexbox
|
| 413 |
-
<div className="relative z-10
|
| 414 |
{/* Header */}
|
| 415 |
-
<div ref={headerRef} className="text-center mb-
|
| 416 |
-
<p className="services-label text-xs md:text-sm font-medium text-white/40 uppercase tracking-widest mb-
|
| 417 |
Services
|
| 418 |
</p>
|
| 419 |
-
<div className="services-line h-px w-16 md:w-24 bg-gradient-to-r from-transparent via-white/30 to-transparent mx-auto mb-
|
| 420 |
-
<h2 className="text-
|
| 421 |
{headingText.split(" ").map((word, i) => (
|
| 422 |
<span key={i} className="word inline-block mr-[0.25em]" style={{ transformStyle: "preserve-3d" }}>
|
| 423 |
{word}
|
|
@@ -436,11 +418,10 @@ export default function ServicesSection() {
|
|
| 436 |
{/* Services Horizontal Scroll Container */}
|
| 437 |
<div
|
| 438 |
ref={cardsRef}
|
| 439 |
-
className="flex gap-6 md:gap-8 items-
|
| 440 |
-
style={{
|
| 441 |
-
paddingLeft: "8vw",
|
| 442 |
paddingRight: "8vw",
|
| 443 |
-
minHeight: "auto",
|
| 444 |
overflow: "visible",
|
| 445 |
willChange: "transform",
|
| 446 |
}}
|
|
@@ -451,7 +432,7 @@ export default function ServicesSection() {
|
|
| 451 |
<motion.div
|
| 452 |
key={service.title}
|
| 453 |
ref={(el) => { cardRefs.current[index] = el; }}
|
| 454 |
-
className="service-card group relative p-
|
| 455 |
onMouseMove={(e) => handleMouseMove(e, index)}
|
| 456 |
onMouseEnter={() => handleMouseEnter(index)}
|
| 457 |
onMouseLeave={() => handleMouseLeave(index)}
|
|
@@ -465,104 +446,37 @@ export default function ServicesSection() {
|
|
| 465 |
: "transform 0.5s ease-out",
|
| 466 |
}}
|
| 467 |
>
|
| 468 |
-
{/* Custom cursor follower */}
|
| 469 |
-
{mouseState.isHovering && (
|
| 470 |
-
<motion.div
|
| 471 |
-
className="absolute w-40 h-40 rounded-full pointer-events-none z-0"
|
| 472 |
-
style={{
|
| 473 |
-
background: "radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%)",
|
| 474 |
-
left: `${(mouseState.x + 1) * 50}%`,
|
| 475 |
-
top: `${(mouseState.y + 1) * 50}%`,
|
| 476 |
-
transform: "translate(-50%, -50%)",
|
| 477 |
-
}}
|
| 478 |
-
initial={{ opacity: 0, scale: 0.8 }}
|
| 479 |
-
animate={{ opacity: 1, scale: 1 }}
|
| 480 |
-
exit={{ opacity: 0, scale: 0.8 }}
|
| 481 |
-
transition={{ duration: 0.2 }}
|
| 482 |
-
/>
|
| 483 |
-
)}
|
| 484 |
-
|
| 485 |
-
{/* Animated gradient border */}
|
| 486 |
-
<div
|
| 487 |
-
className="absolute inset-0 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
|
| 488 |
-
style={{
|
| 489 |
-
background: "linear-gradient(135deg, rgba(139,92,246,0.3) 0%, rgba(59,130,246,0.2) 50%, rgba(236,72,153,0.3) 100%)",
|
| 490 |
-
padding: "1px",
|
| 491 |
-
mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
|
| 492 |
-
maskComposite: "exclude",
|
| 493 |
-
WebkitMaskComposite: "xor",
|
| 494 |
-
}}
|
| 495 |
-
/>
|
| 496 |
-
|
| 497 |
-
{/* Glow effect on hover */}
|
| 498 |
-
<motion.div
|
| 499 |
-
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
|
| 500 |
-
style={{
|
| 501 |
-
background: `radial-gradient(800px circle at ${(mouseState.x + 1) * 50}% ${(mouseState.y + 1) * 50}%, rgba(139,92,246,0.06), transparent 50%)`,
|
| 502 |
-
}}
|
| 503 |
-
/>
|
| 504 |
-
|
| 505 |
<div className="card-content relative z-10">
|
| 506 |
-
{/* Icon
|
| 507 |
-
<div className="relative w-
|
| 508 |
-
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-white/5 rounded-
|
| 509 |
-
<
|
| 510 |
-
className="absolute inset-0 flex items-center justify-center text-white/70 group-hover:text-white transition-colors duration-300"
|
| 511 |
-
animate={mouseState.isHovering ? { scale: 1.1 } : { scale: 1 }}
|
| 512 |
-
transition={{ duration: 0.3 }}
|
| 513 |
-
>
|
| 514 |
{service.icon}
|
| 515 |
-
</
|
| 516 |
-
{/* Animated glow ring */}
|
| 517 |
-
<motion.div
|
| 518 |
-
className="absolute -inset-2 rounded-2xl opacity-0 group-hover:opacity-100"
|
| 519 |
-
style={{
|
| 520 |
-
background: "conic-gradient(from 0deg, rgba(139,92,246,0.4), rgba(59,130,246,0.4), rgba(236,72,153,0.4), rgba(139,92,246,0.4))",
|
| 521 |
-
filter: "blur(8px)",
|
| 522 |
-
}}
|
| 523 |
-
animate={{
|
| 524 |
-
rotate: [0, 360],
|
| 525 |
-
}}
|
| 526 |
-
transition={{
|
| 527 |
-
duration: 4,
|
| 528 |
-
repeat: Infinity,
|
| 529 |
-
ease: "linear",
|
| 530 |
-
}}
|
| 531 |
-
/>
|
| 532 |
</div>
|
| 533 |
|
| 534 |
{/* Title */}
|
| 535 |
-
<h3 className="text-
|
| 536 |
{service.title}
|
| 537 |
</h3>
|
| 538 |
|
| 539 |
{/* Description */}
|
| 540 |
-
<p className="text-
|
| 541 |
{service.description}
|
| 542 |
</p>
|
| 543 |
|
| 544 |
{/* Features */}
|
| 545 |
-
<div className="flex flex-wrap gap-2
|
| 546 |
-
{service.features.map((feature
|
| 547 |
-
<
|
| 548 |
key={feature}
|
| 549 |
-
className="px-
|
| 550 |
-
whileHover={{ scale: 1.05, y: -2 }}
|
| 551 |
>
|
| 552 |
{feature}
|
| 553 |
-
</
|
| 554 |
))}
|
| 555 |
</div>
|
| 556 |
|
| 557 |
-
{/* Arrow indicator */}
|
| 558 |
-
<motion.div
|
| 559 |
-
className="absolute top-8 md:top-10 right-8 md:right-10 w-10 h-10 md:w-12 md:h-12 rounded-full bg-white/5 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
| 560 |
-
whileHover={{ scale: 1.2, rotate: 45 }}
|
| 561 |
-
>
|
| 562 |
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-white/60">
|
| 563 |
-
<path d="M7 17L17 7M17 7H7M17 7V17" />
|
| 564 |
-
</svg>
|
| 565 |
-
</motion.div>
|
| 566 |
</div>
|
| 567 |
</motion.div>
|
| 568 |
);
|
|
@@ -580,7 +494,6 @@ export default function ServicesSection() {
|
|
| 580 |
<path d="M5 12h14M12 5l7 7-7 7" />
|
| 581 |
</svg>
|
| 582 |
</motion.div>
|
| 583 |
-
<span className="text-xs uppercase tracking-widest">Scroll to explore</span>
|
| 584 |
</div>
|
| 585 |
</div>
|
| 586 |
</div>
|
|
|
|
| 193 |
|
| 194 |
const viewportWidth = window.innerWidth;
|
| 195 |
const viewportCenter = viewportWidth / 2;
|
| 196 |
+
|
| 197 |
// Get card dimensions - measure actual rendered size after layout
|
| 198 |
const cardWidth = cards[0].getBoundingClientRect().width;
|
| 199 |
const gap = viewportWidth >= 768 ? 32 : 24;
|
| 200 |
+
|
| 201 |
// Calculate positions: cards are positioned with paddingLeft = 8vw initially
|
| 202 |
const paddingLeft = viewportWidth * 0.08;
|
| 203 |
+
|
| 204 |
// First card's center position when at initial position (paddingLeft)
|
| 205 |
const firstCardCenterInitial = paddingLeft + (cardWidth / 2);
|
| 206 |
+
|
| 207 |
// Calculate offset needed to center first card initially
|
| 208 |
// We need to shift container left so first card's center aligns with viewport center
|
| 209 |
const initialOffset = firstCardCenterInitial - viewportCenter;
|
| 210 |
+
|
| 211 |
// Last card's left edge position
|
| 212 |
const lastCardLeftEdge = paddingLeft + (numCards - 1) * (cardWidth + gap);
|
| 213 |
// Last card's center position when at initial position
|
| 214 |
const lastCardCenterInitial = lastCardLeftEdge + (cardWidth / 2);
|
| 215 |
+
|
| 216 |
// Calculate how much we need to scroll to center the last card
|
| 217 |
// Scroll amount = difference between last card center and first card center positions
|
| 218 |
const scrollAmount = lastCardCenterInitial - firstCardCenterInitial;
|
| 219 |
+
|
| 220 |
// Each card gets equal time in the scroll sequence
|
| 221 |
+
const scrollDistancePerCard = Math.max(400, viewportWidth * 0.4);
|
| 222 |
const totalScrollDistance = scrollDistancePerCard * numCards;
|
| 223 |
|
| 224 |
// Setup initial card states with optimized rendering
|
| 225 |
cards.forEach((card, index) => {
|
| 226 |
// Enable hardware acceleration
|
| 227 |
+
gsap.set(card, {
|
| 228 |
+
willChange: "transform, opacity",
|
| 229 |
force3D: true,
|
| 230 |
});
|
| 231 |
+
|
| 232 |
if (index === 0) {
|
| 233 |
+
gsap.set(card, { scale: 1, opacity: 1 });
|
| 234 |
} else {
|
| 235 |
+
gsap.set(card, { scale: 0.94, opacity: 0.6 });
|
| 236 |
}
|
| 237 |
});
|
| 238 |
|
| 239 |
// Set initial position to center first card
|
| 240 |
+
gsap.set(scrollContainer, {
|
| 241 |
x: -initialOffset,
|
| 242 |
willChange: "transform",
|
| 243 |
force3D: true,
|
|
|
|
| 265 |
fastScrollEnd: true,
|
| 266 |
onUpdate: (self) => {
|
| 267 |
const progress = Math.min(1, Math.max(0, self.progress));
|
| 268 |
+
|
| 269 |
// Update horizontal scroll position (centered)
|
| 270 |
const currentX = -initialOffset - (progress * scrollAmount);
|
| 271 |
gsap.set(scrollContainer, { x: currentX });
|
| 272 |
+
|
| 273 |
// Update card states based on progress
|
| 274 |
// Each card gets 1/numCards of the scroll progress
|
| 275 |
const cardProgress = progress * numCards;
|
| 276 |
const activeCardIndex = Math.min(Math.floor(cardProgress), numCards - 1);
|
| 277 |
const cardLocalProgress = Math.max(0, Math.min(1, cardProgress - activeCardIndex));
|
| 278 |
+
|
| 279 |
+
// Batch DOM updates for better performance - NO BLUR for performance
|
| 280 |
cards.forEach((card, index) => {
|
| 281 |
const distance = Math.abs(index - activeCardIndex);
|
|
|
|
| 282 |
let scale: number;
|
| 283 |
let opacity: number;
|
| 284 |
+
|
|
|
|
| 285 |
if (distance === 0 && index === activeCardIndex) {
|
|
|
|
| 286 |
const focusProgress = Math.min(1, cardLocalProgress * 2);
|
| 287 |
+
scale = 0.94 + (0.06 * focusProgress);
|
| 288 |
+
opacity = 0.7 + (0.3 * focusProgress);
|
|
|
|
| 289 |
} else if (distance === 1 && index === activeCardIndex + 1) {
|
|
|
|
| 290 |
const nextProgress = Math.max(0, Math.min(1, (cardLocalProgress - 1) * 2));
|
| 291 |
+
scale = 0.94 + (0.06 * nextProgress);
|
| 292 |
+
opacity = 0.7 + (0.3 * nextProgress);
|
|
|
|
| 293 |
} else if (distance === 1 && index === activeCardIndex - 1) {
|
|
|
|
| 294 |
const fadeProgress = 1 - Math.max(0, Math.min(1, (cardLocalProgress + 1) * 2));
|
| 295 |
+
scale = 1 - (0.06 * fadeProgress);
|
| 296 |
+
opacity = 1 - (0.3 * fadeProgress);
|
|
|
|
| 297 |
} else {
|
| 298 |
+
scale = 0.94;
|
| 299 |
+
opacity = index < activeCardIndex ? 0.5 : 0.6;
|
|
|
|
|
|
|
| 300 |
}
|
| 301 |
+
|
|
|
|
| 302 |
if (progress >= 0.99 && index === numCards - 1) {
|
| 303 |
scale = 1;
|
| 304 |
opacity = 1;
|
|
|
|
| 305 |
}
|
| 306 |
+
|
| 307 |
+
gsap.set(card, { scale, opacity });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
});
|
| 309 |
},
|
| 310 |
onLeave: () => {
|
| 311 |
+
gsap.set(cards[numCards - 1], { scale: 1, opacity: 1 });
|
|
|
|
| 312 |
gsap.set(scrollContainer, { x: -initialOffset - scrollAmount });
|
| 313 |
},
|
| 314 |
onEnterBack: () => {
|
| 315 |
// Reset to first card centered when scrolling back up
|
| 316 |
+
gsap.set(cards[0], { scale: 1, opacity: 1 });
|
| 317 |
for (let i = 1; i < numCards; i++) {
|
| 318 |
+
gsap.set(cards[i], { scale: 0.94, opacity: 0.6 });
|
| 319 |
}
|
| 320 |
gsap.set(scrollContainer, { x: -initialOffset });
|
| 321 |
},
|
| 322 |
+
onRefresh: (self) => {
|
| 323 |
// Ensure proper state after refresh
|
| 324 |
+
const progress = self.progress || 0;
|
| 325 |
if (progress === 0) {
|
| 326 |
+
gsap.set(cards[0], { scale: 1, opacity: 1 });
|
| 327 |
gsap.set(scrollContainer, { x: -initialOffset });
|
| 328 |
} else if (progress >= 0.99) {
|
| 329 |
+
gsap.set(cards[numCards - 1], { scale: 1, opacity: 1 });
|
| 330 |
gsap.set(scrollContainer, { x: -initialOffset - scrollAmount });
|
| 331 |
}
|
| 332 |
},
|
|
|
|
| 342 |
ScrollTrigger.refresh();
|
| 343 |
}, 300);
|
| 344 |
};
|
| 345 |
+
|
| 346 |
window.addEventListener("resize", handleResize);
|
| 347 |
+
|
| 348 |
// Store cleanup function
|
| 349 |
(sectionRef.current as any)._scrollCleanup = () => {
|
| 350 |
window.removeEventListener("resize", handleResize);
|
|
|
|
| 380 |
<section
|
| 381 |
id="services"
|
| 382 |
ref={sectionRef}
|
| 383 |
+
className="relative bg-[#050505] overflow-visible"
|
| 384 |
style={{ perspective: "1000px", minHeight: "100vh" }}
|
| 385 |
>
|
| 386 |
{/* Full-screen dark overlay to hide other sections */}
|
|
|
|
| 391 |
<div className="absolute inset-0 bg-gradient-radial from-blue-500/8 via-purple-500/5 to-transparent blur-3xl animate-pulse" />
|
| 392 |
</div>
|
| 393 |
|
| 394 |
+
{/* Main content - flexbox with top alignment for proper pinning */}
|
| 395 |
+
<div className="relative z-10 h-screen flex flex-col pt-12 md:pt-16">
|
| 396 |
{/* Header */}
|
| 397 |
+
<div ref={headerRef} className="text-center mb-4 md:mb-6 px-4 flex-shrink-0">
|
| 398 |
+
<p className="services-label text-xs md:text-sm font-medium text-white/40 uppercase tracking-widest mb-2 md:mb-3">
|
| 399 |
Services
|
| 400 |
</p>
|
| 401 |
+
<div className="services-line h-px w-16 md:w-24 bg-gradient-to-r from-transparent via-white/30 to-transparent mx-auto mb-3 md:mb-4" />
|
| 402 |
+
<h2 className="text-2xl md:text-3xl lg:text-4xl font-light text-white/90 tracking-tight leading-tight" style={{ perspective: "1000px" }}>
|
| 403 |
{headingText.split(" ").map((word, i) => (
|
| 404 |
<span key={i} className="word inline-block mr-[0.25em]" style={{ transformStyle: "preserve-3d" }}>
|
| 405 |
{word}
|
|
|
|
| 418 |
{/* Services Horizontal Scroll Container */}
|
| 419 |
<div
|
| 420 |
ref={cardsRef}
|
| 421 |
+
className="flex gap-6 md:gap-8 items-start py-6 md:py-8 flex-1"
|
| 422 |
+
style={{
|
| 423 |
+
paddingLeft: "8vw",
|
| 424 |
paddingRight: "8vw",
|
|
|
|
| 425 |
overflow: "visible",
|
| 426 |
willChange: "transform",
|
| 427 |
}}
|
|
|
|
| 432 |
<motion.div
|
| 433 |
key={service.title}
|
| 434 |
ref={(el) => { cardRefs.current[index] = el; }}
|
| 435 |
+
className="service-card group relative p-5 md:p-6 bg-white/[0.02] backdrop-blur-sm border border-white/5 rounded-2xl md:rounded-3xl overflow-hidden cursor-pointer flex-shrink-0 w-[80vw] sm:w-[60vw] md:w-[320px] lg:w-[350px] h-auto"
|
| 436 |
onMouseMove={(e) => handleMouseMove(e, index)}
|
| 437 |
onMouseEnter={() => handleMouseEnter(index)}
|
| 438 |
onMouseLeave={() => handleMouseLeave(index)}
|
|
|
|
| 446 |
: "transform 0.5s ease-out",
|
| 447 |
}}
|
| 448 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
<div className="card-content relative z-10">
|
| 450 |
+
{/* Icon */}
|
| 451 |
+
<div className="relative w-12 h-12 md:w-14 md:h-14 mb-4 md:mb-5">
|
| 452 |
+
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-white/5 rounded-xl" />
|
| 453 |
+
<div className="absolute inset-0 flex items-center justify-center text-white/70 group-hover:text-white transition-colors duration-300">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
{service.icon}
|
| 455 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
</div>
|
| 457 |
|
| 458 |
{/* Title */}
|
| 459 |
+
<h3 className="text-xl md:text-2xl font-medium text-white/90 mb-2 md:mb-3 group-hover:text-white transition-colors duration-300">
|
| 460 |
{service.title}
|
| 461 |
</h3>
|
| 462 |
|
| 463 |
{/* Description */}
|
| 464 |
+
<p className="text-sm md:text-base text-white/50 leading-relaxed mb-4 md:mb-5 group-hover:text-white/70 transition-colors duration-300">
|
| 465 |
{service.description}
|
| 466 |
</p>
|
| 467 |
|
| 468 |
{/* Features */}
|
| 469 |
+
<div className="flex flex-wrap gap-2">
|
| 470 |
+
{service.features.map((feature) => (
|
| 471 |
+
<span
|
| 472 |
key={feature}
|
| 473 |
+
className="px-3 py-1.5 text-xs bg-white/5 text-white/50 rounded-full border border-white/10 group-hover:bg-white/10 group-hover:text-white/80 group-hover:border-white/20 transition-all duration-300"
|
|
|
|
| 474 |
>
|
| 475 |
{feature}
|
| 476 |
+
</span>
|
| 477 |
))}
|
| 478 |
</div>
|
| 479 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
</div>
|
| 481 |
</motion.div>
|
| 482 |
);
|
|
|
|
| 494 |
<path d="M5 12h14M12 5l7 7-7 7" />
|
| 495 |
</svg>
|
| 496 |
</motion.div>
|
|
|
|
| 497 |
</div>
|
| 498 |
</div>
|
| 499 |
</div>
|
src/components/SplashCursor.jsx
CHANGED
|
@@ -3,17 +3,17 @@
|
|
| 3 |
import { useEffect, useRef } from 'react';
|
| 4 |
|
| 5 |
function SplashCursor({
|
| 6 |
-
SIM_RESOLUTION =
|
| 7 |
-
DYE_RESOLUTION =
|
| 8 |
-
CAPTURE_RESOLUTION =
|
| 9 |
DENSITY_DISSIPATION = 3.5,
|
| 10 |
VELOCITY_DISSIPATION = 2,
|
| 11 |
PRESSURE = 0.1,
|
| 12 |
-
PRESSURE_ITERATIONS =
|
| 13 |
CURL = 3,
|
| 14 |
SPLAT_RADIUS = 0.2,
|
| 15 |
-
SPLAT_FORCE =
|
| 16 |
-
SHADING =
|
| 17 |
COLOR_UPDATE_SPEED = 10,
|
| 18 |
BACK_COLOR = { r: 0.5, g: 0, b: 0 },
|
| 19 |
TRANSPARENT = true
|
|
|
|
| 3 |
import { useEffect, useRef } from 'react';
|
| 4 |
|
| 5 |
function SplashCursor({
|
| 6 |
+
SIM_RESOLUTION = 64,
|
| 7 |
+
DYE_RESOLUTION = 512,
|
| 8 |
+
CAPTURE_RESOLUTION = 256,
|
| 9 |
DENSITY_DISSIPATION = 3.5,
|
| 10 |
VELOCITY_DISSIPATION = 2,
|
| 11 |
PRESSURE = 0.1,
|
| 12 |
+
PRESSURE_ITERATIONS = 10,
|
| 13 |
CURL = 3,
|
| 14 |
SPLAT_RADIUS = 0.2,
|
| 15 |
+
SPLAT_FORCE = 4000,
|
| 16 |
+
SHADING = false,
|
| 17 |
COLOR_UPDATE_SPEED = 10,
|
| 18 |
BACK_COLOR = { r: 0.5, g: 0, b: 0 },
|
| 19 |
TRANSPARENT = true
|