mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
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:
@@ -228,3 +228,15 @@
|
|||||||
scroll-behavior: auto !important;
|
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 */}
|
{/* two Cherenkov glow segments circling the ring in opposite directions */}
|
||||||
{animate && (
|
{animate && (
|
||||||
<>
|
<>
|
||||||
<motion.rect
|
<rect
|
||||||
x="14" y="14" width="172" height="172" rx="16"
|
x="14" y="14" width="172" height="172" rx="16"
|
||||||
fill="none" stroke="#a5f3fc" strokeWidth="4" strokeLinecap="round" filter="url(#splash-glow)"
|
fill="none" stroke="#a5f3fc" strokeWidth="4" strokeLinecap="round" filter="url(#splash-glow)"
|
||||||
pathLength={1} strokeDasharray="0.16 0.84"
|
pathLength={1} strokeDasharray="0.16 0.84"
|
||||||
initial={{ strokeDashoffset: 0 }}
|
className="splash-ring-a"
|
||||||
animate={{ strokeDashoffset: [0, -1] }}
|
|
||||||
transition={{ duration: 5, ease: "linear", repeat: Infinity }}
|
|
||||||
/>
|
/>
|
||||||
<motion.rect
|
<rect
|
||||||
x="14" y="14" width="172" height="172" rx="16"
|
x="14" y="14" width="172" height="172" rx="16"
|
||||||
fill="none" stroke="#67e8f9" strokeWidth="2.5" strokeLinecap="round" filter="url(#splash-glow)"
|
fill="none" stroke="#67e8f9" strokeWidth="2.5" strokeLinecap="round" filter="url(#splash-glow)"
|
||||||
pathLength={1} strokeDasharray="0.06 0.94" opacity="0.7"
|
pathLength={1} strokeDasharray="0.06 0.94" opacity="0.7"
|
||||||
initial={{ strokeDashoffset: 0 }}
|
className="splash-ring-b"
|
||||||
animate={{ strokeDashoffset: [0, 1] }}
|
|
||||||
transition={{ duration: 7, ease: "linear", repeat: Infinity }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* letters with a gentle breath */}
|
{/* letters with a gentle breath */}
|
||||||
<motion.g
|
<g className={animate ? "splash-breath" : undefined}>
|
||||||
animate={animate ? { opacity: [0.9, 1, 0.9] } : undefined}
|
|
||||||
transition={animate ? { duration: 5, ease: "easeInOut", repeat: Infinity } : undefined}
|
|
||||||
>
|
|
||||||
{/* S */}
|
{/* 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" />
|
<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 */}
|
{/* 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" />
|
<path d="M 78,118 H 36 V 164 H 78" fill="none" stroke="#4aade0" strokeWidth="14" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
{/* H */}
|
{/* 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" />
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -144,14 +137,11 @@ export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; secti
|
|||||||
style={{ animationDuration: "4.5s" }}
|
style={{ animationDuration: "4.5s" }}
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
className={!entering && animate ? "splash-float" : undefined}
|
||||||
style={{ filter: entering ? "url(#splash-water)" : undefined }}
|
style={{ filter: entering ? "url(#splash-water)" : undefined }}
|
||||||
initial={{ opacity: 1, scale: 1 }}
|
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 }}
|
animate={entering ? { opacity: [1, 1, 0], scale: [1, 1.06, 1.14] } : undefined}
|
||||||
transition={
|
transition={entering ? { duration: 1.2, ease: "easeInOut", times: [0, 0.7, 1] } : undefined}
|
||||||
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" } }
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SuchMark animate={animate} />
|
<SuchMark animate={animate} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion, useAnimation, useScroll, useTransform } from "framer-motion";
|
import { motion, useAnimation } from "framer-motion";
|
||||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { Monitor } from "lucide-react";
|
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 ===
|
// === ALL HOOKS MUST BE CALLED BEFORE ANY CONDITIONAL RETURNS ===
|
||||||
const monitorControls = useAnimation();
|
const monitorControls = useAnimation();
|
||||||
const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-sky-500" : "text-cyan-400/70");
|
const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-sky-500" : "text-cyan-400/70");
|
||||||
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
||||||
|
|
||||||
// Parallax Scroll hooks (called unconditionally)
|
|
||||||
const { scrollY } = useScroll();
|
|
||||||
const yRange = useTransform(scrollY, [0, 1000], [0, 250]);
|
|
||||||
|
|
||||||
// Position calculations
|
// Position calculations
|
||||||
const computerX = position?.x ?? 18;
|
const computerX = position?.x ?? 18;
|
||||||
const computerY = position?.y ?? 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 */}
|
{/* Slowly-pulsing Cherenkov "reactor pool" bloom */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute h-[70vmax] w-[70vmax] -translate-x-1/2 -translate-y-1/2"
|
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 }}
|
initial={{ opacity: 0, scale: 0.85 }}
|
||||||
animate={{ opacity: [0.55, 0.9, 0.55], scale: [0.95, 1.05, 0.95] }}
|
animate={{ opacity: [0.55, 0.9, 0.55], scale: [0.95, 1.05, 0.95] }}
|
||||||
transition={{ duration: 11, ease: "easeInOut", repeat: Infinity }}
|
transition={{ duration: 11, ease: "easeInOut", repeat: Infinity }}
|
||||||
@@ -207,8 +203,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
transition={{ duration: 1.5 }}
|
transition={{ duration: 1.5 }}
|
||||||
style={{
|
style={{
|
||||||
left: `${computerX}%`,
|
left: `${computerX}%`,
|
||||||
top: `${computerY}%`,
|
top: `${computerY}%`
|
||||||
y: yRange
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<motion.div animate={monitorControls}>
|
<motion.div animate={monitorControls}>
|
||||||
@@ -222,11 +217,10 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.svg
|
<svg
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
preserveAspectRatio="none"
|
preserveAspectRatio="none"
|
||||||
className="w-full h-full opacity-50 dark:opacity-70 absolute inset-0"
|
className="w-full h-full opacity-50 dark:opacity-70 absolute inset-0"
|
||||||
style={{ y: yRange }}
|
|
||||||
>
|
>
|
||||||
{paths.map((d: string, i: number) => (
|
{paths.map((d: string, i: number) => (
|
||||||
<StreamLine
|
<StreamLine
|
||||||
@@ -238,7 +232,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
|
|||||||
index={i}
|
index={i}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</motion.svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user