Docs/Skills and code/skill video-visual-production

.claude/skills/video-visual-production/render/scenes-b0.js

Code259 lines15.8 KBSkill: video-visual-production

What the file says about itself

Scene types added for beat B0 (the hook): disc, plant, black, workers, person, chips3, dots, loop, pivotno. Same contract as scenes.js: pure functions (env, t, props, S); S = { cues: seconds, dur, assets }.

// Scene types added for beat B0 (the hook): disc, plant, black, workers, person, chips3, dots, loop, pivotno.
// Same contract as scenes.js: pure functions (env, t, props, S); S = { cues: seconds, dur, assets }.
const D = require('./draw');
const K = require('./contacts');
const { C, FONT, ease, prog, lerp, clamp, txt, panel, background, roundPath } = D;

const fade = (t, t0, d = 0.35) => ease.out3(prog(t, t0, d));
const HOLE = '#1d1712';
const NIGHT = '#181411'; // a dark ground that is not black (so the black-frame check only fires on declared black)

function silverDisc(ctx, cx, cy, r, rot, alpha, flat) {
  ctx.globalAlpha = alpha;
  if (flat) ctx.fillStyle = '#b4babf';
  else { const g = ctx.createRadialGradient(cx - r * 0.3, cy - r * 0.3, r * 0.1, cx, cy, r); g.addColorStop(0, '#e6e9eb'); g.addColorStop(1, '#8e959b'); ctx.fillStyle = g; }
  ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.fill();
  if (!flat) {
    ctx.strokeStyle = 'rgba(60,60,60,0.16)'; ctx.lineWidth = 1.5;
    for (const k of [0.45, 0.6, 0.75, 0.9]) { ctx.beginPath(); ctx.arc(cx, cy, r * k, 0, Math.PI * 2); ctx.stroke(); }
    ctx.fillStyle = 'rgba(255,255,255,0.22)';
    for (const s of [0, Math.PI]) { ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, r * 0.98, rot + s, rot + s + 0.55); ctx.closePath(); ctx.fill(); }
  }
  ctx.fillStyle = HOLE; ctx.beginPath(); ctx.arc(cx, cy, r * 0.17, 0, Math.PI * 2); ctx.fill();
  if (!flat) { ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, r * 0.29, 0, Math.PI * 2); ctx.stroke(); }
  ctx.globalAlpha = 1;
}

// ------------------------------------------------------------ disc: a blank disc with chips and a name, or a filling grid
function disc(env, t, P, S) {
  background(env);
  const { ctx } = env, q = S.cues;
  if (P.mode === 'grid') {
    const cols = 14, rows = 7, gap = 122, x0 = 960 - ((cols - 1) / 2) * gap, y0 = 540 - ((rows - 1) / 2) * gap;
    const tint = ease.out3(prog(t, q.tint, 1.4)) * 0.2;
    if (tint > 0) { ctx.globalAlpha = tint; ctx.fillStyle = C.accent; ctx.fillRect(0, 0, D.W, D.H); ctx.globalAlpha = 1; }
    const items = [];
    for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) items.push({ x: x0 + c * gap, y: y0 + r * gap, d: Math.hypot(c - (cols - 1) / 2, (r - (rows - 1) / 2) * 1.4) });
    items.sort((a, b) => a.d - b.d);
    const span = Math.max(0.5, q.tint - q.grid - 0.2);
    items.forEach((it, i) => {
      const p = ease.back(prog(t, q.grid + (i / items.length) * span, 0.32));
      if (p > 0) silverDisc(ctx, it.x, it.y, 44 * p, 0, clamp(p * 2), true);
    });
    return;
  }
  // turn mode
  const cx = 600, cy = 540, r = 270;
  const e = ease.out3(prog(t, 0, 0.6));
  silverDisc(ctx, cx, cy, r * (0.85 + 0.15 * e), t * 1.1, e, false);
  P.chips.forEach((c, i) => {
    const a = fade(t, q[c.cue], 0.35), py = 350 + i * 135;
    if (a <= 0) return;
    ctx.font = `56px ${FONT.med}`;
    const w = ctx.measureText(c.text).width + 80, x = 1060 + 40 * (1 - a);
    panel(ctx, x, py - 56, w, 100, { r: 50, fill: C.panel, stroke: C.accent, lw: 3, alpha: a });
    txt(env, c.text, x + 40, py + 10, { font: FONT.med, size: 56, alpha: a, role: 'label' });
  });
  const np = prog(t, q.name, K.stamp);
  if (np > 0) txt(env, P.name, cx, cy + 128, { font: FONT.bold, size: 58 * (1.15 - 0.15 * ease.out3(np)), color: C.paperInk, align: 'center', base: 'middle', alpha: ease.out3(np * 3), role: 'label' });
}

