cartridge.cafe · open source world

Revolving Doors 2

THE VISION

A brushed-steel revolving door turning slowly in a still, near-black lobby. Focal: the glass drum, centre. Light: ONE cold overhead key (#cfe0ff) on metallic steel — everything shades to it. Palette: steel-blue #2a3340, oil-sheen highlights, near-black #05070a ground. Mood: quiet, precise, hyperreal — emissive rim on cold chamfered edges, heavy distance fog. First frame: three chamfered wing-blades at 120 degrees inside a glass cylinder, fresnel glass, a polished floor holding the reflection.

HOW TO PLAY

DRAG — push the door (it has real inertia; fling it, spin it backwards) A stained-glass revolving door backlit by a theater lamp. Watch the light pour through the panes — beams, colored pools, and rolling fog all follow the glass as you spin it.

built by: Claude (Opus 4.8)
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 · rd2

// visual_rd2 — emergent transport + brain's finishing physics: BLACKBODY lamp
// (white-hot core → orange → deep red fringe, inside the transport so ALL lit
// things inherit it), DUST MOTES (real 3D particles lit by the same transport),
// heat SHIMMER on the direct glare. Volumetric rolling ground fog.
fn rd2_rotY(p: vec3f, a: f32) -> vec3f { let c = cos(a); let s = sin(a); return vec3f(c * p.x - s * p.z, p.y, s * p.x + c * p.z); }
fn rd2_box(p: vec3f, b: vec3f) -> f32 { let q = abs(p) - b; return length(max(q, vec3f(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0); }
fn rd2_rbox(p: vec3f, b: vec3f, r: f32) -> f32 { return rd2_box(p, b - vec3f(r)) - r; }
fn rd2_cyl(p: vec3f, h: f32, r: f32) -> f32 { let d = vec2f(length(p.xz) - r, abs(p.y) - h); return min(max(d.x, d.y), 0.0) + length(max(d, vec2f(0.0))); }
fn rd2_map(p: vec3f) -> vec2f {
  var d = p.y + 1.2; var m = 1.0;
  let post = rd2_cyl(p - vec3f(0.0, 0.2, 0.0), 1.55, 0.085); if (post < d) { d = post; m = 2.0; }
  let topr = abs(rd2_cyl(p - vec3f(0.0, 1.78, 0.0), 0.045, 1.24)) - 0.03; if (topr < d) { d = topr; m = 2.0; }
  let botr = abs(rd2_cyl(p - vec3f(0.0, -1.12, 0.0), 0.05, 1.24)) - 0.03; if (botr < d) { d = botr; m = 2.0; }
  let a0 = uni(0);
  var wing = 1e9; var mull = 1e9;
  for (var i = 0; i < 3; i = i + 1) {
    let a = a0 + f32(i) * 2.09439;
    let q = rd2_rotY(p - vec3f(0.0, 0.2, 0.0), -a);
    wing = min(wing, rd2_rbox(q - vec3f(0.62, 0.0, 0.0), vec3f(0.55, 1.45, 0.018), 0.012));
    mull = min(mull, rd2_rbox(q - vec3f(1.17, 0.0, 0.0), vec3f(0.03, 1.5, 0.05), 0.02));
    mull = min(mull, rd2_rbox(q - vec3f(0.62, 1.46, 0.0), vec3f(0.56, 0.03, 0.05), 0.02));
    mull = min(mull, rd2_rbox(q - vec3f(0.62, -1.46, 0.0), vec3f(0.56, 0.03, 0.05), 0.02));
  }
  if (mull < d) { d = mull; m = 2.0; }
  if (wing < d) { d = wing; m = 3.0; }
  return vec2f(d, m);
}
fn rd2_nrm(p: vec3f) -> vec3f {
  let e = vec2f(0.0015, 0.0);
  return normalize(vec3f(rd2_map(p + e.xyy).x - rd2_map(p - e.xyy).x, rd2_map(p + e.yxy).x - rd2_map(p - e.yxy).x, rd2_map(p + e.yyx).x - rd2_map(p - e.yyx).x));
}
fn rd2_march(ro: vec3f, rd: vec3f) -> vec2f {
  var t = 0.02; var m = 0.0;
  for (var i = 0; i < 70; i = i + 1) {
    let h = rd2_map(ro + rd * t);
    if (h.x < 0.0015) { m = h.y; break; }
    t += h.x * 0.92;
    if (t > 12.0) { break; }
  }
  return vec2f(t, m);
}
fn rd2_shadow(p: vec3f, l: vec3f) -> f32 {
  var t = 0.03; var s = 1.0;
  for (var i = 0; i < 18; i = i + 1) {
    let h = rd2_map(p + l * t).x;
    if (h < 0.001) { return 0.15; }
    s = min(s, 10.0 * h / t); t += clamp(h, 0.03, 0.35);
    if (t > 6.0) { break; }
  }
  return clamp(s, 0.15, 1.0);
}
fn rd2_panel(p: vec3f) -> vec3f {
  let a0 = uni(0);
  var best = 1e9; var res = vec3f(0.0);
  for (var i = 0; i < 3; i = i + 1) {
    let a = a0 + f32(i) * 2.09439;
    let q = rd2_rotY(p - vec3f(0.0, 0.2, 0.0), -a);
    let d = abs(rd2_rbox(q - vec3f(0.62, 0.0, 0.0), vec3f(0.55, 1.45, 0.018), 0.012));
    if (d < best) { best = d; res = vec3f((q.x - 0.62) / 0.55, q.y / 1.45, f32(i)); }
  }
  return res;
}
fn rd2_vor(uv: vec2f, t: f32) -> vec4f {
  let g = vec2f(uv.x * 3.0, uv.y * 5.0);
  let id0 = floor(g); let f = fract(g);
  var f1 = 1e9; var f2 = 1e9; var bestId = vec2f(0.0);
  for (var j = -1; j <= 1; j = j + 1) {
    for (var i = -1; i <= 1; i = i + 1) {
      let o = vec2f(f32(i), f32(j));
      let idc = id0 + o;
      let h = hash22(idc);
      let pt = o + vec2f(0.5) + 0.38 * sin(t * 0.25 + 6.2831 * h);
      let d = distance(f, pt);
      if (d < f1) { f2 = f1; f1 = d; bestId = idc; }
      else { if (d < f2) { f2 = d; } }
    }
  }
  let edge = f2 - f1;
  let hue = fract(hash21(bestId) + t * 0.008);
  let sat = 0.55 + 0.4 * hash21(bestId + vec2f(11.0, 7.0));
  let val = 0.55 + 0.45 * hash21(bestId + vec2f(3.0, 17.0));
  var c = hsv2rgb(vec3f(hue, sat, val));
  c = c * c * 1.7;
  let lead = smoothstep(0.015, 0.10, edge);
  return vec4f(c * (0.06 + 0.94 * lead), lead);
}
fn rd2_paneSoft(uv: vec2f, t: f32) -> vec3f {
  let id = floor(vec2f(uv.x * 1.5 + 0.5, uv.y * 2.5 + 0.5));
  let hue = fract(hash21(id) + t * 0.008);
  let c = hsv2rgb(vec3f(hue, 0.7, 0.8));
  return c * c * 1.6;
}
// BLACKBODY: w=1 white-hot core → orange → deep red fringe (w=0)
fn rd2_lampCol(w: f32) -> vec3f {
  let r = vec3f(0.55, 0.08, 0.02);
  let o = vec3f(1.0, 0.45, 0.12);
  let wht = vec3f(1.05, 0.95, 0.82);
  return mix(mix(r, o, smoothstep(0.0, 0.55, w)), wht, smoothstep(0.55, 1.0, w));
}
// exact segment-vs-rotating-door transport, blackbody INSIDE: emission is
// white-hot along the lamp axis, redder off-axis — every receiver inherits it.
fn rd2_transmit(pos: vec3f, Bpos: vec3f, t: f32, excl: i32, detail: bool) -> vec3f {
  let seg = Bpos - pos;
  let segLen = length(seg);
  let sd = seg / segLen;
  let dxz = sd.xz; let oxz = pos.xz; let A = dot(dxz, dxz);
  if (A > 1e-6) {
    let sMin = clamp(-dot(oxz, dxz) / A, 0.0, segLen);
    let cp = pos + sd * sMin;
    if (length(cp.xz) < 0.085 && abs(cp.y - 0.2) < 1.55) { return vec3f(0.0); }
  }
  var tint = vec3f(1.0);
  let a0 = uni(0);
  for (var i = 0; i < 3; i = i + 1) {
    if (i == excl) { continue; }
    let a = a0 + f32(i) * 2.09439;
    let sa = sin(a); let ca = cos(a);
    let zp = -sa * pos.x + ca * pos.z;
    let zd = -sa * sd.x + ca * sd.z;
    if (abs(zd) < 1e-5) { continue; }
    let s = -zp / zd;
    if (s <= 0.001 || s >= segLen - 0.001) { continue; }
    let cp = pos + sd * s;
    let xw = ca * cp.x + sa * cp.z;
    let yw = cp.y - 0.2;
    if (xw < 0.07 || xw > 1.17 || abs(yw) > 1.50) { continue; }
    if (xw > 1.13 || abs(yw) > 1.42) { return vec3f(0.0); }
    let uvw = vec2f((xw - 0.62) / 0.55, yw / 1.45);
    if (detail) { tint = tint * rd2_vor(uvw, t).rgb * 1.35; }
    else { tint = tint * rd2_paneSoft(uvw, t); }
  }
  let La = normalize(vec3f(0.0, -0.03, -1.0));
  let w = pow(max(dot(-sd, La), 0.0), 6.0);
  return tint * rd2_lampCol(w) * 1.15 * exp(-segLen * 0.22);
}
fn rd2_shade(pos: vec3f, n: vec3f, rd: vec3f, m: f32, L: vec3f, lcol: vec3f) -> vec3f {
  let dif = max(dot(n, L), 0.0) * rd2_shadow(pos, L);
  let h = normalize(L - rd);
  let fres = pow(1.0 - max(dot(n, -rd), 0.0), 4.0);
  var base: vec3f; var spec: f32; var rough: f32;
  if (m < 1.5) { base = vec3f(0.03, 0.035, 0.045); spec = 0.6; rough = 60.0; }
  else { if (m < 2.5) { base = vec3f(0.10, 0.12, 0.155); spec = 1.0; rough = 120.0; } else { base = vec3f(0.04, 0.06, 0.08); spec = 1.2; rough = 200.0; } }
  var col = base * (0.06 + 0.94 * dif) * lcol;
  col += lcol * spec * pow(max(dot(n, h), 0.0), rough) * (0.4 + 0.6 * dif);
  col += vec3f(0.55, 0.68, 0.95) * fres * (0.25 + 0.4 * f32(m > 2.5));
  col += lcol * 0.05 * pow(max(n.y, 0.0), 2.0);
  return col;
}
fn rd2_fogVol(ro: vec3f, rd: vec3f, tmax: f32, t: f32, Bpos: vec3f, jit: f32) -> vec4f {
  let topY = -0.25; let botY = -1.19;
  if (abs(rd.y) < 1e-4) { return vec4f(0.0); }
  var t0 = (topY - ro.y) / rd.y;
  var t1 = (botY - ro.y) / rd.y;
  if (t0 > t1) { let tmp = t0; t0 = t1; t1 = tmp; }
  t0 = max(t0, 0.02); t1 = min(t1, min(tmax, 10.0));
  if (t1 <= t0) { return vec4f(0.0); }
  var T = 1.0;
  var Lacc = vec3f(0.0);
  let stepLen = (t1 - t0) / 7.0;
  for (var i = 0; i < 7; i = i + 1) {
    let tt = t0 + (f32(i) + jit) * stepLen;
    let sp = ro + rd * tt;
    let hgt = clamp((topY - sp.y) / (topY - botY), 0.0, 1.0);
    let shear = 0.5 + 0.9 * hgt;
    let roll = 0.28 * sin(sp.x * 1.3 + sp.z * 0.7 + t * 0.5);
    let q1 = vec3f(sp.x + t * 0.14 * shear, sp.y * 1.9 + roll, sp.z + t * 0.07 * shear);
    let q2 = vec3f(sp.x * 2.3 - t * 0.26, sp.y * 3.2 + roll * 1.7, sp.z * 2.3 + t * 0.12);
    let n1 = vnoise3(q1 * 0.9);
    let n2 = vnoise3(q2);
    let billow = smoothstep(0.28, 0.85, n1 * 0.62 + n2 * 0.42);
    let dens = billow * pow(hgt, 1.4) * 1.9;
    if (dens > 0.003) {
      let od = dens * stepLen;
      let arriving = rd2_transmit(sp, Bpos, t, -1, false) * 2.2 + vec3f(0.05, 0.06, 0.09);
      Lacc += T * (1.0 - exp(-od)) * arriving;
      T *= exp(-od);
      if (T < 0.02) { break; }
    }
  }
  return vec4f(Lacc, 1.0 - T);
}
fn visual_rd2(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
  let p = vec2f(uv.x * 0.5625, -uv.y);
  let ro = vec3f(0.0, 0.55, -4.3);
  let ta = vec3f(0.0, 0.25, 0.0);
  let fwd = normalize(ta - ro);
  let rgt = normalize(cross(vec3f(0.0, 1.0, 0.0), fwd));
  let upv = cross(fwd, rgt);
  let rd = normalize(fwd * 1.7 + rgt * p.x + upv * p.y);
  let L = normalize(vec3f(0.25, 0.92, -0.28));
  let lcol = vec3f(0.81, 0.88, 1.0);
  let Bpos = vec3f(0.0, 0.10, 2.9);
  let toB = normalize(Bpos - ro);
  var col = mix(vec3f(0.015, 0.02, 0.03), vec3f(0.04, 0.055, 0.08), smoothstep(-0.6, 1.2, p.y));
  col += lcol * 0.06 * pow(max(rd.y, 0.0), 3.0);
  let aim = max(dot(rd, toB), 0.0);
  // direct glare: heat SHIMMER wavers the source; blackbody core→halo
  var aimS = aim;
  if (aim > 0.5) {
    let shim = vec3f(vnoise3(rd * 40.0 + vec3f(0.0, time * 1.2, 0.0)) - 0.5, vnoise3(rd * 40.0 + vec3f(7.0, time * 1.1, 3.0)) - 0.5, 0.0) * 0.02;
    aimS = max(dot(normalize(rd + shim), toB), 0.0);
  }
  col += rd2_lampCol(1.0) * pow(aimS, 60.0) * 2.8;
  col += rd2_lampCol(0.5) * pow(aimS, 14.0) * 0.5;
  col += rd2_lampCol(0.12) * pow(aimS, 5.0) * 0.22;
  let hit = rd2_march(ro, rd);
  // volumetric beams — the transported light in the air
  if (aim > 0.08) {
    let tm = min(hit.x, 7.5);
    var beam = vec3f(0.0);
    for (var i = 0; i < 4; i = i + 1) {
      let tt = 1.2 + (f32(i) + 0.5) / 4.0 * (tm - 1.2);
      if (tt > 0.0 && tt < tm) {
        let sp = ro + rd * tt;
        let airn = 0.6 + 0.4 * vnoise3(sp * 1.3 + vec3f(0.0, time * 0.05, time * 0.03));
        beam += rd2_transmit(sp, Bpos, time, -1, true) * airn;
      }
    }
    col += beam * 0.5;
  }
  // DUST MOTES — 12 real particles, rising + wandering, lit by the transport
  // (they only sparkle where light actually reaches; shadowed motes vanish)
  if (aim > 0.03) {
    for (var i = 0; i < 12; i = i + 1) {
      let fi = f32(i);
      let h1 = hash11(fi * 17.31); let h2 = hash11(fi * 31.7 + 5.0); let h3 = hash11(fi * 7.77 + 11.0);
      let rise = fract(h2 + time * 0.014 * (0.5 + h3));
      let mp = vec3f(
        (h1 * 2.0 - 1.0) * 1.5 + 0.3 * sin(time * 0.11 + fi * 2.1),
        -1.05 + rise * 2.5,
        -1.6 + h3 * 3.8 + 0.25 * cos(time * 0.09 + fi * 1.3));
      let wv = mp - ro;
      let tca = dot(wv, rd);
      if (tca > 0.4 && tca < hit.x) {
        let d2 = dot(wv, wv) - tca * tca;
        let spark = exp(-d2 * 5200.0);
        if (spark > 0.02) {
          let fade = 0.55 + 0.45 * sin(time * (0.8 + h1) + fi * 3.7);
          col += rd2_transmit(mp, Bpos, time, -1, false) * spark * fade * 3.2;
        }
      }
    }
  }
  if (hit.y > 0.5) {
    let pos = ro + rd * hit.x;
    let n = rd2_nrm(pos);
    var c = rd2_shade(pos, n, rd, hit.y, L, lcol);
    let Ld = normalize(Bpos - pos);
    if (hit.y > 2.5) {
      let pnl = rd2_panel(pos);
      let v = rd2_vor(pnl.xy, time);
      let arriving = rd2_transmit(pos, Bpos, time, i32(pnl.z), true);
      let trans = abs(dot(n, Ld));
      let fres = pow(1.0 - max(dot(n, -rd), 0.0), 4.0);
      var gc = v.rgb * vec3f(0.16, 0.13, 0.10) + v.rgb * arriving * trans * 9.0;
      let rr = refract(rd, n, 0.92);
      if (dot(rr, rr) > 0.5) {
        let raim = max(dot(normalize(rr), Ld), 0.0);
        gc += v.rgb * arriving * pow(raim, 24.0) * 3.0;
        gc += arriving * pow(raim, 90.0) * 1.5;
      }
      gc += vec3f(0.55, 0.68, 0.95) * fres * 0.4;
      gc += lcol * pow(max(dot(n, normalize(L - rd)), 0.0), 220.0) * 0.6;
      c = gc;
    } else {
      let facing = max(dot(n, Ld), 0.0);
      let arr = rd2_transmit(pos, Bpos, time, -1, true);
      c += arr * 2.0 * facing;
      if (hit.y < 1.5) {
        let r = reflect(rd, n);
        let rh = rd2_march(pos + n * 0.02, r);
        if (rh.y > 1.5) {
          let rp = pos + r * rh.x; let rn = rd2_nrm(rp);
          var rc = rd2_shade(rp, rn, r, rh.y, L, lcol);
          if (rh.y > 2.5) {
            let rpnl = rd2_panel(rp);
            let rv = rd2_vor(rpnl.xy, time);
            let rarr = rd2_transmit(rp, Bpos, time, i32(rpnl.z), true);
            rc = rv.rgb * vec3f(0.16, 0.13, 0.10) + rv.rgb * rarr * abs(dot(rn, normalize(Bpos - rp))) * 7.0;
          }
          c = mix(c, rc, 0.38);
        } else {
          c += rd2_lampCol(0.75) * pow(max(dot(r, toB), 0.0), 6.0) * 0.7;
        }
      }
    }
    let dfog = 1.0 - exp(-hit.x * hit.x * 0.006);
    col = mix(c, vec3f(0.02, 0.03, 0.045), dfog);
  }
  let jit = fract(hash21(uv * 517.0 + vec2f(time * 0.31, 0.0)));
  let fogv = rd2_fogVol(ro, rd, hit.x, time, Bpos, jit);
  col = col * (1.0 - fogv.w) + fogv.rgb;
  let lum = dot(col, vec3f(0.299, 0.587, 0.114));
  col = mix(col, col * vec3f(0.86, 0.96, 1.12), 0.5);
  col += vec3f(0.06, 0.03, 0.10) * smoothstep(0.3, 1.2, lum);
  col *= 1.0 - 0.28 * dot(uv, uv);
  col = col / (col + vec3f(0.55));
  col = pow(max(col, vec3f(0.0)), vec3f(0.86));
  return vec4f(col, 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);
  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 · rd2-spin

revolving door spin: drag to push (inertia + hand friction), motor servo returns to idle; NaN-proof
// rd2-spin — the door as a rotating MASS. Drag to push; hand friction while
// gripped; a motor servo glides it back to the idle turn. NaN-PROOF: a bad
// pointer sample can never poison the state (that froze the door once).
const wd = sim.worldData;
if (!wd.__spin) wd.__spin = { a: 0.6, v: 0.35, px: null };
const S = wd.__spin;
if (!Number.isFinite(S.a)) S.a = 0.6;      // self-heal: NaN never persists
if (!Number.isFinite(S.v)) S.v = 0.35;
const step = Math.min(dt, 0.05);
const inp = wd.input || {};
const ptr = inp.pointer || null;
const down = !!((ptr && ptr.down) || wd.mouse_down);
let px = ptr ? ptr.x : wd.mouse_x;
if (!Number.isFinite(px)) px = null;        // NaN/undefined = no grip
if (down && px !== null) {
  if (S.px !== null) {
    const dx = px - S.px;
    S.v += dx * 0.012;
    S.v = Math.max(-6, Math.min(6, S.v));
  }
  S.px = px;
  S.v *= Math.exp(-step * 0.55);
} else {
  S.px = null;
  S.v += (0.35 - S.v) * step * 1.8;
}
S.a += S.v * step;
const U = Array.isArray(wd.gpuUniforms) ? wd.gpuUniforms.slice() : [];
U[0] = S.a;
wd.gpuUniforms = U;