9
Additive vs Subtractive Color Mixing
Drag any circle to move it
idle
106 lines · vanilla
view source
const ADD = ["#ff0000", "#00ff00", "#0000ff"];
const SUB = ["#00ffff", "#ff00ff", "#ffff00"];
const PAIRS = [
[[0, 1, "R+G = YELLOW"], [1, 2, "G+B = CYAN"], [0, 2, "R+B = MAGENTA"]],
[[0, 1, "C+M = BLUE"], [1, 2, "M+Y = RED"], [0, 2, "C+Y = GREEN"]],
];
const TRIPLE = ["R+G+B = WHITE", "C+M+Y = BLACK"];
let circles, dragIdx, wasDown;
let px, py;
let W = 0, H = 0;
function init({ width, height }) {
W = width; H = height;
circles = [];
for (let p = 0; p < 2; p++) {
for (let i = 0; i < 3; i++) {
const a = (-90 + i * 120) * Math.PI / 180;
circles.push({ panel: p, bx: Math.cos(a) * 0.55, by: Math.sin(a) * 0.55, ph: p * 2.1 + i * 1.7 });
}
}
px = new Float32Array(6); py = new Float32Array(6);
dragIdx = -1; wasDown = false;
}
function panelRect(k) {
// side-by-side in landscape, stacked in portrait
let x, y, w, h;
if (W >= H) { w = W / 2; h = H; x = k * w; y = 0; }
else { w = W; h = H / 2; x = 0; y = k * h; }
const cx = x + w / 2, cy = y + 26 + (h - 26) / 2;
const r = Math.min(w, h - 30) * 0.30;
return { x, y, w, h, cx, cy, r };
}
function tick({ ctx, time, width, height, input }) {
if (width !== W || height !== H) { W = width; H = height; }
input.consumeClicks();
// circle pixel positions (base + idle drift)
for (let i = 0; i < 6; i++) {
const c = circles[i], p = panelRect(c.panel);
const dx = Math.sin(time * 0.5 + c.ph) * 0.12;
const dy = Math.cos(time * 0.37 + c.ph * 1.7) * 0.12;
px[i] = p.cx + (c.bx + dx) * p.r;
py[i] = p.cy + (c.by + dy) * p.r;
c._dx = dx; c._dy = dy;
}
// press + drag: grab the nearest circle, move its base so it tracks the pointer
if (input.mouseDown && !wasDown) {
let best = -1, bestD = 1e9;
for (let i = 0; i < 6; i++) {
const p = panelRect(circles[i].panel);
const d = Math.hypot(input.mouseX - px[i], input.mouseY - py[i]);
if (d < p.r * 1.25 && d < bestD) { bestD = d; best = i; }
}
dragIdx = best;
}
if (!input.mouseDown) dragIdx = -1;
wasDown = input.mouseDown;
if (dragIdx >= 0) {
const c = circles[dragIdx], p = panelRect(c.panel);
const mx = Math.max(p.x + 10, Math.min(p.x + p.w - 10, input.mouseX));
const my = Math.max(p.y + 30, Math.min(p.y + p.h - 10, input.mouseY));
c.bx = (mx - p.cx) / p.r - c._dx;
c.by = (my - p.cy) / p.r - c._dy;
px[dragIdx] = mx; py[dragIdx] = my;
}
// panels
for (let k = 0; k < 2; k++) {
const p = panelRect(k);
ctx.save();
ctx.beginPath(); ctx.rect(p.x, p.y, p.w, p.h); ctx.clip();
ctx.fillStyle = k === 0 ? "#000" : "#fff";
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.globalCompositeOperation = k === 0 ? "lighter" : "multiply";
const cols = k === 0 ? ADD : SUB;
for (let i = 0; i < 3; i++) {
const j = k * 3 + i;
ctx.fillStyle = cols[i];
ctx.beginPath(); ctx.arc(px[j], py[j], p.r, 0, Math.PI * 2); ctx.fill();
}
ctx.restore();
}
ctx.strokeStyle = "#555"; ctx.lineWidth = 1;
if (W >= H) { ctx.beginPath(); ctx.moveTo(W / 2 + 0.5, 0); ctx.lineTo(W / 2 + 0.5, H); ctx.stroke(); }
else { ctx.beginPath(); ctx.moveTo(0, H / 2 + 0.5); ctx.lineTo(W, H / 2 + 0.5); ctx.stroke(); }
// overlap labels
ctx.font = "bold 11px monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
for (let k = 0; k < 2; k++) {
const p = panelRect(k);
const o = k * 3;
const gx = (px[o] + px[o + 1] + px[o + 2]) / 3, gy = (py[o] + py[o + 1] + py[o + 2]) / 3;
ctx.fillStyle = k === 0 ? "#000" : "#fff";
let all = true;
for (const [a, b, label] of PAIRS[k]) {
const i = o + a, j = o + b;
const d = Math.hypot(px[i] - px[j], py[i] - py[j]);
if (d < p.r * 1.6) {
const mx = (px[i] + px[j]) / 2, my = (py[i] + py[j]) / 2;
ctx.fillText(label, mx + (mx - gx) * 0.45, my + (my - gy) * 0.45);
}
if (d >= p.r * 1.45) all = false;
}
if (all) ctx.fillText(TRIPLE[k], gx, gy);
}
// titles + hints (HUD)
for (let k = 0; k < 2; k++) {
const p = panelRect(k);
ctx.fillStyle = k === 0 ? "#fff" : "#000";
ctx.font = "bold 13px monospace"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic";
ctx.fillText(k === 0 ? "ADDITIVE — light (RGB)" : "SUBTRACTIVE — pigment (CMY)", p.x + p.w / 2, p.y + 18);
ctx.font = "11px monospace";
ctx.fillStyle = k === 0 ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.55)";
ctx.fillText("drag a circle", p.x + p.w / 2, p.y + p.h - 10);
}
}
Comments (0)
Log in to comment.