diff --git a/app/components/hero/Hero.tsx b/app/components/hero/Hero.tsx index 3dd4a6e..870c0b3 100644 --- a/app/components/hero/Hero.tsx +++ b/app/components/hero/Hero.tsx @@ -19,9 +19,13 @@ import { HERO_SKYLINE_MASK, HERO_STARS, HERO_WHITEOUT, - MOBILE_SCRUB, } from "./sceneConfig"; -import CometTrailBackground from "./CometTrailBackground"; +import dynamic from "next/dynamic"; + +const CometTrailBackground = dynamic( + () => import("./CometTrailBackground"), + { ssr: false }, +); configureScrollTrigger(); @@ -52,6 +56,12 @@ export default function Hero() { if (prefersReducedMotion) { if (cometBackgroundLayer) { + if (isMobile) { + gsap.set(cometBackgroundLayer, { + background: + "linear-gradient(150deg, #4a0eff 0%, #6C17FE 20%, #9B30FF 40%, #F31667 65%, #ff6b4a 85%, #FFA21F 100%)", + }); + } gsap.set(cometBackgroundLayer, { autoAlpha: 1 }); } if (heroText) { @@ -60,13 +70,30 @@ export default function Hero() { return; } - const scrub = isMobile ? MOBILE_SCRUB : HERO_SCENE_SCROLL.scrub; + // On real phones, momentum scrolling traverses the full hero in under a + // second. A numeric scrub adds lag, and because the reveal and whiteout + // are separate tweens their catch-up windows don't overlap — creating a + // visible gradient flash. `true` = zero lag, exact scroll tracking. + const scrub = isMobile ? true : HERO_SCENE_SCROLL.scrub; if (heroText) { gsap.set(heroText, { autoAlpha: 1 }); } if (cometBackgroundLayer) { + // On mobile, use a CSS gradient instead of the WebGL canvas. Mobile + // GPUs promote WebGL canvases inside SVG foreignObject to independent + // compositing layers that ignore parent opacity/visibility, causing + // the gradient to paint through even when autoAlpha is 0. Setting + // the background here (not in JSX) guarantees it appears atomically + // with autoAlpha: 0 — the gradient is never in the DOM before GSAP + // hides it. + if (isMobile) { + gsap.set(cometBackgroundLayer, { + background: + "linear-gradient(150deg, #4a0eff 0%, #6C17FE 20%, #9B30FF 40%, #F31667 65%, #ff6b4a 85%, #FFA21F 100%)", + }); + } gsap.set(cometBackgroundLayer, { autoAlpha: 0 }); gsap.to(cometBackgroundLayer, { autoAlpha: 1, @@ -159,8 +186,9 @@ export default function Hero() { ref={cometBackgroundLayerRef} aria-hidden="true" className="absolute inset-0 z-2" + style={{ opacity: 0, visibility: "hidden" }} > - + {!isMobile && } {/* Sky before buildings: same z, so DOM order alone puts the flock diff --git a/app/components/timeline/RocketTrailAnimation.tsx b/app/components/timeline/RocketTrailAnimation.tsx index 39d59b0..1daba51 100644 --- a/app/components/timeline/RocketTrailAnimation.tsx +++ b/app/components/timeline/RocketTrailAnimation.tsx @@ -11,7 +11,6 @@ import { useNearViewport } from "@/app/hooks/useNearViewport"; import { usePrefersReducedMotion } from "@/app/hooks/usePrefersReducedMotion"; import { configureScrollTrigger } from "@/app/lib/scrollTrigger"; import { - MOBILE_TIMELINE_SCRUB, MOBILE_TRAIL_WAVE, ROCKET_FILL_PATH, ROCKET_STROKE_PATH, @@ -175,7 +174,11 @@ export default function RocketTrailAnimation() { }; // --- Scroll-driven rocket slide --- - const scrub = isMobile ? MOBILE_TIMELINE_SCRUB : TIMELINE_SCROLL.scrub; + // On real phones, momentum scrolling traverses the section in under a + // second. A numeric scrub adds lag that desynchronises the rocket slide + // from the exit fade, causing a visible gradient flash. `true` = zero + // lag, exact scroll tracking. + const scrub = isMobile ? true : TIMELINE_SCROLL.scrub; const tl = gsap.timeline({ scrollTrigger: { trigger, @@ -262,6 +265,12 @@ export default function RocketTrailAnimation() { + {/* On mobile, use a CSS gradient instead of the WebGL canvas. + Mobile GPUs promote WebGL canvases inside SVG foreignObject to + independent compositing layers that ignore the SVG mask, causing + the gradient to paint as a full unclipped rectangle. A CSS + gradient background stays in the normal compositing path and + respects the mask correctly. */}
- {shaderActive && } + {!isMobile && shaderActive && ( + + )}