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>
135 lines
4.5 KiB
HTML
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 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>
|