This commit is contained in:
2026-09-05 10:33:26 -04:00
parent fd68efdb78
commit 17bb43851e
6 changed files with 1679 additions and 45 deletions

View File

@@ -0,0 +1,367 @@
import { For, type JSX } from "solid-js";
/**
* Pixel-art campfire sprite, faithful to the app's CampfireFrames: a 14×14
* grid where the log base never moves and only the flame and sparks animate.
* `state` picks the frame set and cadence — idle simmers, running dances,
* ready exhales, error pulses dead embers.
*/
const FIRE_PALETTE: Record<string, string> = {
o: "#e7873a",
y: "#ffce70",
w: "#ffffff",
b: "#8b5a2b",
d: "#5a3a1b",
s: "#ffd166",
r: "#c43a30",
R: "#f25240",
x: "#ff7864",
m: "rgba(255,255,255,0.5)",
k: "rgba(255,255,255,0.26)",
e: "#a8582a",
E: "#d87636",
B: "#4890fc",
L: "#92c7ff",
C: "#e9f8ff",
S: "#bfe0ff"
};
interface CampfireFrame {
rows: string[];
}
const IDLE_FRAMES: CampfireFrame[] = [
{
rows: [
"..............",
"..............",
"..............",
"..............",
"..............",
"..............",
"..............",
".....oyo......",
"....oyyyo.....",
"..dddeeeeeEd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
},
{
rows: [
"..............",
"..............",
".........k....",
"..............",
"..............",
"..............",
"..............",
"......oyo.....",
"....oyyyo.....",
"..dddEEEEEEd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
}
];
const RUN_FRAMES: CampfireFrame[] = [
{
rows: [
"..............",
"..............",
".....s.yy.....",
".....oyyo.....",
"....oywwyo....",
"...oyywwyyo...",
"...oyywwyyo...",
"...oyywwyyo...",
"....oywwyo....",
"..dddddddddd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
},
{
rows: [
"..............",
".....s........",
".....yy.......",
".....oyyo.....",
"....oywwyo....",
"...oyywwyyo...",
"...oyywwyyo...",
"...oyywwyyo...",
"....oywwyo....",
"..dddddddddd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
}
];
const READY_FRAMES: CampfireFrame[] = [
{
rows: [
"..............",
"..............",
"..............",
"..............",
"..............",
"......yy......",
".....oyyo.....",
"....oyyyyo....",
"....oyyyyo....",
"..dddyyyyyyd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
},
{
rows: [
"..............",
"..............",
"..............",
"..............",
"........m.....",
".....yy.......",
".....oyyo.....",
"....oyyyyo....",
"....oyyyyo....",
"..dddyyyyyyd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
}
];
/* App CampfireFrames.ques0-5: cool blue flame, tip dips, sparks spit. */
const QUES_FRAMES: CampfireFrame[] = [
{
rows: [
"..............",
"..............",
".....S.LL.....",
".....BLLB.....",
"....BLCCLB....",
"...BLLCCLLB...",
"...BLLCCLLB...",
"...BLLCCLLB...",
"....BLCCLB....",
"..dddccccccd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
},
{
rows: [
"..............",
".....S........",
".....LL.......",
".....BLLB.....",
"....BLCCLB....",
"...BLLCCLLB...",
"...BLLCCLLB...",
"...BLLCCLLB...",
"....BLCCLB....",
"..dddccccccd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
},
{
rows: [
"..............",
".....S........",
"......LL......",
".....BLLB.....",
"....BLCCLB....",
"...BLLCCLLB...",
"...BLLCCLLB...",
"...BLLCCLLB...",
"....BLCCLB....",
"..dddccccccd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
},
{
rows: [
".....S........",
"..............",
"......LL......",
".....BLLB.....",
"....BLCCLB....",
"...BLLCCLLB...",
"...BLLCCLLB...",
"...BLLCCLLB...",
"....BLCCLB....",
"..dddccccccd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
},
{
rows: [
".....S........",
"..............",
".......LL.....",
".....BLLB.....",
"....BLCCLB....",
"...BLLCCLLB...",
"...BLLCCLLB...",
"...BLLCCLLB...",
"....BLCCLB....",
"..dddccccccd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
},
{
rows: [
"........S.....",
"..............",
".......LL.....",
".....BLLB.....",
"....BLCCLB....",
"...BLLCCLLB...",
"...BLLCCLLB...",
"...BLLCCLLB...",
"....BLCCLB....",
"..dddccccccd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
}
];
const ERROR_FRAMES: CampfireFrame[] = [
{
rows: [
"..............",
"..............",
"..............",
"..............",
"..............",
"..............",
"..............",
"..............",
"..............",
"..dddrrrrrrd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
},
{
rows: [
"........x.....",
"..............",
".....x........",
"..............",
"..............",
"..............",
"..............",
"..............",
"..............",
"..dddRRRRRRd..",
"..dbbbbbbbbd..",
"..............",
"..............",
".............."
]
}
];
export type CampfireState = "idle" | "running" | "ready" | "question" | "error";
const STATE_FRAMES: Record<CampfireState, CampfireFrame[]> = {
idle: IDLE_FRAMES,
running: RUN_FRAMES,
ready: READY_FRAMES,
question: QUES_FRAMES,
error: ERROR_FRAMES
};
const STATE_FPS: Record<CampfireState, number> = {
idle: 2,
running: 9,
ready: 4,
question: 4.5,
error: 4
};
export function Campfire(props: { state: CampfireState; pixel?: number }) {
const pixel = () => props.pixel ?? 3;
return (
<div
class="animate-campfire grid"
style={{
"grid-template-columns": `repeat(14, ${pixel()}px)`,
/* One full cycle: every frame exactly one frame-interval long. */
"--campfire-duration": `${(1000 * STATE_FRAMES[props.state].length) / STATE_FPS[props.state] / 1000}s`
}}
>
{/* Slot phase lives in the keyframes per layer (campfire-slot-N-x);
delay-offset layers blank a wrap frame under WebKit. */}
<For each={STATE_FRAMES[props.state]}>
{(frame, i) => {
const n = STATE_FRAMES[props.state].length;
const phase = String.fromCharCode(97 + (i() % 26));
return (
<div
class="campfire-frame col-span-full row-start-1"
style={{ "--slot-kf": `campfire-slot-${n}-${phase}` }}
>
<For each={frame.rows}>
{(row) => (
<div class="flex" style={{ height: `${pixel()}px` }}>
<For each={row.split("")}>
{(ch) => (
<span
class="inline-block"
style={{
width: `${pixel()}px`,
height: `${pixel()}px`,
background:
ch === "." ? "transparent" : FIRE_PALETTE[ch]
}}
/>
)}
</For>
</div>
)}
</For>
</div>
);
}}
</For>
</div>
);
}