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
+8 -14
View File
@@ -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 */}
<motion.div
className="absolute h-[70vmax] w-[70vmax] -translate-x-1/2 -translate-y-1/2"
style={{ ...glowStyle, left: `${computerX}%`, top: `${computerY}%`, y: yRange }}
style={{ ...glowStyle, left: `${computerX}%`, top: `${computerY}%` }}
initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: [0.55, 0.9, 0.55], scale: [0.95, 1.05, 0.95] }}
transition={{ duration: 11, ease: "easeInOut", repeat: Infinity }}
@@ -205,10 +201,9 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
scale: 1
}}
transition={{ duration: 1.5 }}
style={{
left: `${computerX}%`,
top: `${computerY}%`,
y: yRange
style={{
left: `${computerX}%`,
top: `${computerY}%`
}}
>
<motion.div animate={monitorControls}>
@@ -222,11 +217,10 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
</motion.div>
</motion.div>
<motion.svg
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="w-full h-full opacity-50 dark:opacity-70 absolute inset-0"
style={{ y: yRange }}
>
{paths.map((d: string, i: number) => (
<StreamLine
@@ -238,7 +232,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
index={i}
/>
))}
</motion.svg>
</svg>
</div>
);
};