// ------------------------------------------------------------ black: a declared black frame (silence with a picture)
function black(env) {
  env.ctx.fillStyle = '#000'; env.ctx.fillRect(0, 0, D.W, D.H);
}

// ------------------------------------------------------------ plant: a factory silhouette, dark windows, gate, date, notices
function plant(env, t, P, S) {
  const { ctx } = env, q = S.cues;
  ctx.fillStyle = NIGHT; ctx.fillRect(0, 0, D.W, D.H);
  const bx = 560, bw = 800, gy = 800;
  const sil = '#2a221b';
  ctx.fillStyle = sil;
  ctx.fillRect(bx, 520, bw, gy - 520);
  for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.moveTo(bx + i * 200, 520); ctx.lineTo(bx + i * 200, 430); ctx.lineTo(bx + i * 200 + 200, 520); ctx.closePath(); ctx.fill(); }
  ctx.fillRect(1280, 330, 56, 200);
  ctx.fillStyle = '#0f0c0a'; ctx.fillRect(0, gy, D.W, D.H - gy);
  // windows
  const litK = P.windows === 'lit' ? 1 : (q.lit != null ? ease.out3(prog(t, q.lit, 0.5)) : 0);
  for (let r = 0; r < 2; r++) for (let i = 0; i < 6; i++) {
    const x = bx + 44 + i * 122, y = 566 + r * 86;
    ctx.fillStyle = litK > 0 ? `rgba(232,168,38,${0.85 * litK})` : '#171310';
    ctx.fillRect(x, y, 78, 52);
    ctx.strokeStyle = '#3a3027'; ctx.lineWidth = 3; ctx.strokeRect(x, y, 78, 52);
  }
  // gate: open (leaves pushed to the sides), or closing, or closed
  const gx = 880, gw = 160, gtop = 690;
  ctx.fillStyle = '#0f0c0a'; ctx.fillRect(gx, gtop, gw, gy - gtop);
  const closeK = P.gate === 'closed' ? 1 : P.gate === 'closing' ? ease.inOut(prog(t, q.close, K.merge)) : 0;
  const leaf = lerp(14, gw / 2, closeK);
  ctx.fillStyle = '#5a4a38';
  ctx.fillRect(gx, gtop, leaf, gy - gtop); ctx.fillRect(gx + gw - leaf, gtop, leaf, gy - gtop);
  ctx.strokeStyle = '#2a221b'; ctx.lineWidth = 3;
  for (let k = 1; k < 4; k++) { const f = k / 4; ctx.beginPath(); ctx.moveTo(gx + leaf * f, gtop); ctx.lineTo(gx + leaf * f, gy); ctx.moveTo(gx + gw - leaf * f, gtop); ctx.lineTo(gx + gw - leaf * f, gy); ctx.stroke(); }
  if (closeK >= 1) { ctx.fillStyle = C.accent; ctx.fillRect(gx + gw / 2 - 5, gtop + 60, 10, 54); }
  // notices
  if (P.date) txt(env, P.date, 120, 200, { font: FONT.bold, size: 118, color: C.accent, alpha: ease.out3(prog(t, q.date, K.stamp)), role: 'number' });
  if (P.place) txt(env, P.place, 960, 900, { font: FONT.semi, size: 60, color: C.muted, align: 'center', alpha: fade(t, q.place, 0.5), role: 'label' });
  if (P.maint) {
    const a = fade(t, q.maint, 0.4);
    panel(ctx, 140, 230, 640, 190, { fill: C.panel, stroke: C.panelEdge, lw: 3, alpha: a });
    txt(env, '"' + P.maint + '"', 460, 330, { font: FONT.semi, size: 76, align: 'center', alpha: a, role: 'label' });
    txt(env, P.maintSource, 460, 392, { font: FONT.reg, size: 34, color: C.muted, align: 'center', alpha: a, role: 'source' });
  }
  if (P.days) {
    for (let i = 0; i < 3; i++) {
      const a = fade(t, q.days + i * 0.18, 0.3);
      const lit = i === 2 ? ease.out3(prog(t, q.third, 0.35)) : 0;
      ctx.globalAlpha = a; ctx.lineWidth = 5; ctx.strokeStyle = lit > 0 ? C.accent : C.dim;
      if (lit > 0) { ctx.fillStyle = C.accent; ctx.globalAlpha = a * lit; ctx.fillRect(1470 + i * 120, 560, 90, 90); ctx.globalAlpha = a; }
      ctx.strokeRect(1470 + i * 120, 560, 90, 90); ctx.globalAlpha = 1;
    }
  }
  if (P.arrow) { // the escape arrow ends at the gate; a faceless figure waits outside
    const p = ease.inOut(prog(t, q.arrow, 1.4));
    if (p > 0) {
      ctx.strokeStyle = C.accent; ctx.lineWidth = 9; ctx.lineCap = 'round'; ctx.beginPath();
      const p0 = [250, 700], p1 = [560, 330], p2 = [940, 690]; let last = null; ctx.moveTo(p0[0], p0[1]);
      for (let i = 1; i <= 40; i++) { last = D.qpt(p0, p1, p2, (i / 40) * p); ctx.lineTo(last.x, last.y); }
      ctx.stroke(); ctx.lineCap = 'butt'; if (last && p > 0.05) D.arrowHead(ctx, last.x, last.y, last.ang, 34, C.accent);
    }
  }
  if (P.figure) D.person(ctx, 1560, 700, 190, C.muted, fade(t, q.figure, 0.5));
}

