cartridge.cafe · open source world

dockstar-reel

the Dockstar pitch as an 80-second film: ten acts of light converging into one answer, real type, no slides

THE VISION

Night #0D0A14 with neon string-light glow: magenta #FF3DCB, violet #9D5CFF, lavender #D9B8FF, one coral #FF5A1F focal. Text sits left in Figtree bold, art lives right and behind. Each act is one picture: cover = dots converging into one answer; problem = a chain of boxes lighting up to a coral escalation; numbers = three columns rising; the flip = tension bars climbing to an agree-circle vs. agree-circle first then a gradient bar; shift = fan-out vs converge; how = tiers 125 to 25 to 5 to 1; proof = 27 dots becoming one in four seconds; competitors = rulers measuring vs one decision; start = the answer alone, glowing. Mood: a bar at night, warm, alive, converging.

HOW TO PLAY

No controls. Watch. The Dockstar pitch as an 80-second film: ten acts, eight seconds each, looping. Press REC to take it home as an MP4.

built by: Claude (Fable 5.1)
2 visual shaders · 0 shader modules · 7 step hooks · runs on WebGPU in the browser
This source is part of the cartridge.cafe commons: readable by anyone, reusable inside other cafe worlds with lineage attribution.

— VISUAL SHADERS (WGSL) —

visual · reel

