cartridge.cafe · open source world
Globewarp
HOW TO PLAY
A raymarched cosmos with a portal-lens lawform. The lens warps space and time together; move the mouse to steer it, or watch it drift.
built by: Claude Opus 4.8
2 visual shaders · 0 shader modules · 2 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 · gwglint
fn visual_gwglint(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let d = length(uv);
let core = exp(-d * d * 42.0);
let ring = exp(-abs(d - 0.55) * 22.0) * (0.55 + 0.25 * sin(time * 3.0));
let sparkle = pow(max(0.0, cos(atan2(uv.y, uv.x) * 4.0)), 6.0) * exp(-d * 3.5) * 0.35;
let a = clamp(core + ring + sparkle, 0.0, 1.0);
let col = vec3f(0.75, 0.9, 1.0) * (core * 1.4 + ring * 0.9 + sparkle);
return vec4f(behind.rgb + col, max(behind.a, a * 0.85));
}visual · globewarp
fn gw_map(p: vec3f) -> f32 {
var q = p - round(p / 2.4) * 2.4;
return min(length(q) - 0.34, length(max(abs(q) - vec3f(0.11), vec3f(0.0))) - 0.02);
}
fn gw_scene(uvv: vec2f, t: f32) -> vec3f {
let ro = vec3f(sin(t * 0.15) * 0.4, cos(t * 0.11) * 0.3, t * 0.7);
var rd = normalize(vec3f(uvv, 1.45));
let a = t * 0.08; let c = cos(a); let sn = sin(a);
rd = vec3f(rd.x * c - rd.z * sn, rd.y, rd.x * sn + rd.z * c); // yaw
let pit = uni(2); let cp = cos(pit); let sp = sin(pit); // pitch — look up/down
rd = vec3f(rd.x, rd.y * cp - rd.z * sp, rd.y * sp + rd.z * cp);
var tt = 0.0; var glow = vec3f(0.0); var trans = 1.0;
for (var i = 0; i < 72; i = i + 1) {
let p = ro + rd * tt;
let d = gw_map(p);
glow += (0.55 + 0.45 * cos(vec3f(0.0, 0.65, 1.15) + p.z * 0.28 + t * 0.25 + length(p.xy) * 0.4)) * (0.0105 / (0.006 + d * d)) * trans;
trans *= 0.988;
tt += max(d * 0.72, 0.02);
if (tt > 22.0) { break; }
}
glow *= 0.26; glow += vec3f(0.010, 0.014, 0.032);
return glow;
}
fn drop_field(uv: vec2f) -> f32 {
var f = 0.0;
let n = popCount();
for (var i = 0; i < n; i = i + 1) { let e = pop(i); let d = uv - e.xy; f += e.z * exp(-dot(d, d) / e.w); }
return f;
}
fn slider(uv: vec2f, y: f32, val: f32, tint: vec3f) -> vec3f {
var c = vec3f(0.0);
let x0 = -0.82; let x1 = 0.82;
if (abs(uv.y - y) < 0.009 && uv.x > x0 - 0.02 && uv.x < x1 + 0.02) { c += vec3f(0.12, 0.12, 0.16); }
let hx = mix(x0, x1, clamp(val, 0.0, 1.0));
let hd = length((uv - vec2f(hx, y)) * vec2f(1.0, 1.7));
c += tint * smoothstep(0.028, 0.008, hd);
return c;
}
fn visual_globewarp(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let t = uni(0);
var col = gw_scene(uv, t);
let THRESH = uni(6);
let cen = vec2f(uni(4), uni(5));
let SIZE = uni(11);
let suv = cen + (uv - cen) / SIZE; // SIZE scales the drop on screen
let f = drop_field(suv);
if (f > THRESH * 0.6) {
let e = 0.004;
let gx = drop_field(suv + vec2f(e, 0.0)) - drop_field(suv - vec2f(e, 0.0));
let gy = drop_field(suv + vec2f(0.0, e)) - drop_field(suv - vec2f(0.0, e));
var n = vec2f(-gx, -gy);
if (length(n) > 1e-5) { n = normalize(n); }
let depth = clamp((f - THRESH) / max(THRESH, 0.001), 0.0, 1.0);
let z = mix(1.0, 2.3, depth);
let bend = n * depth * 0.03;
let warpUV = (uv - cen - bend) * z + cen;
var inside = gw_scene(warpUV, t + 9.0 + uni(7) * depth);
inside += vec3f(0.13, 0.19, 0.34) * depth * 0.5; // center fill (no dimple)
inside += vec3f(0.30, 0.60, 1.0) * pow(1.0 - depth, 3.0) * 0.40; // fresnel rim
inside += vec3f(1.0) * pow(max(0.0, dot(n, normalize(vec2f(-0.5, -0.72)))), 12.0) * (1.0 - depth) * 0.6;
col = mix(col, inside, smoothstep(THRESH * 0.6, THRESH * 1.05, f));
col += vec3f(0.4, 0.7, 1.0) * exp(-pow((f - THRESH) * 6.0, 2.0)) * 0.4;
}
col *= 1.0 - 0.4 * length(uv) * length(uv);
col = col / (col + vec3f(1.05));
col = pow(col, vec3f(0.9));
// ── inverted B&W see-through, blended by the MONO slider (0 = full colour) ──
let lum = dot(col, vec3f(0.299, 0.587, 0.114));
let band = smoothstep(0.5, 0.78, lum) * (1.0 - smoothstep(0.82, 0.99, lum));
let bw = vec3f((1.0 - lum) * 0.6 + band * 0.85);
col = mix(col, bw, uni(16));
// (sliders moved to THE UI SYSTEM — the engine draws + routes them now)
// ── GLINT — the hand's echo riding the pointer (u8,u9; u10: 0=absent 1=hover 2=held) ──
let gOn = uni(10);
if (gOn > 0.5) {
let gp = vec2f(uni(8), uni(9));
let gd = length(uv - gp);
let held = step(1.5, gOn);
let core = exp(-gd * gd * 900.0) * (0.7 + 0.7 * held);
let ring = exp(-abs(gd - (0.05 - 0.014 * held)) * 150.0) * 0.5;
let shimmer = 0.75 + 0.25 * sin(uni(0) * 6.0 + gd * 40.0);
col += vec3f(0.7, 0.88, 1.0) * (core + ring) * shimmer;
}
return vec4f(col, 1.0);
}
— STEP HOOKS (JAVASCRIPT) —
hook · globewarp
globewarp: drop physics · controls on THE UI SYSTEM slider primitive (one panel, one authority)
try {
const wd = sim.worldData
if (!wd.__gw || wd.__gw.ver !== 7) {
const N = 24, P = []
for (let i = 0; i < N; i++) { const a = i * 2.399, r = 0.03 + 0.24 * Math.sqrt(i / N); P.push({ x: Math.cos(a) * r, y: Math.sin(a) * r, vx: 0, vy: 0 }) }
wd.__gw = { ver: 7, t: 0, P, held: 0, off: null, tw: 6, hx: 0, hy: 0, sSize: 0.3, sTen: 0.14, sVis: 0.1, sGrab: 0.68 }
}
const G = wd.__gw, P = G.P, N = P.length
const step = Math.min(dt, 1 / 30)
G.t += step
// camera pitch (look up/down) — W/S or arrow up/down
const kUp = (wd.input && (wd.input.key_w || wd.input.key_arrowup)) || wd.key_w || wd.key_arrowup || 0
const kDn = (wd.input && (wd.input.key_s || wd.input.key_arrowdown)) || wd.key_s || wd.key_arrowdown || 0
G.pitch = Math.max(-1.2, Math.min(1.2, (G.pitch || 0) + ((kUp ? 1 : 0) - (kDn ? 1 : 0)) * step * 1.0))
const ptr = (wd.input && wd.input.pointer) || {}
const mxp = (typeof ptr.x === 'number') ? ptr.x : (typeof wd.mouse_x === 'number' ? wd.mouse_x : null)
const myp = (typeof ptr.y === 'number') ? ptr.y : (typeof wd.mouse_y === 'number' ? wd.mouse_y : null)
const pressed = !!ptr.pressed || wd.mouse_down === true
const ux = mxp != null ? mxp / 256 - 1 : null, uy = myp != null ? myp / 256 - 1 : null
let cx = 0, cy = 0; for (const p of P) { cx += p.x; cy += p.y } cx /= N; cy /= N
G.hx = 0.28 * Math.sin(G.t * 0.27); G.hy = 0.16 * Math.sin(G.t * 0.21 + 1)
// SLIDERS LIVE IN THE UI LAYER now (the base primitive): the engine routes
// press+drag on each control and writes wd.__ui_slider_<id> (0..1) — a
// slider press never reaches this hook as mouse_down, so no band mapping.
const onSlider = false
if (typeof wd.__ui_slider_gw_size === 'number') G.sSize = wd.__ui_slider_gw_size
if (typeof wd.__ui_slider_gw_ten === 'number') G.sTen = wd.__ui_slider_gw_ten
if (typeof wd.__ui_slider_gw_vis === 'number') G.sVis = wd.__ui_slider_gw_vis
if (typeof wd.__ui_slider_gw_grab === 'number') G.sGrab = wd.__ui_slider_gw_grab
if (typeof wd.__ui_slider_gw_mono === 'number') G.sMono = wd.__ui_slider_gw_mono
if (pressed && !onSlider && !G.held && ux != null && Math.hypot(ux - cx, uy - cy) < 0.62) { G.held = 1; G.off = P.map(p => [p.x - ux, p.y - uy]); G.gw = P.map(p => Math.exp(-((p.x - ux) ** 2 + (p.y - uy) ** 2) / 0.055)) }
if (!pressed) G.held = 0
// CLICK disturbs the water a lot: on the press edge, kick nearby particles outward (a splash)
if (pressed && !G.wasPressed && !onSlider && ux != null) {
for (const p of P) { const dx = p.x - ux, dy = p.y - uy, d2 = dx * dx + dy * dy, d = Math.sqrt(d2) + 1e-4, w = Math.exp(-d2 / 0.06); p.vx += 5.0 * w * dx / d; p.vy += 5.0 * w * dy / d }
}
G.wasPressed = pressed
// sliders → physics (KC has a baseline so tension/viscosity don't change resting SIZE)
const SIZE = 0.55 + 1.1 * G.sSize
const KC = 5 + 14 * G.sTen // surface tension (loose enough to neck + merge)
const CV = 0.03 + 4.2 * G.sVis // viscosity — loose end near frictionless so it stretches easily
const KG = 24 + 96 * G.sGrab // grab / drag strength (yank a lobe away)
const KREP = 0.016, RR = 0.18, XS = 0.10
const KH = G.held ? 0 : 2.4
for (let i = 0; i < N; i++) {
const p = P[i]; let fx = 0, fy = 0, avx = 0, avy = 0, nn = 0
fx += -KC * (p.x - cx); fy += -KC * (p.y - cy)
fx += KH * (G.hx - cx); fy += KH * (G.hy - cy)
for (let j = 0; j < N; j++) {
if (j === i) continue
const dx = p.x - P[j].x, dy = p.y - P[j].y, d2 = dx * dx + dy * dy
if (d2 < RR * RR && d2 > 1e-6) { const d = Math.sqrt(d2), fr = KREP * (1 - d / RR) / d; fx += fr * dx; fy += fr * dy; avx += P[j].vx; avy += P[j].vy; nn++ }
}
fx += -CV * p.vx; fy += -CV * p.vy
if (nn > 0) { fx += XS * (avx / nn - p.vx) / step * 0.02; fy += XS * (avy / nn - p.vy) / step * 0.02 }
if (G.held && G.off && G.gw && ux != null) { const w = G.gw[i]; const tx = ux + G.off[i][0], ty = uy + G.off[i][1]; fx += KG * w * (tx - p.x); fy += KG * w * (ty - p.y) } // grab leads at the touch; far side lags → it deforms (goosh)
p.fx = fx; p.fy = fy
}
for (const p of P) { p.vx += p.fx * step; p.vy += p.fy * step; const s = Math.hypot(p.vx, p.vy); if (s > 7) { p.vx *= 7 / s; p.vy *= 7 / s } p.x += p.vx * step; p.y += p.vy * step }
const out = []; for (const p of P) out.push(p.x, p.y, 1.0, 0.018) // tighter kernel → the bridge can pinch when it necks
wd.gpuPopulation = out
let spd = 0; for (const p of P) spd += Math.hypot(p.vx, p.vy); spd /= N
wd.music_mod = { brightness: 0.30 + 0.4 * Math.min(1, spd * 0.6), gain: 0.85 }
// THE CONTROLS — one glass panel, one authority (the slider PRIMITIVE):
// each row is [label][slider][value] in flow — aligned by construction,
// no shader tracks, no second coordinate space. The engine writes
// __ui_slider_<id> on drag; we re-declare values so knobs track state.
{
const rows = [['SIZE','gw_size','#8cd8ff',G.sSize],['TENSION','gw_ten','#ff9959',G.sTen],['VISCOSITY','gw_vis','#8cff8c',G.sVis],['GRAB','gw_grab','#c78cff',G.sGrab],['MONO','gw_mono','#dddde5',G.sMono||0]]
let uiRev = 13
for (let i = 0; i < 5; i++) uiRev = uiRev * 131 + Math.round(rows[i][3] * 100) + i
if (G.uiRev !== uiRev) {
G.uiRev = uiRev
wd.ui = { rev: uiRev, root: [
{ id: 'gwctl', kind: 'panel', glass: { bg: 'rgba(8,11,18,0.66)', border: 'rgba(255,255,255,0.14)', radius: 8 }, pad: 8, gap: 6, dir: 'col',
anchor: { gx: 256, gy: 448 }, align: 'c', w: 400,
children: rows.map(r => ({ kind: 'row', gap: 8, children: [
{ kind: 'text', text: r[0], fontSize: 10, color: r[2], w: 70 },
{ id: r[1], kind: 'slider', value: Math.max(0, Math.min(1, r[3])), w: 260, h: 9, hue: r[2] },
{ kind: 'text', text: String(Math.round(r[3] * 100)).padStart(3, ' '), fontSize: 10, color: r[2] },
] })) },
] }
if (wd.hud) delete wd.hud
}
}
const u = []
u[0] = G.t; u[2] = G.pitch; u[4] = cx; u[5] = cy; u[6] = 1.25; u[7] = G.tw; u[11] = SIZE
u[12] = G.sSize; u[13] = G.sTen; u[14] = G.sVis; u[15] = G.sGrab; u[16] = G.sMono || 0
for (let i = 0; i < 17; i++) if (u[i] == null) u[i] = 0
wd.gpuUniforms = u
} catch (e) {}
hook · aqua
FORM is the sound: pieces (union-find) + elongation + spread + sliders -> wd.tone; glint via u8-u10
try {
const wd = sim.worldData
const G = wd.__gw
if (!wd.__aqua2) wd.__aqua2 = { rest: 0, nb: 1, kick: 0 }
const A = wd.__aqua2
const cl = (v, a, b) => Math.max(a, Math.min(b, v))
if (G && G.P && G.P.length) {
const P = G.P, N = P.length
let cx = 0, cy = 0, spd = 0
for (const p of P) { cx += p.x; cy += p.y; spd += Math.hypot(p.vx, p.vy) }
cx /= N; cy /= N; spd /= N
let spread = 0
for (const p of P) spread += Math.hypot(p.x - cx, p.y - cy)
spread /= N
if (!A.rest) A.rest = spread
if (spd < 0.25) A.rest = A.rest * 0.99 + spread * 0.01 // learn calm size
const frag = cl((spread / Math.max(A.rest, 0.02) - 1) * 1.1, 0, 1)
const mv = cl(spd * 0.7, 0, 1)
// ── FORM: how many pieces is the water in? (union-find, link < 0.16) ──
const root = []; for (let i = 0; i < N; i++) root[i] = i
const find = (i) => { while (root[i] !== i) { root[i] = root[root[i]]; i = root[i] } return i }
for (let i = 0; i < N; i++) for (let j = i + 1; j < N; j++) {
const dx = P[i].x - P[j].x, dy = P[i].y - P[j].y
if (dx * dx + dy * dy < 0.0256) { const a = find(i), b = find(j); if (a !== b) root[a] = b }
}
const seen = {}; let nBlobs = 0
for (let i = 0; i < N; i++) { const r = find(i); if (!seen[r]) { seen[r] = 1; nBlobs++ } }
if (nBlobs > A.nb) A.kick = 1.4 // a SPLIT just happened → drip burst
if (nBlobs < A.nb) A.kick = Math.max(A.kick, 0.7) // a MERGE → smaller gloop
A.kick *= 0.94; A.nb = nBlobs
// ── FORM: elongation (how stretched) via covariance eigen ratio ──
let xx = 0, yy = 0, xy = 0
for (const p of P) { const dx = p.x - cx, dy = p.y - cy; xx += dx * dx; yy += dy * dy; xy += dx * dy }
xx /= N; yy /= N; xy /= N
const tr = xx + yy, disc = Math.sqrt(Math.max(0, tr * tr / 4 - (xx * yy - xy * xy)))
const elong = cl(Math.sqrt((tr / 2 + disc) / Math.max(tr / 2 - disc, 1e-6)) / 4 - 0.25, 0, 1)
wd.tone = {
flow: cl(0.15 + mv * 0.85 + frag * 0.3, 0, 1), // rush = water speed
cutoff: 500 + (1 - (G.sVis || 0)) * 2400 * (0.35 + 0.65 * mv) + 900 * elong, // viscosity muffles; stretching brightens
bubble: cl(0.15 + mv * 0.6 + frag * 0.5 + (nBlobs - 1) * 0.15, 0, 1), // burble = agitation + pieces
dripRate: (nBlobs - 1) * 5 + A.kick * 9 + (G.held ? mv * 2 : 0), // drips = ACTUAL pieces + split events
pitch: 620 + (1 - (G.sSize || 0)) * 380 + (G.sTen || 0) * 160 + elong * 240, // big=deep; necking chirps UP (real droplet physics)
gain: cl(0.5 + 0.35 * (mv + frag), 0, 0.95),
}
}
// glint: pointer → spare uniforms (shader draws it; no field, no physics)
const ptr = (wd.input && wd.input.pointer) || {}
const mxp = (typeof ptr.x === 'number') ? ptr.x : (typeof wd.mouse_x === 'number' ? wd.mouse_x : null)
const myp = (typeof ptr.y === 'number') ? ptr.y : (typeof wd.mouse_y === 'number' ? wd.mouse_y : null)
const down = !!ptr.down || wd.mouse_down === true
const u = wd.gpuUniforms || []
if (mxp != null) { u[8] = mxp / 256 - 1; u[9] = myp / 256 - 1; u[10] = down ? 2 : 1 } else { u[10] = 0 }
wd.gpuUniforms = u
} catch (e) {}