// ------------------------------------------------------------ workers: a worker in the plant, tally marks, a pay slip
function workers(env, t, P, S) {
  background(env);
  const { ctx } = env, q = S.cues;
  const a0 = fade(t, 0, 0.5);
  D.factory(ctx, 470, 640, 440, C.dim, { alpha: a0, window: C.bg1 });
  D.person(ctx, 470, 690, 150, C.accent, fade(t, q.worker, 0.4));
  // tally marks: groups of five, drawn one by one
  const total = P.tally, start = q.tally, per = 0.1;
  ctx.strokeStyle = C.ink; ctx.lineWidth = 9; ctx.lineCap = 'round';
  for (let i = 0; i < total; i++) {
    const a = prog(t, start + i * per, 0.12); if (a <= 0) continue;
    const g = Math.floor(i / 5), k = i % 5, x0 = 940 + g * 190, y0 = 290;
    ctx.globalAlpha = a; ctx.beginPath();
    if (k < 4) { ctx.moveTo(x0 + k * 32, y0); ctx.lineTo(x0 + k * 32, y0 + 120 * a); } else { ctx.moveTo(x0 - 14, y0 + 100); ctx.lineTo(x0 - 14 + 150 * a, y0 + 20); }
    ctx.stroke();
  }
  ctx.globalAlpha = 1; ctx.lineCap = 'butt';
  txt(env, P.union, 1330, 540, { font: FONT.semi, size: 56, align: 'center', alpha: fade(t, q.union, 0.4), role: 'label' });
  // pay slip with a slice offered
  const sa = fade(t, q.slip, 0.4);
  if (sa > 0) {
    panel(ctx, 1130, 660, 400, 230, { fill: C.panel, stroke: C.panelEdge, lw: 3, alpha: sa });
    ctx.globalAlpha = sa * 0.8; ctx.fillStyle = C.dim;
    for (let i = 0; i < 4; i++) ctx.fillRect(1160, 700 + i * 36, 260 - (i % 2) * 60, 12);
    ctx.globalAlpha = 1;
    const sl = ease.inOut(prog(t, q.slice, 0.7));
    ctx.globalAlpha = sa; ctx.setLineDash([10, 8]); ctx.lineWidth = 4; ctx.strokeStyle = C.accent;
    ctx.strokeRect(1430 + 70 * sl, 660, 100, 230); ctx.setLineDash([]); ctx.globalAlpha = 1;
  }
  txt(env, P.pay, 1330, 620, { font: FONT.semi, size: 52, color: C.accent, align: 'center', alpha: fade(t, q.pay, 0.4), role: 'label' });
}