fn rl_dot(p: vec2f, c: vec2f, r: f32, col: vec3f) -> vec3f {
  let d = length(p - c);
  let core = smoothstep(r, r * 0.3, d);
  let halo = exp(-d * d / (r * r * 9.0)) * 0.35;
  return col * core + col * halo;
}
fn rl_box(p: vec2f, c: vec2f, b: vec2f, col: vec3f, k: f32) -> vec3f {
  let d = sdRoundedBox(p - c, b, 0.03);
  let fill = smoothstep(0.004, -0.004, d);
  let edge = smoothstep(0.012, 0.0, abs(d));
  return col * (fill * 0.22 + edge * 0.9) * k;
}
fn rl_ease(t: f32) -> f32 { return t * t * (3.0 - 2.0 * t); }
fn rl_scene(id: i32, p: vec2f, t: f32, T: f32) -> vec3f {
  let magenta = vec3f(1.0, 0.24, 0.80);
  let violet = vec3f(0.62, 0.36, 1.0);
  let lavender = vec3f(0.85, 0.72, 1.0);
  let coral = vec3f(1.0, 0.35, 0.12);
  let dim = vec3f(0.38, 0.30, 0.48);
  var col = vec3f(0.0);
  if (id == 0 || id == 9) {
    let focal = select(vec2f(1.0, 0.1), vec2f(0.85, 0.3), id == 9);
    let dh = length(p - focal);
    col += magenta * exp(-dh * dh * 1.8) * 0.14;
    for (var i = 0; i < 90; i++) {
      let fi = f32(i);
      let h = hash21(vec2f(fi * 1.37, 7.1));
      let h2 = hash21(vec2f(fi * 2.91, 3.3));
      let h3 = hash21(vec2f(fi * 0.53, 11.7));
      let start = vec2f(-0.3 + h * 2.1, -0.95 + h2 * 1.9);
      let tt = fract(T * 0.05 + h3);
      let e = tt * tt;
      let pos = mix(start, focal, e * 0.94);
      let r = mix(0.018, 0.008, e) * (0.7 + h2 * 0.6);
      var c = magenta;
      if (h3 < 0.33) { c = violet; } else if (h3 < 0.55) { c = lavender; }
      col += rl_dot(p, pos, r, c);
    }
    col += rl_dot(p, focal, 0.09, coral) * 1.2;
    col += vec3f(1.0, 0.88, 0.75) * smoothstep(0.05, 0.0, dh) * 0.9;
    col += coral * exp(-dh * dh * 12.0) * 0.45;
  } else if (id == 1) {
    for (var k = 0; k < 4; k++) {
      let fk = f32(k);
      let c = vec2f(-1.1 + fk * 0.74, 0.55);
      let lit = smoothstep(fk * 0.22, fk * 0.22 + 0.12, t);
      var cc = dim;
      if (k == 3) { cc = coral; }
      col += rl_box(p, c, vec2f(0.3, 0.15), cc, 0.3 + lit * 0.7);
      if (k == 3) { let d3 = length(p - c); col += coral * exp(-d3 * d3 * 6.0) * 0.35 * lit; }
      if (k < 3) { let ax = c.x + 0.37; let a = smoothstep(0.012, 0.0, abs(p.y - c.y)) * step(ax - 0.06, p.x) * step(p.x, ax + 0.06); col += dim * a * 0.8; }
    }
  } else if (id == 2) {
    for (var k = 0; k < 3; k++) {
      let fk = f32(k);
      let x = 0.35 + fk * 0.5;
      let hmax = select(select(0.37, 0.52, k == 1), 0.95, k == 2);
      let hgt = hmax * rl_ease(clamp(t * 1.6 - fk * 0.2, 0.0, 1.0));
      let base = 0.9;
      let inside = step(abs(p.x - x), 0.16) * step(base - hgt, p.y) * step(p.y, base);
      let grad = clamp((base - p.y) / max(hgt, 0.001), 0.0, 1.0);
      let c = mix(magenta, violet, grad);
      col += c * inside * (0.55 + 0.45 * grad);
      let dtop = length(p - vec2f(x, base - hgt));
      col += c * exp(-dtop * dtop * 60.0) * 0.5;
    }
  } else if (id == 3) {
    let y1 = 0.35; let y2 = 0.8;
    for (var k = 0; k < 12; k++) {
      let fk = f32(k);
      let x = -1.3 + fk * 0.17;
      let hh = 0.04 + fk * 0.024;
      let show = smoothstep(fk / 12.0, fk / 12.0 + 0.08, t);
      let inside = step(abs(p.x - x), 0.07) * step(y1 - hh, p.y) * step(p.y, y1);
      col += mix(dim, coral, fk / 11.0) * inside * show;
    }
    let a1 = vec2f(0.85, y1 - 0.12);
    let s1 = smoothstep(0.92, 1.0, t);
    col += rl_dot(p, a1, 0.13, magenta) * 1.1 * s1;
    let a2 = vec2f(-1.3, y2);
    col += rl_dot(p, a2, 0.13, magenta) * 1.1;
    let w = 2.1 * rl_ease(clamp(t * 1.3, 0.0, 1.0));
    let bar = step(a2.x + 0.12, p.x) * step(p.x, a2.x + 0.12 + w) * step(abs(p.y - y2), 0.05);
    let g = clamp((p.x - a2.x) / 2.1, 0.0, 1.0);
    col += mix(mix(magenta, violet, g * 1.6), vec3f(0.24, 0.95, 0.69), smoothstep(0.6, 1.0, g)) * bar;
  } else if (id == 4) {
    let ox = -0.75; let cx = 0.8;
    col += rl_dot(p, vec2f(ox - 0.6, 0.5), 0.05, dim) * 1.2;
    for (var k = 0; k < 3; k++) { col += rl_dot(p, vec2f(ox - 0.15, 0.3 + f32(k) * 0.2), 0.03, dim); }
    for (var k = 0; k < 15; k++) { let fk = f32(k); col += rl_dot(p, vec2f(ox + 0.35 + f32(k % 5) * 0.09, 0.32 + floor(fk / 5.0) * 0.18), 0.018, dim * 0.8); }
    let pulse = fract(t * 1.5);
    col += rl_dot(p, vec2f(ox - 0.6 + pulse * 0.95, 0.5), 0.02, dim) * 0.8;
    for (var k = 0; k < 15; k++) { let fk = f32(k); let h = hash21(vec2f(fk, 3.0)); var c = violet; if (h < 0.3) { c = magenta; } else if (h < 0.5) { c = lavender; } col += rl_dot(p, vec2f(cx - 0.55 + f32(k % 5) * 0.09, 0.32 + floor(fk / 5.0) * 0.18), 0.018, c); }
    for (var k = 0; k < 3; k++) { col += rl_dot(p, vec2f(cx + 0.15, 0.3 + f32(k) * 0.2), 0.03, magenta); }
    col += rl_dot(p, vec2f(cx + 0.6, 0.5), 0.06, coral) * 1.3;
    col += rl_dot(p, vec2f(cx - 0.55 + pulse * 1.15, 0.5), 0.02, magenta) * 0.9;
  } else if (id == 5) {
    for (var k = 0; k < 25; k++) { let fk = f32(k); let a = smoothstep(fk / 25.0 * 0.4, fk / 25.0 * 0.4 + 0.05, t); col += rl_dot(p, vec2f(0.1 + fk * 0.065, 0.9), 0.016, violet) * a; }
    for (var k = 0; k < 5; k++) { let fk = f32(k); let a = smoothstep(0.45 + fk * 0.06, 0.5 + fk * 0.06, t); col += rl_dot(p, vec2f(0.3 + fk * 0.3, 0.6), 0.028, magenta) * a; }
    let a1 = smoothstep(0.82, 0.95, t);
    col += rl_dot(p, vec2f(0.9, 0.28), 0.07, coral) * 1.3 * a1;
    let dd = length(p - vec2f(0.9, 0.28));
    col += coral * exp(-dd * dd * 8.0) * 0.4 * a1;
  } else if (id == 6) {
    let focal = vec2f(0.95, 0.4);
    let e = rl_ease(clamp(t * 1.15, 0.0, 1.0));
    for (var k = 0; k < 27; k++) {
      let fk = f32(k);
      let h = hash21(vec2f(fk * 1.7, 5.0)); let h2 = hash21(vec2f(fk * 0.9, 9.0));
      let start = vec2f(0.0 + h * 1.7, -0.5 + h2 * 1.6);
      let pos = mix(start, focal, e * 0.95);
      var c = violet; if (h2 < 0.35) { c = magenta; } else if (h2 < 0.55) { c = lavender; }
      col += rl_dot(p, pos, mix(0.022, 0.01, e), c);
    }
    col += rl_dot(p, focal, 0.08, coral) * 1.2 * smoothstep(0.7, 1.0, e);
  } else if (id == 7) {
    for (var k = 0; k < 6; k++) {
      let fk = f32(k);
      let c = vec2f(0.35 + f32(k % 3) * 0.5, 0.25 + floor(fk / 3.0) * 0.5);
      let pulse = 0.6 + 0.4 * sin(T * 2.0 - fk * 0.9);
      var cc = magenta; if (k % 3 == 1) { cc = violet; } else if (k % 3 == 2) { cc = lavender; }
      col += rl_box(p, c, vec2f(0.2, 0.17), cc, 0.5 + pulse * 0.5);
    }
  } else if (id == 8) {
    for (var k = 0; k < 5; k++) {
      let fk = f32(k);
      let y = 0.2 + fk * 0.17;
      let inx = step(-1.0, p.x) * step(p.x, 0.2);
      let line = smoothstep(0.008, 0.0, abs(p.y - y)) * inx;
      col += dim * line * 0.9;
      let tick = step(0.9, fract((p.x + 1.0) * 8.0)) * step(y - 0.035, p.y) * step(p.y, y) * inx;
      col += dim * tick * 0.9;
      let m = -1.0 + fract(t * 0.5 + fk * 0.2) * 1.2;
      col += rl_dot(p, vec2f(m, y - 0.045), 0.014, dim);
    }
    let focal = vec2f(1.0, 0.55);
    col += rl_dot(p, focal, 0.1, coral) * 1.3;
    let df = length(p - focal);
    col += coral * exp(-df * df * 6.0) * 0.4;
    for (var k = 0; k < 20; k++) { let fk = f32(k); let h = hash21(vec2f(fk * 2.3, 1.0)); let h2 = hash21(vec2f(fk * 1.1, 4.0)); let e = fract(T * 0.12 + h2); let pos = mix(vec2f(0.5 + h * 1.2, -0.2 + h2 * 1.4), focal, e * 0.9); col += rl_dot(p, pos, 0.014, magenta); }
  }
  return col;
}
fn visual_reel(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
  let p = vec2f(uv.x * 1.778, uv.y);
  let night = vec3f(0.051, 0.039, 0.078);
  let act = i32(uni(0)); let tIn = uni(1); let T = uni(2); let blend = uni(3); let prev = i32(uni(4));
  var col = night;
  let dv = length(p - vec2f(-1.0, 0.4));
  col += vec3f(0.62, 0.36, 1.0) * exp(-dv * dv * 0.9) * 0.07;
  var art = rl_scene(act, p, tIn, T);
  if (blend < 1.0) { art = mix(rl_scene(prev, p, 1.0, T), art, blend); }
  col += art;
  for (var j = 0; j < 15; j++) { let fj = f32(j); let x = -1.7 + fj * (3.4 / 14.0); let sag = 0.06 * sin(fj / 14.0 * 6.283 + 0.5); let lp = vec2f(x, -0.9 + sag + 0.012 * sin(T * 0.8 + fj)); var lc = vec3f(1.0, 0.24, 0.8); let m = j % 4; if (m == 1) { lc = vec3f(0.62, 0.36, 1.0); } else if (m == 2) { lc = vec3f(1.0, 0.35, 0.12); } else if (m == 3) { lc = vec3f(0.85, 0.72, 1.0); } col += rl_dot(p, lp, 0.014, lc); }
  let wy = -0.9 + 0.06 * sin((p.x + 1.7) / 3.4 * 6.283 + 0.5);
  col += vec3f(0.25, 0.18, 0.32) * smoothstep(0.004, 0.0, abs(p.y - wy)) * 0.5;
  let fadeIn = smoothstep(0.0, 0.08, tIn); let fadeOut = 1.0 - smoothstep(0.93, 1.0, tIn);
  let a = fadeIn * fadeOut;
  let slide = (1.0 - fadeIn) * 0.06;
  let tp = vec2f(-1.4, -0.72 + slide); let ts = vec2f(2.2, 1.1);
  let cu = (p - tp) / ts;
  if (cu.x >= 0.0 && cu.x <= 1.0 && cu.y >= 0.0 && cu.y <= 1.0) {
    let s = sprite(act, cu);
    let srgb = pow(max(s.rgb, vec3f(0.0)), vec3f(1.0 / 2.2));
    col = mix(col, srgb, s.a * a);
  }
  return vec4f(clamp(col, vec3f(0.0), vec3f(1.0)), 1.0);
}

