Files
Gabriel Vidal 9a2627f1ea feat(ai-agent): /avatars/hemp-henry/ animated sprite collection page
First installed clips (idle, walk, happy) + manifest and a static
collection page playing each sheet via CSS steps(). Backend SPA
catch-all now serves a subdirectory's index.html, and the PWA service
worker lets /avatars/* navigations through to the network.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 02:34:32 +02:00

135 lines
4.5 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hemp Henry — avatar sprite collection</title>
<meta name="description" content="Animated sprite-sheet clips for the ai-agent avatar." />
<link rel="icon" href="../../icon.svg" />
<style>
:root {
--bg: #0a0a0a;
--card: #141414;
--border: #262626;
--text: #e5e5e5;
--dim: #737373;
--accent: #a78bfa;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font: 15px/1.5 ui-sans-serif, system-ui, sans-serif;
padding: 2rem 1.25rem 4rem;
}
header {
max-width: 960px;
margin: 0 auto 2rem;
display: flex;
align-items: center;
gap: 1.25rem;
}
header img {
width: 84px;
height: 84px;
object-fit: cover;
border-radius: 16px;
border: 1px solid var(--border);
}
h1 { margin: 0; font-size: 1.4rem; }
header p { margin: 0.2rem 0 0; color: var(--dim); font-size: 0.9rem; }
header a { color: var(--accent); text-decoration: none; }
.grid {
max-width: 960px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6rem;
}
.stage {
width: 160px;
height: 160px;
display: grid;
place-items: end center;
}
.sprite {
width: 160px;
height: 160px;
background-repeat: no-repeat;
background-size: auto 160px;
animation: play var(--dur) steps(var(--n)) infinite;
}
.card.paused .sprite { animation-play-state: paused; }
@keyframes play { to { background-position-x: var(--end); } }
.name { font-weight: 600; text-transform: capitalize; }
.meta { color: var(--dim); font-size: 0.78rem; }
.kind {
font-size: 0.68rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--accent);
border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
border-radius: 999px;
padding: 0.1rem 0.55rem;
}
.links { color: var(--dim); font-size: 0.78rem; }
.links a { color: var(--dim); }
.links a:hover { color: var(--text); }
.empty { max-width: 960px; margin: 3rem auto; color: var(--dim); text-align: center; }
@media (prefers-reduced-motion: reduce) { .sprite { animation: none; } }
</style>
</head>
<body>
<header>
<img src="reference.jpg" alt="Hemp Henry reference" />
<div>
<h1>Hemp Henry</h1>
<p>The ai-agent avatar — every animation clip, straight off its sprite sheet.</p>
<p><a href="manifest.json">manifest.json</a> · 4 frames per clip, generated by
nano&nbsp;banana from one reference, background removed by the brain
<code>bgremove</code> model.</p>
</div>
</header>
<main class="grid" id="grid"></main>
<p class="empty" id="empty" hidden>No sheets published yet — run
<code>scripts/gen-avatar-sprites.sh install</code>.</p>
<script>
const EMOTES = new Set(["happy", "investigating", "celebrate", "thinking", "oops"]);
fetch("manifest.json")
.then((r) => (r.ok ? r.json() : {}))
.then((manifest) => {
const grid = document.getElementById("grid");
const clips = Object.entries(manifest);
if (!clips.length) document.getElementById("empty").hidden = false;
for (const [clip, meta] of clips) {
const n = meta.frames || 4;
const fps = meta.fps || 6;
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `
<div class="stage"><div class="sprite" style="
background-image:url('${meta.src || clip + ".png"}');
--n:${n}; --dur:${(n / fps).toFixed(3)}s; --end:-${n * 160}px;"></div></div>
<div class="name">${clip}</div>
<span class="kind">${EMOTES.has(clip) ? "emote" : "action"}</span>
<div class="meta">${n} frames · ${fps} fps${meta.loop === false ? " · one-shot" : ""}</div>
<div class="links"><a href="${meta.src || clip + ".png"}">sheet</a></div>`;
card.addEventListener("click", () => card.classList.toggle("paused"));
grid.appendChild(card);
}
});
</script>
</body>
</html>