From 44acfbca5dc4d42920e6a2976a9c3093b4890763 Mon Sep 17 00:00:00 2001 From: jagadeep kalluri Date: Mon, 3 Aug 2026 18:12:32 -0500 Subject: [PATCH 1/2] fix: eliminate gradient flash on real mobile devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On real phones, momentum scrolling traverses the 250vh hero section in under a second. Two issues combined to produce a full-screen gradient flash: 1. The reveal (autoAlpha 0→1) and whiteout (autoAlpha 1→0) were separate ScrollTrigger tweens each with 0.6s scrub lag. During fast scrolling they caught up at different times, leaving the gradient at full opacity in the gap. Fixed by using scrub: true (zero lag) on mobile so both tweens track scroll position exactly. 2. The WebGL canvas inside SVG foreignObject was promoted to an independent GPU compositing layer on mobile, ignoring parent opacity/visibility. Fixed by replacing the WebGL path with a CSS gradient on mobile, set via GSAP (not JSX) so it appears atomically with autoAlpha: 0. Additionally, CometTrailBackground is now loaded with next/dynamic ssr: false to prevent the WebGL canvas from appearing in server HTML. Co-Authored-By: Claude Opus 4.6 --- app/components/hero/Hero.tsx | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) 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 From 9df7fd639dbcd2c5f047429bba92a7fa048a52e6 Mon Sep 17 00:00:00 2001 From: jagadeep kalluri Date: Mon, 3 Aug 2026 20:16:26 -0500 Subject: [PATCH 2/2] fix: eliminate gradient flash in rocket trail on real mobile devices Same root cause as the hero gradient flash: mobile GPUs promote WebGL canvases inside SVG foreignObject to independent compositing layers that ignore SVG masks. Replace WebGL with CSS gradient on mobile and use scrub: true for zero-lag scroll tracking. Co-Authored-By: Claude Opus 4.6 --- .../timeline/RocketTrailAnimation.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) 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 && ( + + )}