// ------------------------------------------------------------ person: a name card and the thing he turns away from
function person(env, t, P, S) {
  background(env);
  const { ctx } = env, q = S.cues;
  const cp = fade(t, 0, 0.5), x0 = 170 - 60 * (1 - cp), cx = x0 + 350;
  panel(ctx, x0, 250, 700, 580, { fill: C.panel, stroke: C.panelEdge, lw: 3, alpha: cp });
  D.person(ctx, cx, 430, 230, C.accent, cp);
  txt(env, P.name, cx, 640, { font: FONT.semi, size: 70, align: 'center', alpha: cp, role: 'name' });
  txt(env, P.role, cx, 700, { font: FONT.reg, size: 40, color: C.muted, align: 'center', alpha: fade(t, 0.25, 0.4), role: 'label' });
  const la = fade(t, q.land, 0.5);
  if (la > 0) { // a field: rows of furrows
    ctx.globalAlpha = la; ctx.strokeStyle = C.good; ctx.lineWidth = 6;
    for (let r = 0; r < 4; r++) { ctx.beginPath(); for (let x = 0; x <= 420; x += 14) { const y = 470 + r * 54 + Math.sin(x / 38 + r) * 10; x === 0 ? ctx.moveTo(1130 + x, y) : ctx.lineTo(1130 + x, y); } ctx.stroke(); }
    ctx.globalAlpha = 1;
    txt(env, P.land, 1340, 400, { font: FONT.bold, size: 70, color: mixLand(la), align: 'center', alpha: la, role: 'label' });
  }
  const ap = ease.inOut(prog(t, q.away, 1.0));
  if (ap > 0) { // an arrow that turns away from the field
    ctx.strokeStyle = C.accent; ctx.lineWidth = 9; ctx.lineCap = 'round'; ctx.beginPath();
    const p0 = [1130, 420], p1 = [1330, 190], p2 = [1720, 330]; let last = null; ctx.moveTo(p0[0], p0[1]);
    for (let i = 1; i <= 40; i++) { last = D.qpt(p0, p1, p2, (i / 40) * ap); ctx.lineTo(last.x, last.y); }
    ctx.stroke(); ctx.lineCap = 'butt'; if (last && ap > 0.05) D.arrowHead(ctx, last.x, last.y, last.ang, 34, C.accent);
  }
}
const mixLand = () => C.ink;

// ------------------------------------------------------------ chips3: three things on the road
function chips3(env, t, P, S) {
  background(env);
  const { ctx } = env, q = S.cues;
  const xs = [380, 960, 1540];
  P.items.forEach((it, i) => {
    const a = fade(t, q[it.cue], 0.45), x = xs[i], y = 470 + 20 * (1 - a);
    if (a <= 0) return;
    panel(ctx, x - 270, 250, 540, 560, { fill: C.panel, stroke: C.panelEdge, lw: 3, alpha: a });
    if (it.icon === 'glass') D.glass(ctx, x, y, 260, C.accent, a);
    else if (it.icon === 'clock') {
      D.clockIcon(ctx, x, y, 120, C.ink, { alpha: a, minute: 0.5 });
      ctx.globalAlpha = a * 0.5; ctx.fillStyle = C.accent; ctx.beginPath(); ctx.moveTo(x, y); ctx.arc(x, y, 112, -Math.PI / 2, Math.PI / 2); ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1;
    } else { // medal
      ctx.globalAlpha = a; ctx.fillStyle = C.good; ctx.beginPath(); ctx.moveTo(x - 60, y - 150); ctx.lineTo(x - 10, y - 20); ctx.lineTo(x - 70, y - 20); ctx.closePath(); ctx.fill();
      ctx.beginPath(); ctx.moveTo(x + 60, y - 150); ctx.lineTo(x + 10, y - 20); ctx.lineTo(x + 70, y - 20); ctx.closePath(); ctx.fill();
      ctx.fillStyle = C.accent; ctx.beginPath(); ctx.arc(x, y + 40, 90, 0, Math.PI * 2); ctx.fill();
      ctx.fillStyle = C.bg0; ctx.beginPath(); for (let k = 0; k < 10; k++) { const r = k % 2 ? 22 : 52, ang = -Math.PI / 2 + (k * Math.PI) / 5; ctx.lineTo(x + Math.cos(ang) * r, y + 40 + Math.sin(ang) * r); } ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1;
    }
    txt(env, it.label, x, 720, { font: FONT.semi, size: 52, align: 'center', alpha: a, role: 'label' });
    if (it.sub) txt(env, it.sub, x, 775, { font: FONT.reg, size: 40, color: C.muted, align: 'center', alpha: a, role: 'label' });
  });
}

