creator docs · reference · generated
Canvas theme colours — the look's tokens as colours a canvas can paint
This is the same document the Vibes code generator reads when it
builds apps — rendered from
prompts/pkg/llms/canvas-theme-colors.md on every deploy, so it
can't drift from what the platform actually does. Read it from your terminal any
time with npx vibes-diy skills.
Canvas pixels read the app's look from lib/look.js: resolve the theme tokens to concrete colours at mount, and repaint when the owner restyles.
The platform injects the app's colours as CSS variables (--background, --surface, --primary,
--secondary, --accent, --text-primary, --text-secondary, --border), and everything
outside a canvas follows them through Tailwind utilities. A 2D canvas, a p5 sketch, a Three.js
scene, a cellular world or a Blockly theme needs real colours instead, and a literal like
"#00e5ff" is the one part of a themed vibe its owner can never restyle. One module turns tokens
into colours every one of those can paint with; a literal is only the fallback for an app with no
theme injected.
lib/look.js
js// look.js — the look's tokens as colours a 2D canvas or a Three.js scene can paint with.
// A computed colour can come back as oklch(), color-mix() or a translucent rgba(); filling one pixel
// over `under` turns any of them into the opaque hex the eye sees, which THREE.Color, p5's color()
// and ctx.fillStyle all read.
export function cssColor(host, token, fallback, under = "#ffffff") {
const probe = document.createElement("span");
probe.style.display = "none";
probe.style.color = `var(${token}, ${fallback})`;
host.appendChild(probe);
const value = getComputedStyle(probe).color;
probe.remove();
const ctx = document.createElement("canvas").getContext("2d");
ctx.fillStyle = under;
ctx.fillRect(0, 0, 1, 1);
ctx.fillStyle = fallback;
ctx.fillStyle = value; // a value the canvas cannot parse leaves the fallback in place
ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
return `#${[r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("")}`;
}
// A look may give its surface or border as a tint over the background (a dark look's surface
// can be rgba(255,255,255,0.04)), so every other token lands on the resolved backdrop.
export function readLook(host) {
const backdrop = cssColor(host, "--background", "#f7f3ea");
return {
backdrop,
surface: cssColor(host, "--surface", "#ffffff", backdrop),
lead: cssColor(host, "--primary", "#5b4bff", backdrop),
accent: cssColor(host, "--accent", "#ff7a59", backdrop),
ink: cssColor(host, "--text-primary", "#1a1325", backdrop),
line: cssColor(host, "--border", "#d9d2c3", backdrop),
};
}
// The Style tab restyles a running app by rewriting a <style> in the document head, with no
// remount, so the canvas is told: onChange gets a fresh look and repaints without a restart.
export function watchLook(host, onChange) {
const observer = new MutationObserver(() => onChange(readLook(host)));
observer.observe(document.head, { childList: true, subtree: true, characterData: true });
return () => observer.disconnect();
}
Using it
- Read inside the code that builds the drawing, never at module scope: a scene's
useEffect, a sketch'ssetup, a world's mount. A rebuilt instance then picks up the current theme. - Call
watchLookin that same effect and return its cleanup, and keep every colour the drawing uses in onepaint(look)it runs at mount and on each change, so a restyle recolours the canvas in place without restarting the game or losing its state. cssColoralone serves a single token (cssColor(host, "--accent", "#00e5ff")); pass the resolved backdrop as the fourth argument for a token that may be a translucent tint.- The hex is opaque. Keep alpha in the drawing library's hands (p5's
setAlpha, a Three.js material'sopacity,ctx.globalAlpha) rather than appending digits to the string.