cartridge.cafe · open source world
CAN YOU FEEL NOTHING?
HOW TO PLAY
CLICK — eyes/mouth/nose; child head (flame); WELL WATER (splash) HOLD near child — reach Both eyes closed: mouth = happy closed ↔ open. Eyes open: mouth = horrified closed ↔ open. You are the face at the well.
built by: Claude (Cursor)
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 · feel_nothing
fn cf_rot2(a: f32) -> mat2x2<f32> {
let s = sin(a);
let c = cos(a);
return mat2x2<f32>(vec2f(c, s), vec2f(-s, c));
}
fn cf_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 cf_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 cf_vnoise(p: vec2f) -> f32 {
let i = floor(p);
let f = fract(p);
let u = f * f * (3.0 - 2.0 * f);
let a = cf_hash21(i + vec2f(0.0, 0.0));
let b = cf_hash21(i + vec2f(1.0, 0.0));
let c = cf_hash21(i + vec2f(0.0, 1.0));
let d = cf_hash21(i + vec2f(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
fn cf_mask(d: f32, aa: f32) -> f32 {
return 1.0 - smoothstep(-aa, aa, d);
}
fn cf_ellipse(p: vec2f, c: vec2f, r: vec2f, a: f32) -> f32 {
let q = cf_rot2(a) * (p - c);
return (length(q / r) - 1.0) * min(r.x, r.y);
}
fn cf_line(p: vec2f, a: vec2f, b: vec2f, w: f32) -> f32 {
let d = cf_sdSegment(p, a, b);
return 1.0 - smoothstep(w, w + 0.008, d);
}
fn visual_feel_nothing(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let p = vec2f(uv.x, -uv.y);
let pointer = vec2f(uni(0), uni(1));
let held = clamp(uni(2), 0.0, 1.0);
let pulse = clamp(uni(3), 0.0, 1.0);
let leftOpen = clamp(uni(4), 0.0, 1.0);
let rightOpen = clamp(uni(5), 0.0, 1.0);
let mouthOpenState = clamp(uni(6), 0.0, 1.0);
let selectedPart = uni(7);
let childFlame = clamp(uni(8), 0.0, 1.0);
let headShow = 1.0 - childFlame;
let childHead = vec2f(-0.205, -0.255);
let proximity = 1.0 - smoothstep(0.12, 1.05, length(pointer - childHead));
let reaching = held * smoothstep(0.10, 0.52, proximity);
let concern = clamp(max(proximity * 0.88, reaching) + pulse * 0.22, 0.0, 1.0);
let breath = 0.94 + 0.06 * sin(time * 0.72);
var col = mix(vec3f(0.040, 0.052, 0.075), vec3f(0.105, 0.125, 0.165), clamp(p.y * 0.5 + 0.5, 0.0, 1.0));
col += vec3f(0.055, 0.065, 0.085) * exp(-dot(p - vec2f(0.0, 0.05), p - vec2f(0.0, 0.05)) * 0.75);
let wall = cf_vnoise(p * 4.0 + vec2f(0.0, time * 0.008));
col += vec3f(0.018, 0.020, 0.025) * wall * 0.45;
col *= 1.0 - 0.24 * pow(clamp(length(p) * 0.72, 0.0, 1.0), 3.0);
let grid = floor((p + vec2f(1.0)) * 28.0);
let cell = fract((p + vec2f(1.0)) * 28.0) - vec2f(0.5);
let dustSeed = cf_hash21(grid);
let dust = step(0.986, dustSeed) * (1.0 - smoothstep(0.035, 0.10, length(cell)));
col += vec3f(0.20, 0.22, 0.27) * dust * (0.15 + 0.25 * breath);
let warm = mix(vec3f(0.42, 0.48, 0.62), vec3f(1.75, 0.42, 0.18), concern);
let headD = cf_ellipse(p, vec2f(0.0, 0.31), vec2f(0.54, 0.49), 0.0);
let headFill = cf_mask(headD, 0.012);
let headRim = 1.0 - smoothstep(0.008, 0.028, abs(headD));
col = mix(col, vec3f(0.125, 0.140, 0.175) + warm * 0.035, headFill * 0.88);
col += warm * headRim * (0.16 + concern * 0.30) * breath;
let gaze = clamp((pointer - vec2f(0.0, 0.10)) * 0.09, vec2f(-0.045), vec2f(0.045));
let eyeLOpen = step(0.5, leftOpen);
let eyeROpen = step(0.5, rightOpen);
let eyeH = 0.018 + concern * 0.062;
let eyeLH = mix(0.004, eyeH, eyeLOpen);
let eyeRH = mix(0.004, eyeH, eyeROpen);
let eyeLD = cf_ellipse(p, vec2f(-0.205, 0.36), vec2f(0.145, eyeLH), -0.03);
let eyeRD = cf_ellipse(p, vec2f(0.205, 0.36), vec2f(0.145, eyeRH), 0.03);
let eyeLMask = cf_mask(eyeLD, 0.008);
let eyeRMask = cf_mask(eyeRD, 0.008);
let eyeMask = max(eyeLMask, eyeRMask);
col = mix(col, vec3f(0.78, 0.79, 0.76) + warm * concern * 0.22, eyeMask * (0.68 + concern * 0.25));
let pupilL = cf_mask(length(p - (vec2f(-0.205, 0.36) + gaze)) - (0.020 + concern * 0.019), 0.006) * eyeLOpen;
let pupilR = cf_mask(length(p - (vec2f(0.205, 0.36) + gaze)) - (0.020 + concern * 0.019), 0.006) * eyeROpen;
col = mix(col, vec3f(0.002, 0.003, 0.005), max(pupilL, pupilR));
col += warm * max(pupilL, pupilR) * pulse * 0.40;
let hoverL = 1.0 - smoothstep(0.12, 0.19, length(pointer - vec2f(-0.205, 0.36)));
let hoverR = 1.0 - smoothstep(0.12, 0.19, length(pointer - vec2f(0.205, 0.36)));
let selectedL = 1.0 - smoothstep(0.10, 0.20, abs(selectedPart - 1.0));
let selectedR = 1.0 - smoothstep(0.10, 0.20, abs(selectedPart - 2.0));
let eyeHaloL = 1.0 - smoothstep(0.155, 0.180, length(p - vec2f(-0.205, 0.36)));
let eyeHaloR = 1.0 - smoothstep(0.155, 0.180, length(p - vec2f(0.205, 0.36)));
col += warm * (eyeHaloL * (hoverL * 0.08 + selectedL * pulse * 0.30) + eyeHaloR * (hoverR * 0.08 + selectedR * pulse * 0.30));
let browLift = concern * 0.085;
let browL = cf_line(p, vec2f(-0.34, 0.49 + browLift * 0.25), vec2f(-0.09, 0.48 + browLift), 0.014);
let browR = cf_line(p, vec2f(0.09, 0.48 + browLift), vec2f(0.34, 0.49 + browLift * 0.25), 0.014);
col += warm * (browL + browR) * (0.22 + concern * 0.72);
let nose = cf_line(p, vec2f(0.0, 0.34), vec2f(-0.025, 0.19), 0.009)
+ cf_line(p, vec2f(-0.025, 0.19), vec2f(0.038, 0.18), 0.009);
col += vec3f(0.28, 0.29, 0.31) * nose * 0.35;
let selectedNose = 1.0 - smoothstep(0.10, 0.20, abs(selectedPart - 4.0));
col += warm * nose * selectedNose * pulse * 0.62;
let mouthExpr = uni(9);
let horrLine = cf_line(p, vec2f(-0.100, 0.054), vec2f(0.100, 0.054), 0.012);
let horrFrownL = cf_line(p, vec2f(-0.088, 0.060), vec2f(0.0, 0.046), 0.009);
let horrFrownR = cf_line(p, vec2f(0.0, 0.046), vec2f(0.088, 0.060), 0.009);
let exprHorrClosed = clamp(horrLine + (horrFrownL + horrFrownR) * 0.72, 0.0, 1.0);
let horrOpenD = cf_ellipse(p, vec2f(0.0, 0.050), vec2f(0.092, 0.058), 0.0);
let exprHorrOpen = cf_mask(horrOpenD, 0.008);
let smileArcD = cf_ellipse(p, vec2f(0.0, 0.054), vec2f(0.108, 0.028), 0.0);
let smileStroke = 1.0 - smoothstep(0.005, 0.017, abs(smileArcD));
let exprSmileClosed = smileStroke * step(0.028, p.y) * step(p.y, 0.054);
let smileOpenD = cf_ellipse(p, vec2f(0.0, 0.044), vec2f(0.105, 0.062), 0.0);
let smileOpenFill = cf_mask(smileOpenD, 0.007) * step(p.y, 0.060) * step(0.018, p.y);
let smileOpenRing = (1.0 - smoothstep(0.004, 0.016, abs(smileOpenD))) * step(p.y, 0.040);
let exprSmileOpen = clamp(smileOpenRing + smileOpenFill * 0.82, 0.0, 1.0);
var mouthMask = exprHorrClosed;
if (mouthExpr > 0.5 && mouthExpr < 1.5) {
mouthMask = exprHorrOpen;
} else if (mouthExpr > 1.5 && mouthExpr < 2.5) {
mouthMask = exprSmileClosed;
} else if (mouthExpr >= 2.5) {
mouthMask = exprSmileOpen;
}
col = mix(col, vec3f(0.003, 0.004, 0.007), mouthMask);
let horrRim = (1.0 - smoothstep(0.005, 0.018, abs(horrOpenD))) * select(0.0, 1.0, mouthExpr > 0.5 && mouthExpr < 1.5);
col += warm * horrRim * (0.18 + concern * 0.35);
let smileWarm = select(0.0, 1.0, mouthExpr > 1.5);
col += warm * mouthMask * smileWarm * 0.14;
let hoverMouth = 1.0 - smoothstep(0.11, 0.19, length(pointer - vec2f(0.0, 0.055)));
let selectedMouth = 1.0 - smoothstep(0.10, 0.20, abs(selectedPart - 3.0));
let mouthHalo = 1.0 - smoothstep(0.12, 0.17, length(p - vec2f(0.0, 0.055)));
col += warm * mouthHalo * (hoverMouth * 0.075 + selectedMouth * pulse * 0.32);
let wellOuterD = cf_ellipse(p, vec2f(0.0, -0.62), vec2f(0.48, 0.155), 0.0);
let wellInnerD = cf_ellipse(p, vec2f(0.0, -0.605), vec2f(0.355, 0.082), 0.0);
let wellStone = cf_mask(wellOuterD, 0.010) * (1.0 - cf_mask(wellInnerD, 0.008));
col = mix(col, vec3f(0.31, 0.32, 0.35) + vec3f(wall * 0.12), wellStone);
let wellRim = 1.0 - smoothstep(0.006, 0.025, abs(wellOuterD));
col += vec3f(0.72, 0.75, 0.82) * wellRim * 0.62;
let wellVoid = cf_mask(wellInnerD, 0.007);
let wellLocal = (p - vec2f(0.0, -0.605)) / vec2f(0.355, 0.082);
let wellR = length(wellLocal);
let wellAng = atan2(wellLocal.y, wellLocal.x);
let rippleA = sin(wellR * 15.5 - time * 2.0 + wellAng * 2.5);
let rippleB = sin(wellR * 26.0 + time * 1.4 - wellAng * 1.2);
let ripples = 0.5 + 0.24 * rippleA + 0.14 * rippleB;
let waterDeep = vec3f(0.012, 0.026, 0.050);
let waterLit = vec3f(0.11, 0.22, 0.36);
let waterCol = mix(waterDeep, waterLit, ripples * (1.0 - wellR * 0.82));
col = mix(col, waterCol, wellVoid * 0.95);
col += vec3f(0.42, 0.66, 0.90) * wellVoid * smoothstep(0.35, 0.82, ripples) * smoothstep(1.0, 0.25, wellR) * (0.14 + breath * 0.06);
let depthRing = 1.0 - smoothstep(0.008, 0.025, abs(cf_ellipse(p, vec2f(0.0, -0.635), vec2f(0.24, 0.046), 0.0)));
col += vec3f(0.06, 0.075, 0.10) * depthRing * 0.35;
let splashCount = i32(uni(10));
for (var si = 0u; si < 20u; si++) {
if (i32(si) >= splashCount) { break; }
let bi = 11 + i32(si) * 4;
let sp = vec4f(uni(bi), uni(bi + 1), uni(bi + 2), uni(bi + 3));
let spLife = sp.z;
if (spLife <= 0.01) { continue; }
let spSize = 0.005 + sp.w * 0.014;
let velHint = vec2f(sp.w * 0.018, -sp.w * 0.022);
let core = cf_mask(length(p - sp.xy) - spSize, 0.0035) * spLife;
let streak = cf_mask(length(p - (sp.xy + velHint)) - spSize * 0.55, 0.003) * spLife * 0.5;
let halo = exp(-dot(p - sp.xy, p - sp.xy) * 220.0) * spLife * 0.25;
let dropCol = mix(vec3f(0.45, 0.78, 1.0), vec3f(0.95, 0.99, 1.0), spLife);
col = mix(col, dropCol, clamp(core + streak, 0.0, 1.0));
col += vec3f(0.65, 0.92, 1.0) * (core + halo) * 0.42;
}
let childHeadD = length(p - childHead) - 0.078;
let childHeadMask = cf_mask(childHeadD, 0.006);
let childTorsoD = cf_ellipse(p, vec2f(-0.165, -0.405), vec2f(0.090, 0.145), -0.34);
let childTorso = cf_mask(childTorsoD, 0.007);
let shoulderA = vec2f(-0.145, -0.355);
let shoulderB = vec2f(-0.205, -0.350);
let flap = 0.58 + concern * 0.18;
let angA = -0.75 + sin(time * 3.4) * flap + sin(time * 6.1 + 1.1) * 0.28;
let angB = -2.05 + sin(time * 4.3 + 0.7) * (flap + 0.08) + sin(time * 7.2 + 2.4) * 0.26;
let handA = shoulderA + cf_rot2(angA) * vec2f(0.138, 0.0);
let handB = shoulderB + cf_rot2(angB) * vec2f(0.148, 0.0);
let childArmReach = cf_line(p, shoulderA, handA, 0.026);
let childArmGuard = cf_line(p, shoulderB, handB, 0.026);
let childHandReach = cf_mask(length(p - handA) - 0.025, 0.005);
let childHandGuard = cf_mask(length(p - handB) - 0.025, 0.005);
let childLegA = cf_line(p, vec2f(-0.135, -0.505), vec2f(-0.055, -0.575), 0.030);
let childLegB = cf_line(p, vec2f(-0.175, -0.510), vec2f(-0.225, -0.575), 0.030);
let childBodyMask = clamp(childTorso + childArmReach + childArmGuard + childLegA + childLegB, 0.0, 1.0);
let childSkinMask = clamp(childHeadMask * headShow + childHandReach + childHandGuard, 0.0, 1.0);
col += vec3f(1.05, 0.29, 0.075) * exp(-dot(p - childHead, p - childHead) * 28.0) * 0.30 * headShow;
col = mix(col, vec3f(0.48, 0.17, 0.10), childBodyMask);
col += vec3f(1.25, 0.34, 0.10) * (1.0 - smoothstep(0.008, 0.026, abs(childTorsoD))) * 0.30;
col = mix(col, vec3f(1.20, 0.69, 0.38), childSkinMask);
let childHair = childHeadMask * headShow * smoothstep(childHead.y - 0.010, childHead.y + 0.050, p.y);
col = mix(col, vec3f(0.15, 0.055, 0.040), childHair * 0.94);
let childFringeL = cf_mask(length(p - (childHead + vec2f(-0.033, 0.025))) - 0.030, 0.004) * childHeadMask * headShow;
let childFringeR = cf_mask(length(p - (childHead + vec2f(0.030, 0.031))) - 0.027, 0.004) * childHeadMask * headShow;
col = mix(col, vec3f(0.12, 0.040, 0.032), max(childFringeL, childFringeR) * 0.88);
let childGaze = clamp((pointer - childHead) * 0.38, vec2f(-0.011), vec2f(0.011));
let childEyeLPos = childHead + vec2f(-0.026, 0.004);
let childEyeRPos = childHead + vec2f(0.026, 0.004);
let childEyeL = cf_mask(length(p - childEyeLPos) - 0.015, 0.003) * headShow;
let childEyeR = cf_mask(length(p - childEyeRPos) - 0.015, 0.003) * headShow;
col = mix(col, vec3f(1.05, 1.02, 0.92), max(childEyeL, childEyeR));
let childPupilL = cf_mask(length(p - (childEyeLPos + childGaze)) - 0.0065, 0.002) * headShow;
let childPupilR = cf_mask(length(p - (childEyeRPos + childGaze)) - 0.0065, 0.002) * headShow;
col = mix(col, vec3f(0.035, 0.018, 0.018), max(childPupilL, childPupilR));
let childBrowL = cf_line(p, childHead + vec2f(-0.055, 0.034), childHead + vec2f(-0.010, 0.050), 0.006) * headShow;
let childBrowR = cf_line(p, childHead + vec2f(0.010, 0.050), childHead + vec2f(0.055, 0.034), 0.006) * headShow;
col = mix(col, vec3f(0.20, 0.065, 0.045), clamp(childBrowL + childBrowR, 0.0, 1.0));
let childMouthD = cf_ellipse(p, childHead + vec2f(0.0, -0.038), vec2f(0.018, 0.024), 0.0);
let childMouth = cf_mask(childMouthD, 0.004) * headShow;
col = mix(col, vec3f(0.10, 0.018, 0.020), childMouth);
let childMouthRim = (1.0 - smoothstep(0.003, 0.009, abs(childMouthD))) * headShow;
col += vec3f(1.15, 0.28, 0.12) * childMouthRim * 0.24;
let childHeadRim = (1.0 - smoothstep(0.006, 0.018, abs(childHeadD))) * headShow;
col += vec3f(1.55, 0.66, 0.28) * childHeadRim * (0.34 + concern * 0.48);
let selectedChild = 1.0 - smoothstep(0.10, 0.20, abs(selectedPart - 5.0));
col += vec3f(1.35, 0.55, 0.18) * childHeadMask * selectedChild * pulse * 0.35 * headShow;
let fBase = childHead + vec2f(0.0, -0.025);
let fc = p - fBase;
let fNoise = cf_vnoise(fc * vec2f(7.0, 11.0) + vec2f(time * 3.1, -time * 6.2));
let sway = sin(time * 11.0 + fc.y * 12.0) * 0.026 + sin(time * 18.5 + fc.x * 9.0) * 0.016 + fNoise * 0.022;
let fu = vec2f(fc.x + sway, fc.y);
let flick = 0.88 + 0.12 * sin(time * 14.7) + 0.06 * sin(time * 23.0);
let flameOuterD = length(vec2f(fu.x * 0.72, (fu.y - 0.015) * 1.12)) - 0.108 * flick;
let flameOuter = cf_mask(flameOuterD, 0.018) * childFlame;
let flameMidD = length(vec2f(fu.x * 0.62, (fu.y + 0.01) * 1.45)) - 0.062 * flick;
let flameMid = cf_mask(flameMidD, 0.014) * childFlame;
let flameCoreD = length(vec2f(fu.x * 0.50, fu.y * 1.85)) - 0.032 * flick;
let flameCore = cf_mask(flameCoreD, 0.008) * childFlame;
let tongueL = length(vec2f((fu.x + 0.052) * 1.05, (fu.y - 0.05) * 1.55)) - 0.058;
let tongueR = length(vec2f((fu.x - 0.048) * 1.08, (fu.y - 0.04) * 1.50)) - 0.055;
let tongues = (cf_mask(tongueL, 0.012) + cf_mask(tongueR, 0.012)) * childFlame * (0.65 + 0.35 * sin(time * 9.3 + fNoise));
var flameCol = vec3f(0.95, 0.14, 0.02);
flameCol = mix(flameCol, vec3f(1.0, 0.38, 0.05), flameMid);
flameCol = mix(flameCol, vec3f(1.0, 0.96, 0.68), flameCore);
let flameBody = clamp(flameOuter + flameMid * 0.92 + tongues * 0.75, 0.0, 1.0);
let wick = cf_line(p, childHead + vec2f(0.0, -0.062), childHead + vec2f(0.0, -0.105), 0.0055) * childFlame;
col = mix(col, vec3f(0.08, 0.05, 0.04), wick);
let ignite = exp(-dot(vec2f(fc.x * 1.15, fc.y + 0.07), vec2f(fc.x * 1.15, fc.y + 0.07)) * 42.0) * childFlame;
col += vec3f(1.35, 0.42, 0.08) * ignite * (0.55 + 0.45 * sin(time * 13.0));
col = mix(col, flameCol, flameBody);
col += vec3f(1.55, 0.72, 0.22) * flameCore * (0.85 + 0.15 * sin(time * 19.0));
let sparkUv = (p - childHead) * vec2f(9.0, 14.0) + vec2f(0.0, -time * 5.8);
let sparkCell = floor(sparkUv);
let sparkLocal = fract(sparkUv) - vec2f(0.5);
let sparkSeed = cf_hash21(sparkCell);
let sparkPhase = fract(sparkSeed * 13.1 - time * 2.8);
let sparkPop = step(0.90, sparkSeed) * step(sparkPhase, 0.42);
let sparkCore = exp(-dot(sparkLocal, sparkLocal) * 95.0) * sparkPop;
let sparkTrail = exp(-dot(sparkLocal + vec2f(0.0, 0.18), sparkLocal + vec2f(0.0, 0.18)) * 40.0) * sparkPop * 0.35;
col += vec3f(1.45, 0.85, 0.35) * (sparkCore + sparkTrail) * childFlame * 1.15;
let emberUv = (p - childHead) * vec2f(5.5, 8.0) + vec2f(time * 0.4, -time * 3.2);
let emberSeed = cf_hash21(floor(emberUv));
let ember = step(0.94, emberSeed) * (1.0 - smoothstep(0.0, 0.45, length(fract(emberUv) - 0.5)));
col += vec3f(1.2, 0.35, 0.06) * ember * childFlame * 0.9;
col += vec3f(1.15, 0.45, 0.10) * childFlame * exp(-dot(p - childHead, p - childHead) * 22.0) * (0.38 + 0.14 * sin(time * 7.2));
col += vec3f(1.0, 0.55, 0.12) * childFlame * exp(-dot(p - (childHead + vec2f(0.0, 0.12)), p - (childHead + vec2f(0.0, 0.12))) * 18.0) * 0.28;
let caughtHalo = exp(-dot(p - childHead, p - childHead) * 75.0) * reaching;
col += vec3f(1.65, 0.35, 0.12) * caughtHalo * 0.36;
let pointerHalo = exp(-dot(p - pointer, p - pointer) * 110.0);
col += warm * pointerHalo * (0.035 + concern * 0.10);
if (col.x != col.x || col.y != col.y || col.z != col.z) { col = vec3f(0.0); }
return vec4f(col, 1.0);
}
— STEP HOOKS (JAVASCRIPT) —
hook · feel_input
well splash via uniforms
const wd = sim.worldData;
const inp = wd.input || { pointer: {} };
const ptr = inp.pointer || {};
wd.__resets = wd.__resets || [];
if (!wd.__resets.includes('__feel')) wd.__resets.push('__feel');
if (wd.__fresh) delete wd.__fresh;
if (!wd.__feel) {
wd.__feel = {
leftOpen: 1,
rightOpen: 1,
mouthOpen: 0,
selectedPart: 0,
pulse: 0,
childFlame: 0,
mouthExpr: 0,
};
}
const S = wd.__feel;
S.leftOpen = S.leftOpen ? 1 : 0;
S.rightOpen = S.rightOpen ? 1 : 0;
S.mouthOpen = S.mouthOpen ? 1 : 0;
function rand() {
return typeof sim.rand === 'function' ? sim.rand() : Math.random();
}
function syncMouthExpr() {
const bothShut = !S.leftOpen && !S.rightOpen;
if (bothShut) {
S.mouthExpr = S.mouthOpen ? 3 : 2;
} else {
S.mouthExpr = S.mouthOpen ? 1 : 0;
}
}
if (!S.splashPool) {
S.splashPool = [];
for (let i = 0; i < 72; i++) {
S.splashPool.push({ x: 0, y: 0, vx: 0, vy: 0, life: 0, size: 1 });
}
}
function hitWell(x, y) {
const ix = (x - 0) / 0.36;
const iy = (y - -0.605) / 0.095;
if (ix * ix + iy * iy < 1.08) return true;
const ox = (x - 0) / 0.46;
const oy = (y - -0.62) / 0.15;
return ox * ox + oy * oy < 1.02 && ix * ix + iy * iy > 0.75;
}
function spawnSplash(cx, cy) {
const bursts = 16 + Math.floor(rand() * 12);
for (let n = 0; n < bursts; n++) {
const s = S.splashPool.find((p) => p.life <= 0);
if (!s) break;
const ang = rand() * Math.PI * 2;
const spd = 0.45 + rand() * 0.85;
s.x = cx + (rand() - 0.5) * 0.1;
s.y = cy + (rand() - 0.5) * 0.05;
s.vx = Math.cos(ang) * spd * 0.5;
s.vy = Math.sin(ang) * spd * 0.35 + 0.25 + rand() * 0.45;
s.life = 0.5 + rand() * 0.75;
s.size = 0.55 + rand() * 0.9;
}
}
const mx = ptr.x != null ? ptr.x : 256;
const my = ptr.y != null ? ptr.y : 256;
const px = (mx - 255) / 256;
const py = -(my - 255) / 256;
const held = ptr.down || inp.actionHeld ? 1 : 0;
const childHeadX = -0.205;
const childHeadY = -0.255;
function hitPart(x, y) {
const eyeR = 0.11;
const mouthR = 0.11;
const noseR = 0.10;
const childHeadR = 0.085;
if (Math.hypot(x - -0.205, y - 0.36) < eyeR) return 1;
if (Math.hypot(x - 0.205, y - 0.36) < eyeR) return 2;
if (Math.hypot(x - 0, y - 0.055) < mouthR) return 3;
const dNose = Math.min(
Math.hypot(x - 0, y - 0.28),
Math.hypot(x - -0.012, y - 0.19),
Math.hypot(x - 0.02, y - 0.185)
);
if (dNose < noseR) return 4;
if (Math.hypot(x - childHeadX, y - childHeadY) < childHeadR) return 5;
return 0;
}
if (ptr.pressed || inp.action) {
if (hitWell(px, py)) {
spawnSplash(px, py);
S.selectedPart = 6;
S.pulse = 1;
} else {
const part = hitPart(px, py);
if (part === 1) {
S.leftOpen = S.leftOpen ? 0 : 1;
S.selectedPart = 1;
S.pulse = 1;
} else if (part === 2) {
S.rightOpen = S.rightOpen ? 0 : 1;
S.selectedPart = 2;
S.pulse = 1;
} else if (part === 3) {
S.mouthOpen = S.mouthOpen ? 0 : 1;
S.selectedPart = 3;
S.pulse = 1;
} else if (part === 4) {
S.selectedPart = 4;
S.pulse = 1;
} else if (part === 5) {
S.childFlame = S.childFlame ? 0 : 1;
S.selectedPart = 5;
S.pulse = 1;
} else {
S.selectedPart = 0;
}
}
}
for (const s of S.splashPool) {
if (s.life <= 0) continue;
s.life -= dt;
s.vy -= 1.15 * dt;
s.x += s.vx * dt;
s.y += s.vy * dt;
s.vx *= Math.max(0, 1 - 3.0 * dt);
}
syncMouthExpr();
S.pulse = Math.max(0, S.pulse - dt * 4.2);
const pop = [];
for (const s of S.splashPool) {
if (s.life > 0) pop.push(s);
}
pop.sort((a, b) => b.life - a.life);
const maxDraw = 20;
const u = [
px,
py,
held,
S.pulse,
S.leftOpen,
S.rightOpen,
S.mouthOpen,
S.selectedPart,
S.childFlame,
S.mouthExpr,
Math.min(pop.length, maxDraw),
];
for (let i = 0; i < maxDraw; i++) {
const s = pop[i];
if (s) u.push(s.x, s.y, s.life, s.size);
else u.push(0, 0, 0, 0);
}
wd.gpuPopulation = [];
wd.gpuUniforms = u;