// ------------------------------------------------------------ dots: three open questions
function dots(env, t, P, S) {
  background(env);
  const { ctx } = env, q = S.cues;
  for (let i = 0; i < P.count; i++) {
    const a = ease.back(prog(t, q.dots + i * 0.22, 0.4));
    ctx.globalAlpha = clamp(a * 2); ctx.fillStyle = C.accent; ctx.beginPath(); ctx.arc(960 + (i - (P.count - 1) / 2) * 240, 460, 70 * Math.max(0, a), 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1;
  }
  txt(env, P.label, 960, 720, { font: FONT.bold, size: 96, align: 'center', alpha: fade(t, q.dots, 0.5), role: 'keyword' });
}

// ------------------------------------------------------------ loop: the way out bends into a trap
function loop(env, t, P, S) {
  background(env);
  const { ctx } = env, q = S.cues;
  const p = ease.inOut(prog(t, q.loop, 1.8));
  const pts = [];
  for (let x = 330; x <= 880; x += 10) pts.push([x, 560]);
  for (let a = Math.PI; a <= Math.PI * 4.2; a += 0.08) pts.push([1090 + Math.cos(a) * 210 * (1 - (a - Math.PI) * 0.045), 560 + Math.sin(a) * 210 * (1 - (a - Math.PI) * 0.045)]);
  const n = Math.floor(pts.length * p);
  if (n > 1) {
    ctx.strokeStyle = C.accent; ctx.lineWidth = 10; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]);
    for (let i = 1; i < n; i++) ctx.lineTo(pts[i][0], pts[i][1]);
    ctx.stroke(); ctx.lineCap = 'butt';
    const a = pts[n - 1], b = pts[Math.max(0, n - 3)]; D.arrowHead(ctx, a[0], a[1], Math.atan2(a[1] - b[1], a[0] - b[0]), 34, C.accent);
  }
  txt(env, P.exit, 600, 480, { font: FONT.med, size: 52, color: C.muted, align: 'center', alpha: fade(t, q.exit, 0.4), role: 'label' });
  txt(env, P.trap, 960, 920, { font: FONT.bold, size: 84, align: 'center', alpha: fade(t, q.trap, 0.5), role: 'keyword' });
}

// ------------------------------------------------------------ pivotno: the first rung (hollow) and a running business he says no to
function pivotno(env, t, P, S) {
  background(env);
  const { ctx } = env, q = S.cues;
  const lx = 1330, y1 = 720, y2 = 500, rail = 120;
  ctx.fillStyle = C.dim; ctx.fillRect(lx - rail - 7, 300, 14, 560); ctx.fillRect(lx + rail - 7, 300, 14, 560);
  const ra = fade(t, q.rung, 0.4);
  ctx.strokeStyle = C.dim; ctx.lineWidth = 4; ctx.setLineDash([12, 12]); ctx.strokeRect(lx - rail, y2 - 11, rail * 2, 22);
  ctx.globalAlpha = ra; ctx.strokeStyle = C.accent; ctx.strokeRect(lx - rail, y1 - 11, rail * 2, 22); ctx.setLineDash([]); ctx.globalAlpha = 1;
  txt(env, P.label, lx, 255, { font: FONT.semi, size: 56, color: C.accent, align: 'center', alpha: ra, role: 'label' });
  const fa = fade(t, q.factory, 0.45);
  D.factory(ctx, 560, 520, 330, C.ink, { alpha: fa, window: C.bg1, smokeT: fa > 0 ? t - q.factory : null, smokeColor: C.muted });
  const np = prog(t, q.no, K.label);
  if (np > 0) {
    ctx.globalAlpha = 0.5 * ease.out3(np); ctx.fillStyle = C.bg0; ctx.fillRect(330, 300, 460, 440); ctx.globalAlpha = 1;
    txt(env, P.no, 560, 520, { font: FONT.bold, size: 190 * (1.2 - 0.2 * ease.out3(np)), color: C.accent, align: 'center', base: 'middle', alpha: ease.out3(np * 3), role: 'keyword' });
  }
  txt(env, P.running, 560, 800, { font: FONT.med, size: 46, color: C.muted, align: 'center', alpha: fa, role: 'label' });
}

module.exports = { disc, black, plant, workers, person, chips3, dots, loop, pivotno };

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.