diff --git a/src/app.css b/src/app.css index 09abb51..9f37863 100644 --- a/src/app.css +++ b/src/app.css @@ -722,3 +722,30 @@ a.hover-underline-animation:hover::after { display: none; } } +/* ── Nook landing: fan card shake, builds with fan speed ───────── */ +.fan-card-shake { + transform-origin: left center; + animation-name: fan-shake; + animation-duration: var(--fan-speed, 0.4s); + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; +} + +@keyframes fan-shake { + 0%, 100% { transform: rotate(0deg); } + 50% { transform: rotate(calc(var(--fan-amp, 0deg) * -1)); } +} + +/* Sputtering sparks off the fan card at max speed */ +.fan-spark { + position: absolute; + border-radius: 9999px; + background: #ffd23f; + box-shadow: 0 0 4px 1px rgba(255, 140, 0, 0.9); + animation: fan-spark-fly var(--sd, 0.5s) ease-out var(--sdel, 0s) infinite; +} +@keyframes fan-spark-fly { + 0% { transform: translate(0, 0) scale(1); opacity: 0; } + 10% { opacity: 1; } + 100% { transform: translate(var(--sx, 20px), var(--sy, 30px)) scale(0.4); opacity: 0; } +} diff --git a/src/components/nook/FeatureBreakdowns.tsx b/src/components/nook/FeatureBreakdowns.tsx index f20cea6..a4d28c0 100644 --- a/src/components/nook/FeatureBreakdowns.tsx +++ b/src/components/nook/FeatureBreakdowns.tsx @@ -250,14 +250,25 @@ function ApprovalDemo() { /* ── Demo 3: fans throttle up under load, then back down ──────────── */ +/* Spark pool: deterministic pseudo-random, count gated by fan speed. */ +const sparkRand = (i: number, k: number) => { + const x = Math.sin(i * 127.1 + k * 311.7) * 43758.5453; + return x - Math.floor(x); +}; +const SPARKS = Array.from({ length: 20 }, (_, i) => ({ + sx: `${Math.round(10 + sparkRand(i, 1) * 26)}px`, + sy: `${Math.round(18 + sparkRand(i, 2) * 30)}px`, + sd: `${(0.4 + sparkRand(i, 3) * 0.25).toFixed(2)}s`, + sdel: `${(sparkRand(i, 4) * 1.2).toFixed(2)}s`, + size: sparkRand(i, 5) > 0.5 ? 3 : 2 +})); + function FansDemo() { const [rpm, setRpm] = createSignal(1270); const [maxed, setMaxed] = createSignal(false); onMount(() => { let frame = 0; const timers: ReturnType[] = []; - /* Linear ramp toward `target` — close enough to the app's spring - at marketing-frame rates, with zero easing-library surface. */ const ramp = (target: number, durationMs: number) => { cancelAnimationFrame(frame); const start = rpm(); @@ -293,8 +304,20 @@ function FansDemo() { { rpm: Math.round(rpm() * 0.96), frac: (rpm() * 0.96) / 6000 } ]; + const shakeIntensity = () => + Math.max(0, Math.min(1, (rpm() - 2500) / (6000 - 2500))); + /* 5 sparks when the shake starts, ~20 at max speed. */ + const sparkCount = () => + shakeIntensity() > 0 ? 5 + Math.floor(shakeIntensity() * 15) : 0; return ( - +
+
{(f, i) => ( @@ -338,7 +361,33 @@ function FansDemo() {
-
+ +
0 ? "1" : "0", + transition: "opacity 0.3s" + }} + > + + {(s) => ( + + )} + +
+ ); } diff --git a/src/components/nook/IslandMock.tsx b/src/components/nook/IslandMock.tsx index 9d8ee7d..2fec0f7 100644 --- a/src/components/nook/IslandMock.tsx +++ b/src/components/nook/IslandMock.tsx @@ -33,13 +33,18 @@ export const CARD_STROKE = "rgba(255,255,255,0.07)"; * One module-card surface shared by every mock panel — the app's * cardChrome(): rounded 12, hairline stroke, white-on-black fill. */ -export function ModuleCard(props: { children: JSX.Element; class?: string }) { +export function ModuleCard(props: { + children: JSX.Element; + class?: string; + style?: Record; +}) { return (
{props.children}