Cut desktop TBT: splash idle animations framer-motion -> CSS; lighten nebula

- The splash's always-on animations (logo float/breath, the two traveling
  glow rings) move from framer-motion (JS rAF every frame on the main thread)
  to CSS keyframes (browser/compositor). framer now only drives the one-shot
  click-to-enter water exit.
- Nebula: density 8 -> 4 and dropped the scroll parallax (the Forced-reflow
  source); reduced-motion still gets the static background.
This commit is contained in:
jwinterm
2026-06-06 17:16:13 -04:00
parent c9b6803a77
commit a5815e20ee
3 changed files with 29 additions and 33 deletions
+12
View File
@@ -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; }
@@ -35,29 +35,22 @@ function SuchMark({ animate }: { animate: boolean }) {
{/* two Cherenkov glow segments circling the ring in opposite directions */}
{animate && (
<>
<motion.rect
<rect
x="14" y="14" width="172" height="172" rx="16"
fill="none" stroke="#a5f3fc" strokeWidth="4" strokeLinecap="round" filter="url(#splash-glow)"
pathLength={1} strokeDasharray="0.16 0.84"
initial={{ strokeDashoffset: 0 }}
animate={{ strokeDashoffset: [0, -1] }}
transition={{ duration: 5, ease: "linear", repeat: Infinity }}
className="splash-ring-a"
/>
<motion.rect
<rect
x="14" y="14" width="172" height="172" rx="16"
fill="none" stroke="#67e8f9" strokeWidth="2.5" strokeLinecap="round" filter="url(#splash-glow)"
pathLength={1} strokeDasharray="0.06 0.94" opacity="0.7"
initial={{ strokeDashoffset: 0 }}
animate={{ strokeDashoffset: [0, 1] }}
transition={{ duration: 7, ease: "linear", repeat: Infinity }}
className="splash-ring-b"
/>
</>
)}
{/* letters with a gentle breath */}
<motion.g
animate={animate ? { opacity: [0.9, 1, 0.9] } : undefined}
transition={animate ? { duration: 5, ease: "easeInOut", repeat: Infinity } : undefined}
>
<g className={animate ? "splash-breath" : undefined}>
{/* S */}
<path d="M 78,36 H 36 V 58 H 78 V 82 H 36" fill="none" stroke="#35c98e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
{/* U */}
@@ -66,7 +59,7 @@ function SuchMark({ animate }: { animate: boolean }) {
<path d="M 78,118 H 36 V 164 H 78" fill="none" stroke="#4aade0" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
{/* H */}
<path d="M 118,118 V 164 M 162,118 V 164 M 118,141 H 162" fill="none" stroke="#45c99e" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
</motion.g>
</g>
</svg>
);
}
@@ -144,14 +137,11 @@ export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; secti
style={{ animationDuration: "4.5s" }}
/>
<motion.div
className={!entering && animate ? "splash-float" : undefined}
style={{ filter: entering ? "url(#splash-water)" : undefined }}
initial={{ opacity: 1, scale: 1 }}
animate={entering ? { opacity: [1, 1, 0], scale: [1, 1.06, 1.14] } : { opacity: 1, scale: 1, y: animate ? [0, -8, 0] : 0 }}
transition={
entering
? { duration: 1.2, ease: "easeInOut", times: [0, 0.7, 1] }
: { opacity: { duration: 0.8 }, scale: { duration: 0.8 }, y: { duration: 6, repeat: Infinity, ease: "easeInOut" } }
}
animate={entering ? { opacity: [1, 1, 0], scale: [1, 1.06, 1.14] } : undefined}
transition={entering ? { duration: 1.2, ease: "easeInOut", times: [0, 0.7, 1] } : undefined}
>
<SuchMark animate={animate} />
</motion.div>