/* global React, window */ // ============================================================ // PIXEL TUXEDO CAT — voxel-style 3D pixel art, drawn to // (depth layer + offset top layer = extruded "3D pixel" volume). // Canvas keeps the DOM light (1 node vs ~300 rects). // ============================================================ const PX_TOP = { K: "#22221f", W: "#f4f0e7", P: "#e6929d", G: "#8ab35f", H: "#ffffff", D: "#0c0c0d" }; const PX_DEPTH = { K: "#050505", W: "#c7c0b1", P: "#ad6671", G: "#557e3a", H: "#cfcabd", D: "#000" }; function gridFront(eyesClosed) { const W = 16, H = 18; const g = Array.from({ length: H }, () => Array(W).fill(".")); const R = (x0, y0, x1, y1, c) => { for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) if (g[y] && g[y][x] !== undefined) g[y][x] = c; }; const S = (x, y, c) => { if (g[y] && x >= 0 && x < W) g[y][x] = c; }; S(4, 0, "K"); S(11, 0, "K"); R(3, 1, 5, 1, "K"); R(10, 1, 12, 1, "K"); R(3, 1, 12, 8, "K"); S(4, 1, "P"); S(11, 1, "P"); R(5, 6, 10, 8, "W"); if (eyesClosed) { R(5, 5, 6, 5, "D"); R(9, 5, 10, 5, "D"); } else { R(5, 4, 6, 5, "G"); R(9, 4, 10, 5, "G"); S(5, 4, "H"); S(9, 4, "H"); } R(7, 7, 8, 7, "P"); R(4, 9, 11, 9, "K"); R(3, 10, 12, 16, "K"); R(6, 9, 9, 9, "W"); R(6, 10, 9, 13, "W"); R(7, 14, 8, 14, "W"); R(4, 16, 6, 17, "W"); R(9, 16, 11, 17, "W"); R(12, 11, 14, 12, "K"); R(13, 12, 14, 16, "K"); return { g, W, H }; } function gridSide(frame) { const W = 22, H = 13; const g = Array.from({ length: H }, () => Array(W).fill(".")); const R = (x0, y0, x1, y1, c) => { for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) if (g[y] && g[y][x] !== undefined) g[y][x] = c; }; const S = (x, y, c) => { if (g[y] && x >= 0 && x < W) g[y][x] = c; }; R(0, 2, 1, 6, "K"); S(0, 1, "K"); // tail R(2, 3, 15, 8, "K"); // body R(14, 1, 20, 7, "K"); // head S(15, 0, "K"); R(17, 0, 19, 0, "K"); // ears R(15, 5, 19, 7, "W"); R(4, 7, 14, 8, "W"); // chest + belly R(18, 5, 20, 7, "W"); // muzzle R(17, 3, 18, 4, "G"); S(17, 3, "H"); // eye S(20, 5, "P"); // nose // legs — two frames for a trot if (frame === 0) { R(4, 9, 5, 11, "K"); R(12, 9, 13, 12, "K"); R(7, 9, 8, 12, "K"); R(15, 9, 16, 11, "K"); } else { R(4, 9, 5, 12, "K"); R(12, 9, 13, 11, "K"); R(7, 9, 8, 11, "K"); R(15, 9, 16, 12, "K"); } // white socks R(4, 11, 5, 11, "W"); R(7, 11, 8, 11, "W"); R(12, 11, 13, 11, "W"); R(15, 11, 16, 11, "W"); return { g, W, H }; } function paintGrid(ctx, data, scale, withShadow, palTop, palDepth) { palTop = palTop || PX_TOP; palDepth = palDepth || PX_DEPTH; const off = Math.max(1, Math.round(scale * 0.22)); ctx.imageSmoothingEnabled = false; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); if (withShadow) { ctx.save(); ctx.fillStyle = "rgba(20,12,5,.26)"; ctx.beginPath(); ctx.ellipse((data.W / 2) * scale, (data.H + 1.1) * scale, (data.W / 2.3) * scale, 1.1 * scale, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } const draw = (pal, dx, dy) => { for (let y = 0; y < data.H; y++) for (let x = 0; x < data.W; x++) { const c = data.g[y][x]; if (c === ".") continue; ctx.fillStyle = pal[c] || "#000"; ctx.fillRect(Math.round(x * scale + dx), Math.round(y * scale + dy), scale, scale); } }; draw(palDepth, off, off); // depth (bottom-right) draw(palTop, 0, 0); // top } function PixelCanvas({ data, size, scale, pad = 1, shadowPad = 2, palTop, palDepth }) { const ref = React.useRef(null); const dpr = (window.devicePixelRatio || 1); const cw = (data.W + pad) * scale; const ch = (data.H + pad + shadowPad) * scale; React.useEffect(() => { const cv = ref.current; if (!cv) return; const ctx = cv.getContext("2d"); cv.width = cw * dpr; cv.height = ch * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); paintGrid(ctx, data, scale, true, palTop, palDepth); }); return ; } function PixelTuxedoCat({ size = 150, variant = "sit", eyesClosed, blink = true, style = {} }) { const closedDefault = variant === "nap"; const [closed, setClosed] = React.useState(closedDefault); React.useEffect(() => { if (!blink || closedDefault) return; let t; const loop = () => { t = setTimeout(() => { setClosed(true); setTimeout(() => setClosed(false), 150); loop(); }, 2400 + Math.random() * 3600); }; loop(); return () => clearTimeout(t); }, [blink, closedDefault]); const shut = eyesClosed != null ? eyesClosed : (closedDefault || closed); const data = gridFront(shut); const scale = Math.max(4, Math.round(size / data.W)); return (
{variant === "nap" && ( zz )}
); } function PixelTuxedoSide({ size = 150, walking = true, style = {} }) { const [frame, setFrame] = React.useState(0); const data = gridSide(walking ? frame : 0); const scale = Math.max(3, Math.round(size / data.W)); return (
{walking && setFrame((f) => (f ? 0 : 1))} />}
); } // drives the 2-frame walk cycle (paused when not walking via CSS class on ancestor) function WalkFramer({ onTick }) { React.useEffect(() => { let id; const tick = () => { onTick(); }; id = setInterval(tick, 180); return () => clearInterval(id); }, []); return null; } // brown-curly-hair girl (cover companion) const GIRL_TOP = { H: "#6e4a2e", h: "#8c5f3a", S: "#ebb78f", E: "#2a1a10", M: "#b85f4d", T: "#C4603D", W: "#f4f0e7" }; const GIRL_DEPTH = { H: "#49301a", h: "#5e3f25", S: "#c9966f", E: "#000", M: "#8a4536", T: "#9a4528", W: "#cfc9bd" }; function gridGirl() { const W = 14, H = 18; const g = Array.from({ length: H }, () => Array(W).fill(".")); const R = (x0, y0, x1, y1, c) => { for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) if (g[y] && g[y][x] !== undefined) g[y][x] = c; }; const S = (x, y, c) => { if (g[y] && x >= 0 && x < W) g[y][x] = c; }; // curly hair mass R(2, 1, 11, 9, "H"); // curl bumps on top S(3, 0, "H"); S(5, 0, "H"); S(8, 0, "H"); S(10, 0, "H"); S(2, 0, "h"); S(11, 0, "h"); // side curls R(2, 9, 3, 12, "H"); R(10, 9, 11, 12, "H"); S(2, 12, "h"); S(11, 12, "h"); // face R(4, 3, 9, 9, "S"); // fringe R(4, 3, 9, 3, "H"); S(4, 4, "H"); S(9, 4, "H"); // eyes + mouth S(5, 5, "E"); S(8, 5, "E"); R(6, 7, 7, 7, "M"); // neck R(6, 10, 7, 10, "S"); // shirt R(3, 11, 10, 17, "T"); R(5, 11, 8, 11, "W"); // arms R(2, 12, 2, 16, "T"); R(11, 12, 11, 16, "T"); // hair highlights S(3, 2, "h"); S(4, 2, "h"); S(9, 6, "h"); return { g, W, H }; } function PixelGirl({ size = 150, style = {} }) { const data = gridGirl(); const scale = Math.max(4, Math.round(size / data.W)); return (
); } window.PixelTuxedoCat = PixelTuxedoCat; window.PixelTuxedoSide = PixelTuxedoSide; window.PixelGirl = PixelGirl;