visual · base_bg


fn visual_base_bg(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
  let g = uv.y * 0.5 + 0.5;
  var col = mix(vec3f(0.075, 0.085, 0.115), vec3f(0.028, 0.033, 0.05), g);
  col += vec3f(0.05, 0.06, 0.09) * (0.5 + 0.5 * sin(uv.x * 2.0 + uv.y * 3.0)) * 0.06;
  col += 0.010 * (vnoise(uv * 34.0) - 0.5);
  // BLANK: no horizon line (Galen, Aug 31: "mobile base has a blue line — we need
  // a blank"). The old soft floor line at uv.y≈0.35 was baked into every born
  // world's backdrop and read as a stray blue line; a blank base is a clean
  // neutral ground the builder paints over, nothing to remove first.
  return vec4f(col, 1.0);
}

— STEP HOOKS (JAVASCRIPT) —

hook · player

input + the player avatar — reading keys/touch, moving the body
// ── PLAYER — this node owns INPUT and the player's body.
// Read sim.input (keys, pointer, touch) and move the avatar here.
// Nothing else should read raw input — other nodes react to what
// this one writes into sim.worldData (position, facing, intent).
// Build WITHIN this node: dock it (dock_node {"id":"player"}), replace
// this body, undock. It hot-swaps live — no reload, ever.
// GROW SUB-NODES for per-player parts: add_step_hook {"hookId":"player:p1-model"}
// — a sub-node is a FULL node (own history, own hold), so two builders
// can each hold their own player's model without touching each other.

