better fan ui
This commit is contained in:
27
src/app.css
27
src/app.css
@@ -722,3 +722,30 @@ a.hover-underline-animation:hover::after {
|
|||||||
display: none;
|
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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -250,14 +250,25 @@ function ApprovalDemo() {
|
|||||||
|
|
||||||
/* ── Demo 3: fans throttle up under load, then back down ──────────── */
|
/* ── 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() {
|
function FansDemo() {
|
||||||
const [rpm, setRpm] = createSignal(1270);
|
const [rpm, setRpm] = createSignal(1270);
|
||||||
const [maxed, setMaxed] = createSignal(false);
|
const [maxed, setMaxed] = createSignal(false);
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let frame = 0;
|
let frame = 0;
|
||||||
const timers: ReturnType<typeof setTimeout>[] = [];
|
const timers: ReturnType<typeof setTimeout>[] = [];
|
||||||
/* 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) => {
|
const ramp = (target: number, durationMs: number) => {
|
||||||
cancelAnimationFrame(frame);
|
cancelAnimationFrame(frame);
|
||||||
const start = rpm();
|
const start = rpm();
|
||||||
@@ -293,8 +304,20 @@ function FansDemo() {
|
|||||||
{ rpm: Math.round(rpm() * 0.96), frac: (rpm() * 0.96) / 6000 }
|
{ 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 (
|
return (
|
||||||
<ModuleCard class="mx-auto w-full max-w-[320px]">
|
<div class="relative mx-auto w-full max-w-[320px]">
|
||||||
|
<ModuleCard
|
||||||
|
class="fan-card-shake relative z-10"
|
||||||
|
style={{
|
||||||
|
"--fan-amp": `${(shakeIntensity() * 2.6).toFixed(2)}deg`,
|
||||||
|
"--fan-speed": `${(0.4 - shakeIntensity() * 0.28).toFixed(3)}s`
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="space-y-3 text-left">
|
<div class="space-y-3 text-left">
|
||||||
<For each={fans()}>
|
<For each={fans()}>
|
||||||
{(f, i) => (
|
{(f, i) => (
|
||||||
@@ -338,7 +361,33 @@ function FansDemo() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModuleCard>
|
</ModuleCard>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute z-0"
|
||||||
|
style={{
|
||||||
|
right: "6px",
|
||||||
|
bottom: "-2px",
|
||||||
|
opacity: sparkCount() > 0 ? "1" : "0",
|
||||||
|
transition: "opacity 0.3s"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={SPARKS.slice(0, sparkCount())}>
|
||||||
|
{(s) => (
|
||||||
|
<span
|
||||||
|
class="fan-spark"
|
||||||
|
style={{
|
||||||
|
width: `${s.size}px`,
|
||||||
|
height: `${s.size}px`,
|
||||||
|
"--sx": s.sx,
|
||||||
|
"--sy": s.sy,
|
||||||
|
"--sd": s.sd,
|
||||||
|
"--sdel": s.sdel
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
* One module-card surface shared by every mock panel — the app's
|
||||||
* cardChrome(): rounded 12, hairline stroke, white-on-black fill.
|
* 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<string, string>;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`rounded-xl ${props.class ?? ""}`}
|
class={`rounded-xl ${props.class ?? ""}`}
|
||||||
style={{
|
style={{
|
||||||
background: CARD_FILL,
|
background: CARD_FILL,
|
||||||
"box-shadow": `inset 0 0 0 1px ${CARD_STROKE}`
|
"box-shadow": `inset 0 0 0 1px ${CARD_STROKE}`,
|
||||||
|
...props.style
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="p-3">{props.children}</div>
|
<div class="p-3">{props.children}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user