cartridge.cafe · open source world
juggling head game
HOW TO PLAY
MOUSE — move both hands at the bottom Heads start in the air. Hit them with the TOP of your hands (breakout-style) to launch upward. Each launch adds to your juggle streak. A head that hits the floor respawns at the centre and resets the streak. Beat your best consecutive juggles.
built by: Claude (Cursor)
2 visual shaders · 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 · juggle
fn jn_rot2(a: f32) -> mat2x2<f32> {
let s = sin(a);
let c = cos(a);
return mat2x2<f32>(vec2f(c, s), vec2f(-s, c));
}
fn jn_hash21(p: vec2f) -> f32 {
var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
p3 = p3 + dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
fn jn_mask(d: f32, aa: f32) -> f32 {
return 1.0 - smoothstep(-aa, aa, d);
}
fn jn_sdSegment(p: vec2f, a: vec2f, b: vec2f) -> f32 {
let pa = p - a;
let ba = b - a;
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
fn jn_lineSeg(p: vec2f, a: vec2f, b: vec2f, w: f32) -> f32 {
let d = jn_sdSegment(p, a, b);
return 1.0 - smoothstep(w, w + 2.2, d);
}
fn visual_juggle(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let pc = vec2f(256.0 + uv.x * 256.0, 256.0 + uv.y * 256.0);
let handL = vec2f(uni(0), uni(1));
let handRgt = vec2f(uni(2), uni(3));
let head0 = vec2f(uni(4), uni(5));
let head1 = vec2f(uni(6), uni(7));
let head2 = vec2f(uni(8), uni(9));
let heads = array<vec2f, 3>(head0, head1, head2);
let vivid = vec3f(1.0, 0.0, 0.06);
var col = vec3f(0.09, 0.08, 0.11);
let gx = i32(clamp(floor(pc.x * 19.0 / 512.0), 0.0, 18.0));
let gy = i32(clamp(floor(pc.y * 12.0 / 512.0), 0.0, 11.0));
let gix = gy * 19 + gx;
if (uni(28 + gix) > 0.12) {
col = vivid;
}
col += vec3f(0.02, 0.015, 0.02) * jn_hash21(pc * 0.08);
let floorY = 486.0;
let floorBand = 1.0 - smoothstep(floorY - 2.0, floorY + 8.0, pc.y);
col = mix(col, vec3f(0.16, 0.12, 0.11), floorBand * 0.85);
let bodyC = vec2f(uni(26), uni(27));
let shL = vec2f(uni(22), uni(23));
let shR = vec2f(uni(24), uni(25));
let elL = vec2f(uni(18), uni(19));
let elR = vec2f(uni(20), uni(21));
let chestC = bodyC + vec2f(0.0, -14.0);
let chestD = length((pc - chestC) / vec2f(72.0, 38.0)) - 1.0;
let pelvisD = length((pc - bodyC) / vec2f(58.0, 48.0)) - 1.0;
let torso = clamp(jn_mask(chestD, 1.6) + jn_mask(pelvisD, 1.5), 0.0, 1.0);
let torsoTop = step(pc.y, bodyC.y - 38.0);
col = mix(col, vec3f(0.30, 0.26, 0.32), torso * torsoTop * 0.94);
let collar = 1.0 - smoothstep(0.010, 0.028, abs(pc.y - (bodyC.y - 36.0)));
col += vec3f(0.40, 0.36, 0.42) * collar * (1.0 - smoothstep(38.0, 44.0, abs(pc.x - bodyC.x))) * 0.42;
let armCol = vec3f(0.42, 0.36, 0.40);
let shPad = jn_mask(length(pc - shL) - 11.0, 1.4) + jn_mask(length(pc - shR) - 11.0, 1.4);
let uaL = jn_lineSeg(pc, shL, elL, 10.0);
let faL = jn_lineSeg(pc, elL, handL, 8.5);
let uaR = jn_lineSeg(pc, shR, elR, 10.0);
let faR = jn_lineSeg(pc, elR, handRgt, 8.5);
col = mix(col, armCol, clamp(shPad + uaL + faL + uaR + faR, 0.0, 1.0) * 0.92);
let hRad = 26.0;
let headR = 17.0;
for (var hi = 0; hi < 3; hi++) {
let hpos = heads[hi];
let hd = length(pc - hpos) - headR;
let hmask = jn_mask(hd, 1.2);
let skin = vec3f(0.96, 0.78, 0.62);
col = mix(col, skin, hmask * 0.95);
let eyeL = jn_mask(length(pc - (hpos + vec2f(-6.0, -3.0))) - 3.2, 0.8);
let eyeR = jn_mask(length(pc - (hpos + vec2f(6.0, -3.0))) - 3.2, 0.8);
col = mix(col, vec3f(0.12, 0.08, 0.08), max(eyeL, eyeR));
let mouthD = length((pc - hpos) / vec2f(8.0, 5.0) - vec2f(0.0, 2.5)) - 1.0;
col = mix(col, vec3f(0.55, 0.12, 0.12), jn_mask(mouthD, 0.6) * 0.7);
col += vec3f(1.0, 0.35, 0.2) * (1.0 - smoothstep(0.0, 3.0, abs(hd))) * 0.22;
}
let gdL = length(pc - handL) - hRad;
let gmaskL = jn_mask(gdL, 1.1);
col = mix(col, vec3f(0.88, 0.72, 0.58), gmaskL);
col += vec3f(1.0, 0.9, 0.75) * (1.0 - smoothstep(0.0, 2.5, abs(gdL))) * 0.18;
let gdR = length(pc - handRgt) - hRad;
let gmaskR = jn_mask(gdR, 1.1);
col = mix(col, vec3f(0.88, 0.72, 0.58), gmaskR);
col += vec3f(1.0, 0.9, 0.75) * (1.0 - smoothstep(0.0, 2.5, abs(gdR))) * 0.18;
return vec4f(col, 1.0);
}
visual · juice_plate
fn visual_juice_plate(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
return vec4f(0.0);
}— STEP HOOKS (JAVASCRIPT) —
hook · juggle
revert uniform faces
const wd = sim.worldData;
const inp = wd.input || { pointer: {} };
const ptr = inp.pointer || {};
wd.__resets = ['__g'];
if (wd.__fresh) delete wd.__fresh;
function spawnHeadInAir(head, slot) {
const offsets = [-44, 0, 44];
const off = offsets[slot] || 0;
head.x = 256 + off;
head.y = 188 + slot * 18;
head.vx = off * 0.55;
head.vy = -(105 + slot * 18);
}
if (!wd.__g) {
wd.__g = {
body: { x: 256, y: 452 },
handL: { x: 170, y: 430 },
handR: { x: 342, y: 430 },
heads: [{ x: 0, y: 0, vx: 0, vy: 0 }, { x: 0, y: 0, vx: 0, vy: 0 }, { x: 0, y: 0, vx: 0, vy: 0 }],
lastH: [
{ x: 212, y: 188 },
{ x: 256, y: 206 },
{ x: 300, y: 224 },
],
elbowL: { x: 200, y: 410 },
elbowR: { x: 312, y: 410 },
bestRed: 0,
};
for (let i = 0; i < 3; i++) spawnHeadInAir(wd.__g.heads[i], i);
}
const G = wd.__g;
if (!G.body) G.body = { x: 256, y: 452 };
if (!G.handL) G.handL = { x: 170, y: 430 };
if (!G.handR) G.handR = { x: 342, y: 430 };
if (!G.elbowL) G.elbowL = { x: 200, y: 410 };
if (!G.elbowR) G.elbowR = { x: 312, y: 410 };
if (!G.heads || G.heads.length < 3) {
G.heads = [{ x: 0, y: 0, vx: 0, vy: 0 }, { x: 0, y: 0, vx: 0, vy: 0 }, { x: 0, y: 0, vx: 0, vy: 0 }];
for (let i = 0; i < 3; i++) spawnHeadInAir(G.heads[i], i);
}
if (!G.lastH) G.lastH = G.heads.map((h) => ({ x: h.x, y: h.y }));
const GW = 19;
const GH = 12;
const GCELLS = GW * GH;
if (G.bestRed == null) {
G.bestRed = G.bestStreak != null ? G.bestStreak : 0;
}
if (G.bestRed > GCELLS) G.bestRed = 0;
if (!G.grid || G.grid.length !== GCELLS) {
G.grid = new Array(GCELLS).fill(0);
}
function countInkCells(g) {
let n = 0;
for (let i = 0; i < g.length; i++) {
if (g[i] > 0) n += 1;
}
return n;
}
function clearPaint(g) {
for (let i = 0; i < g.length; i++) g[i] = 0;
}
function stampDisk(g, x, y, radius) {
const cx = (x / 512) * GW;
const cy = (y / 512) * GH;
const rx = (radius / 512) * GW + 0.55;
const ry = (radius / 512) * GH + 0.55;
const x0 = Math.max(0, Math.floor(cx - rx));
const x1 = Math.min(GW - 1, Math.ceil(cx + rx));
const y0 = Math.max(0, Math.floor(cy - ry));
const y1 = Math.min(GH - 1, Math.ceil(cy + ry));
for (let iy = y0; iy <= y1; iy++) {
for (let ix = x0; ix <= x1; ix++) {
const dx = (ix + 0.5) / GW - x / 512;
const dy = (iy + 0.5) / GH - y / 512;
if (dx * dx + dy * dy <= (radius / 512) * (radius / 512) * 2.8) {
g[iy * GW + ix] = 1;
}
}
}
}
function stampSeg(g, ax, ay, bx, by, radius) {
const dist = Math.hypot(bx - ax, by - ay);
const steps = Math.max(2, Math.ceil(dist / 6));
for (let k = 0; k <= steps; k++) {
const t = k / steps;
stampDisk(g, ax + (bx - ax) * t, ay + (by - ay) * t, radius);
}
}
function playPunchSplat() {
wd.__play_sound = [
{ frequency: 520, duration: 0.05, volume: 0.22, type: 'triangle' },
{ frequency: 280, duration: 0.07, volume: 0.14, type: 'sine' },
];
}
function playDeathSplat() {
wd.__play_sound = [
{ frequency: 72, duration: 0.42, volume: 0.58, type: 'sawtooth' },
{ frequency: 48, duration: 0.55, volume: 0.5, type: 'square' },
{ frequency: 110, duration: 0.18, volume: 0.32, type: 'triangle' },
];
}
const pdt = Math.min(dt, 0.05);
const handR = 26;
const headR = 17;
const floorY = 486;
const minX = 24;
const maxX = 488;
const minY = 40;
const mx = ptr.x != null ? ptr.x : 256;
const my = ptr.y != null ? ptr.y : 400;
const handY = Math.max(395, Math.min(472, my));
const spread = 72 + (handY - 395) * 0.08;
const tgtLx = Math.max(minX + handR, Math.min(mx - spread * 0.55, 248));
const tgtRx = Math.max(264, Math.min(mx + spread * 0.55, maxX - handR));
const smooth = Math.min(1, 14 * pdt);
G.handL.x += (tgtLx - G.handL.x) * smooth;
G.handL.y += (handY - G.handL.y) * smooth;
G.handR.x += (tgtRx - G.handR.x) * smooth;
G.handR.y += (handY - G.handR.y) * smooth;
const body = G.body;
const shoulderL = { x: body.x - 48, y: body.y - 48 };
const shoulderR = { x: body.x + 48, y: body.y - 48 };
const L1 = 58;
const L2 = 52;
function solveIK(shoulder, hand, bend) {
const dx = hand.x - shoulder.x;
const dy = hand.y - shoulder.y;
let d = Math.hypot(dx, dy);
const maxReach = L1 + L2 - 2;
const minReach = Math.abs(L1 - L2) + 2;
d = Math.max(minReach, Math.min(maxReach, d));
const base = Math.atan2(dy, dx);
const c2 = (L1 * L1 + d * d - L2 * L2) / (2 * L1 * d);
const knee = Math.acos(Math.max(-1, Math.min(1, c2)));
const a1 = base + bend * knee;
return {
x: shoulder.x + Math.cos(a1) * L1,
y: shoulder.y + Math.sin(a1) * L1,
};
}
G.elbowL = solveIK(shoulderL, G.handL, 1);
G.elbowR = solveIK(shoulderR, G.handR, -1);
const grav = 520 * pdt;
function bouncePaddleTop(head, hand) {
const dx = head.x - hand.x;
const paddleTop = hand.y - handR * 0.78;
const halfW = handR * 1.12;
if (Math.abs(dx) > halfW + headR) return false;
if (head.vy <= 0) return false;
if (head.y < paddleTop - headR - 6) return false;
if (head.y > hand.y + handR * 0.25) return false;
head.y = paddleTop - headR - 0.5;
const lift = 300 + Math.min(220, head.vy * 0.45);
head.vy = -lift;
head.vx = dx * 4.5 + head.vx * 0.55;
return true;
}
for (let i = 0; i < G.heads.length; i++) {
const head = G.heads[i];
head.vy += grav;
head.x += head.vx * pdt;
head.y += head.vy * pdt;
head.vx *= 1 - 0.28 * pdt;
if (head.x < minX + headR) {
head.x = minX + headR;
head.vx = Math.abs(head.vx) * 0.82;
}
if (head.x > maxX - headR) {
head.x = maxX - headR;
head.vx = -Math.abs(head.vx) * 0.82;
}
if (head.y < minY + headR) {
head.y = minY + headR;
head.vy = Math.abs(head.vy) * 0.75;
}
if (bouncePaddleTop(head, G.handL)) playPunchSplat();
if (bouncePaddleTop(head, G.handR)) playPunchSplat();
if (head.y >= floorY - headR) {
const roundScore = countInkCells(G.grid);
if (roundScore > G.bestRed) G.bestRed = roundScore;
playDeathSplat();
clearPaint(G.grid);
spawnHeadInAir(head, i);
G.lastH[i].x = head.x;
G.lastH[i].y = head.y;
}
}
const heads = G.heads;
const last = G.lastH;
for (let i = 0; i < 3; i++) {
stampSeg(G.grid, last[i].x, last[i].y, heads[i].x, heads[i].y, 11);
stampDisk(G.grid, heads[i].x, heads[i].y, 12);
}
const redNow = countInkCells(G.grid);
if (redNow > G.bestRed) G.bestRed = redNow;
const u = [
G.handL.x,
G.handL.y,
G.handR.x,
G.handR.y,
heads[0].x,
heads[0].y,
heads[1].x,
heads[1].y,
heads[2].x,
heads[2].y,
0,
0,
last[0].x,
last[0].y,
last[1].x,
last[1].y,
last[2].x,
last[2].y,
G.elbowL.x,
G.elbowL.y,
G.elbowR.x,
G.elbowR.y,
shoulderL.x,
shoulderL.y,
shoulderR.x,
shoulderR.y,
body.x,
body.y,
];
for (let i = 0; i < GCELLS; i++) u.push(G.grid[i]);
wd.gpuUniforms = u;
for (let i = 0; i < 3; i++) {
last[i].x = heads[i].x;
last[i].y = heads[i].y;
}
const scoreLine = 'PAINT ' + redNow + ' / ' + GCELLS;
const bestLine = 'BEST ' + G.bestRed;
wd.hud = [
{
id: 'score',
type: 'text',
x: '0',
y: '70px',
width: '100%',
textAlign: 'center',
text: scoreLine + '\n' + bestLine,
fontSize: '30px',
color: '#ff4444',
},
];