const { useState, useEffect, useRef, useMemo } = React;
// ---------- Palettes ----------
const PALETTES = {
ink: { bg: "#0A0500", fg: "#FF5C6E", alt: "#15D528", name: "ink & ember" },
forest: { bg: "#022D07", fg: "#FF001F", alt: "#15D528", name: "forest & flare" },
bone: { bg: "#F2EBDD", fg: "#1A1A1A", alt: "#FF001F", name: "bone & black" },
bruise: { bg: "#0B0820", fg: "#FF4DA6", alt: "#7A4DFF", name: "bruise & bubblegum" },
};
// ---------- Primitive Stamps ----------
// Each stamp is a function that returns SVG given (cx, cy, size, color, alt)
const STAMPS = {
dot: (cx, cy, s, c) => (
),
block: (cx, cy, s, c) => (
),
bar: (cx, cy, s, c) => (
),
arch: (cx, cy, s, c) => (
),
cup: (cx, cy, s, c) => (
),
tri: (cx, cy, s, c) => (
),
triDown: (cx, cy, s, c) => (
),
ring: (cx, cy, s, c) => (
),
cross: (cx, cy, s, c) => (
),
notch: (cx, cy, s, c) => (
),
half: (cx, cy, s, c) => (
),
};
const STAMP_KEYS = Object.keys(STAMPS);
// ---------- Preset creatures ----------
// 6x6 grids — each cell is { stamp, color: 'fg'|'alt' } or null
const blank = () => Array.from({ length: 6 }, () => Array(6).fill(null));
const PRESETS = {
doc: (() => {
const g = blank();
// doc: a tall figure with a cup (hat) and dot eyes
g[0][2] = { stamp: "cup", color: "fg" };
g[0][3] = { stamp: "cup", color: "fg" };
g[1][1] = { stamp: "arch", color: "fg" };
g[1][2] = { stamp: "block", color: "fg" };
g[1][3] = { stamp: "block", color: "fg" };
g[1][4] = { stamp: "arch", color: "fg" };
g[2][2] = { stamp: "dot", color: "alt" };
g[2][3] = { stamp: "dot", color: "alt" };
g[3][1] = { stamp: "block", color: "fg" };
g[3][2] = { stamp: "bar", color: "fg" };
g[3][3] = { stamp: "bar", color: "fg" };
g[3][4] = { stamp: "block", color: "fg" };
g[4][2] = { stamp: "block", color: "fg" };
g[4][3] = { stamp: "block", color: "fg" };
g[5][1] = { stamp: "half", color: "fg" };
g[5][4] = { stamp: "half", color: "fg" };
return g;
})(),
duck: (() => {
const g = blank();
// duck: a squat round body, beak triangle
g[1][3] = { stamp: "arch", color: "alt" };
g[1][4] = { stamp: "block", color: "alt" };
g[2][2] = { stamp: "tri", color: "fg" };
g[2][3] = { stamp: "ring", color: "alt" };
g[2][4] = { stamp: "arch", color: "alt" };
g[3][1] = { stamp: "half", color: "alt" };
g[3][2] = { stamp: "block", color: "alt" };
g[3][3] = { stamp: "block", color: "alt" };
g[3][4] = { stamp: "block", color: "alt" };
g[4][1] = { stamp: "cup", color: "alt" };
g[4][2] = { stamp: "cup", color: "alt" };
g[4][3] = { stamp: "cup", color: "alt" };
g[4][4] = { stamp: "cup", color: "alt" };
g[5][2] = { stamp: "tri", color: "fg" };
g[5][3] = { stamp: "tri", color: "fg" };
return g;
})(),
totem: (() => {
const g = blank();
g[0][2] = { stamp: "tri", color: "fg" };
g[0][3] = { stamp: "tri", color: "fg" };
g[1][2] = { stamp: "ring", color: "fg" };
g[1][3] = { stamp: "ring", color: "fg" };
g[2][1] = { stamp: "bar", color: "fg" };
g[2][2] = { stamp: "cross", color: "alt" };
g[2][3] = { stamp: "cross", color: "alt" };
g[2][4] = { stamp: "bar", color: "fg" };
g[3][2] = { stamp: "block", color: "fg" };
g[3][3] = { stamp: "block", color: "fg" };
g[4][1] = { stamp: "half", color: "alt" };
g[4][2] = { stamp: "notch", color: "fg" };
g[4][3] = { stamp: "notch", color: "fg" };
g[4][4] = { stamp: "half", color: "alt" };
g[5][2] = { stamp: "arch", color: "fg" };
g[5][3] = { stamp: "arch", color: "fg" };
return g;
})(),
bug: (() => {
const g = blank();
g[1][1] = { stamp: "dot", color: "fg" };
g[1][4] = { stamp: "dot", color: "fg" };
g[2][1] = { stamp: "half", color: "alt" };
g[2][2] = { stamp: "ring", color: "fg" };
g[2][3] = { stamp: "ring", color: "fg" };
g[2][4] = { stamp: "half", color: "alt" };
g[3][2] = { stamp: "block", color: "alt" };
g[3][3] = { stamp: "block", color: "alt" };
g[4][1] = { stamp: "tri", color: "fg" };
g[4][2] = { stamp: "cup", color: "alt" };
g[4][3] = { stamp: "cup", color: "alt" };
g[4][4] = { stamp: "tri", color: "fg" };
g[5][2] = { stamp: "dot", color: "fg" };
g[5][3] = { stamp: "dot", color: "fg" };
return g;
})(),
};
// ---------- Renderer for a 6x6 grid ----------
function CreatureSVG({ grid, size = 360, palette, animated = false, t = 0 }) {
const cell = size / 6;
return (
);
}
// ---------- Stamp swatch icon (for palette) ----------
function StampIcon({ stamp, color, bg }) {
return (
);
}
// ---------- Main App ----------
function App() {
const [tweaks, setTweak] = useTweaks(/*EDITMODE-BEGIN*/{
"paletteKey": "forest",
"creatureName": "doc",
"showGuides": true,
"wobble": true
}/*EDITMODE-END*/);
const palette = PALETTES[tweaks.paletteKey] || PALETTES.forest;
const [grid, setGrid] = useState(() => PRESETS.doc.map(r => r.slice()));
const [activeStamp, setActiveStamp] = useState("block");
const [activeColor, setActiveColor] = useState("fg");
const [tool, setTool] = useState("paint"); // paint | erase
const [history, setHistory] = useState([]);
const [t, setT] = useState(0);
const [savedCreatures, setSavedCreatures] = useState([]);
const [name, setName] = useState("doc");
const [shake, setShake] = useState(false);
// animation tick
useEffect(() => {
if (!tweaks.wobble) return;
let raf;
const tick = () => {
setT(v => v + 1);
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [tweaks.wobble]);
const pushHistory = (g) => {
setHistory(h => [...h.slice(-30), g.map(r => r.slice())]);
};
const setCell = (r, c) => {
pushHistory(grid);
setGrid(g => {
const ng = g.map(row => row.slice());
if (tool === "erase") ng[r][c] = null;
else ng[r][c] = { stamp: activeStamp, color: activeColor };
return ng;
});
};
const undo = () => {
setHistory(h => {
if (!h.length) return h;
const last = h[h.length - 1];
setGrid(last);
return h.slice(0, -1);
});
};
const clear = () => {
pushHistory(grid);
setGrid(blank());
};
const loadPreset = (key) => {
pushHistory(grid);
setGrid(PRESETS[key].map(r => r.slice()));
setName(key);
setTweak("creatureName", key);
};
const randomize = () => {
pushHistory(grid);
const ng = blank();
const fillCount = 12 + Math.floor(Math.random() * 14);
for (let i = 0; i < fillCount; i++) {
const r = Math.floor(Math.random() * 6);
const c = Math.floor(Math.random() * 6);
// mirror for symmetry
const stamp = STAMP_KEYS[Math.floor(Math.random() * STAMP_KEYS.length)];
const color = Math.random() < 0.7 ? "fg" : "alt";
ng[r][c] = { stamp, color };
ng[r][5 - c] = { stamp, color };
}
setGrid(ng);
setShake(true);
setTimeout(() => setShake(false), 400);
};
const save = () => {
if (!grid.flat().some(Boolean)) return;
setSavedCreatures(s => [{ id: Date.now(), grid: grid.map(r => r.slice()), name: name || "untitled" }, ...s].slice(0, 12));
};
const deleteSaved = (id) => {
setSavedCreatures(s => s.filter(x => x.id !== id));
};
const exportSVG = () => {
const size = 600;
const cell = size / 6;
let inner = ``;
grid.forEach((row, ry) => row.forEach((c, cx) => {
if (!c) return;
const color = c.color === "alt" ? palette.alt : palette.fg;
const px = cx * cell + cell / 2;
const py = ry * cell + cell / 2;
const s = cell;
const stamps = {
dot: ``,
block: ``,
bar: ``,
arch: ``,
cup: ``,
tri: ``,
triDown: ``,
ring: ``,
cross: ``,
notch: ``,
half: ``,
};
inner += stamps[c.stamp] || "";
}));
const svg = ``;
const blob = new Blob([svg], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${name || "creature"}.svg`;
a.click();
URL.revokeObjectURL(url);
};
const cellCount = grid.flat().filter(Boolean).length;
return (
block party
a creature studio
{Object.entries(PALETTES).map(([k, p]) => (
))}
setTweak("paletteKey", v)}
options={Object.keys(PALETTES).map(k => ({ value: k, label: k }))}
/>
setTweak("showGuides", v)} />
setTweak("wobble", v)} />
{Object.keys(PRESETS).map(k => (
loadPreset(k)}>{k}
))}
);
}
ReactDOM.createRoot(document.getElementById("root")).render();