cartridge.cafe · open source world
FABRIC
HOW TO PLAY
MOVE — a gravitational lens rides your cursor, bending the sky behind it. W/S — grow / shrink the lens mass. A/D — dial chromatic dispersion: each color feels gravity differently, so arcs fringe into rainbows (D for more). SPACE — anchor the current lens (up to 9); the fabric rings. SHIFT — undo the last anchor. ENTER — toggle the grid. R — reset: release every anchor at once. The point: no goal — real thin-lens optics. Einstein rings happen because the math says so.
1 visual shader · 0 shader modules · 1 step hook · 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 · fb_world
fn fb_bg(p: vec2f, t: f32) -> vec3f {
// the unlensed sky: nebula, dust, two starfield depths
var c = vec3f(0.010, 0.008, 0.022);
// nebula — indigo body, rose folds, gold filaments
let q = p * 1.6 + vec2f(0.3, -0.2);
let n1 = fbm(q + vec2f(t * 0.004, 0.0), 5);
let n2 = fbm(q * 2.3 + vec2f(7.0, 3.0) - vec2f(0.0, t * 0.003), 4);
c += vec3f(0.10, 0.05, 0.28) * smoothstep(0.35, 0.85, n1) * 1.1;
c += vec3f(0.30, 0.08, 0.22) * smoothstep(0.55, 0.9, n1 * 0.6 + n2 * 0.6) * 0.9;
c += vec3f(0.55, 0.38, 0.12) * pow(smoothstep(0.62, 0.95, n2), 2.0) * 0.8;
// dark dust lane
c *= 1.0 - 0.55 * smoothstep(0.55, 0.8, fbm(q * 1.3 + vec2f(21.0, 13.0), 4));
// stars — two depths, hue-varied, twinkling
for (var l = 0; l < 2; l++) {
let fl = f32(l);
let sp = p * (14.0 + fl * 26.0) + fl * 31.0;
let cell = floor(sp);
let h = hash21(cell);
let h2 = hash21(cell + 91.0);
let fp = fract(sp) - 0.5 + (vec2f(h, h2) - 0.5) * 0.6;
let star = step(0.982 - fl * 0.006, h) * smoothstep(0.24, 0.02, length(fp));
let tw = 0.55 + 0.45 * sin(t * (0.6 + h * 1.6) + h * 40.0);
let hue = mix(vec3f(0.75, 0.82, 1.05), vec3f(1.05, 0.85, 0.6), h2);
c += hue * star * tw * (1.4 - fl * 0.5);
}
return c;
}
fn fb_mass(i: i32) -> vec3f { return vec3f(uni(9 + i * 3), uni(10 + i * 3), uni(11 + i * 3)); }
// total lens deflection: sample the sky at beta = theta - sum(m_i * d_i / r_i^2)
fn fb_beta(uv: vec2f, mScale: f32, t: f32) -> vec2f {
var beta = uv;
let nA = i32(uni(0));
for (var i = 0; i < 9; i++) {
if (i >= nA) { break; }
let m = fb_mass(i);
let d = uv - m.xy;
let r2 = max(dot(d, d), 0.0004);
beta -= d * (m.z * mScale) / r2;
}
// the cursor lens
let md = uv - vec2f(uni(1), uni(2));
let mr2 = max(dot(md, md), 0.0004);
beta -= md * (uni(3) * mScale) / mr2;
// gravitational ripple from the last anchor drop
let rt = uni(6);
if (rt < 4.0) {
let ro = uv - vec2f(uni(7), uni(8));
let rr = length(ro);
let wave = sin((rr - rt * 0.9) * 26.0) * exp(-pow((rr - rt * 0.9) * 3.2, 2.0)) * exp(-rt * 1.1);
beta += (ro / max(rr, 0.01)) * wave * 0.02;
}
return beta;
}
fn visual_fb_world(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let t = time;
let chroma = uni(5) * 0.9;
// lensed sky as a five-tap SPECTRUM: each band of light feels gravity a
// little differently, so anything that bends smears into a rainbow arc.
// At chroma 0 every tap coincides and the sky is untouched.
var col = vec3f(0.0);
for (var sTap = 0; sTap < 5; sTap++) {
let fs = (f32(sTap) / 4.0 - 0.5) * 2.0;
let b = fb_beta(uv, 1.0 + fs * chroma, t);
let sky = fb_bg(b, t);
var wgt = vec3f(0.05, 0.05, 1.0);
if (sTap == 0) { wgt = vec3f(1.0, 0.05, 0.0); }
else if (sTap == 1) { wgt = vec3f(0.55, 0.45, 0.0); }
else if (sTap == 2) { wgt = vec3f(0.05, 0.9, 0.1); }
else if (sTap == 3) { wgt = vec3f(0.0, 0.45, 0.6); }
col += sky * wgt;
}
col /= vec3f(1.65, 1.9, 1.7);
let bG = fb_beta(uv, 1.0, t);
// spacetime grid, drawn in SOURCE coordinates — lines funnel into the wells
let g01 = uni(4);
if (g01 > 0.01) {
let gc = bG * 7.0;
let gx = smoothstep(0.06, 0.0, abs(fract(gc.x) - 0.5) - 0.44);
let gy = smoothstep(0.06, 0.0, abs(fract(gc.y) - 0.5) - 0.44);
col += vec3f(0.10, 0.22, 0.28) * max(gx, gy) * g01 * 0.8;
}
// the masses themselves: dark cores wearing hot photon rings
let nA = i32(uni(0));
for (var i = 0; i < 10; i++) {
var m: vec3f;
var held = false;
if (i < nA) { m = fb_mass(i); }
else if (i == nA) { m = vec3f(uni(1), uni(2), uni(3)); held = true; }
else { break; }
if (m.z < 0.0005) { continue; }
let r = length(uv - m.xy);
let rs = sqrt(m.z) * 0.55; // event-horizon scale from the Einstein radius
col = mix(col, vec3f(0.0), smoothstep(rs, rs * 0.55, r));
var ringCol = vec3f(3.2, 2.1, 1.0);
if (held) { ringCol = vec3f(1.2, 2.6, 3.2); }
let flick = 0.85 + 0.15 * sin(t * 5.0 + f32(i) * 9.0 + r * 40.0);
let ringW = (34.0 / max(rs, 0.02)) * rs;
let split = chroma * rs * 0.35;
let ringSpec = vec3f(
exp(-pow((r - rs * 1.18 - split) * ringW, 2.0) * 3.0),
exp(-pow((r - rs * 1.18) * ringW, 2.0) * 3.0),
exp(-pow((r - rs * 1.18 + split) * ringW, 2.0) * 3.0));
col += ringCol * ringSpec * flick;
// faint accretion shimmer just outside the ring
let sh = exp(-pow((r - rs * 1.7) * 8.0 / max(rs, 0.05), 2.0));
col += vec3f(0.5, 0.3, 0.15) * sh * (0.4 + 0.3 * sin(t * 1.3 + atan2(uv.y - m.y, uv.x - m.x + 1e-5) * 3.0 - t));
}
// NaN scrub — nothing poisoned may reach the bloom
if (col.x != col.x || col.y != col.y || col.z != col.z) { col = vec3f(0.01, 0.01, 0.02); }
return vec4f(clamp(col, vec3f(0.0), vec3f(60.0)), 1.0);
}— STEP HOOKS (JAVASCRIPT) —
hook · fabric_core
FABRIC: cursor-driven gravitational lens, anchored masses, ripples
try {
const wd = sim.worldData
if (!wd.__fb) wd.__fb = {
anchors: [], mx: 0, my: 0, ms: 0.012,
grid: 0, gridT: 0, chroma: 0.35, rippleT: 99, rx: 0, ry: 0, prev: {},
}
const F = wd.__fb
const dt2 = Math.min(dt, 0.05)
const pressed = k => { const now = !!wd['key_' + k]; const was = !!F.prev[k]; F.prev[k] = now; return now && !was }
// cursor -> lens (normalized field coords, smooth pursuit)
const tx = ((wd.mouse_x ?? 256) - 256) / 256
const ty = ((wd.mouse_y ?? 256) - 256) / 256
F.mx += (tx - F.mx) * Math.min(1, dt2 * 10)
F.my += (ty - F.my) * Math.min(1, dt2 * 10)
// W/S: lens mass (exponential feel)
if (wd.key_w) F.ms = Math.min(0.12, F.ms * (1 + 1.6 * dt2))
if (wd.key_s) F.ms = Math.max(0.001, F.ms * (1 - 1.6 * dt2))
// A/D: dispersion
const chromaWas = F.chroma
if (wd.key_d) F.chroma = Math.min(1, F.chroma + 0.8 * dt2)
if (wd.key_a) F.chroma = Math.max(0, F.chroma - 0.8 * dt2)
if (F.chroma !== chromaWas && typeof window !== 'undefined') {
F.capT = (F.capT || 0) + dt2
if (F.capT > 0.25) {
F.capT = 0
window.dispatchEvent(new CustomEvent('cafe:caption', { detail: {
text: 'dispersion ' + F.chroma.toFixed(2) + (F.chroma < 0.05 ? ' \u00b7 light rejoined' : ' \u00b7 colors part ways near mass'), kind: 'typing' } }))
}
} else if (!wd.key_a && !wd.key_d) F.capT = 99
// SPACE: anchor the current lens; the fabric rings like a bell
if (pressed('space') && F.anchors.length < 9) {
F.anchors.push({ x: F.mx, y: F.my, m: F.ms })
F.rippleT = 0; F.rx = F.mx; F.ry = F.my
}
if (pressed('shift') && F.anchors.length > 0) F.anchors.pop()
if (pressed('enter')) F.grid = 1 - F.grid
// R: reset — every anchor released at once; the fabric rings from center
if (pressed('r') && F.anchors.length > 0) {
F.anchors = []
F.rippleT = 0; F.rx = 0; F.ry = 0
}
F.gridT += ((F.grid ? 1 : 0) - F.gridT) * Math.min(1, dt2 * 4)
F.rippleT += dt2
// anchored masses waltz slowly about the center — the bending never sleeps
const spin = 0.05 * dt2
for (const a of F.anchors) {
const c = Math.cos(spin), s = Math.sin(spin)
const x = a.x * c - a.y * s, y = a.x * s + a.y * c
a.x = x; a.y = y
}
const u = [F.anchors.length, F.mx, F.my, F.ms, F.gridT, F.chroma, F.rippleT, F.rx, F.ry]
for (const a of F.anchors) u.push(a.x, a.y, a.m)
wd.gpuUniforms = u
let world = null
for (const f of sim.fields.values()) if ((f.name || '').startsWith('FABRIC')) world = f
if (world) {
world.name = 'FABRIC \u00b7 ' + F.anchors.length + ' masses \u00b7 lens ' + F.ms.toFixed(3) +
' \u00b7 chroma ' + F.chroma.toFixed(2) + ' \u00b7 move mouse \u00b7 SPACE anchor \u00b7 W/S mass \u00b7 A/D chroma \u00b7 ENTER grid \u00b7 SHIFT undo \u00b7 R reset'
}
} catch (e) { /* keep the sim alive */ }