hook · world

the stage — terrain, physics, weather, the rules of matter
// ── WORLD — this node owns the STAGE: terrain, gravity, collisions,
// weather, time-of-day. If it is true everywhere for everyone, it
// belongs here. Entities and players READ the stage this node sets.
// GROW SUB-NODES for places: add_step_hook {"hookId":"world:atrium"},
// {"hookId":"world:caves"} — each room/space its own dockable node.

hook · entities

everything that lives in the world — spawns, rosters, behavior
// ── ENTITIES — this node owns every non-player thing that moves or
// lives: spawn tables, rosters, per-entity behavior ticks. Give each
// KIND its own SUB-NODE rather than one blob: add_step_hook
// {"hookId":"entities:wolves"} — each independently held and healed.

hook · rules

the game of it — goals, scoring, win/lose, rounds
// ── RULES — this node owns the GAME: objectives, scoring, win/lose,
// round flow. It reads what player/world/entities write and decides
// what it MEANS. HUD displays what this node concludes.

hook · hud

what the player sees about the game — score, prompts, state
// ── HUD — this node owns the display layer. Use THE UI SYSTEM:
// write sim.worldData.ui = { root: [ { id:'score', anchor:{gx:8,gy:8},
// align:'tl', children:[{kind:'text', text:'SCORE 0'}] } ] } — panels,
// text, meters, buttons (click:'my_action' lands in wd.__uiClick).
// The engine SOLVES the layout (worldData.__uiRects is readable data)
// and KEEPS YOUR UI OUT FROM UNDER THE CAFE'S CHROME automatically.
// Show conclusions, not internals. (Legacy wd.hud still works.)

hook · net

shared state — what this world syncs when crews play together
// ── NET — this node owns SHARED STATE: when this world goes
// multiplayer (arena), what state is authoritative-server truth vs
// local cosmetic? Declare the world's mpManifest here as it grows.
// Until arena wiring lands this node idles — but it EXISTS, so the
// day the world goes shared is a hot-swap, not a rebuild.

hook · director

by claude-code
const wd = sim.worldData
const ACT = 8, N = 10
wd.__t = (wd.__t || 0) + Math.min(dt, 0.05)
const T = wd.__t % (ACT * N)
const act = Math.floor(T / ACT)
const tIn = (T - act * ACT) / ACT
const prev = (act + N - 1) % N
const b = Math.min(1, (T - act * ACT) / 0.8)
const blend = b * b * (3 - 2 * b)
const u = []
u[0] = act; u[1] = tIn; u[2] = T; u[3] = blend; u[4] = prev
wd.gpuUniforms = u
wd.ui = { rev: 1, root: [ { id: 'prog', kind: 'panel', anchor: { vx: 0.5, vy: 0.985 }, align: 'bc', w: 300, glass: false,
  children: [ { kind: 'meter', id: 'm', value: T / (ACT * N), hue: '#FF3DCB', label: 'ACT ' + (act + 1) + ' / ' + N } ] } ] }