vibes.diy/docs Home Discord
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's setup, a world's mount. A rebuilt instance then picks up the current theme.
  • Call watchLook in that same effect and return its cleanup, and keep every colour the drawing uses in one paint(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.
  • cssColor alone 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's opacity, ctx.globalAlpha) rather than appending digits to the string.