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;