diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 9b56441..e34c840 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -228,3 +228,15 @@ scroll-behavior: auto !important; } } + +/* Splash hero idle animations as CSS (browser/compositor driven) instead of + framer-motion, so they cost no JS main-thread time. The reduced-motion block + above neutralizes them. */ +@keyframes splash-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } +@keyframes splash-breath { 0%, 100% { opacity: 0.9; } 50% { opacity: 1; } } +@keyframes splash-ring-a { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -1; } } +@keyframes splash-ring-b { from { stroke-dashoffset: 0; } to { stroke-dashoffset: 1; } } +.splash-float { animation: splash-float 6s ease-in-out infinite; } +.splash-breath { animation: splash-breath 5s ease-in-out infinite; } +.splash-ring-a { animation: splash-ring-a 5s linear infinite; } +.splash-ring-b { animation: splash-ring-b 7s linear infinite; } diff --git a/apps/web/src/components/landing/hero-splash.tsx b/apps/web/src/components/landing/hero-splash.tsx index 462784b..ff18473 100644 --- a/apps/web/src/components/landing/hero-splash.tsx +++ b/apps/web/src/components/landing/hero-splash.tsx @@ -35,29 +35,22 @@ function SuchMark({ animate }: { animate: boolean }) { {/* two Cherenkov glow segments circling the ring in opposite directions */} {animate && ( <> - - )} {/* letters with a gentle breath */} - + {/* S */} {/* U */} @@ -66,7 +59,7 @@ function SuchMark({ animate }: { animate: boolean }) { {/* H */} - + ); } @@ -144,14 +137,11 @@ export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; secti style={{ animationDuration: "4.5s" }} /> diff --git a/packages/ui/src/procedural/nebula-field.tsx b/packages/ui/src/procedural/nebula-field.tsx index 4eebb2a..f9c050d 100644 --- a/packages/ui/src/procedural/nebula-field.tsx +++ b/packages/ui/src/procedural/nebula-field.tsx @@ -1,6 +1,6 @@ 'use client'; -import { motion, useAnimation, useScroll, useTransform } from "framer-motion"; +import { motion, useAnimation } from "framer-motion"; import { useEffect, useState, useCallback, useMemo } from "react"; import { cn } from "../lib/utils"; import { Monitor } from "lucide-react"; @@ -79,16 +79,12 @@ const StreamLine = ({ d, themeMode, onHit, width, index }: StreamLineProps) => { ); }; -export const NebulaField = ({ className, density = 8, themeMode = 'dark', position }: NebulaProps) => { +export const NebulaField = ({ className, density = 4, themeMode = 'dark', position }: NebulaProps) => { // === ALL HOOKS MUST BE CALLED BEFORE ANY CONDITIONAL RETURNS === const monitorControls = useAnimation(); const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-sky-500" : "text-cyan-400/70"); const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); - // Parallax Scroll hooks (called unconditionally) - const { scrollY } = useScroll(); - const yRange = useTransform(scrollY, [0, 1000], [0, 250]); - // Position calculations const computerX = position?.x ?? 18; const computerY = position?.y ?? 18; @@ -191,7 +187,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi {/* Slowly-pulsing Cherenkov "reactor pool" bloom */} @@ -222,11 +217,10 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi - {paths.map((d: string, i: number) => ( ))} - + ); };