Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions app/components/hero/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -159,8 +186,9 @@ export default function Hero() {
ref={cometBackgroundLayerRef}
aria-hidden="true"
className="absolute inset-0 z-2"
style={{ opacity: 0, visibility: "hidden" }}
>
<CometTrailBackground />
{!isMobile && <CometTrailBackground />}
</div>

{/* Sky before buildings: same z, so DOM order alone puts the flock
Expand Down
28 changes: 24 additions & 4 deletions app/components/timeline/RocketTrailAnimation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -262,6 +265,12 @@ export default function RocketTrailAnimation() {
</filter>
</defs>

{/* 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. */}
<foreignObject
x="0"
y="-180"
Expand All @@ -271,9 +280,20 @@ export default function RocketTrailAnimation() {
>
<div
className="h-full w-full"
style={{ height: "100%", width: "100%" }}
style={{
height: "100%",
width: "100%",
...(isMobile
? {
background:
"linear-gradient(150deg, #4a0eff 0%, #6C17FE 20%, #9B30FF 40%, #F31667 65%, #ff6b4a 85%, #FFA21F 100%)",
}
: {}),
}}
>
{shaderActive && <BrandShaderBackground lazyLoad={false} />}
{!isMobile && shaderActive && (
<BrandShaderBackground lazyLoad={false} />
)}
</div>
</foreignObject>

Expand Down