Storybook — Coloring / Paint Canvas
A friendly coloring activity for a children's storybook app, built entirely from inline SVG and vanilla JavaScript. A chunky crayon palette drives a flood-fill canvas where tapping or dragging across a hand-drawn fox scene paints each region with the active color, complete with a gentle pop animation. It ships with a ten-crayon box, a rainbow mode that cycles hues on every fill, undo and clear controls, an active-crayon readout, and a save-to-PNG export rendered through a canvas — all keyboard friendly and reduced-motion aware.
MCP
Code
:root {
--bg: #fff8ef;
--card: #ffffff;
--ink: #2c2350;
--ink-soft: #6b6390;
--primary: #ff8a3d;
--secondary: #5ec5d6;
--accent: #ffd23f;
--pink: #ff6f9c;
--green: #7bd389;
--line: #2c2350;
--r: 22px;
--r-lg: 28px;
--shadow: 0 10px 0 rgba(44, 35, 80, 0.08), 0 16px 30px rgba(44, 35, 80, 0.12);
--shadow-sm: 0 5px 0 rgba(44, 35, 80, 0.1);
--font-head: "Baloo 2", system-ui, sans-serif;
--font-body: "Nunito", system-ui, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--font-body);
line-height: 1.5;
color: var(--ink);
background: radial-gradient(circle at 12% 8%, #fff0dc 0, transparent 45%),
radial-gradient(circle at 92% 4%, #e3f6f9 0, transparent 42%),
radial-gradient(circle at 80% 96%, #ffe7ef 0, transparent 48%), var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.page {
max-width: 980px;
margin: 0 auto;
padding: clamp(18px, 4vw, 40px);
}
/* ---------- Masthead ---------- */
.masthead {
text-align: center;
margin-bottom: clamp(18px, 3vw, 30px);
}
.kicker {
display: inline-block;
margin: 0 0 10px;
padding: 6px 16px;
font-family: var(--font-head);
font-weight: 700;
font-size: 0.82rem;
letter-spacing: 0.04em;
color: var(--ink);
background: var(--accent);
border: 3px solid var(--line);
border-radius: 999px;
box-shadow: var(--shadow-sm);
text-transform: uppercase;
}
.title {
margin: 0;
font-family: var(--font-head);
font-weight: 800;
font-size: clamp(2.1rem, 7vw, 3.4rem);
line-height: 1.05;
color: var(--ink);
}
.title span {
color: var(--primary);
text-shadow: 3px 3px 0 var(--accent);
}
.lede {
max-width: 46ch;
margin: 12px auto 0;
color: var(--ink-soft);
font-weight: 600;
}
/* ---------- Studio layout ---------- */
.studio {
display: grid;
grid-template-columns: 168px 1fr;
gap: clamp(14px, 2.4vw, 24px);
align-items: start;
}
/* ---------- Palette ---------- */
.palette {
position: sticky;
top: 16px;
background: var(--card);
border: 4px solid var(--line);
border-radius: var(--r-lg);
padding: 16px 14px;
box-shadow: var(--shadow);
}
.crayons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.crayon {
position: relative;
height: 60px;
border: 3px solid var(--line);
border-radius: 14px;
cursor: pointer;
padding: 0;
background: var(--crayon, #fff);
box-shadow: var(--shadow-sm);
transition: transform 0.14s ease, box-shadow 0.14s ease;
overflow: hidden;
}
.crayon::before {
/* crayon tip */
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 16px;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.55),
rgba(255, 255, 255, 0)
);
clip-path: polygon(20% 100%, 80% 100%, 50% 0);
}
.crayon::after {
/* paper wrapper line */
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 18px;
height: 3px;
background: rgba(44, 35, 80, 0.35);
}
.crayon:hover {
transform: translateY(-3px) rotate(-2deg);
}
.crayon:focus-visible {
outline: 4px solid var(--secondary);
outline-offset: 3px;
}
.crayon[aria-checked="true"] {
transform: translateY(-6px) scale(1.04);
box-shadow: 0 9px 0 rgba(44, 35, 80, 0.16), 0 0 0 4px var(--accent);
}
.crayon[aria-checked="true"]::before {
height: 22px;
}
.crayon .check {
position: absolute;
inset: auto 4px 4px auto;
width: 22px;
height: 22px;
display: grid;
place-items: center;
border-radius: 50%;
background: #fff;
border: 2px solid var(--line);
font-weight: 800;
font-size: 0.8rem;
opacity: 0;
transform: scale(0.4);
transition: opacity 0.12s ease, transform 0.16s ease;
}
.crayon[aria-checked="true"] .check {
opacity: 1;
transform: scale(1);
}
.rainbow-btn {
display: flex;
align-items: center;
gap: 9px;
width: 100%;
margin-top: 14px;
padding: 11px 12px;
font-family: var(--font-head);
font-weight: 700;
font-size: 0.92rem;
color: var(--ink);
background: var(--card);
border: 3px solid var(--line);
border-radius: 14px;
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: transform 0.14s ease, background 0.14s ease;
}
.rainbow-btn:hover {
transform: translateY(-2px);
}
.rainbow-btn:focus-visible {
outline: 4px solid var(--secondary);
outline-offset: 3px;
}
.rainbow-btn[aria-pressed="true"] {
background: linear-gradient(
90deg,
#ff6f9c,
#ff8a3d,
#ffd23f,
#7bd389,
#5ec5d6
);
color: #fff;
text-shadow: 0 1px 2px rgba(44, 35, 80, 0.5);
}
.rainbow-swatch {
width: 22px;
height: 22px;
flex: none;
border-radius: 50%;
border: 2px solid var(--line);
background: conic-gradient(
#ff6f9c,
#ff8a3d,
#ffd23f,
#7bd389,
#5ec5d6,
#a98bff,
#ff6f9c
);
}
/* ---------- Canvas ---------- */
.canvas-wrap {
display: flex;
flex-direction: column;
gap: 14px;
}
.canvas-frame {
background: #fffdf8;
border: 4px solid var(--line);
border-radius: var(--r-lg);
padding: 12px;
box-shadow: var(--shadow);
}
.art {
display: block;
width: 100%;
height: auto;
border-radius: var(--r);
touch-action: none;
}
.fillable {
cursor: pointer;
transition: fill 0.18s ease;
}
.fillable:hover {
filter: brightness(0.97) saturate(1.05);
}
.fillable:focus-visible {
outline: 3px dashed var(--secondary);
outline-offset: 2px;
}
.ink {
fill: none;
stroke: var(--line);
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none;
}
.ink .dot {
fill: var(--line);
stroke: none;
}
/* pop animation when a region is filled */
.art .pop {
animation: pop 0.32s ease;
transform-box: fill-box;
transform-origin: center;
}
@keyframes pop {
0% {
transform: scale(1);
}
45% {
transform: scale(1.06);
}
100% {
transform: scale(1);
}
}
/* ---------- Canvas tools ---------- */
.canvas-tools {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.tool {
display: inline-flex;
align-items: center;
gap: 7px;
min-height: 48px;
padding: 0 18px;
font-family: var(--font-head);
font-weight: 700;
font-size: 0.96rem;
color: var(--ink);
background: var(--card);
border: 3px solid var(--line);
border-radius: 999px;
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: transform 0.14s ease, background 0.14s ease;
}
.tool span[aria-hidden] {
font-size: 1.05rem;
}
.tool:hover {
transform: translateY(-2px);
}
.tool:active {
transform: translateY(1px);
}
.tool:focus-visible {
outline: 4px solid var(--secondary);
outline-offset: 3px;
}
.tool--primary {
background: var(--green);
color: var(--ink);
}
.active-crayon {
display: inline-flex;
align-items: center;
gap: 9px;
margin-left: auto;
padding: 8px 16px;
font-weight: 800;
font-size: 0.9rem;
background: #fff;
border: 3px solid var(--line);
border-radius: 999px;
box-shadow: var(--shadow-sm);
}
.active-dot {
width: 20px;
height: 20px;
flex: none;
border-radius: 50%;
border: 2px solid var(--line);
background: var(--primary);
}
/* ---------- Footer ---------- */
.foot {
margin-top: 28px;
text-align: center;
color: var(--ink-soft);
font-weight: 600;
font-size: 0.88rem;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 24px);
padding: 12px 22px;
font-family: var(--font-head);
font-weight: 700;
color: #fff;
background: var(--ink);
border: 3px solid #fff;
border-radius: 999px;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 720px) {
.studio {
grid-template-columns: 1fr;
}
.palette {
position: static;
}
.crayons {
grid-template-columns: repeat(4, 1fr);
}
.rainbow-btn {
justify-content: center;
}
}
@media (max-width: 420px) {
.crayons {
grid-template-columns: repeat(3, 1fr);
}
.active-crayon {
margin-left: 0;
width: 100%;
justify-content: center;
}
.tool {
flex: 1 1 auto;
justify-content: center;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
// ---- Crayon set (soft storybook palette) ----
var CRAYONS = [
{ name: "Pumpkin orange", color: "#ff8a3d" },
{ name: "Bubblegum pink", color: "#ff6f9c" },
{ name: "Sunny yellow", color: "#ffd23f" },
{ name: "Leafy green", color: "#7bd389" },
{ name: "Sky blue", color: "#5ec5d6" },
{ name: "Grape purple", color: "#a98bff" },
{ name: "Cherry red", color: "#ff5a5a" },
{ name: "Chocolate brown", color: "#a4673a" },
{ name: "Snowy white", color: "#ffffff" },
{ name: "Storm gray", color: "#9aa0b5" },
];
var RAINBOW = [
"#ff6f9c",
"#ff8a3d",
"#ffd23f",
"#7bd389",
"#5ec5d6",
"#a98bff",
];
// ---- DOM ----
var crayonsEl = document.getElementById("crayons");
var rainbowBtn = document.getElementById("rainbowBtn");
var art = document.getElementById("art");
var undoBtn = document.getElementById("undoBtn");
var clearBtn = document.getElementById("clearBtn");
var saveBtn = document.getElementById("saveBtn");
var activeDot = document.getElementById("activeDot");
var activeName = document.getElementById("activeName");
var toastEl = document.getElementById("toast");
var regions = Array.prototype.slice.call(
art.querySelectorAll(".fillable")
);
// ---- State ----
var activeIndex = 0;
var rainbowOn = false;
var rainbowStep = 0;
// history of { region, prevFill } so undo restores exactly
var history = [];
var BLANK = "#ffffff";
// ---- Toast helper ----
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 1700);
}
// ---- Build the crayon palette ----
CRAYONS.forEach(function (cr, i) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "crayon";
btn.style.setProperty("--crayon", cr.color);
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", i === 0 ? "true" : "false");
btn.setAttribute("aria-label", cr.name);
btn.title = cr.name;
btn.dataset.index = String(i);
btn.tabIndex = i === 0 ? 0 : -1;
var check = document.createElement("span");
check.className = "check";
check.setAttribute("aria-hidden", "true");
check.textContent = "✓";
btn.appendChild(check);
crayonsEl.appendChild(btn);
});
var crayonBtns = Array.prototype.slice.call(
crayonsEl.querySelectorAll(".crayon")
);
function selectCrayon(i, focusIt) {
activeIndex = i;
rainbowOn = false;
rainbowBtn.setAttribute("aria-pressed", "false");
crayonBtns.forEach(function (b, idx) {
var on = idx === i;
b.setAttribute("aria-checked", on ? "true" : "false");
b.tabIndex = on ? 0 : -1;
});
var cr = CRAYONS[i];
activeDot.style.background = cr.color;
activeName.textContent = cr.name;
if (focusIt) crayonBtns[i].focus();
}
// Palette: click + keyboard (radiogroup arrow navigation)
crayonsEl.addEventListener("click", function (e) {
var btn = e.target.closest(".crayon");
if (!btn) return;
selectCrayon(Number(btn.dataset.index), false);
});
crayonsEl.addEventListener("keydown", function (e) {
var idx = activeIndex;
var next = idx;
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
next = (idx + 1) % CRAYONS.length;
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
next = (idx - 1 + CRAYONS.length) % CRAYONS.length;
} else if (e.key === "Home") {
next = 0;
} else if (e.key === "End") {
next = CRAYONS.length - 1;
} else {
return;
}
e.preventDefault();
selectCrayon(next, true);
});
// ---- Rainbow mode ----
rainbowBtn.addEventListener("click", function () {
rainbowOn = !rainbowOn;
rainbowBtn.setAttribute("aria-pressed", rainbowOn ? "true" : "false");
if (rainbowOn) {
crayonBtns.forEach(function (b) {
b.setAttribute("aria-checked", "false");
});
activeName.textContent = "Rainbow!";
activeDot.style.background =
"conic-gradient(#ff6f9c,#ff8a3d,#ffd23f,#7bd389,#5ec5d6,#a98bff,#ff6f9c)";
toast("🌈 Rainbow mode on — every fill cycles colors");
} else {
selectCrayon(activeIndex, false);
}
});
// ---- Which color to apply right now ----
function pickColor() {
if (rainbowOn) {
var c = RAINBOW[rainbowStep % RAINBOW.length];
rainbowStep++;
return c;
}
return CRAYONS[activeIndex].color;
}
// ---- Fill a region ----
function fillRegion(region) {
if (!region || !region.classList.contains("fillable")) return;
var color = pickColor();
var prev = region.getAttribute("fill") || BLANK;
if (prev === color) return; // nothing to change
history.push({ region: region, prev: prev });
if (history.length > 60) history.shift();
region.setAttribute("fill", color);
// pop animation
region.classList.remove("pop");
// force reflow so the animation can restart
void region.getBoundingClientRect();
region.classList.add("pop");
}
// ---- Pointer painting (click + drag flood by region) ----
var painting = false;
var lastRegion = null;
function regionFromEvent(e) {
var t = e.target;
if (t && t.classList && t.classList.contains("fillable")) return t;
// when dragging, target may be the SVG itself — resolve via point
var x = e.clientX,
y = e.clientY;
if (x == null) return null;
var el = document.elementFromPoint(x, y);
if (el && el.classList && el.classList.contains("fillable")) return el;
return null;
}
art.addEventListener("pointerdown", function (e) {
var region = regionFromEvent(e);
if (!region) return;
painting = true;
lastRegion = region;
fillRegion(region);
if (art.setPointerCapture) {
try {
art.setPointerCapture(e.pointerId);
} catch (err) {
/* ignore */
}
}
});
art.addEventListener("pointermove", function (e) {
if (!painting) return;
var region = regionFromEvent(e);
if (region && region !== lastRegion) {
lastRegion = region;
fillRegion(region);
}
});
function stopPaint() {
painting = false;
lastRegion = null;
}
art.addEventListener("pointerup", stopPaint);
art.addEventListener("pointercancel", stopPaint);
window.addEventListener("blur", stopPaint);
// ---- Keyboard fill on focused region (a11y) ----
regions.forEach(function (r) {
r.setAttribute("tabindex", "0");
r.setAttribute("role", "button");
var label = r.getAttribute("data-name") || "region";
r.setAttribute("aria-label", "Color the " + label);
});
art.addEventListener("keydown", function (e) {
if (e.key !== "Enter" && e.key !== " ") return;
var region = e.target;
if (region && region.classList.contains("fillable")) {
e.preventDefault();
fillRegion(region);
}
});
// ---- Undo ----
undoBtn.addEventListener("click", function () {
var last = history.pop();
if (!last) {
toast("Nothing to undo yet");
return;
}
last.region.setAttribute("fill", last.prev);
if (rainbowOn && rainbowStep > 0) rainbowStep--;
toast("Undone");
});
// ---- Clear ----
clearBtn.addEventListener("click", function () {
regions.forEach(function (r) {
r.setAttribute("fill", BLANK);
});
history = [];
rainbowStep = 0;
toast("Cleared — fresh page!");
});
// ---- Save as PNG ----
saveBtn.addEventListener("click", function () {
try {
var clone = art.cloneNode(true);
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
// inline a white backdrop so saved PNG isn't transparent
var serialized = new XMLSerializer().serializeToString(clone);
var svgData =
'<?xml version="1.0" encoding="UTF-8"?>' + serialized;
var blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
var url = URL.createObjectURL(blob);
var img = new Image();
img.onload = function () {
var scale = 2;
var canvas = document.createElement("canvas");
canvas.width = 480 * scale;
canvas.height = 360 * scale;
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
URL.revokeObjectURL(url);
canvas.toBlob(function (pngBlob) {
if (!pngBlob) {
toast("Couldn't save here — try another browser");
return;
}
var a = document.createElement("a");
a.href = URL.createObjectURL(pngBlob);
a.download = "my-coloring-page.png";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () {
URL.revokeObjectURL(a.href);
}, 1000);
toast("🖼️ Saved your masterpiece!");
}, "image/png");
};
img.onerror = function () {
URL.revokeObjectURL(url);
toast("Couldn't save here — try another browser");
};
img.src = url;
} catch (err) {
toast("Couldn't save here — try another browser");
}
});
// init
selectCrayon(0, false);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Storybook — Coloring / Paint Canvas</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700;800&family=Nunito:wght@400;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<p class="kicker">Tinkertown Story Studio</p>
<h1 class="title">Coloring <span>Canvas</span></h1>
<p class="lede">
Tap a region to fill it with your crayon. Drag to swoosh color across
the picture. Try rainbow mode for a surprise!
</p>
</header>
<main class="studio" aria-label="Coloring activity">
<!-- Crayon palette -->
<section class="palette" aria-label="Crayon palette">
<h2 class="visually-hidden">Pick a crayon</h2>
<div
class="crayons"
id="crayons"
role="radiogroup"
aria-label="Choose a color"
></div>
<button
type="button"
class="rainbow-btn"
id="rainbowBtn"
aria-pressed="false"
>
<span class="rainbow-swatch" aria-hidden="true"></span>
<span class="rainbow-label">Rainbow mode</span>
</button>
</section>
<!-- Canvas -->
<section class="canvas-wrap" aria-label="Drawing">
<div class="canvas-frame">
<svg
id="art"
class="art"
viewBox="0 0 480 360"
role="img"
aria-label="A friendly fox waving from a hill under the sun, ready to be colored"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Sky -->
<rect class="fillable" data-name="sky" x="0" y="0" width="480" height="360" fill="#fff" />
<!-- Sun -->
<circle class="fillable" data-name="sun" cx="408" cy="64" r="40" fill="#fff" />
<g class="ink" aria-hidden="true">
<line x1="408" y1="6" x2="408" y2="-2" />
<line x1="466" y1="64" x2="474" y2="64" />
<line x1="450" y1="22" x2="456" y2="16" />
<line x1="366" y1="22" x2="360" y2="16" />
<line x1="450" y1="106" x2="456" y2="112" />
</g>
<!-- Hill -->
<path
class="fillable"
data-name="hill"
d="M0 268 Q 240 196 480 268 L 480 360 L 0 360 Z"
fill="#fff"
/>
<!-- Bush -->
<path
class="fillable"
data-name="bush"
d="M40 278 q -8 -34 26 -34 q 6 -26 36 -18 q 26 -16 42 12 q 30 4 18 34 q -8 16 -30 12 l -76 0 q -22 2 -16 -18 Z"
fill="#fff"
/>
<!-- Fox body -->
<path
class="fillable"
data-name="body"
d="M196 312 q -26 -2 -28 -40 q -4 -50 38 -64 q 38 -12 70 12 q 36 26 22 78 q -6 18 -28 16 Z"
fill="#fff"
/>
<!-- Fox belly -->
<path
class="fillable"
data-name="belly"
d="M210 308 q -8 -28 14 -52 q 18 -16 38 0 q 18 22 8 52 q -30 10 -60 0 Z"
fill="#fff"
/>
<!-- Fox head -->
<circle class="fillable" data-name="head" cx="242" cy="176" r="58" fill="#fff" />
<!-- Snout -->
<path
class="fillable"
data-name="snout"
d="M214 188 q 28 26 56 0 q -2 30 -28 30 q -26 0 -28 -30 Z"
fill="#fff"
/>
<!-- Ears -->
<path class="fillable" data-name="earL" d="M196 128 L 178 86 L 222 116 Z" fill="#fff" />
<path class="fillable" data-name="earR" d="M288 128 L 306 86 L 262 116 Z" fill="#fff" />
<!-- Tail -->
<path
class="fillable"
data-name="tail"
d="M300 296 q 56 -8 68 -56 q 16 28 -4 60 q -22 34 -64 26 Z"
fill="#fff"
/>
<!-- Ink outlines on top -->
<g class="ink" aria-hidden="true">
<path d="M0 268 Q 240 196 480 268" />
<path d="M40 278 q -8 -34 26 -34 q 6 -26 36 -18 q 26 -16 42 12 q 30 4 18 34 q -8 16 -30 12 l -76 0 q -22 2 -16 -18 Z" />
<path d="M196 312 q -26 -2 -28 -40 q -4 -50 38 -64 q 38 -12 70 12 q 36 26 22 78 q -6 18 -28 16 Z" />
<path d="M210 308 q -8 -28 14 -52 q 18 -16 38 0 q 18 22 8 52" />
<circle cx="242" cy="176" r="58" />
<path d="M214 188 q 28 26 56 0 q -2 30 -28 30 q -26 0 -28 -30 Z" />
<path d="M196 128 L 178 86 L 222 116 Z" />
<path d="M288 128 L 306 86 L 262 116 Z" />
<path d="M300 296 q 56 -8 68 -56 q 16 28 -4 60 q -22 34 -64 26 Z" />
<circle cx="408" cy="64" r="40" />
<!-- Face details (not fillable) -->
<circle class="dot" cx="222" cy="168" r="6" />
<circle class="dot" cx="262" cy="168" r="6" />
<circle class="dot" cx="242" cy="200" r="7" />
<path d="M242 207 q 0 12 -12 14" />
<path d="M242 207 q 0 12 12 14" />
</g>
</svg>
</div>
<div class="canvas-tools" role="toolbar" aria-label="Canvas tools">
<button type="button" class="tool" id="undoBtn">
<span aria-hidden="true">↶</span> Undo
</button>
<button type="button" class="tool" id="clearBtn">
<span aria-hidden="true">✕</span> Clear
</button>
<button type="button" class="tool tool--primary" id="saveBtn">
<span aria-hidden="true">⬇</span> Save picture
</button>
<span class="active-crayon" aria-live="polite">
<span class="active-dot" id="activeDot"></span>
<span id="activeName">Pumpkin orange</span>
</span>
</div>
</section>
</main>
<footer class="foot">
<p>
Illustrative kids' UI only — fictional stories, characters, and audio.
</p>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Coloring / Paint Canvas
A no-mess coloring book page rendered with a single inline SVG: a waving fox on a hill beneath the sun, drawn as fillable regions sitting under a layer of thick ink outlines. Coloring is flood-fill by region rather than freehand — tap any shape to flood it with the active crayon, or hold and drag the pointer across the picture to splash color over every region you sweep. Each fill lands with a soft pop, and the face details and outlines stay crisp on top because they are not paintable.
A chunky two-up crayon box offers ten storybook colors and behaves as a proper radiogroup: click a crayon or arrow-key between them, and the lifted, ringed crayon plus the live active-crayon chip below the canvas always show what you are about to paint with. The rainbow button flips into a special mode where every fill steps through the next color of the spectrum, so a single drag leaves a banded streak.
Undo walks back one fill at a time by restoring each region’s previous color, Clear resets the whole page to blank, and Save picture rasterizes the SVG onto a 2x canvas and downloads it as a PNG. The whole thing is keyboard usable — focus a region and press Enter or Space to fill it — respects reduced-motion preferences, and collapses from a sidebar palette to a stacked, full-width layout on small screens.
Illustrative kids’ UI only — fictional stories, characters, and audio.