cartridge.cafe · open source world
Tideglass
HOW TO PLAY
CLICK — turn dials, work levers, travel (chevrons at the screen edges) DRAG — aim the great lens No words will help you. Watch the bells. Read the stone. Match the sky. Wake the vault from the sea.
built by: Claude Fable 5
1 visual shader · 11 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 · tideglass
// private segment-distance — the visual must be SELF-SUFFICIENT (its lone tg_lib
// dependency caused recurring quarantines when the browser composed it early)
fn mod_tgv_seg(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);
}
// A MOON that reads as a moon: sphere-lit terminator, maria, limb halo (BONES Jul 30)
fn mod_tgv_moon(px: vec2f, ctr: vec2f, R: f32, elId: f32, t: f32) -> vec3f {
// BONES->SUNS (Galen Aug 10): the plain sphere-lit disc is retired; each sky
// body is now ARCANE SUN TECHNOLOGY (mod_tg_suntech, tg_lib). ruling vs other
// is read from uni(78); the two call sites already gate presence, so alt stays
// full — matching the moons' always-visible behavior.
let rulingEl = uni(78);
let isRuling = select(0.0, 1.0, abs(elId - rulingEl) < 0.25);
let mm = vec2f(uni(27), uni(28));
let hov = smoothstep(R * 2.2, R * 0.8, length(mm - ctr));
return mod_tg_suntech(px, ctr, R, elId, t, isRuling, 1.0, hov);
}
// ── THE SATCHEL — a diegetic inventory strip along the bottom edge, drawn once any
// item is owned. SDF icons only (no baked pixels): a gear is a ring + cos(k*theta)
// teeth (teeth count 8/12/16/20 IS the icon); the spanner is a shaft + open C-jaw.
// Reads uni(61) invMask (1 spanner ·2 g8 ·4 g12 ·8 g16 ·16 g20), uni(62) heldItem. ──
fn mod_tg_icon_gear(px: vec2f, c: vec2f, R: f32, teeth: f32, t: f32) -> f32 {
let d = px - c; let r = length(d); let a = atan2(d.y, d.x) + t * 0.4;
let rim = R + 3.0 * smoothstep(-0.1, 0.5, cos(a * teeth));
return clamp(smoothstep(rim, rim - 2.5, r) - smoothstep(R * 0.30, R * 0.30 - 2.5, r), 0.0, 1.0);
}
fn mod_tg_icon_spanner(px: vec2f, c: vec2f, s: f32) -> f32 {
let d = px - c;
let handle = mod_tgv_seg(d, vec2f(-s, 0.0), vec2f(s * 0.4, 0.0));
var m = smoothstep(0.22 * s, 0.14 * s, handle);
let hc = d - vec2f(0.7 * s, 0.0);
let rr = abs(length(hc) - 0.36 * s);
m = max(m, smoothstep(0.14 * s, 0.07 * s, rr) * step(hc.x, 0.12 * s));
return clamp(m, 0.0, 1.0);
}
fn mod_tg_bit(mask: f32, bit: f32) -> f32 { return floor(mask / bit) - 2.0 * floor(mask / (bit * 2.0)); }
fn mod_tg_satchel(cIn: vec3f, px: vec2f, mm: vec2f, t: f32) -> vec3f {
var c = cIn;
let inv = uni(61); let held = i32(uni(62) + 0.5);
if (inv < 0.5 && held == 0 && !(uni(108) > 0.5 && uni(109) < 0.5)) { return c; } // nothing carried yet -> no strip
let bandY = 486.0;
let band = smoothstep(28.0, 24.0, abs(px.y - bandY)) * step(96.0, px.x) * step(px.x, 416.0);
c = mix(c, vec3f(0.04, 0.05, 0.06), band * 0.55);
// BONES (Galen Jul 30): the satchel owns a RESERVED plate — no pickups or
// level elements may live inside x 100..412, y bandY-28..512 (audited).
let plateD = sdRoundedBox(px - vec2f(256.0, bandY), vec2f(152.0, 26.0), 12.0);
c = mix(c, vec3f(0.020, 0.024, 0.032), smoothstep(1.5, -1.5, plateD) * 0.85);
c += vec3f(0.30, 0.34, 0.42) * smoothstep(2.0, 0.0, abs(plateD)) * 0.5;
// BONES: the deny flash — a locked interactive answers with a red pulse (uni79..81)
if (uni(79) > 0.01) {
let dnp = px - vec2f(uni(80), uni(81));
c += vec3f(0.95, 0.22, 0.16) * smoothstep(3.5, 0.5, abs(length(dnp) - (30.0 + (1.0 - uni(79)) * 26.0))) * uni(79) * 0.9;
}
var slotX = array<f32, 5>(128.0, 192.0, 256.0, 320.0, 384.0);
var bits = array<f32, 5>(1.0, 2.0, 4.0, 8.0, 16.0);
var teeth = array<f32, 5>(0.0, 8.0, 12.0, 16.0, 20.0); // slot 0 = spanner
for (var i = 0; i < 5; i = i + 1) {
let ctr = vec2f(slotX[i], bandY);
c += vec3f(0.10, 0.11, 0.13) * tg_ring(px, ctr, 22.0, 19.0); // slot frame
if (i == 3) {
// SLOT 3 is RESERVED for the tideglass pearl (the unused 16t gear is gone).
if (uni(108) > 0.5 && uni(109) < 0.5) { // carried, not yet seated
let rr = length(px - ctr);
c += mix(vec3f(0.55, 0.72, 0.82), vec3f(1.0, 1.0, 0.96), smoothstep(13.0, 0.0, rr)) * smoothstep(12.0, 9.0, rr);
c += vec3f(0.9, 0.96, 1.0) * smoothstep(4.5, 0.0, length(px - (ctr - vec2f(3.0, 3.0)))) * 0.85; // specular
}
} else {
let owned = mod_tg_bit(inv, bits[i]);
if (owned > 0.5 && held != i + 1) {
if (i == 0) { c += vec3f(1.0, 0.82, 0.40) * mod_tg_icon_spanner(px, ctr, 14.0); }
else { c += vec3f(0.90, 0.78, 0.45) * mod_tg_icon_gear(px, ctr, 14.0, teeth[i], t); }
}
}
}
if (held >= 1) { // the held item rides the cursor
if (held == 1) { c += vec3f(1.0, 0.85, 0.45) * mod_tg_icon_spanner(px, mm, 16.0); }
else { c += vec3f(1.0, 0.82, 0.45) * mod_tg_icon_gear(px, mm, 15.0, teeth[held - 1], t); }
}
return c;
}
fn visual_tideglass(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let t = uni(3);
let px = (uv * 0.5 + 0.5) * 512.0;
let view = i32(uni(0) + 0.5);
var c = mod_tg_scene(view, uv, px, t);
// cross-fade during travel
let fade = uni(2);
if (fade > 0.003) {
let pv = i32(uni(1) + 0.5);
c = mix(c, mod_tg_scene(pv, uv, px, t), fade);
}
// ── nav chevrons (hook mirrors these hit zones) ──
let mm = vec2f(uni(27), uni(28));
let act = uni(24);
let fin = uni(17);
// ═══ SCENE-TABLE CHEVRONS — drawn from DATA (uni 88..103). One truth with the
// hook's NAV table: invisible or unclickable doors are impossible now. ═══
for (var ni = 0; ni < 4; ni++) {
let nbase = 88 + ni * 4;
if (uni(nbase + 3) > 0.5) {
let cpos = vec2f(uni(nbase), uni(nbase + 1));
let ca = mod_tg_chev(px, cpos, i32(uni(nbase + 2)), smoothstep(55.0, 20.0, length(mm - cpos)), t);
c = mix(c, ca.rgb + c, ca.a * 0.9);
}
}
// INSPECT SKIN (Galen): unmistakably a different mode — cool blue cast + a faint
// 64-unit coordinate grid; game clicks are suppressed while this shows.
if (uni(87) > 0.5) {
c = c * vec3f(0.78, 0.88, 1.12) + vec3f(0.008, 0.022, 0.055);
let gx = abs(fract(px.x / 64.0) - 0.5);
let gy = abs(fract(px.y / 64.0) - 0.5);
let gridL = max(smoothstep(0.47, 0.5, gx), smoothstep(0.47, 0.5, gy));
c += vec3f(0.10, 0.22, 0.40) * gridL * 0.18;
let brd = max(smoothstep(6.0, 2.0, px.x), max(smoothstep(6.0, 2.0, px.y), max(smoothstep(506.0, 510.0, px.x), smoothstep(506.0, 510.0, px.y))));
c += vec3f(0.15, 0.35, 0.75) * brd * 0.5;
}
// click-confirm: a thin gold ring blooms from every registered click
if (uni(84) > 0.01) {
let ckp = px - vec2f(uni(85), uni(86));
c += vec3f(1.0, 0.85, 0.45) * smoothstep(2.5, 0.5, abs(length(ckp) - (12.0 + (1.0 - uni(84)) * 30.0))) * uni(84) * 0.8;
}
if (view == 0) { // the shore
// (right chevron removed — it never worked once the moon rose and the finale
// pinned the view; the building is the way in, the risen dome the way up.)
// an arrow over the dome — ONLY once the dome has actually breached the sea
// (uni(26)=vault); by day there's no dome, so no orphaned chevron over water.
if (uni(26) > 0.03) {
let dm = vec2f(214.0, 318.0);
let dhov = smoothstep(92.0, 28.0, length(mm - dm));
let da = mod_tg_chev(px, vec2f(214.0, 262.0), 3, 0.4 + 0.6 * dhov, t);
c = mix(c, da.rgb + c, da.a * 0.9 * min(uni(26) * 3.0, 1.0));
}
// the observatory building answers the cursor too — its own door is the gate
let bc = vec2f(440.0, 300.0);
let bhov = smoothstep(82.0, 30.0, length(mm - bc));
c += vec3f(0.22, 0.52, 0.62) * bhov * (0.10 + 0.05 * sin(t * 2.0));
// right chevron → into the observatory (the gate). Restored: the finale no longer
// pins the view, so a plain right-arrow off the shore works again.
// left chevron → into the sea-cave, a grotto in the rock to the left (always open)
// ── ACT 1 v2: the tide answers the moon. At low tide a wet-sand band emerges at
// the foot of the shore and the stranded 8t gear surfaces on the tideline. ──
let tideL = uni(60);
if (tideL < 0.30) {
let low = smoothstep(0.30, 0.05, tideL);
let bnd = smoothstep(440.0, 472.0, px.y);
c = mix(c, vec3f(0.16, 0.15, 0.12) * (0.7 + 0.3 * sin(px.x * 0.1)), bnd * low * 0.5);
c += vec3f(0.30, 0.40, 0.42) * smoothstep(3.0, 0.0, abs(px.y - 450.0)) * low * 0.25;
// (the 8t gear pickup moved to the sea-cave plank — no gear surfaces here)
}
// the moon answers a touch (glass moon → mirror-still): a soft disc high in the sky
// BONES (Galen Jul 30): moons READABLE in the sky — element colors the disc,
// the ring pulses when a moon is chosen, and the SEA ANSWERS the element.
if (uni(113) < 0.0005) { // suns belong to the SHORE - never over THE CROSSING
let mc0 = vec2f(400.0, 96.0);
let mh0 = smoothstep(46.0, 20.0, length(mm - mc0));
let elId = uni(78);
var mcol = vec3f(0.62, 0.78, 0.95);
if (elId > 0.5) { mcol = vec3f(1.0, 0.55, 0.25); }
c += mod_tgv_moon(px, mc0, 26.0, elId, t) * (0.85 + 0.20 * mh0);
c += mcol * tg_ring(px, mc0, 34.0, 31.0) * (uni(75) * (0.22 + 0.22 * sin(t * 2.2)) + mh0 * 0.15);
if (uni(73) > 1.5) {
c += mod_tgv_moon(px, vec2f(338.0, 62.0), 13.0, 1.0 - step(0.5, elId), t) * 0.8;
}
}
// (red tint band retired — the marched ocean itself converts via uni(82))
}
if (view == 13) { // the drowned quarter → up the shaft
}
if (view == 8) { // the sea-cave → shore (right) · MIRROR ISLAND (left)
// BONES (Galen Jul 30): the mirror-island exit existed in the hook but was NEVER drawn — the door gets its knob
}
if (view == 5) { // the observatory room → back down to the shore
// once the constellation is gold, the heart becomes a door: a pulsing ring
// at the centre invites the click that dives into the orb (view 6).
if (uni(38) > 0.5) {
let oc = vec2f(256.0, 256.0);
let rp = length(px - oc);
let hov = smoothstep(84.0, 24.0, length(mm - oc));
let pulse = 0.5 + 0.5 * sin(t * 2.5);
let ring = smoothstep(3.5, 0.0, abs(rp - (34.0 + pulse * 7.0)));
c += vec3f(1.0, 0.82, 0.36) * ring * (0.22 + hov * 0.55);
}
}
if (view == 6) { // inside the orb → back to the constellation
// once the core is woken, its heart becomes a door: a pulsing ring dives to the orrery
if (uni(39) > 0.5) {
let oc = vec2f(256.0, 256.0);
let rp = length(px - oc);
let hov = smoothstep(84.0, 24.0, length(mm - oc));
let pulse = 0.5 + 0.5 * sin(t * 2.5);
let ring = smoothstep(3.5, 0.0, abs(rp - (30.0 + pulse * 6.0)));
c += vec3f(1.0, 0.85, 0.40) * ring * (0.25 + hov * 0.55) * uni(43);
}
}
if (view == 7) { // the orrery → back down into the core
}
if (view == 1) {
// ── ACT 1 v2: THE DROWNED GATE. The disc's lower half sits underwater; at low
// tide it surfaces to reveal four notch-targets at 6 o'clock. A lever swings
// the targets 12 o'clock (up) <-> 6 o'clock (down). uni(77) notch, uni(60) tide. ──
let notch = uni(77);
// (blue half-ring reveal deleted — tide is pinned; it overlapped the doorway. Galen Jul 30)
let lc = vec2f(432.0, 360.0);
let lh = smoothstep(34.0, 14.0, length(mm - lc));
c += vec3f(0.70, 0.65, 0.40) * tg_ring(px, lc, 20.0, 14.0) * (0.4 + 0.5 * lh);
let ly = lc + vec2f(0.0, mix(-13.0, 13.0, notch));
c += mix(vec3f(0.95, 0.85, 0.5), vec3f(0.4, 0.9, 0.85), notch) * tg_disc(px, ly, 7.0);
}
if (view == 4) { // the record room → back to the gate
// RESTORED (Jul 30, stale-clobber): tablet -> brambles, right edge
}
if (view == 2) {
if (uni(14) > 0.5) { // ascend to the lens
}
}
if (view == 3) {
}
if (view == 12) { // the thicket -> back to the lens
}
// ── the satchel inventory strip (over the scene, under the click ripple) ──
if (uni(113) < 0.0005) { c = mod_tg_satchel(c, px, mm, t); }
// click ripple
let cp = uni(25);
if (cp > 0.003) {
let cd = length(px - mm);
c += vec3f(0.9, 0.85, 0.7) * exp(-pow((cd - (1.0 - cp) * 60.0) * 0.25, 2.0)) * cp * 0.5;
}
// finale white bloom breath
c += vec3f(1.0, 0.95, 0.85) * fin * (1.0 - fin) * 1.2;
// ── the island's one grade: amber highlights, teal-indigo shadows ──
c = max(c, vec3f(0.0));
let lum = dot(c, vec3f(0.2126, 0.7152, 0.0722));
c = mix(vec3f(lum), c, 1.09); // painterly saturation
c += vec3f(0.030, 0.016, 0.0) * smoothstep(0.30, 1.30, lum); // gold in the light
c += vec3f(0.0, 0.010, 0.020) * (1.0 - smoothstep(0.0, 0.30, lum)); // sea-glass in the dark
c *= 1.0 - 0.16 * dot(uv * 0.72, uv * 0.72); // breath of corner shadow
// gentle grain
c += (hash21(floor(px * 1.7)) - 0.5) * 0.012;
return vec4f(max(c, vec3f(0.0)), 1.0);
}
— SHADER MODULES —
module · tg_air
fn mod_tg_airship(cIn: vec3f, p: vec2f, t: f32, dawn: f32, sel: f32, night: f32) -> vec3f {
var c = cIn;
// ===== SHARED FRAME: drift / bob / pitch folded in ONCE for every aspect =====
let sway = 0.030 * sin(t * 0.047 + 0.3) + 0.011 * sin(t * 0.113 + 1.7); // lateral
let bob = 0.013 * sin(t * 0.190 + 0.6) + 0.004 * sin(t * 0.370 + 2.1); // vertical
let pitch = 0.022 * sin(t * 0.190 + 2.17); // nose pitch (rad)
let atmSun = mod_tgo_suncol(sel); // warm dawn key
let ac = vec2f(-0.58, -0.50) + vec2f(sway, bob);
let aq = rot2(pitch) * (p - ac);
// ══════════════════════ ATMOSPHERE (behind the hull) ══════════════════════
// (a) soft dawn haze halo hugging the envelope crown
{
let hn = aq / vec2f(0.315, 0.150);
let hd = length(hn);
let bloom = exp(-max(hd - 0.55, 0.0) * 3.2);
let crown = smoothstep(0.30, 0.02, abs(hd - 1.0)) * smoothstep(0.0, -0.10, aq.y);
let lit = clamp(0.5 + 0.5 * dot(normalize(aq + vec2f(1e-4, 1e-4)), normalize(vec2f(0.85, 0.55))), 0.0, 1.0);
let haloCol = mix(vec3f(0.42, 0.50, 0.62), atmSun, lit * 0.72);
c += haloCol * (bloom * 0.16 + crown * 0.22 * lit) * dawn;
}
// (b) reflection + silhouette on the sea far below (ac already carries drift)
{
let refY = 0.375;
let rc = vec2f(ac.x, refY);
var rp = p - rc;
rp.x += 0.020 * sin(p.y * 24.0 + t * 1.3) + 0.010 * sin(p.y * 57.0 - t * 0.7);
let rq = vec2f(rp.x, -rp.y * 2.7);
let envM = 1.0 - smoothstep(0.72, 1.16, length(rq / vec2f(0.26, 0.10)));
let gonM = 1.0 - smoothstep(0.70, 1.20, length((rq - vec2f(0.0, 0.15)) / vec2f(0.090, 0.045)));
let shipM = max(envM, gonM * 0.85);
let band = smoothstep(0.300, 0.340, p.y) * (1.0 - smoothstep(0.44, 0.50, p.y));
let refl = shipM * band * dawn * step(uni(113), 0.0005); // the sea reflection belongs to the SHORE — in the cine it read as a horizon line bound to the ship (Galen)
let reflCol = mix(vec3f(0.05, 0.06, 0.09), atmSun * 0.75, 0.42);
c = mix(c, reflCol, refl * 0.32);
}
// (c) two slow-tumbling distant gulls
{
for (var i = 0; i < 2; i = i + 1) {
let fi = f32(i);
let ph = fi * 3.10;
let bx = -0.88 + glsl_mod(t * 0.013 + fi * 0.57, 1.9);
let by = -0.64 + 0.05 * sin(t * 0.21 + ph) + fi * 0.055;
let bp = p - vec2f(bx, by);
let s = 0.024;
let flap = 0.55 + 0.42 * sin(t * 3.1 + ph);
let wL = sdSegment(bp, vec2f(0.0, 0.0), vec2f(-s, -flap * s));
let wR = sdSegment(bp, vec2f(0.0, 0.0), vec2f( s, -flap * s));
let bm = (1.0 - smoothstep(0.0, 0.006, min(wL, wR))) * dawn * step(uni(113), 0.0005); // gulls stay home too
c = mix(c, vec3f(0.10, 0.10, 0.14), bm * 0.55);
}
}
// ══════════════════════════ ENVELOPE (gas bag) ══════════════════════════
{
let Rx = 0.26;
let Ry = 0.10;
let ex = aq.x / Rx;
let ey = aq.y / Ry;
let f = ex*ex + ey*ey - 1.0;
let gr = 2.0 * length(vec2f(aq.x/(Rx*Rx), aq.y/(Ry*Ry)));
let d = f / max(gr, 1e-4);
let aa = 0.006;
let cover = smoothstep(aa, -aa, d);
if (cover > 0.002) {
let rx = max(1.0 - ex*ex, 0.0);
let zn2 = max(rx - ey*ey, 0.0);
let zn = sqrt(zn2);
let N = normalize(vec3f(aq.x/(Rx*Rx), aq.y/(Ry*Ry), zn/Ry));
let V = vec3f(0.0, 0.0, 1.0);
let phi = atan2(ey, zn + 0.0004);
let Ngore = 14.0;
let gg = phi * (Ngore / 6.2831853);
let sp = gg - floor(gg + 0.5);
let pid = floor(gg + 0.5);
let sd = abs(sp);
let seam = smoothstep(0.055, 0.0, sd);
let bulge = cos(sp * 3.14159);
let stitch = smoothstep(0.05, 0.02, sd) * smoothstep(0.35, 0.95, sin(phi*170.0)*0.5+0.5);
let rib = cos(aq.y * 250.0) * 0.5 + 0.5;
let weave = fbm3(aq * vec2f(80.0, 150.0)) - 0.5;
let Lk = normalize(vec3f(0.55, 0.42, 0.55));
let Lf = normalize(vec3f(-0.35, -0.92, 0.32));
let warm = mod_tgo_suncol(sel);
let sky = vec3f(0.50, 0.60, 0.74);
let Hk = normalize(Lk + V);
let diffK = max(dot(N, Lk), 0.0);
let diffF = max(dot(N, Lf), 0.0);
var albedo = vec3f(0.86, 0.80, 0.67);
albedo *= 0.94 + 0.09 * hash11(pid * 3.17 + 1.7);
albedo *= 1.0 + weave * 0.06;
let ambient = 0.30;
var fab = albedo * (ambient * (0.6 + 0.4*sky)
+ diffK * 1.20 * warm
+ diffF * 0.40 * sky);
fab += pow(max(dot(N, Hk), 0.0), 6.0) * warm * 0.10;
fab *= mix(0.80, 1.0, bulge);
fab *= 1.0 - seam * 0.35;
fab += stitch * warm * 0.06;
fab *= 1.0 + (rib - 0.5) * 0.05;
let sil = pow(1.0 - clamp(N.z, 0.0, 1.0), 3.0);
let sunward = max(dot(normalize(N.xy + vec2f(1e-4, 0.0)), normalize(Lk.xy)), 0.0);
fab += sil * sunward * warm * 0.22;
let noseM = smoothstep(-0.205, -0.250, aq.x);
let tailM = smoothstep( 0.208, 0.252, aq.x);
let brassM = max(noseM, tailM);
var brassAlb = vec3f(0.66, 0.48, 0.22);
brassAlb = mix(vec3f(0.40, 0.50, 0.40), brassAlb, smoothstep(0.0, 0.5, brassM));
let specB = pow(max(dot(N, Hk), 0.0), 42.0);
var brass = brassAlb * (0.26 + diffK*1.25*warm + diffF*0.30*sky) + specB * warm * 1.6;
let bandm = max(smoothstep(0.012, 0.0, abs(aq.x + 0.205)),
smoothstep(0.012, 0.0, abs(aq.x - 0.208)));
let rivet = smoothstep(0.55, 0.95, sin(phi*90.0)*0.5+0.5);
brass = mix(brass, brass*1.35 + warm*0.18*rivet, bandm);
var col = mix(fab, brass, brassM);
let crownRim = pow(1.0 - clamp(N.z, 0.0, 1.0), 2.3) * smoothstep(-0.05, 0.85, -N.y);
col += crownRim * warm * 1.05;
col *= 1.0 - smoothstep(0.05, 0.85, N.y) * 0.32;
let ao = smoothstep(0.11, 0.0, abs(aq.x))
* smoothstep(0.045, 0.0, abs(aq.y - 0.088));
col *= 1.0 - ao * 0.45;
c = mix(c, col, cover * dawn);
}
}
// ══════════ EMPENNAGE (fins + stabilizers + rudder + guy-wires) ══════════
{
let PI2 = 6.2831853;
let aa = 0.0018;
let sunc = mod_tgo_suncol(sel);
let ldir = normalize(vec2f(0.72, 0.34));
let skyc = vec3f(0.56, 0.64, 0.80);
let fab = vec3f(0.86, 0.80, 0.66);
let brass = vec3f(0.72, 0.50, 0.24);
let brassHi = mix(vec3f(1.0, 0.86, 0.55), sunc, 0.4);
let steel = vec3f(0.70, 0.73, 0.78);
let trimw = 0.0065;
// horizontal stabilizer (edge-on, behind)
{
let hc = vec2f(0.235, 0.004);
let hq = aq - hc;
let rr = vec2f(0.090, 0.020);
let de = length(hq / rr) - 1.0;
let dHS = de * 0.020;
let cov = smoothstep(aa, -aa, dHS);
if (cov > 0.001) {
var col = fab * 0.90;
col *= (1.0 + 0.04 * sin(hq.x * PI2 / 0.020));
col *= (0.96 + 0.05 * fbm3(aq * 34.0));
let kk = clamp(dot(normalize(hq + vec2f(0.0, 0.001)), ldir), 0.0, 1.0);
col += sunc * kk * 0.10;
let tr = smoothstep(-trimw, -0.001, dHS);
col = mix(col, mix(brass, brassHi, 0.5), tr * 0.9);
let seam = smoothstep(0.0016, 0.0, abs(hq.x - 0.052)) * step(hq.x, 0.082);
col *= (1.0 - 0.4 * seam);
c = mix(c, col, cov * dawn);
}
}
// taut guy-wires bracing fins to the envelope (fine-line LOD: shore only)
if (uni(113) < 0.0005) {
let wW = 0.0018;
let tipD = vec2f(0.272, -0.126);
let tipV = vec2f(0.260, 0.080);
let a1 = vec2f(0.050, -0.082);
let a2 = vec2f(0.140, -0.058);
let a3 = vec2f(0.062, 0.080);
let dw1 = sdSegment(aq, tipD, a1);
let dw2 = sdSegment(aq, tipD, a2);
let dw3 = sdSegment(aq, tipV, a3);
let dw = min(min(dw1, dw2), dw3);
let cov = smoothstep(wW + aa, wW - aa, dw);
if (cov > 0.001) {
var col = steel;
col += sunc * 0.12;
col = mix(col, col * skyc, 0.15);
c = mix(c, col, cov * dawn * 0.8);
}
}
// ventral fin
{
let vF = vec2f(0.172, 0.000);
let vK = vec2f(0.290, 0.000);
let vT = vec2f(0.262, 0.086);
let e0 = vK - vF; let n0 = normalize(vec2f(e0.y, -e0.x));
let e1 = vT - vK; let n1 = normalize(vec2f(e1.y, -e1.x));
let e2 = vF - vT; let n2 = normalize(vec2f(e2.y, -e2.x));
let d0 = dot(aq - vF, n0);
let d1 = dot(aq - vK, n1);
let d2 = dot(aq - vT, n2);
let d = max(max(d0, d1), d2);
let cov = smoothstep(aa, -aa, d);
if (cov > 0.001) {
let ctr = vec2f(0.241, 0.029);
var col = fab * 0.94;
let ao = mix(0.72, 1.0, smoothstep(0.0, 0.060, aq.y));
col *= ao;
let kk = clamp(dot(normalize(aq - ctr), ldir), 0.0, 1.0);
col += sunc * kk * 0.16;
col *= (1.0 + 0.045 * sin(aq.x * PI2 / 0.017));
let rl = smoothstep(0.80, 0.99, cos(aq.x * PI2 / 0.017));
col *= (1.0 - 0.10 * rl);
col *= (0.96 + 0.05 * fbm3(aq * 36.0));
let seam = smoothstep(0.0016, 0.0, abs(d1 + 0.026 + 0.0012 * sin(t * 0.7)));
col *= (1.0 - 0.45 * seam * cov);
let tr = smoothstep(-trimw, -0.0008, d);
col = mix(col, mix(brass, brassHi, kk), tr * 0.92);
c = mix(c, col, cov * dawn);
}
}
// dorsal fin + rudder (frontmost)
{
let vF = vec2f(0.160, 0.000);
let vT = vec2f(0.276, -0.135);
let vK = vec2f(0.300, 0.000);
let e0 = vT - vF; let n0 = normalize(vec2f(e0.y, -e0.x));
let e1 = vK - vT; let n1 = normalize(vec2f(e1.y, -e1.x));
let e2 = vF - vK; let n2 = normalize(vec2f(e2.y, -e2.x));
let d0 = dot(aq - vF, n0);
let d1 = dot(aq - vT, n1);
let d2 = dot(aq - vK, n2);
let d = max(max(d0, d1), d2);
let cov = smoothstep(aa, -aa, d);
if (cov > 0.001) {
let ctr = vec2f(0.245, -0.045);
var col = fab;
let ao = mix(0.70, 1.0, smoothstep(0.0, 0.075, -aq.y));
col *= ao;
let topF = smoothstep(0.0, -0.11, aq.y);
col = mix(col, col * skyc * 1.5, topF * 0.22);
let kk = clamp(dot(normalize(aq - ctr), ldir), 0.0, 1.0);
col += sunc * kk * 0.14;
col *= (1.0 + 0.045 * sin(aq.x * PI2 / 0.017));
let rl = smoothstep(0.80, 0.99, cos(aq.x * PI2 / 0.017));
col *= (1.0 - 0.10 * rl);
col *= (0.96 + 0.05 * fbm3(aq * 36.0));
let hinge = -0.030 + 0.0015 * sin(t * 0.7);
let seam = smoothstep(0.0016, 0.0, abs(d1 - hinge));
col *= (1.0 - 0.45 * seam * cov);
let rud = smoothstep(hinge, hinge + 0.004, d1);
col = mix(col, col * 1.04, rud * 0.6);
let tr = smoothstep(-trimw, -0.0008, d);
col = mix(col, mix(brass, brassHi, kk), tr * 0.92);
c = mix(c, col, cov * dawn);
let k1 = sdCircle(aq - vec2f(0.283, -0.085), 0.0042);
let k2 = sdCircle(aq - vec2f(0.290, -0.030), 0.0042);
let kc = smoothstep(aa, -aa, min(k1, k2));
c = mix(c, mix(brass, brassHi, 0.6), kc * dawn);
}
}
}
// ═══════════════════════ GONDOLA (teak-and-brass cabin) ═══════════════════════
{
let gc = vec2f(0.0, 0.15);
let gq = aq - gc;
if (abs(gq.x) < 0.145 && abs(gq.y) < 0.09) {
let hb = vec2f(0.086, 0.036);
let aaU = 0.0018;
let sun = mod_tgo_suncol(sel);
let keyDir = normalize(vec2f(0.72, 0.60));
let fillDir = normalize(vec2f(-0.25, -1.0));
let sag = 0.012 * (1.0 - smoothstep(0.0, hb.x, abs(gq.x)));
let bb = vec2f(hb.x, hb.y + sag * 0.5);
let gqh = vec2f(gq.x, gq.y - sag * 0.5);
let dHull = sdRoundedBox(gqh, bb, 0.019);
let hull = smoothstep(aaU, -aaU, dHull);
let nrm = normalize(vec2f(gq.x / hb.x, gq.y / (hb.y * 1.6)) + vec2f(0.0, 0.0002));
let diff = max(dot(nrm, keyDir), 0.0);
let fill = max(dot(nrm, fillDir), 0.0);
var col = vec3f(0.0);
var alpha = 0.0;
if (hull > 0.001) {
var grain = fbm3(vec2f(gq.x * 30.0, gq.y * 150.0));
grain += 0.30 * fbm3(vec2f(gq.x * 85.0 + 4.0, gq.y * 30.0));
var teak = mix(vec3f(0.33, 0.175, 0.085), vec3f(0.47, 0.285, 0.15),
clamp(grain * 0.6 + 0.4, 0.0, 1.0));
let plankN = 4.0;
let ph = fract((gq.y / (hb.y * 2.0) + 0.5) * plankN);
let seam = smoothstep(0.0, 0.09, ph) * smoothstep(0.0, 0.09, 1.0 - ph);
teak *= mix(0.60, 1.0, seam);
var lit = vec3f(0.32)
+ 0.95 * diff * sun
+ 0.26 * fill * vec3f(0.5, 0.63, 0.85);
let belly = smoothstep(0.15, 1.0, gq.y / hb.y);
lit += belly * 0.55 * sun * vec3f(1.0, 0.86, 0.70);
let topAO = smoothstep(-hb.y * 1.05, -hb.y * 0.15, gq.y);
lit *= mix(0.5, 1.0, topAO);
col = teak * lit;
let brass = vec3f(0.72, 0.52, 0.22);
let brassLit = brass * (0.42 + 0.85 * diff) * (0.6 + 0.4 * sun)
+ sun * pow(diff, 8.0) * 0.6;
let railD = abs(gq.y - (-hb.y + 0.006)) - 0.0055;
let rail = smoothstep(aaU, -aaU, railD);
let bx = abs(gq.x) - 0.068;
let bandD = abs(bx) - 0.0055;
let band = smoothstep(aaU, -aaU, bandD);
let brassMask = max(rail, band) * hull;
let rivY = fract(gq.y * 42.0);
let rivet = smoothstep(0.30, 0.18, abs(rivY - 0.5)) * band * hull;
col = mix(col, brassLit, brassMask * 0.95);
col = mix(col, brassLit * 1.25 + 0.10, rivet * 0.6);
alpha = hull;
}
// windows: 3 warm-lit glass panes
{
let wx = gq.x - clamp(round(gq.x / 0.046), -1.0, 1.0) * 0.046;
let winY = -0.003;
let wq = vec2f(wx, gq.y - winY);
let wHalf = vec2f(0.0125, 0.0135);
let dWin = sdRoundedBox(wq, wHalf, 0.004);
let winIn = smoothstep(aaU, -aaU, dWin) * hull;
let frame = smoothstep(aaU, -aaU, abs(dWin - 0.0006) - 0.0026) * hull;
if (winIn > 0.001 || frame > 0.001) {
let brassF = vec3f(0.75, 0.55, 0.24) * (0.5 + 0.8 * diff);
let paneV = wq.y / wHalf.y;
let core = smoothstep(1.0, 0.0, length(wq / wHalf));
let warmLamp = vec3f(1.05, 0.74, 0.36);
let coolSky = vec3f(0.48, 0.60, 0.82);
var glass = warmLamp * (0.55 + 0.85 * core);
let refl = smoothstep(0.2, -0.9, paneV);
glass = mix(glass, coolSky, 0.40 * refl);
let streak = smoothstep(0.03, 0.0, abs(wq.x + wq.y * 0.8 - 0.004));
glass += vec3f(0.9, 0.95, 1.0) * streak * 0.35;
col = mix(col, brassF, frame);
col = mix(col, glass, winIn);
alpha = max(alpha, max(winIn, frame));
}
}
c = mix(c, col, clamp(alpha, 0.0, 1.0) * dawn);
// keel skid + contact shadow
{
let skidY = hb.y + 0.006 - 0.012 * (1.0 - smoothstep(0.0, hb.x * 0.9, abs(gq.x)));
let dSkid = sdSegment(gq, vec2f(-hb.x * 0.72, skidY), vec2f(hb.x * 0.72, skidY)) - 0.0035;
let skid = smoothstep(aaU, -aaU, dSkid);
let skidLit = max(dot(normalize(vec2f(0.4, 1.0)), keyDir), 0.0);
let skidCol = vec3f(0.60, 0.44, 0.20) * (0.45 + 0.8 * skidLit) + sun * 0.30;
let contact = smoothstep(0.012, 0.0, dSkid) * (1.0 - skid);
c = mix(c, c * 0.6, contact * 0.5 * dawn);
c = mix(c, skidCol, skid * dawn);
}
// rudder at stern + brass hinge pin
{
let rq = gq - vec2f(hb.x + 0.010, 0.002);
let dRud = sdRoundedBox(rq, vec2f(0.011, 0.019), 0.004);
let rud = smoothstep(aaU, -aaU, dRud);
let rgrain = fbm3(vec2f(rq.x * 120.0, rq.y * 60.0));
let rudLit = max(dot(normalize(vec2f(0.8, 0.4)), keyDir), 0.0);
let rudCol = mix(vec3f(0.30, 0.16, 0.08), vec3f(0.44, 0.26, 0.14), rgrain * 0.6 + 0.4)
* (0.4 + 0.9 * rudLit * length(sun));
let pin = smoothstep(aaU, -aaU, sdCircle(rq - vec2f(-0.009, 0.0), 0.004));
c = mix(c, rudCol, rud * dawn);
c = mix(c, vec3f(0.80, 0.60, 0.28), pin * rud * dawn);
}
// warm lamp bloom leaking from the window row
{
let wx = gq.x - clamp(round(gq.x / 0.046), -1.0, 1.0) * 0.046;
let wq = vec2f(wx, gq.y + 0.003);
let g = exp(-dot(wq, wq) / 0.00035);
c += vec3f(1.0, 0.66, 0.30) * g * 0.22 * dawn;
}
}
}
// ═══════════ RIGGING (suspension between envelope and gondola) ═══════════
// FINE-LINE LOD: thin dark strokes read as noise at cinematic scale (Galen)
if (uni(113) < 0.0005) {
let sunc = mod_tgo_suncol(sel);
let steelDark = vec3f(0.085, 0.095, 0.115);
let steelLit = mix(vec3f(0.58, 0.60, 0.66), sunc, 0.55) * 1.15;
let brassDk = mix(vec3f(0.34, 0.24, 0.12), sunc, 0.25);
let brassLt = mix(vec3f(0.86, 0.64, 0.32), sunc, 0.45) * 1.2;
var rig = 0.0;
var rigCol = vec3f(0.0);
var glint = 0.0;
var beadC = 0.0;
var beadCol= vec3f(0.0);
let N = 9;
for (var i = 0; i < N; i = i + 1) {
let fr = f32(i) / f32(N - 1);
let topX = mix(-0.16, 0.16, fr);
let botX = mix(-0.075, 0.075, fr);
let eyU = 0.10 * sqrt(max(0.0, 1.0 - (topX / 0.26) * (topX / 0.26)));
let A = vec2f(topX, eyU - 0.004);
let B = vec2f(botX, 0.117);
let AB = B - A;
let L2 = max(dot(AB, AB), 1e-6);
let u = clamp(dot(aq - A, AB) / L2, 0.0, 1.0);
let sag = 0.009 * u * (1.0 - u);
let curve= A + AB * u + vec2f(0.0, sag);
let d = length(aq - curve);
let line = smoothstep(0.0028, 0.0014, d);
let lit = clamp(0.30 + 0.70 * smoothstep(-0.22, 0.20, curve.x), 0.0, 1.0);
let col = mix(steelDark, steelLit, lit);
if (line > rig) { rig = line; rigCol = col; }
glint = max(glint, line * lit * lit);
let bA = smoothstep(0.0060, 0.0030, length(aq - A));
let bB = smoothstep(0.0058, 0.0028, length(aq - B));
let bl = clamp(0.30 + 0.70 * smoothstep(-0.22, 0.20, aq.x), 0.0, 1.0);
let bc = mix(brassDk, brassLt, bl);
let bcov = max(bA, bB);
if (bcov > beadC) { beadC = bcov; beadCol = bc; }
}
// attachment batten following the envelope underside curve
{
let ey = 0.10 * sqrt(max(0.0, 1.0 - (aq.x / 0.26) * (aq.x / 0.26))) - 0.004;
let mask = step(abs(aq.x), 0.165);
let dbat = abs(aq.y - ey);
let bat = smoothstep(0.0026, 0.0013, dbat) * mask;
let lit = clamp(0.30 + 0.70 * smoothstep(-0.22, 0.20, aq.x), 0.0, 1.0);
let col = mix(steelDark, steelLit, lit);
if (bat > rig) { rig = bat; rigCol = col; }
glint = max(glint, bat * lit * lit);
}
// faint load-net curtain over the lower envelope
{
let eyLow = 0.10 * sqrt(max(0.0, 1.0 - (aq.x / 0.26) * (aq.x / 0.26)));
let inLower = smoothstep(0.0, 0.020, aq.y)
* smoothstep(0.004, 0.022, eyLow - aq.y)
* step(abs(aq.x), 0.215);
let g1 = abs(fract((aq.x + aq.y) * 26.0) - 0.5);
let g2 = abs(fract((aq.x - aq.y) * 26.0) - 0.5);
let net = (smoothstep(0.055, 0.0, g1) + smoothstep(0.055, 0.0, g2)) * inLower;
let lit = clamp(0.35 + 0.65 * smoothstep(-0.22, 0.20, aq.x), 0.0, 1.0);
c = mix(c, mix(steelDark, steelLit, lit), clamp(net, 0.0, 1.0) * 0.13 * dawn);
}
c = mix(c, rigCol, rig * 0.92 * dawn);
c += sunc * glint * 0.35 * dawn;
c = mix(c, beadCol, beadC * dawn);
c += sunc * beadC * 0.25 * dawn;
}
// ═════════ PROPULSION (nacelles, motion-blur props, exhaust) ═════════
{
let dc = clamp(dawn, 0.0, 1.0);
if (dc > 0.002) {
let aa = 0.0022;
let key = mod_tgo_suncol(sel);
let brass = vec3f(0.42, 0.29, 0.13);
let brassHi = vec3f(0.98, 0.80, 0.44);
let steel = vec3f(0.66, 0.70, 0.78);
let coolFill = vec3f(0.40, 0.50, 0.64);
let L = normalize(vec3f(0.55, 0.45, 0.70));
for (var i = 0; i < 2; i = i + 1) {
let sx = select(-1.0, 1.0, i == 1);
let nc = vec2f(sx * 0.10, 0.13);
let bnac = vec2f(0.05, 0.026);
let pc = nc + vec2f(-0.05, 0.0);
let R = 0.05;
// exhaust heat shimmer, trailing aft
{
let s0 = nc + vec2f(0.052, 0.0);
let tv = aq - s0;
let along = tv.x;
if (along > -0.005 && along < 0.16) {
let n = fbm3(vec2f(tv.x * 26.0 - t * 1.6, tv.y * 46.0 + f32(i) * 7.0));
let wob = (n * 2.0 - 1.0);
let yc = tv.y - wob * 0.014 * smoothstep(0.0, 0.10, along);
let w = 0.014 + along * 0.12;
let prof = smoothstep(w, 0.0, abs(yc));
let fall = smoothstep(0.16, 0.0, along) * smoothstep(-0.005, 0.02, along);
let heat = prof * fall;
c += key * heat * 0.10 * dc;
c = mix(c, c * vec3f(1.05, 1.015, 0.97), heat * 0.6 * dc);
}
}
// outrigger strut
{
let a = nc + vec2f(0.0, -0.02);
let b = vec2f(sx * 0.05, 0.145);
let ds = sdSegment(aq, a, b);
let cov = smoothstep(0.006, 0.003, ds) * dc;
let scol = mix(brass * 0.7, brassHi, 0.30);
c = mix(c, scol, cov * 0.9);
}
// nacelle body
{
let d = aq - nc;
let sd = sdRoundedBox(d, bnac, 0.016);
let cov = smoothstep(aa, -aa, sd) * dc;
if (cov > 0.0) {
let ny = clamp(d.y / bnac.y, -1.0, 1.0);
let nz = sqrt(max(0.0, 1.0 - ny * ny));
let nrm = vec3f(0.0, ny, nz);
let diff = max(dot(nrm, L), 0.0);
var col = brass * (0.30 + 0.75 * diff);
col += coolFill * max(0.0, -ny) * 0.22;
col += brassHi * pow(diff, 10.0) * 0.6;
col *= 0.9 + 0.15 * fbm3(d * 40.0 + vec2f(f32(i) * 3.0));
let rv = abs(fract(d.x * 36.0) - 0.5);
let rrow = smoothstep(0.04, 0.02, abs(d.y - bnac.y * 0.55));
col += brassHi * smoothstep(0.06, 0.0, rv) * rrow * 0.12;
col *= mix(1.0, 0.7, smoothstep(0.0, -bnac.y, d.y) * 0.6);
let rim = smoothstep(-0.004, 0.0, sd);
col = mix(col, brassHi, rim * 0.25 * max(0.0, diff));
c = mix(c, col, cov);
}
}
// spinning propeller: translucent motion-blurred disc
{
let d = aq - pc;
let rr = length(d);
let rn = rr / R;
if (rn < 1.06) {
let ang = atan2(d.y, d.x);
let omega = t * 11.0;
let blades = 0.5 + 0.5 * sin(ang * 3.0 - omega);
let fine = 0.5 + 0.5 * sin(ang * 24.0 - omega * 3.0);
let streak = mix(blades, blades * fine, 0.55);
let a_rad = pow(clamp(1.0 - rn, 0.0, 1.0), 0.55);
let core = smoothstep(0.10, 0.30, rn);
let alpha = a_rad * core * (0.09 + 0.24 * streak) * dc;
let sweep = 0.5 + 0.5 * sin(ang - omega * 0.5);
let tint = mix(steel * 0.7, brassHi, sweep * 0.8);
c = mix(c, tint, clamp(alpha, 0.0, 0.85));
let tip = smoothstep(0.82, 1.0, rn) * smoothstep(0.75, 1.0, streak)
* (1.0 - smoothstep(1.0, 1.06, rn));
c += key * tip * 0.35 * dc;
}
}
// glinting hub
{
let d = aq - pc;
let hd = length(d);
let hub = smoothstep(0.014, 0.010, hd) * dc;
if (hub > 0.0) {
let ny = clamp(d.y / 0.012, -1.0, 1.0);
let nz = sqrt(max(0.0, 1.0 - ny * ny));
let diff = max(dot(vec3f(0.0, ny, nz), L), 0.0);
var hcol = mix(steel * 0.5, steel, diff);
hcol += brassHi * pow(diff, 6.0) * 0.7;
c = mix(c, hcol, hub);
let pip = smoothstep(0.004, 0.0, length(d - vec2f(0.003, 0.003)));
c += brassHi * pip * 0.8 * dc;
}
}
}
}
}
// ═════════ VENTED STEAM — wisp curling off the aft engine ═════════
{
var sp = aq - vec2f(0.12, 0.13);
let rise = clamp(-sp.y, 0.0, 0.36);
sp.x -= rise * 0.55;
sp.x += 0.045 * sin(rise * 20.0 - t * 2.0);
let core = exp(-(sp.x * sp.x) / (0.0016 + rise * 0.022));
let life = smoothstep(0.0, 0.03, rise) * (1.0 - smoothstep(0.12, 0.36, rise));
let puff = 0.45 + 0.55 * vnoise3(vec3f(sp.x * 11.0, sp.y * 11.0, t * 0.6));
let steam = core * life * puff * dawn;
c += mix(vec3f(0.86, 0.90, 0.96), atmSun, 0.20) * steam * 0.30;
}
// ══════════════════ NAV LIGHTS + GLOW (additive, on top) ══════════════════
{
let warm = mod_tgo_suncol(sel);
let lampBoost = (0.80 + night * 0.55) * dawn;
{
let bq = aq / vec2f(0.32, 0.22);
let bloom = exp(-dot(bq, bq) * 2.0);
c += warm * bloom * 0.05 * dawn;
}
let twP = 0.92 + 0.08 * sin(t * 2.7 + 0.4);
let twS = 0.92 + 0.08 * sin(t * 2.3 + 2.1);
let twT = 0.85 + 0.15 * sin(t * 5.1 + 1.0);
// PORT lamp (red)
{
let q = aq - vec2f(-0.135, 0.112);
let r = length(q);
let hot = smoothstep(0.012, 0.0, r);
let lamp = exp(-r * 44.0);
let col = vec3f(1.00, 0.11, 0.07);
c += (col * lamp * 0.85 + mix(col, vec3f(1.0), 0.7) * hot * 1.4) * twP * lampBoost;
}
// STARBOARD lamp (green)
{
let q = aq - vec2f(0.135, 0.112);
let r = length(q);
let hot = smoothstep(0.012, 0.0, r);
let lamp = exp(-r * 44.0);
let col = vec3f(0.10, 1.00, 0.30);
c += (col * lamp * 0.85 + mix(col, vec3f(1.0), 0.7) * hot * 1.4) * twS * lampBoost;
}
// TAIL beacon (warm-white)
{
let q = aq - vec2f(0.262, 0.0);
let r = length(q);
let hot = smoothstep(0.011, 0.0, r);
let lamp = exp(-r * 48.0);
let col = mix(vec3f(1.0), warm, 0.25);
c += (col * lamp * 0.80 + vec3f(1.0) * hot * 1.5) * twT * lampBoost;
}
// NOSE headlamp (warm amber) + forward cone
{
let lp = vec2f(-0.258, 0.0);
let q = aq - lp;
let r = length(q);
let hot = smoothstep(0.016, 0.0, r);
let lamp = exp(-r * 36.0);
let col = mix(vec3f(1.00, 0.82, 0.55), warm, 0.35);
c += (col * lamp * 1.0 + vec3f(1.00, 0.93, 0.78) * hot * 1.6) * lampBoost;
let fwd = -q.x;
let ahead = smoothstep(0.0, 0.015, fwd);
let reach = exp(-fwd * 4.0);
let spread = 0.045 + fwd * 0.5;
let lat = exp(-(q.y * q.y) / (spread * spread));
let flick = 0.9 + 0.1 * sin(t * 7.0);
c += col * ahead * reach * lat * 0.14 * flick * dawn;
}
}
// ═══════════ THE KEEL CRYSTAL (Act I finale) — slung under the gondola; the
// sea-cave's pearl-beam lights it, and only then will the helm turn. ═══════════
{
let powered = uni(110);
let cq = aq - vec2f(0.0, 0.345);
// a hang-wire from the keel down to the stone
let wire = smoothstep(0.0028, 0.0, abs(cq.x)) * step(cq.y, 0.0) * step(-0.12, cq.y) * step(uni(113), 0.0005);
c = mix(c, vec3f(0.14, 0.12, 0.09), wire * 0.6);
// faceted crystal (a tapered bipyramid in 2D)
let dia = abs(cq.x) / 0.028 + abs(cq.y) / 0.058 - 1.0;
let body = smoothstep(0.05, -0.03, dia);
let facet = 0.45 + 0.55 * sin(atan2(cq.y, cq.x) * 4.0 + 0.6);
let dark = vec3f(0.10, 0.15, 0.20) * (0.5 + 0.7 * facet);
let lit = mix(vec3f(0.15, 0.55, 0.75), vec3f(0.7, 1.0, 1.0), facet) * (0.7 + 0.5 * sin(t * 4.0 + cq.y * 30.0));
let cry = mix(dark, lit, powered);
c = mix(c, cry, body);
// the crystal's own light once powered — a cool bloom + a hot core
c += vec3f(0.20, 0.60, 0.85) * powered * exp(-dot(cq, cq) * 55.0) * (0.6 + 0.4 * sin(t * 3.0));
c += vec3f(0.6, 0.95, 1.0) * powered * smoothstep(-0.02, -0.10, dia) * 0.5;
}
// the LASER out of the sea (lower-left, the sea-cave's bearing) to the crystal
{
let powered = uni(110) * step(uni(113), 0.0005); // the works' beam belongs to the SHORE — never the crossing cinematic
if (powered > 0.5) {
let a = vec2f(-1.06, -0.13); // TG_OCU — born at the crystal works' tall spire tip (half off-screen left)
let b = ac + vec2f(0.0, 0.345); // the keel crystal
let ab = b - a; let ap = p - a;
let hh = clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0);
let dd = length(ap - ab * hh);
let core = exp(-dd * dd * 6500.0);
let glow = exp(-dd * dd * 380.0);
c += (vec3f(0.75, 1.0, 1.0) * core + vec3f(0.20, 0.60, 0.90) * glow) * (0.7 + 0.3 * sin(t * 16.0 - hh * 12.0));
}
}
return c;
}
// ═══ THE SUMMONING (uni 115 = s, 0..1 over 12s). Phase map is LAW (shared with
// the hook): 0–.12 bolt · .12–.35 portal opens · .30–.80 ship emerges ·
// .80–.95 seal (flash .92) · ≥.95 clean sky. Portal center TG_PC sits
// up-right of the mooring; the ship slides PC→mooring while its own dawn
// fade brings it up — reads as coming THROUGH without a hard clip. ═══
fn tg_portalR(s: f32) -> f32 {
// the aperture's radius over the summon clock — ONE truth for the portal draw
// and the emergence clip (the ship must come OUT OF the hole, not fade behind it)
return 0.26 * smoothstep(0.12, 0.35, s) * (1.0 - smoothstep(0.80, 0.95, s));
}
fn tg_portalAt(cIn: vec3f, p: vec2f, t: f32, s: f32, PCin: vec2f) -> vec3f {
var c = cIn;
let PC = PCin;
let openA = smoothstep(0.12, 0.35, s); // iris out
let sealA = 1.0 - smoothstep(0.80, 0.95, s); // iris back in
let R = tg_portalR(s);
if (R < 0.004) {
// the seal-flash: one bright collapse spark at s≈0.92
let fl = exp(-abs(s - 0.92) * 90.0);
c += vec3f(0.9, 0.8, 1.1) * fl * exp(-dot(p - PC, p - PC) * 120.0);
return c;
}
let d = p - PC;
let r = length(d);
let a = atan2(d.y, d.x);
// interior: an elsewhere-sky — darker, star-flecked, slow swirl
let inA = smoothstep(R, R * 0.85, r);
let sw = rot2(t * 0.15 + r * 6.0) * d;
let stars = max(fbm(sw * 30.0 + vec2f(9.0), 2) - 0.62, 0.0) * 6.0;
let inCol = vec3f(0.015, 0.020, 0.045) + vec3f(0.5, 0.6, 0.9) * stars * 0.35;
c = mix(c, inCol, inA * 0.92);
// rim: the same arcane ring-language as the suns — bright annulus + a slow
// second ring precessing just inside it
let rim = smoothstep(0.020, 0.004, abs(r - R));
c += vec3f(0.55, 0.70, 1.15) * rim * (0.8 + 0.2 * sin(a * 6.0 - t * 2.0));
let q = rot2(t * 0.4) * d;
let e2 = abs(length(vec2f(q.x, q.y / 0.55)) - R * 0.82);
c += vec3f(0.35, 0.45, 0.85) * smoothstep(0.014, 0.003, e2) * 0.5;
// filament lightning crawling the rim: angular noise spikes, always ≥ 0
let fil = max(fbm(vec2f(a * 3.0, t * 2.2), 2) - 0.55, 0.0) * 4.0;
c += vec3f(0.75, 0.80, 1.2) * fil * smoothstep(0.05, 0.0, abs(r - R)) * 0.8;
// breath of light spilling from the mouth
c += vec3f(0.20, 0.28, 0.55) * exp(-max(r - R, 0.0) * 9.0) * 0.35 * openA * sealA;
return c;
}
fn tg_portal(cIn: vec3f, p: vec2f, t: f32, s: f32) -> vec3f {
return tg_portalAt(cIn, p, t, s, vec2f(-0.24, -0.66)); // the shore's arrival gate
}
fn tg_skybolt(cIn: vec3f, p: vec2f, t: f32, s: f32) -> vec3f {
// the summons as seen from ANY outdoor sky: one violet-white flash-bolt
// dropping toward the portal point during s 0–0.12.
var c = cIn;
let k = smoothstep(0.0, 0.02, s) * (1.0 - smoothstep(0.09, 0.13, s));
if (k < 0.003) { return c; }
let PC = vec2f(-0.24, -0.66);
var m = 0.0;
var aPt = vec2f(PC.x + 0.10, -1.05);
for (var i = 0; i < 5; i = i + 1) {
let fi = f32(i);
let bPt = mix(aPt, PC, 0.30 + fi * 0.05) + vec2f(sin(fi * 17.7 + floor(t * 24.0)) * 0.05, 0.06);
m = max(m, smoothstep(0.010, 0.0, sdSegment(p, aPt, bPt)));
aPt = bPt;
}
m = max(m, smoothstep(0.010, 0.0, sdSegment(p, aPt, PC)));
c += vec3f(0.85, 0.75, 1.25) * m * k;
c += vec3f(0.30, 0.28, 0.55) * k * exp(-(p.y + 0.8) * (p.y + 0.8) * 4.0) * 0.25; // sky afterglow
return c;
}
// ═══ THE CAVE-HOUSE — the outside of the sea-cave machine, crouched on the
// left rocks. Its brass oculus TG_OCU is where the keel-crystal ray is
// born (was: bare ocean). Drawn always; powered (uni 110) wakes the eye. ═══
// TG_OCU = vec2f(-0.80, 0.355) ← THE shared anchor. The laser block below must
// use this exact value as `a`.
fn tg_cavehouse(cIn: vec3f, p: vec2f, t: f32, night: f32, dawn: f32) -> vec3f {
// ═══ THE CRYSTAL WORKS (Galen Aug 10 v2): not a hut — a distant crystal
// building half off the LEFT edge, behind the bell-buoys, subtle against
// the dusk. Three faceted spires rising off the waterline; the tallest
// tip is TG_OCU where the keel-crystal ray is born (uni 110). ═══
var c = cIn;
let H = vec2f(-1.02, 0.17); // base center — half off-screen, on the sea line
let d = p - H;
if (d.x > 0.34 || abs(d.y) > 0.42) { return c; }
let powered = uni(110);
let haze = 0.62; // distance: everything blends toward sky
// three spires: (dx, half-width, height). Tallest carries the oculus.
for (var i = 0; i < 3; i = i + 1) {
var sx = 0.08; var hw = 0.052; var ht = 0.21;
if (i == 1) { sx = -0.04; hw = 0.075; ht = 0.30; } // the tall one
if (i == 2) { sx = 0.19; hw = 0.045; ht = 0.14; }
let q = d - vec2f(sx, 0.0);
// a tapered crystal: width shrinks with height, faceted by a fold on |x|
let up = clamp(-q.y / ht, 0.0, 1.0);
let w = hw * (1.0 - up * 0.82);
let inside = step(abs(q.x), w) * step(-ht, q.y) * step(q.y, 0.015);
if (inside > 0.5) {
// facet shading: a hard crease down the middle, dusk key from the right
let side = step(0.0, q.x);
var fc = mix(vec3f(0.070, 0.088, 0.110), vec3f(0.135, 0.150, 0.170), side);
fc += vec3f(0.16, 0.11, 0.06) * side * (1.0 - up) * 0.5; // sunset kiss on the lit face
fc += vec3f(0.06, 0.14, 0.16) * (0.5 + 0.5 * sin(q.y * 90.0 + f32(i) * 2.1)) * 0.14; // internal lamination
// faint inner life — a slow breathing glow, brighter when powered
fc += vec3f(0.10, 0.30, 0.36) * exp(-abs(q.x) * 26.0) * (0.10 + 0.22 * powered) * (0.6 + 0.4 * sin(t * 0.9 + f32(i)));
c = mix(c, fc, haze); // hazy: the sky bleeds through — it reads distant
}
// crest spark at each tip
let tipd = length(q - vec2f(0.0, -ht));
c += vec3f(0.45, 0.70, 0.80) * exp(-tipd * tipd * 2600.0) * (0.10 + 0.30 * powered);
}
// waterline root: a dark shelf + its broken reflection
let shelf = step(abs(d.y - 0.02), 0.018) * step(abs(d.x - 0.06), 0.30)
;
c = mix(c, vec3f(0.045, 0.050, 0.058), shelf * 0.5);
c += vec3f(0.10, 0.12, 0.13) * step(0.02, d.y) * step(d.y, 0.16) * step(abs(d.x + 0.04), 0.10)
* (0.5 + 0.5 * sin(d.y * 120.0 + t * 1.4)) * 0.10;
// TG_OCU — the tall spire's tip, where the ray is born (world anchor -1.06+0.0? see laser)
let od = p - vec2f(-1.06, -0.13);
let orr = length(od);
c += vec3f(0.25, 0.70, 0.95) * exp(-orr * orr * 2400.0) * powered * (0.7 + 0.3 * sin(t * 3.0));
c += vec3f(0.10, 0.35, 0.55) * exp(-orr * orr * 300.0) * powered * 0.4;
return c;
}
module · tg_flycine
// ══ THE CROSSING — Act I finale cinematic (uni 113 = timeline 0..1). Not a
// baked video: every frame computes live from the timeline — the pixel-
// playback spirit. The helm flash → the airship, belly lit by fire, crosses
// an OCEAN OF FLAMES under a smoke sky and the ruling moon → the ACT I card
// holds until a tap. Author: Claude Fable 5.
fn tgc_h1(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgc_n2(p: vec2f) -> f32 {
let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
return mix(mix(tgc_h1(i), tgc_h1(i + vec2f(1.0, 0.0)), u.x),
mix(tgc_h1(i + vec2f(0.0, 1.0)), tgc_h1(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgc_fbm(p: vec2f, oct: i32) -> f32 {
var v = 0.0; var a = 0.5; var q = p;
for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgc_n2(q); q = q * 2.07 + vec2f(11.0, 5.0); a *= 0.5; }
return v;
}
fn tgc_seg(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);
}
// a stroked letter mark: bright where within w of the stroke
fn tgc_stroke(d: f32, w: f32) -> f32 { return smoothstep(w, w - 1.4, d); }
// ── THE FLAME SEA: an emissive rolling fire-field on the y=0 plane ──
fn tgc_flamesea(ro: vec3f, rd: vec3f, t: f32) -> vec3f {
let ts = -ro.y / rd.y; // rd.y < 0 guaranteed by caller
var sp = ro + rd * ts;
// rolling swell + fine crackle, drifting toward the horizon
let drift = vec2f(t * 0.35, t * 0.9);
var hgt = tgc_fbm(sp.xz * 0.33 + drift * 0.3, 4);
// parallax lick: re-sample where the crest would displace the surface
sp += rd * hgt * 1.3;
hgt = tgc_fbm(sp.xz * 0.33 + drift * 0.3, 4);
let crackle = tgc_fbm(sp.xz * 1.7 + drift, 3);
let tongues = pow(tgc_fbm(sp.xz * 0.9 - drift * 0.6, 3), 2.0);
// emissive palette: deep ember → orange body → yellow crest → white-hot tips
var f = clamp(hgt * 0.75 + crackle * 0.45 + tongues * 0.5, 0.0, 1.6);
var col = mix(vec3f(0.22, 0.025, 0.008), vec3f(0.95, 0.26, 0.03), smoothstep(0.15, 0.7, f));
col = mix(col, vec3f(1.35, 0.85, 0.22), smoothstep(0.7, 1.05, f));
col = mix(col, vec3f(1.6, 1.35, 0.9), smoothstep(1.05, 1.35, f));
// living flicker
col *= 0.85 + 0.3 * tgc_n2(sp.xz * 3.0 + vec2f(t * 4.0, t * 5.0));
// distance: the fire sinks into its own smoke toward the horizon
let dist = clamp(ts * 0.055, 0.0, 1.0);
col = mix(col, vec3f(0.30, 0.06, 0.02), dist * 0.85);
return col;
}
// ── THE AIRSHIP, crossing — a silhouette underlit by the burning sea ──
fn tgc_ship(cIn: vec3f, p: vec2f, s: f32, t: f32) -> vec3f {
// THE REAL SHIP crosses (Galen Aug 10: the cinematic ship must BE the shore
// ship). flycine compiles last, so it borrows mod_tg_airship whole under an
// affine map: cine pos/scale/bank → the ship's home frame at ac(-0.58,-0.50),
// envelope 0.26/0.34 matching the old silhouette's proportions. Its sea-
// reflection band maps below the frame at these scales; the works' laser is
// gated off during the cine inside tg_air.
var c = cIn;
let e = smoothstep(0.04, 0.88, s); // eased leg of the crossing
var pos = vec2f(mix(-0.30, 0.14, e), mix(0.10, -0.335, pow(e, 0.8)));
// THROUGH the gate (Galen): past the mouth the path keeps going along the
// entry direction, so the clip plane swallows her nose-to-tail; gone by s=.94
pos += normalize(vec2f(0.24, -0.23)) * smoothstep(0.68, 0.94, s) * 0.30;
let scale = mix(0.66, 0.115, pow(e, 0.85));
let bank = mix(0.07, -0.03, e) + 0.010 * sin(t * 0.5);
let cb = cos(bank); let sb = sin(bank);
var q = (p - pos - vec2f(0.006 * sin(t * 0.23), 0.010 * sin(t * 0.19))) / scale;
q = vec2f(q.x * cb - q.y * sb, q.x * sb + q.y * cb);
let pm = vec2f(-0.58, -0.50) + q * 0.7647; // cine local → ship home frame
c = mod_tg_airship(c, pm, t, 1.0, -0.16, 0.2);
// THE FIRE BELOW still paints her belly amber — the cine's signature light,
// added over the real hull in cine-local coords (same formula as the old cut)
let env = length(q / vec2f(0.34, 0.125)) - 1.0;
let belly = smoothstep(-0.02, 0.10, q.y) * smoothstep(0.30, 0.20, abs(q.x)) * step(env, 0.0);
c += vec3f(0.95, 0.34, 0.07) * belly * (0.35 + 0.12 * sin(t * 3.1 + q.x * 9.0));
// hot rim hugging the envelope's lower edge — BANDED, not a half-plane (the
// unmasked version washed everything below the centerline + made the clip wedge)
c += vec3f(1.2, 0.5, 0.12) * smoothstep(-0.06, -0.015, env) * (1.0 - smoothstep(0.0, 0.045, env)) * step(0.0, q.y) * 0.30;
return c;
}
fn mod_tg_flycine(p: vec2f, px: vec2f, t: f32) -> vec3f {
let s = clamp(uni(113), 0.0, 1.0);
// camera: fixed wide shot over the burning sea
let ro = vec3f(0.0, 2.3, 6.0);
var uvx = px.x / 256.0 - 1.0;
var uvy = -(px.y / 256.0 - 1.0);
// heat shimmer: the whole frame breathes near the fire line
let heat = smoothstep(0.25, -0.35, uvy) * 0.012;
uvx += (tgc_n2(vec2f(uvx * 7.0, uvy * 7.0 - t * 2.4)) - 0.5) * heat * 2.0;
uvy += (tgc_n2(vec2f(uvx * 6.0 + 9.0, uvy * 6.0 - t * 3.1)) - 0.5) * heat;
let rd = normalize(vec3f(uvx, uvy * 0.75 - 0.14, -1.25));
// ── SKY: smoke banks lit from beneath, the ruling moon drowned in haze ──
// sky lifted (Galen: "just dark") — the smoke itself burns, dark red up top,
// molten toward the fire line, so the frame reads FIRE not void
var c = mix(vec3f(0.30, 0.075, 0.030), vec3f(0.10, 0.030, 0.035), clamp(rd.y * 2.2 + 0.4, 0.0, 1.0));
let smoke = tgc_fbm(vec2f(rd.x * 3.0 + t * 0.10, rd.y * 4.5 - t * 0.05), 4);
c = mix(c, vec3f(0.10, 0.035, 0.03), smoothstep(0.35, 0.75, smoke) * 0.8);
c += vec3f(1.0, 0.36, 0.08) * exp(-abs(rd.y + 0.02) * 9.0) * 0.85; // the fire's under-glow on the smoke
c += vec3f(0.55, 0.16, 0.05) * exp(-abs(rd.y - 0.30) * 3.0) * 0.30; // high smoke banks catch the burn too
// THE FIRE MOON, FURIOUS — the burning eye that rules this crossing (Galen:
// "a really angry version of the fire moon"). Molten core, seething prominence
// licks, a wrath-corona that ignites the smoke around it.
let mdir = normalize(vec3f(0.46, 0.44, -1.0));
let md = distance(normalize(rd), mdir);
let mang = atan2(rd.y - mdir.y, rd.x - mdir.x);
let rage = 0.82 + 0.18 * sin(t * 6.3) * sin(t * 2.9 + 1.7);
let mlick = 0.5 + 0.5 * sin(mang * 9.0 - t * 4.2 + tgc_n2(vec2f(mang * 2.0 + 9.0, t * 0.9)) * 5.5);
let mR = 0.050 * (1.0 + mlick * 0.28);
let mbody = smoothstep(mR, mR * 0.5, md);
var mcol = mix(vec3f(1.35, 0.28, 0.05), vec3f(1.6, 0.95, 0.38), smoothstep(0.040, 0.0, md));
mcol *= 0.85 + 0.45 * tgc_n2(vec2f(mang * 3.0, md * 60.0 - t * 1.6)); // churning surface
c = mix(c, mcol * rage, mbody * 0.96);
c += vec3f(1.25, 0.24, 0.05) * exp(-md * md * 150.0) * 0.85 * rage; // furious corona
c += vec3f(0.80, 0.12, 0.03) * exp(-md * md * 26.0) * 0.38; // wide wrath-glow
c += vec3f(0.55, 0.13, 0.04) * smoke * exp(-md * md * 36.0) * 0.6; // the smoke itself ignites around it
// ── THE OCEAN OF FLAMES ──
if (rd.y < -0.015) {
let sea = tgc_flamesea(ro, rd, t);
c = mix(c, sea, smoothstep(-0.015, -0.05, rd.y));
}
// fire-line blaze right at the horizon
c += vec3f(1.2, 0.5, 0.1) * exp(-pow((rd.y + 0.015) * 26.0, 2.0)) * (0.5 + 0.25 * tgc_n2(vec2f(rd.x * 11.0, t * 1.7)));
// ── EMBERS, rising on the heat ──
{
let cell = vec2f(px.x * 0.055, px.y * 0.055 + t * 1.35);
let id = floor(cell); let fr = fract(cell) - 0.5;
let sp = tgc_h1(id);
let em = smoothstep(0.16, 0.02, length(fr - (vec2f(tgc_h1(id + 7.0), tgc_h1(id + 13.0)) - 0.5) * 0.6));
c += vec3f(1.2, 0.55, 0.12) * em * step(0.965, sp) * (0.4 + 0.6 * sin(t * 5.0 + sp * 40.0)) * smoothstep(-0.45, 0.1, -uvy + 0.4);
}
// ── THE SHIP crosses it all — and leaves through its own gate (Galen):
// an exit portal opens ahead on the path's far end; the ship is clipped
// INTO the hole (parts past the portal plane are already elsewhere). ──
let XPC = vec2f(0.16, -0.34); // where the path ends
let xphase = smoothstep(0.52, 0.60, s) * 0.35; // hold the aperture OPEN (tg_portalR peaks ~0.35)
if (s < 0.945) {
let before = c;
c = tgc_ship(c, p, s, t);
if (s > 0.52) {
let entryDir = normalize(XPC - vec2f(-0.08, -0.11)); // travel direction at the mouth
let inHole = smoothstep(0.015, -0.015, length(p - XPC) - tg_portalR(xphase));
let notYet = smoothstep(0.03, -0.01, dot(p - XPC, entryDir)); // still on our side
c = mix(before, c, max(inHole, notYet));
}
}
if (s > 0.50 && s < 0.97) { c = tg_portalAt(c, p, t, mix(0.12, 0.95, smoothstep(0.50, 0.96, s)), XPC); }
// ── the helm-pull FLASH opens the shot ──
c = mix(c, vec3f(1.0, 0.98, 0.92), smoothstep(0.10, 0.0, s));
// ── THE ACT I CARD (s ≥ 0.82): vignette falls, the mark burns in ──
let card = smoothstep(0.82, 0.96, s);
if (card > 0.001) {
c *= 1.0 - card * 0.62 * smoothstep(1.35, 0.35, length(p)); // the dark comes down
let gold = vec3f(1.0, 0.84, 0.45);
var mk = 0.0;
// A
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(203.0, 238.0), vec2f(214.0, 206.0)), 2.6));
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(214.0, 206.0), vec2f(225.0, 238.0)), 2.6));
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(208.0, 227.0), vec2f(221.0, 227.0)), 2.2));
// C — a ring with its right side open
{
let cd = px - vec2f(247.0, 222.0);
let ring = abs(length(cd) - 15.0);
mk = max(mk, tgc_stroke(ring, 2.6) * smoothstep(0.45, 0.25, cd.x / max(length(cd), 0.001)));
}
// T
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(268.0, 206.0), vec2f(290.0, 206.0)), 2.6));
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(279.0, 206.0), vec2f(279.0, 238.0)), 2.6));
// the numeral I, set apart
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(308.0, 206.0), vec2f(308.0, 238.0)), 2.8));
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(303.0, 206.0), vec2f(313.0, 206.0)), 2.2));
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(303.0, 238.0), vec2f(313.0, 238.0)), 2.2));
// ornament: — ◆ — beneath
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(216.0, 258.0), vec2f(244.0, 258.0)), 1.4));
mk = max(mk, tgc_stroke(abs(px.x - 258.0) + abs(px.y - 258.0) - 5.0, 2.0));
mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(272.0, 258.0), vec2f(300.0, 258.0)), 1.4));
c += gold * mk * card * (0.85 + 0.15 * sin(t * 1.8));
c += gold * mk * card * 0.35 * exp(-tgc_seg(px, vec2f(256.0, 222.0), vec2f(256.0, 222.0)) * 0.0); // soft self-glow
// the tap-to-continue breath, once the card holds
if (s >= 0.995) {
c += vec3f(0.9, 0.78, 0.5) * tg_ring(px, vec2f(256.0, 388.0), 14.0, 11.5) * (0.25 + 0.25 * sin(t * 2.2));
}
}
return max(c, vec3f(0.0));
}
module · tg_kiln
// THE KILN (view 14) — past the burned thicket. Raymarched: the old
// glassmaker's clearing, a beehive stone kiln with a glowing firebox, charred
// bramble framing the edges under a fire-moon night. The island's glass was
// born here; the fire moon has woken it. PAYOFF: click the mouth and a GLASS
// BLOOM rises on the lip (uni(104)). Shore recipe: SDF map → march → normal →
// shadow/AO → materials → fire key light. Anchor (px, LAW): kiln mouth
// (256,290) r60. State: uni(104) kilnBloom · uni(82) elBlend · uni(78) element ·
// mouse uni(27,28) · clickFx uni(84/85/86). Author: Claude Fable 5.
fn tgk_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgk_n2(p: vec2f) -> f32 {
let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
return mix(mix(tgk_h(i), tgk_h(i + vec2f(1.0, 0.0)), u.x),
mix(tgk_h(i + vec2f(0.0, 1.0)), tgk_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgk_fbm(p: vec2f, oct: i32) -> f32 {
var v = 0.0; var a = 0.5; var q = p;
for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgk_n2(q); q = q * 2.03 + vec2f(9.0, 5.0); a *= 0.5; }
return v;
}
fn tgk_sph(p: vec3f, r: f32) -> f32 { return length(p) - r; }
fn tgk_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 tgk_cap(p: vec3f, a: vec3f, b: vec3f, r: f32) -> 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) - r;
}
fn tgk_smin(a: f32, b: f32, k: f32) -> f32 { let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); }
const TGK_KC = vec3f(0.0, 0.95, -1.1); // kiln centre (mouth projects near px 256,290)
// SDF → (dist, matId). mats: 1 kiln stone · 2 mouth-arch brick · 3 ash ground ·
// 4 charred bramble · 5 kiln lip/plinth · 6 glass-bloom stem
fn tgk_map(p: vec3f, bloom: f32, t: f32) -> vec2f {
var d = 1e5; var m = 1.0;
// ── the beehive KILN: stacked stone domes narrowing upward ──
let kp = p - TGK_KC;
var kiln = tgk_sph(kp / vec3f(1.0, 1.15, 1.0), 1.05) * 1.0;
kiln = tgk_smin(kiln, tgk_sph((kp - vec3f(0.0, 0.55, 0.0)) / vec3f(0.82, 1.0, 0.82), 0.78), 0.25);
kiln = tgk_smin(kiln, tgk_sph((kp - vec3f(0.0, 1.05, 0.0)) / vec3f(0.6, 0.9, 0.6), 0.5), 0.22);
// hollow it and cut the FIREBOX MOUTH — an arch on the front face
kiln = max(kiln, -(tgk_sph(kp / vec3f(0.9, 1.05, 0.9), 0.9))); // hollow shell
let mouth = tgk_box(kp - vec3f(0.0, -0.25, 0.95), vec3f(0.42, 0.5, 0.5));
let mouthArch = min(mouth, tgk_sph((kp - vec3f(0.0, 0.2, 0.95)) / vec3f(0.42, 0.42, 0.6), 0.42));
kiln = max(kiln, -mouthArch); // carve the opening
if (kiln < d) { d = kiln; m = 1.0; }
// arch brick lip around the mouth
let ring = abs(length((kp - vec3f(0.0, -0.05, 0.98)).xy / vec2f(0.5, 0.62)) - 1.0) - 0.09;
let brick = max(ring, abs(kp.z - 0.98) - 0.14);
if (brick < d) { d = brick; m = 2.0; }
// ── the ASH GROUND ──
let g = p.y + 0.35 + 0.05 * tgk_fbm(p.xz * 3.0, 3);
if (g < d) { d = g; m = 3.0; }
// stone PLINTH the kiln sits on + a work-LIP shelf at the mouth
let plinth = tgk_box(p - vec3f(0.0, -0.15, -1.1), vec3f(1.25, 0.22, 1.1));
if (plinth < d) { d = plinth; m = 5.0; }
let lip = tgk_box(p - vec3f(0.0, 0.28, 0.05), vec3f(0.6, 0.08, 0.28)); // where the bloom stands
if (lip < d) { d = lip; m = 5.0; }
// ── CHARRED BRAMBLE framing the clearing (twisted arcs at the sides) ──
for (var i = 0; i < 5; i++) {
let fi = f32(i);
let side = select(1.0, -1.0, (i % 2) == 0);
let bx = side * (2.3 + 0.2 * tgk_h(vec2f(fi, 1.0)));
let a = vec3f(bx, -0.35, -0.2 + fi * 0.4);
let b = vec3f(bx * 0.72, 1.4 + 0.3 * tgk_h(vec2f(fi, 3.0)), -1.5 + fi * 0.3);
let mid = (a + b) * 0.5 + vec3f(side * 0.4, 0.2, 0.0);
let br = min(tgk_cap(p, a, mid, 0.05), tgk_cap(p, mid, b, 0.04));
if (br < d) { d = br; m = 4.0; }
}
// ── the GLASS BLOOM stem (only once annealed) — the flower is drawn in shade ──
if (bloom > 0.5) {
let stem = tgk_cap(p, vec3f(0.0, 0.36, 0.05), vec3f(0.0, 1.05, 0.05), 0.045);
if (stem < d) { d = stem; m = 6.0; }
let bulb = tgk_sph(p - vec3f(0.0, 1.12, 0.05), 0.16);
if (bulb < d) { d = bulb; m = 6.0; }
}
return vec2f(d, m);
}
fn tgk_nrm(p: vec3f, bloom: f32, t: f32) -> vec3f {
let e = vec2f(0.006, 0.0);
return normalize(vec3f(
tgk_map(p + e.xyy, bloom, t).x - tgk_map(p - e.xyy, bloom, t).x,
tgk_map(p + e.yxy, bloom, t).x - tgk_map(p - e.yxy, bloom, t).x,
tgk_map(p + e.yyx, bloom, t).x - tgk_map(p - e.yyx, bloom, t).x));
}
fn tgk_march(ro: vec3f, rd: vec3f, bloom: f32, t: f32) -> vec2f {
var tt = 0.02;
for (var i = 0; i < 90; i++) {
let h = tgk_map(ro + rd * tt, bloom, t);
if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
tt += h.x * 0.85; if (tt > 24.0) { break; }
}
return vec2f(-1.0, 0.0);
}
fn tgk_shadow(ro: vec3f, rd: vec3f, bloom: f32, t: f32) -> f32 {
var res = 1.0; var tt = 0.05;
for (var i = 0; i < 14; i++) {
let h = tgk_map(ro + rd * tt, bloom, t).x;
res = min(res, 8.0 * h / tt); tt += clamp(h, 0.03, 0.4);
if (res < 0.03 || tt > 6.0) { break; }
}
return clamp(res, 0.0, 1.0);
}
fn tgk_ao(p: vec3f, n: vec3f, bloom: f32, t: f32) -> f32 {
var occ = 0.0; var w = 1.0;
for (var i = 1; i <= 4; i++) { let hh = 0.05 * f32(i); occ += w * (hh - tgk_map(p + n * hh, bloom, t).x); w *= 0.72; }
return clamp(1.0 - 2.1 * occ, 0.0, 1.0);
}
fn tgk_alb(m: f32, p: vec3f) -> vec3f {
if (m < 1.5) { // kiln stone — fired clay, warm ochre, soot-blushed, coursed
var a = vec3f(0.20, 0.145, 0.105) * (0.75 + 0.5 * tgk_fbm(p.xy * 3.5 + p.zz, 3));
a *= 1.0 - 0.35 * smoothstep(0.35, 0.05, abs(fract(p.y * 3.0) - 0.5)); // stone courses
a *= 1.0 - 0.35 * smoothstep(0.5, 0.0, p.y - TGK_KC.y); // soot up the flue
return a;
} else if (m < 2.5) { return vec3f(0.26, 0.13, 0.08) * (0.8 + 0.4 * tgk_fbm(p.xy * 8.0, 2)); } // mouth brick
else if (m < 3.5) { return vec3f(0.075, 0.065, 0.058) * (0.7 + 0.6 * tgk_fbm(p.xz * 5.0, 3)); } // ash
else if (m < 4.5) { return vec3f(0.045, 0.038, 0.035); } // charred bramble
else if (m < 5.5) { return vec3f(0.10, 0.088, 0.078) * (0.8 + 0.3 * tgk_fbm(p.xz * 4.0, 2)); } // plinth stone
return vec3f(0.4, 0.75, 0.85); // glass (lit specially)
}
fn tgk_shade(ro: vec3f, rd: vec3f, tt: f32, m: f32, bloom: f32, warm: f32, t: f32) -> vec3f {
let p = ro + rd * tt;
let n = tgk_nrm(p, bloom, t);
// KEY = the fire inside the kiln mouth (a warm point light at the firebox)
let fire = TGK_KC + vec3f(0.0, -0.2, 0.7);
let ldir = normalize(fire - p);
let flick = 0.8 + 0.2 * sin(t * 7.0) * sin(t * 4.3 + 1.0);
let fireCol = vec3f(1.5, 0.72, 0.28) * flick;
let dif = max(dot(n, ldir), 0.0);
let sh = tgk_shadow(p + n * 0.03, ldir, bloom, t);
let ao = tgk_ao(p, n, bloom, t);
let fall = 1.6 / (1.0 + 0.42 * dot(fire - p, fire - p)); // the fire reaches the stone
// cool moon fill from above (fire-tinted by elBlend)
let moonFill = mix(vec3f(0.14, 0.18, 0.27), vec3f(0.30, 0.16, 0.13), warm);
var c = tgk_alb(m, p) * (0.10 + 2.6 * dif * sh * fall) * ao + tgk_alb(m, p) * moonFill * (0.55 + 0.5 * max(n.y, 0.0));
c += fireCol * (0.30 + 0.5 * dif) * fall * ao * 0.25; // warm bounce washing the clearing
// the GLASS BLOOM — translucent, refracting ember light, the room's treasure
if (m > 5.5) {
let fres = pow(1.0 - max(dot(-rd, n), 0.0), 2.5);
let inner = fireCol * (0.5 + 0.8 * dif);
c = mix(vec3f(0.18, 0.42, 0.52), vec3f(1.4, 0.85, 0.5), fres) * 0.5 + inner * 0.6;
c += vec3f(1.0, 0.9, 0.7) * pow(max(dot(reflect(rd, n), ldir), 0.0), 40.0) * 1.2; // sparkle
} else {
// specular glints on wet-fired stone
let hv = normalize(ldir - rd);
c += fireCol * pow(max(dot(n, hv), 0.0), 22.0) * sh * 0.4 * fall;
}
return c;
}
fn mod_tg_kiln(pp: vec2f, px: vec2f, t: f32) -> vec3f {
let mm = vec2f(uni(27), uni(28));
let bloom = uni(104);
let warm = clamp(uni(82), 0.0, 1.0);
let ro = vec3f(0.0, 1.35, 3.9);
let uvx = px.x / 256.0 - 1.0;
let uvy = px.y / 256.0 - 1.0;
let rd = normalize(vec3f(uvx, -uvy - 0.05, -1.0));
// ── night sky through the burned canopy: deep dark, fire-moon warmth ──
var c = mix(vec3f(0.020, 0.026, 0.050), vec3f(0.060, 0.030, 0.032), clamp(-uvy, 0.0, 1.0));
c = mix(c, vec3f(0.10, 0.05, 0.04), warm * 0.5);
let st = tgk_h(floor(px / 3.0));
if (st > 0.994) { c += vec3f(0.7, 0.75, 0.95) * (0.4 + 0.6 * sin(t * 1.5 + st * 60.0)) * 0.4; }
// fire-moon low on the right
let mc = vec2f(430.0, 96.0);
c += vec3f(1.2, 0.55, 0.22) * smoothstep(30.0, 22.0, length(px - mc)) * 0.7;
c += vec3f(0.9, 0.4, 0.18) * smoothstep(150.0, 0.0, length(px - mc)) * 0.12;
// ── march the clearing ──
let hit = tgk_march(ro, rd, bloom, t);
if (hit.x > 0.0) {
c = tgk_shade(ro, rd, hit.x, hit.y, bloom, warm, t);
c = mix(c, vec3f(0.03, 0.03, 0.05), smoothstep(7.0, 18.0, hit.x) * 0.4);
}
// ── the FIREBOX: a bed of breathing coals + flame licks in the mouth
// (screen-space, anchored 256,290) ──
let mo = vec2f(256.0, 290.0);
let md = length(px - mo);
let breath = 0.7 + 0.3 * sin(t * 3.0) * sin(t * 1.7);
c += vec3f(1.35, 0.62, 0.24) * smoothstep(64.0, 6.0, md) * breath * (0.55 + 0.35 * (1.0 - bloom));
// coal bed: hot cells at the base of the mouth
if (px.y > 285.0 && md < 48.0) {
let cell = tgk_n2(px * 0.12 + vec2f(0.0, t * 0.5));
c += vec3f(1.5, 0.5, 0.15) * smoothstep(0.45, 0.85, cell) * smoothstep(48.0, 10.0, md) * (0.5 + 0.4 * sin(t * 5.0 + cell * 20.0));
}
// flame licks rising from the mouth
for (var fl = 0; fl < 4; fl++) {
let ff = f32(fl);
let fx = 256.0 + (ff - 1.5) * 13.0;
let flame = smoothstep(9.0, 0.0, abs(px.x - fx - 5.0 * sin(px.y * 0.06 + t * 5.0 + ff)))
* smoothstep(300.0, 250.0, px.y) * smoothstep(250.0, 285.0, px.y);
c += vec3f(1.4, 0.65, 0.2) * flame * (0.4 + 0.3 * sin(t * 8.0 + ff)) * (1.0 - bloom * 0.5);
}
let hov = smoothstep(70.0, 30.0, length(mm - mo));
if (bloom < 0.5) {
// the molten SEED — a bright bead waiting to be worked; hover invites
c += vec3f(1.7, 0.9, 0.4) * smoothstep(18.0, 2.0, length(px - mo - vec2f(0.0, 6.0))) * (0.6 + 0.4 * sin(t * 4.0)) * (0.6 + 0.5 * hov);
} else {
// ── the GLASS BLOOM: a flower of translucent petals on the lip (px ~256,224),
// glowing from within, refracting ember → the island's thank-you ──
let fc = vec2f(256.0, 224.0);
let fd0 = px - fc;
// stem catching light
c += vec3f(0.55, 0.75, 0.85) * smoothstep(3.0, 0.5, abs(fd0.x)) * smoothstep(64.0, 30.0, length(px - vec2f(256.0, 262.0))) * 0.5;
let ang = atan2(fd0.y, fd0.x);
let rr = length(fd0);
// six petals: radius modulated by a 6-lobe rose
let petal = 34.0 * (0.55 + 0.45 * abs(cos(ang * 3.0)));
let inPetal = smoothstep(petal, petal - 10.0, rr);
let glassA = mix(vec3f(0.35, 0.62, 0.75), vec3f(1.5, 0.9, 0.55), 0.5 + 0.5 * cos(ang * 3.0));
c = mix(c, glassA, inPetal * 0.8);
c += glassA * inPetal * (0.3 + 0.3 * sin(t * 2.0)) * 0.5; // inner glow
c += vec3f(1.6, 1.2, 0.9) * smoothstep(9.0, 0.0, rr) * (0.7 + 0.3 * sin(t * 3.0)); // molten heart
c += vec3f(0.9, 1.0, 1.0) * smoothstep(2.0, 0.0, abs(rr - petal)) * inPetal; // petal rim light
// soft light column + settling fireflies above the bloom
c += vec3f(0.5, 0.7, 0.85) * smoothstep(55.0, 0.0, abs(px.x - 256.0)) * smoothstep(300.0, 40.0, px.y) * 0.09;
for (var i = 0; i < 10; i++) {
let fi = f32(i);
let fx = 256.0 + 130.0 * sin(fi * 2.3 + t * 0.3 + fi);
let fy = 90.0 + 220.0 * fract(fi * 0.13 + t * 0.05);
c += vec3f(1.0, 0.8, 0.5) * smoothstep(2.6, 0.5, length(px - vec2f(fx, fy))) * (0.4 + 0.4 * sin(t * 3.0 + fi));
}
}
// rising embers along the charred frame
for (var i = 0; i < 10; i++) {
let fi = f32(i);
let ex = select(40.0, 472.0, (i % 2) == 0) + 26.0 * sin(fi + t * 0.6);
let ey = 512.0 - 512.0 * fract(fi * 0.17 + t * 0.08);
c += vec3f(1.0, 0.5, 0.2) * smoothstep(2.2, 0.4, length(px - vec2f(ex, ey))) * 0.5;
}
// click-confirm ripple
let cfx = uni(84);
if (cfx > 0.003) { let cd = length(px - vec2f(uni(85), uni(86))); c += vec3f(1.0, 0.8, 0.5) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4; }
c = tgk_bolt(c, px, t);
return c;
}
// ═══ THE DISCHARGE — the risen bloom fires the summons skyward (kiln view).
// s = uni(115); visible only for the first beat. Kiln px space, y-down;
// the bloom bulb sits at px(256,224). ═══
fn tgk_bolt(cIn: vec3f, px: vec2f, t: f32) -> vec3f {
var c = cIn;
let s = uni(115);
let k = smoothstep(0.0, 0.015, s) * (1.0 - smoothstep(0.10, 0.14, s));
if (k < 0.003) { return c; }
var aPt = vec2f(256.0, 218.0);
var m = 0.0;
for (var i = 0; i < 6; i = i + 1) {
let fi = f32(i);
let nx = 256.0 + sin(fi * 13.1 + floor(t * 24.0) * 1.7) * (14.0 + fi * 7.0);
let bPt = vec2f(nx, 218.0 - (fi + 1.0) * 38.0);
m = max(m, smoothstep(2.2, 0.0, sdSegment(px, aPt, bPt)));
// one short branch per joint, flickering
let br = vec2f(nx + sin(fi * 7.7 + floor(t * 31.0)) * 26.0, bPt.y - 14.0);
m = max(m, smoothstep(1.4, 0.0, sdSegment(px, bPt, br)) * 0.6);
aPt = bPt;
}
c += vec3f(0.85, 0.75, 1.25) * m * k * 1.4;
c += vec3f(0.9, 0.85, 1.2) * k * exp(-abs(s - 0.05) * 60.0) * 0.10; // one soft full-frame breath
// the bloom itself burns star-white while it gives the call
let bd = length(px - vec2f(256.0, 224.0));
c += vec3f(1.1, 1.0, 1.3) * exp(-bd * bd * 0.004) * k * 0.8;
return c;
}
module · tg_deck
// AIRSHIP DECK (view 9) — raymarched. The moored brass airship at first dawn:
// a planked deck in perspective, rope rigging climbing to a warm gasbag canopy
// overhead, the ship's helm forward, dawn bleeding amber at the horizon. Shore
// recipe: SDF map → march → normal → soft shadow/AO → materials → dawn key light.
// Anchors (px, LAW): spanner (256,404) r34 · hatch→engine (432,256) r44 ·
// helm (256,120) r52. State: uni(53) spannerOwned · uni(56)/uni(68) engineOK ·
// uni(57) moored(dawn) · uni(58) act(1 island,2 flight) · uni(71) fly · uni(44)
// dawn · mouse uni(27,28). Author: Claude Fable 5.
fn tgd_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgd_n2(p: vec2f) -> f32 {
let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
return mix(mix(tgd_h(i), tgd_h(i + vec2f(1.0, 0.0)), u.x),
mix(tgd_h(i + vec2f(0.0, 1.0)), tgd_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgd_fbm(p: vec2f, oct: i32) -> f32 {
var v = 0.0; var a = 0.5; var q = p;
for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgd_n2(q); q = q * 2.02 + vec2f(11.0, 7.0); a *= 0.5; }
return v;
}
fn tgd_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 tgd_rbox(p: vec3f, b: vec3f, r: f32) -> f32 { let q = abs(p) - b; return length(max(q, vec3f(0.0))) - r + min(max(q.x, max(q.y, q.z)), 0.0); }
fn tgd_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 tgd_torusY(p: vec3f, R: f32, r: f32) -> f32 { let q = vec2f(length(p.xz) - R, p.y); return length(q) - r; }
fn tgd_cap(p: vec3f, a: vec3f, b: vec3f, r: f32) -> 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) - r;
}
fn tgd_rotZ(p: vec3f, a: f32) -> vec3f { let c = cos(a); let s = sin(a); return vec3f(c * p.x - s * p.y, s * p.x + c * p.y, p.z); }
// project a SHIP-FRAME point to screen px through the same camera + deck roll the
// geometry uses, so rigging anchored to the rail/balloon tracks them as the deck
// shifts (the SDF renders ship-coord X at world rotZ(X,-roll); project that).
fn tgd_proj(shipPt: vec3f, roll: f32) -> vec2f {
let c = cos(-roll); let s = sin(-roll);
let wp = vec3f(c * shipPt.x - s * shipPt.y, s * shipPt.x + c * shipPt.y, shipPt.z);
let ro = vec3f(0.0, 1.35, 4.4);
let denom = max(ro.z - wp.z, 0.05);
let uvx = (wp.x - ro.x) / denom;
let uvy = -0.14 - (wp.y - ro.y) / denom;
return vec2f((uvx + 1.0) * 256.0, (uvy + 1.0) * 256.0);
}
// anchors in WORLD, composed to project to their px coords under the camera
// (eye (0,1.35,4.4) looking -z, slight downward tilt): deck fills the lower
// frame, helm forward-high, hatch to the right, spanner near on the planks.
const TGD_HELM = vec3f(0.0, 1.62, -2.35); // → px ~ (256,120)
const TGD_HATCH = vec3f(1.62, 0.02, -0.55); // → px ~ (432,256) (deck level, right)
const TGD_SPAN = vec3f(0.0, 0.06, 1.35); // → px ~ (256,404) (near planks)
// SDF → (dist, matId). mats: 1 plank · 2 brass rail · 3 rope · 4 helm wheel ·
// 5 binnacle · 6 hatch iron · 7 spanner · 8 gunwale post · 9 gasbag
fn tgd_map(p: vec3f, fly: f32, spOwned: f32, t: f32) -> vec2f {
var d = 1e5; var m = 1.0;
let roll = fly * 0.06 * sin(t * 0.7); // the deck leans once flight begins
let pr = tgd_rotZ(p, roll);
// hull HALF-WIDTH tapers toward the bow (front = -z), so the ship comes to a
// prow instead of a floating rectangle (Galen: "fill in the end of the ship")
let taper = 2.5 - 1.6 * smoothstep(-1.6, -3.9, pr.z);
// ── the DECK: a thick planked slab, clipped to the tapering hull ──
var deck = tgd_rbox(pr - vec3f(0.0, -0.16, 0.3), vec3f(2.5, 0.16, 3.15), 0.02);
deck = max(deck, abs(pr.x) - taper);
deck = max(deck, pr.z - 2.9); // stern edge
if (deck < d) { d = deck; m = 1.0; }
// ── BULWARK: a solid planked hull wall up each tapering side (the ship has
// sides you can't see through) ──
let bw = tgd_box(vec3f(abs(pr.x) - taper, pr.y - 0.10, pr.z - 0.3), vec3f(0.10, 0.28, 3.1));
if (bw < d) { d = bw; m = 1.0; }
// ── the BOW: where the sides meet, a solid raised forecastle block ──
let bow = tgd_rbox(pr - vec3f(0.0, 0.02, -3.05), vec3f(0.62, 0.40, 0.5), 0.06);
if (bow < d) { d = bow; m = 1.0; }
// a bowsprit spearing forward from the prow
let spr = tgd_cap(pr, vec3f(0.0, 0.30, -3.4), vec3f(0.0, 0.42, -4.4), 0.045);
if (spr < d) { d = spr; m = 8.0; }
// ── brass rail cap riding the top of each bulwark ──
let rail = min(tgd_cap(pr, vec3f(-2.5, 0.40, -2.9), vec3f(-1.0, 0.40, -3.6), 0.05),
tgd_cap(pr, vec3f(2.5, 0.40, -2.9), vec3f(1.0, 0.40, -3.6), 0.05));
let railBack = min(tgd_cap(pr, vec3f(-2.5, 0.40, -2.9), vec3f(-2.5, 0.40, 2.95), 0.05),
tgd_cap(pr, vec3f(2.5, 0.40, -2.9), vec3f(2.5, 0.40, 2.95), 0.05));
let railAll = min(rail, railBack);
if (railAll < d) { d = railAll; m = 2.0; }
// rail posts (repeated along the straight run)
let rpz = clamp(pr.z, -2.6, 2.8);
let rpx = vec3f(pr.x, pr.y, glsl_mod(rpz + 2.3, 1.05) - 0.525);
let post = tgd_box(vec3f(abs(rpx.x) - min(taper, 2.5), rpx.y - 0.08, rpx.z), vec3f(0.05, 0.24, 0.05));
if (post < d) { d = post; m = 8.0; }
// ── HELM: a ship's wheel forward-high on a binnacle column. A torus in the
// X-Y plane facing the camera (+z), plus spokes/handles + hub ──
let hp = pr - TGD_HELM;
var wheel = length(vec2f(length(hp.xy) - 0.60, hp.z)) - 0.05;
for (var k = 0; k < 6; k++) {
let a = f32(k) * 1.0472 + fly * (0.9 + 0.6 * sin(t * 1.5));
let dir = vec3f(cos(a), sin(a), 0.0) * 0.78;
wheel = min(wheel, tgd_cap(hp, vec3f(0.0, 0.0, 0.0), dir, 0.035)); // spoke + handle spike
}
wheel = min(wheel, length(hp) - 0.12); // hub
if (wheel < d) { d = wheel; m = 4.0; }
let binn = tgd_cyl(pr - vec3f(0.0, 0.9, -2.3), 0.55, 0.16); // binnacle column under the helm
if (binn < d) { d = binn; m = 5.0; }
// ── the COMPANIONWAY: a raised housing whose hatch face sits AT the click
// zone px(432,256) (the old flat lid projected ~90px off — Galen). ──
let qp = pr - vec3f(2.05, 0.50, 1.19);
let house = tgd_rbox(qp, vec3f(0.32, 0.50, 0.32), 0.04);
if (house < d) { d = house; m = 6.0; }
let lid = tgd_rbox(qp - vec3f(0.0, 0.54, -0.06), vec3f(0.34, 0.05, 0.38), 0.02);
if (lid < d) { d = lid; m = 6.0; }
let pull = tgd_torusY(vec3f(qp.x, qp.z - 0.33, qp.y - 0.10), 0.10, 0.024);
if (pull < d) { d = pull; m = 2.0; }
// ── SPANNER on the planks (only while unowned) ──
if (spOwned < 0.5) {
let sp = tgd_rotZ(pr - TGD_SPAN, 0.5);
var span = tgd_box(sp, vec3f(0.30, 0.045, 0.05)); // shaft
span = min(span, tgd_torusY(vec3f(sp.x - 0.30, sp.z, sp.y), 0.075, 0.03)); // open jaw ring
span = min(span, tgd_torusY(vec3f(sp.x + 0.30, sp.z, sp.y), 0.06, 0.028));
if (span < d) { d = span; m = 7.0; }
}
// ── the GASBAG canopy overhead: a great rounded hull filling the sky ──
let gb = pr - vec3f(0.0, 4.6, -0.4);
let bag = length(gb / vec3f(3.4, 1.7, 4.2)) - 1.0; // ellipsoid (approx SDF)
let bagD = bag * 1.7;
if (bagD < d) { d = bagD; m = 9.0; }
return vec2f(d, m);
}
fn tgd_nrm(p: vec3f, fly: f32, sp: f32, t: f32) -> vec3f {
let e = vec2f(0.007, 0.0);
return normalize(vec3f(
tgd_map(p + e.xyy, fly, sp, t).x - tgd_map(p - e.xyy, fly, sp, t).x,
tgd_map(p + e.yxy, fly, sp, t).x - tgd_map(p - e.yxy, fly, sp, t).x,
tgd_map(p + e.yyx, fly, sp, t).x - tgd_map(p - e.yyx, fly, sp, t).x));
}
fn tgd_march(ro: vec3f, rd: vec3f, fly: f32, sp: f32, t: f32) -> vec2f {
var tt = 0.02;
for (var i = 0; i < 90; i++) {
let h = tgd_map(ro + rd * tt, fly, sp, t);
if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
tt += h.x * 0.85; if (tt > 30.0) { break; }
}
return vec2f(-1.0, 0.0);
}
fn tgd_shadow(ro: vec3f, rd: vec3f, fly: f32, sp: f32, t: f32) -> f32 {
var res = 1.0; var tt = 0.05;
for (var i = 0; i < 16; i++) {
let h = tgd_map(ro + rd * tt, fly, sp, t).x;
res = min(res, 8.0 * h / tt); tt += clamp(h, 0.03, 0.4);
if (res < 0.02 || tt > 7.0) { break; }
}
return clamp(res, 0.0, 1.0);
}
fn tgd_ao(p: vec3f, n: vec3f, fly: f32, sp: f32, t: f32) -> f32 {
var occ = 0.0; var w = 1.0;
for (var i = 1; i <= 4; i++) { let hh = 0.06 * f32(i); occ += w * (hh - tgd_map(p + n * hh, fly, sp, t).x); w *= 0.72; }
return clamp(1.0 - 2.0 * occ, 0.0, 1.0);
}
fn tgd_alb(m: f32, p: vec3f, engineOK: f32) -> vec3f {
if (m < 1.5) { // planks — warm oiled wood, grain + seams
let grain = tgd_fbm(vec2f(p.x * 2.0, p.z * 22.0), 4);
var a = mix(vec3f(0.14, 0.085, 0.045), vec3f(0.20, 0.13, 0.075), grain);
a *= 0.78 + 0.22 * step(0.06, abs(fract(p.x * 1.6) - 0.5)); // plank seams (dark grooves)
return a;
} else if (m < 2.5) { return vec3f(0.24, 0.17, 0.075) * 1.1; } // brass rail
else if (m < 3.5) { return vec3f(0.12, 0.10, 0.08); } // rope
else if (m < 4.5) { return vec3f(0.20, 0.14, 0.07); } // helm wheel — brass-bound wood
else if (m < 5.5) { return vec3f(0.16, 0.13, 0.10); } // binnacle
else if (m < 6.5) { return vec3f(0.065, 0.06, 0.062) * (0.85 + 0.3 * tgd_fbm(p.xz * 8.0, 2)); } // hatch iron
else if (m < 7.5) { return vec3f(0.28, 0.22, 0.10); } // spanner bronze
else if (m < 8.5) { return vec3f(0.055, 0.05, 0.045); } // posts (dark iron)
return vec3f(0.30, 0.20, 0.14); // gasbag canopy (lit separately)
}
fn tgd_shade(ro: vec3f, rd: vec3f, tt: f32, m: f32, fly: f32, sp: f32, engineOK: f32, dawn: f32, t: f32) -> vec3f {
let p = ro + rd * tt;
let n = tgd_nrm(p, fly, sp, t);
// dawn key: low warm sun from the horizon behind-right
let sun = normalize(vec3f(0.5, 0.22, 0.7));
let key = mix(vec3f(1.15, 0.72, 0.42), vec3f(1.30, 0.60, 0.28), dawn);
let dif = max(dot(n, sun), 0.0);
let sh = tgd_shadow(p + n * 0.03, sun, fly, sp, t);
let ao = tgd_ao(p, n, fly, sp, t);
let sky = vec3f(0.22, 0.24, 0.34); // cool sky fill from above
let skyf = max(dot(n, vec3f(0.0, 1.0, 0.0)), 0.0);
var c = tgd_alb(m, p, engineOK) * (0.14 + 1.0 * dif * sh) * ao + sky * skyf * 0.22 * ao;
// specular (brass & iron & spanner sing)
let hv = normalize(sun - rd);
let spec = pow(max(dot(n, hv), 0.0), 42.0) * sh;
let metal = step(1.5, m) * step(m, 2.5) + step(3.5, m) * step(m, 4.5) + step(6.5, m) * step(m, 7.5);
c += key * spec * (0.2 + 0.9 * metal);
// the GASBAG — cream fabric envelope, gore panels + seams + weave, matching
// the airship on the shore (tg_air): warm dawn key + cool sky fill, not a
// flat brown blob (Galen: "balloon needs texture … like the airship on main")
if (m > 8.5) {
let lp = p - vec3f(0.0, 4.6, -0.4); // local to the bag centre
// longitudinal GORES: panels running along the length (z), seams between
let phi = atan2(lp.y, lp.x);
let Ngore = 16.0;
let gg = phi * (Ngore / 6.2831853);
let sp = gg - floor(gg + 0.5);
let pid = floor(gg + 0.5);
let seam = smoothstep(0.06, 0.0, abs(sp)); // dark stitched seam
let bulge = 0.85 + 0.15 * cos(sp * 3.14159); // each gore rounds outward
// circumferential RIBS + fabric weave
let rib = 0.5 + 0.5 * cos(lp.z * 7.0);
let weave = tgd_fbm(vec2f(phi * 6.0, lp.z * 9.0), 4) - 0.5;
var alb = vec3f(0.86, 0.80, 0.67); // off-white canvas
alb *= 0.93 + 0.10 * tgd_h(vec2f(pid, 3.1)); // per-gore tint variation
alb *= 1.0 + weave * 0.10;
alb *= bulge;
alb *= 1.0 - 0.28 * seam;
alb *= 0.94 + 0.06 * rib;
let warmK = mix(vec3f(1.05, 0.72, 0.42), vec3f(1.25, 0.62, 0.30), dawn);
let sky2 = vec3f(0.45, 0.52, 0.66);
let dK = max(dot(n, sun), 0.0);
let dF = max(dot(n, vec3f(-0.3, -0.9, 0.3)), 0.0);
c = alb * (0.34 + dK * 1.05 * warmK + dF * 0.32 * sky2);
// rim/crown glow where the envelope meets the sky
let rim = pow(1.0 - max(dot(-rd, n), 0.0), 2.2);
c += warmK * rim * (0.18 + 0.28 * dawn);
// painted keel band + nose cap accents
c *= 1.0 - 0.4 * smoothstep(0.05, 0.0, abs(lp.y + 1.55)) * step(abs(lp.x), 3.0);
}
// engineOK: the binnacle + helm rim light up (ship has power)
if (engineOK > 0.5 && (m > 3.5 && m < 5.5)) {
c += vec3f(0.9, 0.75, 0.35) * (0.35 + 0.2 * sin(t * 2.5)) * (0.4 + 0.6 * dif);
}
return c;
}
fn mod_tg_deck(pp: vec2f, px: vec2f, t: f32) -> vec3f {
let mm = vec2f(uni(27), uni(28));
let spOwned = uni(53);
let engineOK = max(uni(56), uni(68));
let crystalPowered = uni(110); // the keel crystal, lit by the cave beam
let helmLive = select(0.0, 1.0, engineOK > 0.5 && crystalPowered > 0.5); // wheel turns only with both
let dawn = clamp(max(uni(44), uni(57)), 0.0, 1.0);
let act = uni(58);
let fly = uni(71);
// ── camera on the deck, looking forward toward the helm ──
let ro = vec3f(0.0, 1.35, 4.4);
let uvx = px.x / 256.0 - 1.0;
let uvy = px.y / 256.0 - 1.0;
let rd = normalize(vec3f(uvx, -uvy - 0.14, -1.0)); // slight downward tilt to see the deck
// ── the SKY: dawn gradient; open cloudscape once in flight (act 2) ──
let horizon = clamp(-uvy * 0.5 + 0.5, 0.0, 1.0);
var sky = mix(mix(vec3f(0.35, 0.22, 0.16), vec3f(0.85, 0.48, 0.26), dawn), // warm horizon
mix(vec3f(0.05, 0.06, 0.12), vec3f(0.12, 0.13, 0.24), dawn), // cool zenith
clamp(-uvy, 0.0, 1.0));
// sun disc at the horizon
let sunp = vec2f(360.0, 300.0);
sky += vec3f(1.2, 0.7, 0.35) * smoothstep(46.0, 30.0, length(px - sunp)) * dawn;
sky += vec3f(0.9, 0.5, 0.28) * smoothstep(220.0, 0.0, length(px - sunp)) * 0.18 * dawn;
// clouds (drift faster in flight)
let drift = t * (0.02 + 0.14 * step(1.5, act));
let cl = tgd_fbm(vec2f(px.x * 0.006 + drift, px.y * 0.010), 4);
sky = mix(sky, mix(vec3f(0.5, 0.34, 0.30), vec3f(0.14, 0.15, 0.26), clamp(-uvy, 0.0, 1.0)),
smoothstep(0.55, 0.85, cl) * clamp(-uvy + 0.6, 0.0, 1.0) * (0.4 + 0.4 * step(1.5, act)));
var c = sky;
// ── march the ship ──
let hit = tgd_march(ro, rd, fly, spOwned, t);
if (hit.x > 0.0) {
c = tgd_shade(ro, rd, hit.x, hit.y, fly, spOwned, engineOK, dawn, t);
// the keel crystal's power runs up into the helm — a cool charge on the brass
if (helmLive > 0.5 && hit.y > 3.5 && hit.y < 5.5) {
let pw = ro + rd * hit.x;
c += vec3f(0.25, 0.6, 0.85) * (0.4 + 0.4 * sin(t * 4.0 - pw.y * 6.0));
}
c = mix(c, sky, smoothstep(9.0, 22.0, hit.x) * 0.35); // aerial haze on far parts
}
// ── SHROUD RIGGING: every rope is anchored to a real 3D point — its FOOT
// cleated to the gunwale rail, its TOP made fast to the balloon's keel —
// and both ends are PROJECTED through the same camera+roll the ship uses.
// So the ropes track the deck as it shifts AND stay attached to the bag
// (Galen: they didn't connect to the shifting deck or the balloon). ──
let roll = fly * 0.06 * sin(t * 0.7);
for (var r = 0; r < 6; r++) {
let side = select(1.0, -1.0, r < 3);
let fi = f32(r % 3);
let footW = vec3f(side * 2.42, 0.42, -1.9 + fi * 1.5); // cleat on the rail
let topW = vec3f(side * 0.92, 3.02 + 0.04 * fi, -1.0 + fi * 0.9); // keel of the balloon
let foot = tgd_proj(footW, roll);
let top = tgd_proj(topW, roll);
let pa = px - foot; let ba = top - foot; let hh = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
let dl = length(pa - ba * hh);
c += vec3f(0.20, 0.15, 0.10) * smoothstep(2.2, 0.4, dl) * 0.75;
c += vec3f(0.9, 0.6, 0.35) * smoothstep(1.4, 0.2, dl) * dawn * 0.3;
// CLEAT at the foot (brass knot gripping the rail) + a small lashing where
// the rope makes fast to the balloon — both prove the anchoring
c += vec3f(0.5, 0.38, 0.18) * smoothstep(6.0, 2.0, length(px - foot)) * 0.9;
c += vec3f(0.95, 0.7, 0.35) * smoothstep(3.0, 0.5, length(px - foot)) * dawn * 0.4;
c += vec3f(0.55, 0.45, 0.28) * smoothstep(4.5, 1.5, length(px - top)) * 0.7;
}
// ── anchor affordances (screen-space light only) ──
// HELM (256,120): dead until engineOK, then lamps + hover
let hc = vec2f(256.0, 120.0);
let hovH = smoothstep(60.0, 24.0, length(mm - hc));
if (helmLive > 0.5) {
// both gears AND crystal: the wheel is takeable — a bright gold-and-cyan pulse
c += mix(vec3f(0.9, 0.75, 0.35), vec3f(0.4, 0.85, 1.0), 0.4 + 0.3 * sin(t * 3.0)) * tg_ring(px, hc, 50.0, 44.0) * (0.4 + 0.4 * sin(t * 3.0)) * (0.5 + 0.5 * hovH);
} else if (engineOK > 0.5) {
// gears turn but the crystal is dark — the helm shows it wants power (cold, waiting)
c += vec3f(0.30, 0.42, 0.5) * tg_ring(px, hc, 50.0, 45.0) * (0.25 + 0.15 * sin(t * 1.5)) * (0.4 + 0.6 * hovH);
} else {
c += vec3f(0.5, 0.42, 0.28) * tg_ring(px, hc, 50.0, 45.0) * hovH * 0.35;
}
if (helmLive > 0.5) {
// the take-the-wheel chevron: an up-glyph bobbing over the helm (matches nav chevrons)
let cb = vec2f(256.0, 54.0 + 4.0 * sin(t * 2.2));
let cd = px - cb;
let arm = abs(cd.y - 0.7 * abs(cd.x) + 5.0); // ^ apex at top, arms falling outward
let chev = smoothstep(3.5, 1.2, arm) * smoothstep(16.0, 14.0, abs(cd.x));
c += mix(vec3f(0.95, 0.8, 0.4), vec3f(0.5, 0.9, 1.0), 0.5 + 0.5 * sin(t * 3.0)) * chev * (0.55 + 0.35 * sin(t * 3.0));
}
// HATCH (432,256): NO UI overlay — the game's own down-CHEVRON (shown when the
// spanner is held) is the affordance now (Galen: stick with chevrons). Here we
// only add scenery: bolt-heads on the raymarched lid and a faint warm breath of
// engine-light from the seam, so it reads as a real hatch you descend.
let qc = vec2f(432.0, 256.0);
let hd = px - qc;
for (var b = 0; b < 4; b++) {
let ba2 = f32(b) * 1.5708 + 0.7854;
let bp = qc + vec2f(cos(ba2), sin(ba2)) * 30.0;
c += vec3f(0.36, 0.30, 0.21) * smoothstep(5.0, 1.5, length(px - bp)) * 0.8;
}
c += vec3f(0.9, 0.5, 0.22) * smoothstep(30.0, 8.0, length(hd)) * 0.06 * (0.6 + 0.4 * sin(t * 2.0)); // faint seam warmth
// SPANNER (256,404): a gold invitation while unowned
if (spOwned < 0.5) {
let sc = vec2f(256.0, 404.0);
c += vec3f(0.95, 0.80, 0.40) * tg_ring(px, sc, 30.0, 24.0) * (0.35 + 0.35 * sin(t * 2.5))
* (0.6 + 0.4 * smoothstep(50.0, 18.0, length(mm - sc)));
}
// click-confirm ripple (shared convention)
let cfx = uni(84);
if (cfx > 0.003) {
let cd = length(px - vec2f(uni(85), uni(86)));
c += vec3f(0.9, 0.8, 0.6) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4;
}
return c;
}
module · tg_engine
// ENGINE ROOM (view 10) — THE GEARHEART. Raymarched brass machine wall in warm
// lamplight: three gear SEATS the player fills, real cogs whose tooth-count and
// radius match the id (8/12/16/20t), spinning off enginePhase and counter-
// rotating through fixed idler pinions, a driven flywheel, and a cadence
// resonator that reads GREEN-GOLD at the tide-bell tgt — the legibility the
// puzzle lacked. Shore recipe: SDF map → march → normal → shadow/AO → material.
// Anchors (px, LAW): seat1 (176,256) · seat2 (256,256) · seat3 (336,256) r46 ·
// seized 12t (90,404) r38. State: uni(51) phase · uni(63/64/65) seat ids ·
// uni(66) shaftCadence · uni(67) targetCadence · uni(68) engineOK · uni(70)
// gear12free · uni(62) held · mouse uni(27,28). Author: Claude Fable 5.
fn tge_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tge_n2(p: vec2f) -> f32 {
let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
return mix(mix(tge_h(i), tge_h(i + vec2f(1.0, 0.0)), u.x),
mix(tge_h(i + vec2f(0.0, 1.0)), tge_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tge_fbm(p: vec2f, oct: i32) -> f32 {
var v = 0.0; var a = 0.5; var q = p;
for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tge_n2(q); q = q * 2.02 + vec2f(13.0, 5.0); a *= 0.5; }
return v;
}
fn tge_cyl(p: vec3f, hz: f32, r: f32) -> f32 { let d = vec2f(length(p.xy) - r, abs(p.z) - hz); return min(max(d.x, d.y), 0.0) + length(max(d, vec2f(0.0))); }
fn tge_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 tge_rotZ2(p: vec3f, a: f32) -> vec3f { let c = cos(a); let s = sin(a); return vec3f(c * p.x - s * p.y, s * p.x + c * p.y, p.z); }
// A COG facing camera (+z): toothed rim by count + raised hub + centre bore,
// rotated by `rot`. pl is local to the gear centre.
fn tge_gear(pl: vec3f, R: f32, teeth: f32, rot: f32) -> f32 {
let cr = cos(rot); let sr = sin(rot);
let q = vec2f(cr * pl.x - sr * pl.y, sr * pl.x + cr * pl.y);
let ang = atan2(q.y, q.x);
let toothH = R * 0.14;
let tw = smoothstep(-0.35, 0.35, cos(ang * teeth)); // square-ish tooth wave
let rimR = (R - toothH * 0.5) + toothH * tw; // radius rises on each tooth
let disc2d = length(q) - rimR;
var g = max(disc2d, abs(pl.z) - R * 0.15); // extrude to a thin cog
let hub = tge_cyl(pl, R * 0.22, R * 0.30); // raised centre hub
g = min(g, hub);
let bore = max(length(q) - R * 0.10, abs(pl.z) - R * 0.35); // axle bore through
g = max(g, -bore);
return g;
}
fn tge_teeth(id: f32) -> f32 {
if (id > 4.5) { return 20.0; } if (id > 3.5) { return 16.0; } if (id > 2.5) { return 12.0; } return 8.0;
}
fn tge_radius(id: f32) -> f32 { return 0.30 + tge_teeth(id) * 0.018; } // 8t≈0.44 … 20t≈0.66
fn tge_sx(k: i32) -> f32 { if (k <= 0) { return -1.19; } if (k == 1) { return 0.0; } return 1.19; }
fn tge_pick(k: i32, a: f32, b: f32, c: f32) -> f32 { return select(select(a, b, k == 1), c, k == 2); }
const TGE_SY = 1.20;
const TGE_SZ = -0.30;
// SDF → (dist, matId). mats: 1 wall plate · 2 rivet/frame brass · 3 cog gold-
// brass · 4 axle steel · 5 flywheel · 6 bell bronze · 7 seized rust · 8 pipe
fn tge_map(p: vec3f, s0: f32, s1: f32, s2: f32, phase: f32, seized: f32, t: f32) -> vec2f {
var d = 1e5; var m = 1.0;
// ── the machine WALL: a riveted plate behind everything ──
let wall = tge_box(p - vec3f(0.0, 1.2, -0.75), vec3f(3.4, 1.9, 0.30));
if (wall < d) { d = wall; m = 1.0; }
let beam = min(tge_box(p - vec3f(0.0, 3.05, -0.55), vec3f(3.4, 0.14, 0.16)),
tge_box(p - vec3f(0.0, -0.62, -0.55), vec3f(3.4, 0.14, 0.16)));
if (beam < d) { d = beam; m = 2.0; }
// side pipes (cylinders standing vertical → local z is world y)
let pl2 = vec3f(p.x + 3.05, p.z + 0.5, p.y - 1.2);
let pr2 = vec3f(p.x - 3.05, p.z + 0.5, p.y - 1.2);
let pipe = min(tge_cyl(pl2, 1.9, 0.10), tge_cyl(pr2, 1.9, 0.10));
if (pipe < d) { d = pipe; m = 8.0; }
// ══ A REAL MESHING GEAR TRAIN ══
// One shared MODULE (tooth pitch) so any two gears' teeth actually interlock;
// pitch radius R = module·teeth/2 (a 20t is bigger than an 8t by that ratio);
// idlers between the fixed seats are SIZED to bridge the gap so pitch circles
// are TANGENT (touch, never overlap or float); rotation is PHASE-LOCKED down
// the chain (φ_next = -φ_prev·T_prev/T_next + interlock-offset) so a tooth of
// one lands in the gap of the next at every mesh, and neighbours counter-turn.
let MOD = 0.0595; // 1.19 seat-gap / 20 → biggest pair just touches
let PIc = 3.14159265;
let gy = TGE_SY; let gz = TGE_SZ;
let sxA = -1.19; let sxB = 0.0; let sxC = 1.19;
let T0 = tge_teeth(s0); let T1 = tge_teeth(s1); let T2 = tge_teeth(s2);
let R0 = MOD * T0 * 0.5; let R1 = MOD * T1 * 0.5; let R2 = MOD * T2 * 0.5;
let f0 = s0 > 1.5; let f1 = s1 > 1.5; let f2 = s2 > 1.5;
// idlers sized so they exactly span the 1.19 gap tangent to both seat gears
let TA = max((1.19 - R0 - R1) / MOD, 5.0); let RA = MOD * TA * 0.5; let xA = sxA + R0 + RA;
let TB = max((1.19 - R1 - R2) / MOD, 5.0); let RB = MOD * TB * 0.5; let xB = sxB + R1 + RB;
// phase chain (β=0, gears in a rightward row): interlock offset = π - π/T
let p0 = phase;
let pA = -p0 * (T0 / TA) + (PIc - PIc / TA);
let p1 = -pA * (TA / T1) + (PIc - PIc / T1);
let pB = -p1 * (T1 / TB) + (PIc - PIc / TB);
let p2 = -pB * (TB / T2) + (PIc - PIc / T2);
// axle boss always (an empty seat is a waiting hub)
for (var k = 0; k < 3; k++) {
let axle = tge_cyl(p - vec3f(tge_sx(k), gy, gz) + vec3f(0.0, 0.0, 0.14), 0.22, 0.11);
if (axle < d) { d = axle; m = 4.0; }
}
// seat gears
if (f0) { let g = tge_gear(p - vec3f(sxA, gy, gz), R0, T0, p0); if (g < d) { d = g; m = 3.0; } }
if (f1) { let g = tge_gear(p - vec3f(sxB, gy, gz), R1, T1, p1); if (g < d) { d = g; m = 3.0; } }
if (f2) { let g = tge_gear(p - vec3f(sxC, gy, gz), R2, T2, p2); if (g < d) { d = g; m = 3.0; } }
// bridging idlers — only where BOTH seats are filled (else the train is broken)
if (f0 && f1) { let g = tge_gear(p - vec3f(xA, gy, gz), RA, TA, pA); if (g < d) { d = g; m = 3.0; } }
if (f1 && f2) { let g = tge_gear(p - vec3f(xB, gy, gz), RB, TB, pB); if (g < d) { d = g; m = 3.0; } }
// ── the driven FLYWHEEL, right of seat C — belt-coupled (drawn in the shader),
// spins at seat C's rate; a smooth heavy wheel (flywheels have no teeth) ──
let flySpin = p2 * 0.6;
let fp = tge_rotZ2(p - vec3f(2.55, 1.20, gz - 0.05), flySpin);
var fly = max(length(fp.xy) - 0.62, abs(fp.z) - 0.14); // outer disc
fly = max(fly, -(length(fp.xy) - 0.50)); // hollow to a heavy rim
for (var k = 0; k < 5; k++) { // spokes
let a = f32(k) * 1.25664;
let dir = vec2f(cos(a), sin(a));
let along = clamp(dot(fp.xy, dir), 0.0, 0.55);
let perp = length(fp.xy - dir * along);
fly = min(fly, max(perp - 0.05, abs(fp.z) - 0.10));
}
fly = min(fly, tge_cyl(fp, 0.17, 0.16));
if (fly < d) { d = fly; m = 5.0; }
// ── the cadence BELL / resonator, top centre ──
let bp = p - vec3f(0.0, 2.55, -0.20);
let bell = max(tge_cyl(bp, 0.20, 0.24), -(bp.y + 0.06)); // open-bottom bell
if (bell < d) { d = bell; m = 6.0; }
// ── the SEIZED 12t gear, rusted in at lower-left ──
if (seized < 0.5) {
let sg = p - vec3f(-1.85, -0.30, 0.05);
let g = tge_gear(sg, 0.42, 12.0, 0.3);
if (g < d) { d = g; m = 7.0; }
}
return vec2f(d, m);
}
fn tge_nrm(p: vec3f, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, t: f32) -> vec3f {
let e = vec2f(0.006, 0.0);
return normalize(vec3f(
tge_map(p + e.xyy, s0, s1, s2, ph, sz, t).x - tge_map(p - e.xyy, s0, s1, s2, ph, sz, t).x,
tge_map(p + e.yxy, s0, s1, s2, ph, sz, t).x - tge_map(p - e.yxy, s0, s1, s2, ph, sz, t).x,
tge_map(p + e.yyx, s0, s1, s2, ph, sz, t).x - tge_map(p - e.yyx, s0, s1, s2, ph, sz, t).x));
}
fn tge_march(ro: vec3f, rd: vec3f, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, t: f32) -> vec2f {
var tt = 0.02;
for (var i = 0; i < 92; i++) {
let h = tge_map(ro + rd * tt, s0, s1, s2, ph, sz, t);
if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
tt += h.x * 0.8; if (tt > 22.0) { break; }
}
return vec2f(-1.0, 0.0);
}
fn tge_shadow(ro: vec3f, rd: vec3f, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, t: f32) -> f32 {
var res = 1.0; var tt = 0.05;
for (var i = 0; i < 14; i++) {
let h = tge_map(ro + rd * tt, s0, s1, s2, ph, sz, t).x;
res = min(res, 8.0 * h / tt); tt += clamp(h, 0.03, 0.4);
if (res < 0.03 || tt > 6.0) { break; }
}
return clamp(res, 0.0, 1.0);
}
fn tge_ao(p: vec3f, n: vec3f, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, t: f32) -> f32 {
var occ = 0.0; var w = 1.0;
for (var i = 1; i <= 4; i++) { let hh = 0.05 * f32(i); occ += w * (hh - tge_map(p + n * hh, s0, s1, s2, ph, sz, t).x); w *= 0.72; }
return clamp(1.0 - 2.1 * occ, 0.0, 1.0);
}
fn tge_alb(m: f32, p: vec3f) -> vec3f {
if (m < 1.5) {
var a = vec3f(0.070, 0.062, 0.058) * (0.8 + 0.4 * tge_fbm(p.xy * 4.0, 3));
let rv = length(fract(p.xy * 1.3) - vec2f(0.5));
a += vec3f(0.10, 0.09, 0.07) * smoothstep(0.12, 0.06, rv) * 0.5; // rivets
return a;
} else if (m < 2.5) { return vec3f(0.22, 0.16, 0.075); }
else if (m < 3.5) { return vec3f(0.30, 0.215, 0.10); } // cog gold-brass
else if (m < 4.5) { return vec3f(0.11, 0.11, 0.12); } // axle steel
else if (m < 5.5) { return vec3f(0.16, 0.13, 0.09); } // flywheel
else if (m < 6.5) { return vec3f(0.26, 0.20, 0.11); } // bell
else if (m < 7.5) { return vec3f(0.12, 0.075, 0.05) * (0.7 + 0.6 * tge_fbm(p.xy * 9.0, 3)); } // rust
return vec3f(0.14, 0.12, 0.10);
}
fn tge_shade(ro: vec3f, rd: vec3f, tt: f32, m: f32, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, engineOK: f32, t: f32) -> vec3f {
let p = ro + rd * tt;
let n = tge_nrm(p, s0, s1, s2, ph, sz, t);
let lp = vec3f(-1.7, 2.6, 2.2);
let ldir = normalize(lp - p);
let lampCol = vec3f(1.25, 0.78, 0.38);
let dif = max(dot(n, ldir), 0.0);
let sh = tge_shadow(p + n * 0.03, ldir, s0, s1, s2, ph, sz, t);
let ao = tge_ao(p, n, s0, s1, s2, ph, sz, t);
let fall = 1.0 / (1.0 + 0.06 * dot(lp - p, lp - p));
var c = tge_alb(m, p) * (0.12 + 1.6 * dif * sh * fall) * ao;
c += vec3f(0.10, 0.09, 0.12) * ao * 0.5;
if (engineOK > 0.5) { c += tge_alb(m, p) * lampCol * 0.18 * (0.6 + 0.4 * sin(t * 3.0)); }
let hv = normalize(ldir - rd);
let spec = pow(max(dot(n, hv), 0.0), 40.0) * sh;
c += lampCol * spec * (0.2 + 0.9 * step(1.5, m));
return c;
}
fn mod_tg_engine(pp: vec2f, px: vec2f, t: f32) -> vec3f {
let mm = vec2f(uni(27), uni(28));
let s0 = uni(63); let s1 = uni(64); let s2 = uni(65);
let phase = uni(51);
let seized = uni(70);
let engineOK = uni(68);
let held = uni(62);
let cad = uni(66); let tgt = uni(67);
let ro = vec3f(0.0, 1.2, 3.5);
let uvx = px.x / 256.0 - 1.0;
let uvy = px.y / 256.0 - 1.0;
let rd = normalize(vec3f(uvx, -uvy, -1.0));
var c = vec3f(0.045, 0.040, 0.045) * (0.8 + 0.3 * tge_fbm(px * 0.01, 2));
c += vec3f(0.55, 0.28, 0.10) * exp(-pow((uvy + 0.7) * 1.8, 2.0)) * 0.35; // lamp glow band
let hit = tge_march(ro, rd, s0, s1, s2, phase, seized, t);
if (hit.x > 0.0) { c = tge_shade(ro, rd, hit.x, hit.y, s0, s1, s2, phase, seized, engineOK, t); }
// the oil lamp, upper-left
let lampP = vec2f(96.0, 70.0);
c += vec3f(1.3, 0.85, 0.42) * smoothstep(20.0, 6.0, length(px - lampP));
c += vec3f(0.8, 0.45, 0.20) * smoothstep(150.0, 0.0, length(px - lampP)) * 0.18;
// ── seat affordances (screen-space) ──
for (var k = 0; k < 3; k++) {
let sc = vec2f(176.0 + f32(k) * 80.0, 256.0);
let id = tge_pick(k, s0, s1, s2);
let hov = smoothstep(52.0, 24.0, length(mm - sc));
if (id < 1.5) {
let ready = select(0.3, 0.85, held > 1.5);
c += vec3f(0.55, 0.62, 0.68) * tg_ring(px, sc, 44.0, 39.0) * (0.3 + 0.3 * sin(t * 2.0)) * ready * (0.5 + 0.5 * hov);
if (held > 1.5 && hov > 0.4) { c += vec3f(0.9, 0.72, 0.35) * tg_ring(px, sc, 30.0 + held * 3.0, 26.0 + held * 3.0) * 0.4; }
} else {
c += vec3f(0.9, 0.75, 0.4) * tg_ring(px, sc, 46.0, 43.0) * hov * 0.3;
}
}
// ── CADENCE RESONATOR: green-gold near tgt, red-amber far ──
let bc = vec2f(256.0, 60.0);
let err = abs(cad - tgt) / max(tgt, 0.001);
let good = 1.0 - clamp(err * 2.5, 0.0, 1.0);
let bellCol = mix(vec3f(1.0, 0.35, 0.12), vec3f(0.30, 1.0, 0.45), good);
c += bellCol * tg_disc(px, bc, 13.0) * (0.5 + 0.35 * sin(t * (1.5 + cad * 6.0)));
c += bellCol * tg_ring(px, bc, 22.0, 18.0) * (0.4 + 0.3 * good);
c += bellCol * smoothstep(70.0, 0.0, length(px - bc)) * good * engineOK * 0.15;
// tgt-vs-actual gauge under the bell
let bx = px.x - 116.0; let by = px.y - 96.0;
c += vec3f(0.18, 0.42, 0.48) * step(0.0, bx) * step(bx, 280.0) * smoothstep(6.0, 2.0, abs(by)) * 0.6;
c += vec3f(0.30, 0.85, 0.45) * step(0.0, bx) * step(bx, clamp(cad, 0.0, 1.0) * 280.0) * smoothstep(5.0, 2.0, abs(by));
c += vec3f(0.95, 0.85, 0.45) * smoothstep(3.0, 1.0, abs(bx - tgt * 280.0)) * smoothstep(9.0, 3.0, abs(by));
// ── seized 12t invitation (90,404) — wants the spanner HELD ──
if (seized < 0.5) {
let gc = vec2f(90.0, 404.0);
let ready = select(0.35, 0.9, held > 0.5 && held < 1.5);
c += vec3f(0.95, 0.80, 0.40) * tg_ring(px, gc, 30.0, 24.0) * (0.3 + 0.35 * sin(t * 2.5)) * ready
* (0.6 + 0.4 * smoothstep(50.0, 18.0, length(mm - gc)));
}
// click-confirm ripple
let cfx = uni(84);
if (cfx > 0.003) { let cd = length(px - vec2f(uni(85), uni(86))); c += vec3f(0.9, 0.8, 0.6) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4; }
return c;
}
module · tg_mirror
// MIRROR ISLE (view 11) — raymarched. The island's reflection gone wrong: a
// black-glass sea, a polished mirror wall holding THE SLUICE WHEEL, standing
// shards of broken reflection. Shore-recipe: SDF map → march → normal → soft
// shadow → material albedo → key light + planar sea reflection + foam collar.
// Anchors (px, LAW): sluice wheel (346,250) r~50 · moon (400,96) r24.
// State: uni(59) sluiceFreed · uni(62) held (1=spanner) · uni(78) element ·
// uni(82) elBlend · mouse uni(27,28). Author: Claude Fable 5.
fn tgm_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgm_n2(p: vec2f) -> f32 {
let i = floor(p); let f = fract(p);
let u = f * f * (3.0 - 2.0 * f);
return mix(mix(tgm_h(i), tgm_h(i + vec2f(1.0, 0.0)), u.x),
mix(tgm_h(i + vec2f(0.0, 1.0)), tgm_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgm_fbm(p: vec2f, oct: i32) -> f32 {
var v = 0.0; var a = 0.5; var q = p;
for (var i = 0; i < 4; i++) {
if (i >= oct) { break; }
v += a * tgm_n2(q); q = q * 2.03 + vec2f(17.0, 9.0); a *= 0.5;
}
return v;
}
fn tgm_sdBox(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 tgm_sdTorusZ(p: vec3f, R: f32, r: f32) -> f32 {
let q = vec2f(length(p.xy) - R, p.z);
return length(q) - r;
}
fn tgm_sdCap(p: vec3f, a: vec3f, b: vec3f, r: f32) -> 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) - r;
}
// wheel geometry constants — wheel center in WORLD, chosen so it projects to
// px(346,250) under the camera below (eye (0,1.7,5.2) → +x right, +uv.y down)
const TGM_WC = vec3f(1.83, 1.83, 0.10);
const TGM_WHEEL_R = 0.92;
// scene SDF → (distance, materialId)
// mats: 1 wall basalt frame · 2 mirror face · 3 wheel bronze · 4 grate iron ·
// 5 silt choke · 6 shard glass · 7 rock ledge
fn tgm_map(p: vec3f, freed: f32, t: f32) -> vec2f {
var d = 1e5; var m = 1.0;
// (the gate slot reads as shadowed depth via jambs/grate/silt — no boolean cut)
// ── the mirror wall: a tall slab behind everything, carved frame + face ──
let wall = tgm_sdBox(p - vec3f(0.0, 2.1, -0.35), vec3f(4.6, 2.2, 0.30));
if (wall < d) { d = wall; m = 1.0; }
// the polished FACE floats a breath in front of the slab
let face = tgm_sdBox(p - vec3f(0.0, 2.05, -0.02), vec3f(4.15, 1.85, 0.015));
if (face < d) { d = face; m = 2.0; }
// ── THE SLUICE WHEEL — torus rim + five spokes + hub, half-proud of the wall ──
let wp = p - TGM_WC;
// freed → the wheel rests TURNED: spokes carry a settled extra angle
// VALVE angle from the hook (U105): rest↔turned + the click-turn kick — the
// wheel VISIBLY TURNS when worked (Galen). Pre-freed it rests at zero.
let spin = uni(105);
var wheel = tgm_sdTorusZ(wp, TGM_WHEEL_R, 0.085);
for (var k = 0; k < 5; k++) {
let a = f32(k) * 1.2566 + 0.35 + spin;
let dir = vec3f(cos(a), sin(a), 0.0) * (TGM_WHEEL_R - 0.06);
wheel = min(wheel, tgm_sdCap(wp, vec3f(0.0), dir, 0.055));
}
wheel = min(wheel, length(wp) - 0.16); // hub
if (wheel < d) { d = wheel; m = 3.0; }
// ── the sluice gate: a slot below the wheel, grated; silted until freed ──
let gp = p - vec3f(1.83, 0.62, -0.02);
// the slot is implied by its iron jambs, lintel, grate and (until freed) silt
let jamb = min(tgm_sdBox(gp - vec3f(-0.42, 0.0, 0.06), vec3f(0.07, 0.52, 0.10)),
tgm_sdBox(gp - vec3f(0.42, 0.0, 0.06), vec3f(0.07, 0.52, 0.10)));
if (jamb < d) { d = jamb; m = 4.0; }
let lintel = tgm_sdBox(gp - vec3f(0.0, 0.55, 0.06), vec3f(0.49, 0.07, 0.10));
if (lintel < d) { d = lintel; m = 4.0; }
// grate bars across the slot
var grate = 1e5;
for (var g = 0; g < 4; g++) {
let gx = -0.255 + f32(g) * 0.17;
grate = min(grate, tgm_sdCap(gp, vec3f(gx, -0.42, 0.05), vec3f(gx, 0.46, 0.05), 0.028));
}
if (grate < d) { d = grate; m = 4.0; }
// silt choke fills the slot until the wheel turns
if (freed < 0.5) {
let silt = tgm_sdBox(gp - vec3f(0.0, -0.10, 0.02), vec3f(0.30, 0.30, 0.09));
let bulge = 0.03 * tgm_fbm(gp.xy * 6.0, 3);
if (silt - bulge < d) { d = silt - bulge; m = 5.0; }
}
// ── broken mirror shards standing out of the black water, left side —
// thin tilted planes that catch the moon on their edges ──
let s1 = tgm_sdBox(tgm_rotY(p - vec3f(-2.45, 0.66, 1.45), 0.42), vec3f(0.028, 0.86, 0.36));
if (s1 < d) { d = s1; m = 6.0; }
let s2 = tgm_sdBox(tgm_rotY(p - vec3f(-1.75, 0.42, 2.15), -0.65), vec3f(0.024, 0.58, 0.27));
if (s2 < d) { d = s2; m = 6.0; }
let s3 = tgm_sdBox(tgm_rotY(p - vec3f(-3.05, 0.35, 2.35), 1.05), vec3f(0.022, 0.44, 0.21));
if (s3 < d) { d = s3; m = 6.0; }
let s4 = tgm_sdBox(tgm_rotY(p - vec3f(-1.15, 0.28, 2.85), 0.2), vec3f(0.02, 0.30, 0.15));
if (s4 < d) { d = s4; m = 6.0; }
// ── wall pilasters + cornice: the mirror is HELD, not floating ──
let pil = min(tgm_sdBox(p - vec3f(-4.25, 2.1, 0.02), vec3f(0.16, 2.15, 0.14)),
tgm_sdBox(p - vec3f(4.25, 2.1, 0.02), vec3f(0.16, 2.15, 0.14)));
if (pil < d) { d = pil; m = 1.0; }
let corn = tgm_sdBox(p - vec3f(0.0, 4.02, 0.05), vec3f(4.45, 0.12, 0.22));
if (corn < d) { d = corn; m = 1.0; }
// ── low broken stones at the wall's foot — gapped in front of the gate so the
// sluice mouth stays visible and its water can reach the sea ──
let lp = vec3f(glsl_mod(p.x + 0.55, 1.1) - 0.55, p.y, p.z);
var ledge = tgm_sdBox(lp - vec3f(0.0, 0.045, 0.55), vec3f(0.42, 0.085, 0.42)) - 0.035 * tgm_fbm(p.xz * 4.0, 3);
ledge = max(ledge, -(abs(p.x - 1.83) - 0.62)); // the gap before the gate
ledge = max(ledge, abs(p.x - 0.6) - 3.6); // bounded run
if (ledge < d) { d = ledge; m = 7.0; }
return vec2f(d, m);
}
fn tgm_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 tgm_nrm(p: vec3f, freed: f32, t: f32) -> vec3f {
let e = vec2f(0.006, 0.0);
return normalize(vec3f(
tgm_map(p + e.xyy, freed, t).x - tgm_map(p - e.xyy, freed, t).x,
tgm_map(p + e.yxy, freed, t).x - tgm_map(p - e.yxy, freed, t).x,
tgm_map(p + e.yyx, freed, t).x - tgm_map(p - e.yyx, freed, t).x));
}
fn tgm_march(ro: vec3f, rd: vec3f, freed: f32, t: f32) -> vec2f {
var tt = 0.02;
for (var i = 0; i < 80; i++) {
let h = tgm_map(ro + rd * tt, freed, t);
if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
tt += h.x * 0.9;
if (tt > 26.0) { break; }
}
return vec2f(-1.0, 0.0);
}
fn tgm_shadow(ro: vec3f, rd: vec3f, freed: f32, t: f32) -> f32 {
var res = 1.0; var tt = 0.06;
for (var i = 0; i < 14; i++) {
let h = tgm_map(ro + rd * tt, freed, t).x;
res = min(res, 9.0 * h / tt);
tt += clamp(h, 0.04, 0.5);
if (res < 0.02 || tt > 8.0) { break; }
}
return clamp(res, 0.0, 1.0);
}
fn tgm_ao(p: vec3f, n: vec3f, freed: f32, t: f32) -> f32 {
var occ = 0.0; var w = 1.0;
for (var i = 1; i <= 4; i++) {
let hh = 0.05 * f32(i);
occ += w * (hh - tgm_map(p + n * hh, freed, t).x);
w *= 0.75;
}
return clamp(1.0 - 2.2 * occ, 0.0, 1.0);
}
// material albedo (+ small procedural detail), matching the island's palette
fn tgm_alb(m: f32, p: vec3f, warm: f32) -> vec3f {
if (m < 1.5) { // basalt frame — carved, wet-streaked
var a = vec3f(0.055, 0.052, 0.060) * (0.75 + 0.5 * tgm_fbm(p.xy * 3.1 + p.zz, 3));
a *= 1.0 - 0.22 * smoothstep(0.35, 0.5, abs(fract(p.y * 2.2) - 0.5)); // course lines
a *= 1.0 - 0.3 * smoothstep(0.6, 0.0, p.y) * tgm_fbm(p.xz * 5.0, 2); // damp foot
return a;
} else if (m < 2.5) { // mirror face — near-black; carved panel seams whisper across it
var f = vec3f(0.012, 0.015, 0.022);
let seam = smoothstep(0.030, 0.012, abs(fract(p.x * 0.62 + 0.5) - 0.5) * 1.6)
+ smoothstep(0.030, 0.012, abs(fract(p.y * 0.62) - 0.5) * 1.6);
f += vec3f(0.010, 0.014, 0.020) * seam;
return f;
} else if (m < 3.5) { // wheel — verdigris bronze
let ver = tgm_fbm(p.xy * 7.0, 3);
return mix(vec3f(0.16, 0.115, 0.055), vec3f(0.075, 0.145, 0.115), smoothstep(0.45, 0.75, ver)) * (1.0 + warm * 0.25);
} else if (m < 4.5) { // iron
return vec3f(0.052, 0.048, 0.050) * (0.8 + 0.4 * tgm_fbm(p.xy * 9.0, 2));
} else if (m < 5.5) { // silt
return vec3f(0.070, 0.075, 0.070) * (0.7 + 0.6 * tgm_fbm(p.xy * 11.0, 3));
} else if (m < 6.5) { // shard glass — cold, faintly teal
return vec3f(0.05, 0.09, 0.10);
}
// wet rock ledge
return vec3f(0.060, 0.062, 0.068) * (0.7 + 0.5 * tgm_fbm(p.xz * 5.0, 3));
}
// shade a solid hit — key light is the moon (cool → warms with elBlend)
fn tgm_shade(ro: vec3f, rd: vec3f, tt: f32, m: f32, freed: f32, warm: f32, t: f32) -> vec3f {
let p = ro + rd * tt;
let n = tgm_nrm(p, freed, t);
let ldir = normalize(vec3f(0.55, 0.62, 0.45));
let key = mix(vec3f(0.72, 0.82, 1.0), vec3f(1.15, 0.62, 0.30), warm);
let dif = max(dot(n, ldir), 0.0);
let sh = tgm_shadow(p + n * 0.03, ldir, freed, t);
let ao = tgm_ao(p, n, freed, t);
let bnc = max(dot(n, vec3f(0.0, -1.0, 0.0)), 0.0) * 0.10; // sea bounce
var c = tgm_alb(m, p, warm) * (0.18 + 1.2 * dif * sh) * ao + key * bnc * ao * 0.4;
c += key * 0.06 * ao; // fill
// speculars
let hv = normalize(ldir - rd);
let spec = pow(max(dot(n, hv), 0.0), 34.0) * sh;
if (m > 1.5 && m < 2.5) {
// the mirror face: sky reflection + fresnel — the wall that holds another world
let rr = reflect(rd, n);
let fres = pow(1.0 - max(dot(-rd, n), 0.0), 3.0);
var sky = mix(vec3f(0.020, 0.030, 0.055), vec3f(0.055, 0.045, 0.075), clamp(rr.y, 0.0, 1.0));
sky = mix(sky, vec3f(0.35, 0.25, 0.18), warm * 0.4);
c += sky * (0.35 + 0.6 * fres);
c += key * pow(max(dot(rr, ldir), 0.0), 60.0) * 0.8; // moon glint in the wall
} else if (m > 5.5 && m < 6.5) {
let fres = pow(1.0 - max(dot(-rd, n), 0.0), 2.5);
c += mix(vec3f(0.10, 0.16, 0.20), vec3f(0.30, 0.16, 0.10), warm) * (0.25 + 0.75 * fres);
c += key * pow(fres, 5.0) * 1.1; // the moonlit shard edge
c += key * spec * 1.3;
} else {
c += key * spec * (0.25 + 0.3 * step(2.5, m));
}
// wet streak below an opened gate — the wall remembers the water
if (freed > 0.5 && m < 2.5) {
let wet = smoothstep(0.5, 0.15, abs(p.x - 1.83)) * smoothstep(1.15, 0.2, p.y);
c *= 1.0 - 0.35 * wet;
}
return c;
}
fn mod_tg_mirror(pp: vec2f, px: vec2f, t: f32) -> vec3f {
let mm = vec2f(uni(27), uni(28));
let freed = uni(59);
let held = uni(62);
let warm = clamp(uni(82), 0.0, 1.0); // eased element morph (fire)
// ── camera: standing on the water before the wall ──
let ro = vec3f(0.0, 1.7, 5.2);
let uvx = px.x / 256.0 - 1.0;
let uvy = px.y / 256.0 - 1.0; // y DOWN in px
let rd = normalize(vec3f(uvx, -uvy + 0.02, -1.0));
// ── sky base: deep night, warms toward ember under the fire moon ──
var c = mix(vec3f(0.012, 0.020, 0.042), vec3f(0.030, 0.022, 0.050), clamp(-uvy, 0.0, 1.0));
c = mix(c, vec3f(0.075, 0.038, 0.030), warm * 0.5);
let st = tgm_h(floor(px / 3.0));
if (st > 0.9965) { c += vec3f(0.7, 0.8, 1.0) * (0.4 + 0.6 * sin(t * 1.3 + st * 90.0)) * 0.5; }
// ── march: solids first; sea plane as the mirror floor ──
let hit = tgm_march(ro, rd, freed, t);
let seaT = select(1e5, (0.0 - ro.y) / rd.y, rd.y < -0.001);
if (hit.x > 0.0 && hit.x < seaT) {
c = tgm_shade(ro, rd, hit.x, hit.y, freed, warm, t);
// depth mist toward the wall
c = mix(c, vec3f(0.020, 0.030, 0.050), smoothstep(6.0, 16.0, hit.x) * 0.5);
} else if (seaT < 1e4) {
// ── the BLACK-GLASS SEA: planar reflection — the true mirror ──
let sp = ro + rd * seaT;
// gentle ripple normal (amplitude grows as the element turns to fire)
let ramp = 0.012 + 0.03 * warm;
let rip = vec2f(tgm_fbm(sp.xz * 2.6 + vec2f(t * 0.14, 0.0), 3) - 0.5,
tgm_fbm(sp.xz * 2.6 + vec2f(0.0, t * 0.11) + 31.7, 3) - 0.5) * ramp;
let sn = normalize(vec3f(rip.x, 1.0, rip.y));
let rr = normalize(reflect(rd, sn));
// march the reflected ray against the solids — the world doubled, darker
let rhit = tgm_march(sp + vec3f(0.0, 0.015, 0.0), rr, freed, t);
var refl: vec3f;
if (rhit.x > 0.0) {
refl = tgm_shade(sp, rr, rhit.x, rhit.y, freed, warm, t) * 0.55;
// the wrongness: the reflection breathes, slightly out of true
refl *= 0.85 + 0.15 * sin(t * 0.7 + sp.x * 2.0);
} else {
refl = mix(vec3f(0.016, 0.024, 0.048), vec3f(0.06, 0.035, 0.03), warm * 0.6);
}
// deep water base + fresnel toward the reflection
let fres = pow(1.0 - max(dot(-rd, sn), 0.0), 2.2);
var sea = mix(vec3f(0.004, 0.008, 0.014), refl, 0.45 + 0.5 * fres);
sea = mix(sea, vec3f(0.10, 0.045, 0.020), warm * 0.25);
// moon lane on the water
let ldir = normalize(vec3f(0.55, 0.62, 0.45));
let lane = pow(max(dot(rr, ldir), 0.0), 130.0);
sea += mix(vec3f(0.7, 0.8, 1.0), vec3f(1.2, 0.6, 0.3), warm) * lane * 1.6;
// FOAM COLLAR — churn where solids break the glass (wall foot, shards)
let dWall = tgm_map(vec3f(sp.x, 0.02, sp.z), freed, t).x;
let foam = smoothstep(0.30, 0.04, dWall) * (0.5 + 0.5 * tgm_n2(sp.xz * 9.0 + vec2f(t * 0.9, 0.0)));
sea += vec3f(0.55, 0.62, 0.70) * foam * 0.35;
c = sea;
// distance mist over far water
c = mix(c, vec3f(0.018, 0.026, 0.048), smoothstep(8.0, 22.0, seaT) * 0.6);
}
// ── the freed sluice: a living thread of water from the gate to the sea ──
if (uni(106) > 0.5) { // the thread runs while the VALVE is open (draining)
// screen-space stream: from the gate mouth (~px 346,352) down to the water
let sx = 346.0 + 5.0 * sin(px.y * 0.05 + t * 2.2);
let sw = smoothstep(7.0, 0.5, abs(px.x - sx)) * smoothstep(340.0, 360.0, px.y) * smoothstep(430.0, 408.0, px.y);
let flow = 0.6 + 0.4 * sin(px.y * 0.35 - t * 9.0);
c += vec3f(0.50, 0.72, 0.85) * sw * flow * 0.9;
let splash = smoothstep(26.0, 4.0, length(px - vec2f(346.0, 424.0)));
c += vec3f(0.55, 0.68, 0.78) * splash * (0.4 + 0.3 * sin(t * 6.0)) * 0.5;
}
// ── anchor affordances (screen-space light only) ──
let wc2 = vec2f(346.0, 250.0);
let hovW = smoothstep(70.0, 28.0, length(mm - wc2));
// hover: the wheel answers with rim light
c += vec3f(0.85, 0.70, 0.35) * tg_ring(px, wc2, 52.0, 47.0) * hovW * 0.35;
// un-freed: a patient gold invitation; holding the spanner sharpens it
if (freed < 0.5) {
let ready = select(0.35, 0.85, held > 0.5 && held < 1.5);
c += vec3f(0.95, 0.80, 0.40) * tg_ring(px, wc2, 60.0, 54.0) * (0.22 + 0.28 * sin(t * 2.2)) * ready;
}
// (no moon in the sluice — the moon puzzle lives on the shore)
return c;
}
module · tg_lib
// ── the four tide-glyphs: crescent, triad, star, ringed bar ──
// unit cell p in [-1,1], returns coverage 0..1
fn mod_tg_glyph(idx: i32, p: vec2f) -> f32 {
var d = 1.0;
if (idx == 0) { // crescent — the hooked tide
let a = length(p) - 0.62;
let b = length(p - vec2f(0.30, -0.12)) - 0.55;
d = max(a, -b);
} else if (idx == 1) { // triad — three drops
let d1 = length(p - vec2f(0.0, -0.38)) - 0.20;
let d2 = length(p - vec2f(-0.38, 0.30)) - 0.20;
let d3 = length(p - vec2f(0.38, 0.30)) - 0.20;
d = min(d1, min(d2, d3));
} else if (idx == 2) { // four-point star — the vault
d = sdStar(p, 0.72, 4, 2.6);
} else { // ringed bar — the mooring
let ring = abs(length(p) - 0.52) - 0.10;
let bar = sdBox(p, vec2f(0.10, 0.72));
d = min(ring, bar);
}
return smoothstep(0.05, -0.05, d);
}
fn mod_tg_gcol(idx: i32) -> vec3f {
if (idx == 0) { return vec3f(1.00, 0.62, 0.22); } // amber
if (idx == 1) { return vec3f(0.25, 0.85, 0.75); } // teal
if (idx == 2) { return vec3f(0.66, 0.48, 1.00); } // violet
return vec3f(1.00, 0.38, 0.48); // rose
}
// ═══ ONE-DAY-grade 3D sky + ocean for the shore (ported technique) ═══
fn mod_tgo_suncol(el: f32) -> vec3f {
return mix(vec3f(1.30, 0.45, 0.16), vec3f(1.15, 1.05, 0.90), smoothstep(0.02, 0.55, el));
}
fn mod_tgo_sky(rd: vec3f, sd: vec3f, md: vec3f, t: f32, vault: f32) -> vec3f {
let el = sd.y;
let y = max(rd.y, 0.0);
let day = smoothstep(-0.10, 0.35, el);
let night = smoothstep(0.05, -0.18, el);
let zen = mix(vec3f(0.012, 0.018, 0.045), vec3f(0.15, 0.30, 0.58), day);
let horDay = mix(vec3f(1.00, 0.42, 0.16), vec3f(0.46, 0.58, 0.72), smoothstep(0.10, 0.55, el));
let hor = mix(vec3f(0.030, 0.032, 0.070), horDay, smoothstep(-0.16, 0.06, el));
var c = mix(hor, zen, pow(y, 0.6));
// the sun: wide bloom, tight halo, HDR core
let sdot = clamp(dot(rd, sd), 0.0, 1.0);
let sunUp = smoothstep(-0.14, 0.02, el);
c += mod_tgo_suncol(el) * pow(sdot, 6.0) * 0.24 * sunUp;
c += mod_tgo_suncol(el) * pow(sdot, 190.0) * 0.85 * sunUp;
c += vec3f(6.0, 4.1, 2.2) * smoothstep(0.99988, 0.99997, sdot) * sunUp;
// the vault-lamp moon of the finale night
let mdot = clamp(dot(rd, md), 0.0, 1.0);
let moonUp = night * smoothstep(-0.05, 0.10, md.y);
c += vec3f(0.45, 0.72, 0.80) * pow(mdot, 90.0) * 0.5 * moonUp;
c += vec3f(2.2, 3.0, 3.1) * smoothstep(0.99985, 0.99995, mdot) * moonUp;
// stars + a faint galaxy band
if (night > 0.02 && rd.y > 0.01) {
let sp = rd.xz / (rd.y + 0.55) * 26.0;
let cell = floor(sp);
let h = hash21(cell);
let tw = 0.55 + 0.45 * sin(t * (0.8 + h) + h * 50.0);
c += vec3f(0.72, 0.78, 0.95) * step(0.990, h) * smoothstep(0.30, 0.05, length(fract(sp) - 0.5)) * night * tw;
let band = pow(max(1.0 - abs(rd.x * 0.8 + rd.y * 0.5 - 0.15), 0.0), 3.5);
c += vec3f(0.07, 0.07, 0.12) * band * night * (0.5 + 0.3 * vnoise(sp * 0.5));
}
// aurora curtains once the vault has risen
if (vault > 0.01 && rd.y > 0.04) {
let ax = rd.x / (rd.y + 0.35);
let wave = sin(ax * 2.2 + t * 0.22) * 0.5 + sin(ax * 5.1 - t * 0.13) * 0.22;
let curt = exp(-pow((rd.y - 0.34 - wave * 0.10) * 4.5, 2.0));
let flick = 0.65 + 0.35 * vnoise(vec2f(ax * 3.0, t * 0.35));
c += vec3f(0.10, 0.85, 0.45) * curt * flick * vault * night * 0.35;
c += vec3f(0.35, 0.20, 0.75) * exp(-pow((rd.y - 0.52 - wave * 0.13) * 5.0, 2.0)) * flick * vault * night * 0.16;
}
// sparse dusk clouds, tinted by the hour
if (rd.y > 0.015 && night < 0.9) {
let cp = rd.xz / (rd.y + 0.14) * 1.4 + vec2f(t * 0.006, t * 0.002);
var cl = fbm(cp * 0.55, 4);
cl = smoothstep(0.46, 0.78, cl);
let cloudLit = mix(vec3f(0.06, 0.06, 0.10), mix(vec3f(1.15, 0.50, 0.28), vec3f(0.85, 0.85, 0.90), smoothstep(0.12, 0.5, el)), max(day, night * 0.12));
c = mix(c, cloudLit, cl * 0.7 * smoothstep(0.015, 0.10, rd.y) * (1.0 - night));
}
return c;
}
fn mod_tgo_ic() -> vec2f { return vec2f(6.5, 16.0); } // the island's rock in sea space
fn mod_tgo_oct(uv0: vec2f, choppy: f32) -> f32 {
let n = gnoise(uv0);
let uv = uv0 + vec2f(n, n);
var wv = 1.0 - abs(sin(uv));
let swv = abs(cos(uv));
wv = mix(wv, swv, wv);
return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
}
fn mod_tgo_surge(pxz: vec2f, st: f32) -> f32 {
let fB2 = clamp(uni(82), 0.0, 1.0);
let dr = length(pxz - mod_tgo_ic());
return exp(-max(dr - 4.6, 0.0) * 0.4) * (sin(st * 1.3 - dr * 0.9) * 0.5 + 0.62) * 0.9 * mix(1.0, 0.4, fB2);
}
fn mod_tgo_map3(p: vec3f, st: f32) -> f32 {
// FIRE MORPHS THE FORM (Galen Jul 30): magma is slow, heavy, unchopped — uni(82)
let fB = clamp(uni(82), 0.0, 1.0);
var freq = 0.16; // freq CONSTANT — morphing it rescales the pattern mid-blend (the residual speed-up)
var amp = mix(0.62, 0.80, fB);
var choppy = mix(4.0, 1.15, fB);
var uv = p.xz;
uv.x = uv.x * 0.75;
var h = 0.0;
for (var i = 0; i < 3; i++) {
var d = mod_tgo_oct((uv + vec2f(st)) * freq, choppy);
d = d + mod_tgo_oct((uv - vec2f(st)) * freq, choppy);
h = h + d * amp;
uv = mat2x2f(1.6, 1.2, -1.2, 1.6) * uv;
freq = freq * 1.9;
amp = amp * mix(0.22, 0.11, fB);
choppy = mix(choppy, 1.0, 0.2);
}
return p.y - h - mod_tgo_surge(p.xz, st);
}
fn mod_tgo_map5(p: vec3f, st: f32) -> f32 {
// FIRE MORPHS THE FORM (Galen Jul 30): magma is slow, heavy, unchopped — uni(82)
let fB = clamp(uni(82), 0.0, 1.0);
var freq = 0.16; // freq CONSTANT — morphing it rescales the pattern mid-blend (the residual speed-up)
var amp = mix(0.62, 0.80, fB);
var choppy = mix(4.0, 1.15, fB);
var uv = p.xz;
uv.x = uv.x * 0.75;
var h = 0.0;
for (var i = 0; i < 5; i++) {
var d = mod_tgo_oct((uv + vec2f(st)) * freq, choppy);
d = d + mod_tgo_oct((uv - vec2f(st)) * freq, choppy);
h = h + d * amp;
uv = mat2x2f(1.6, 1.2, -1.2, 1.6) * uv;
freq = freq * 1.9;
amp = amp * mix(0.22, 0.11, fB);
choppy = mix(choppy, 1.0, 0.2);
}
return p.y - h - mod_tgo_surge(p.xz, st);
}
// cheap height at a point — the buoys ride the SAME sea (2 octaves + surge)
fn mod_tgo_h2(uv0: vec2f, st: f32) -> f32 {
var uv = uv0;
uv.x = uv.x * 0.75;
// FIRE MORPHS THE FORM (Galen Jul 30): magma is slow, heavy, unchopped — uni(82)
let fB = clamp(uni(82), 0.0, 1.0);
var freq = 0.16; // freq CONSTANT — morphing it rescales the pattern mid-blend (the residual speed-up)
var amp = mix(0.62, 0.80, fB);
var choppy = mix(4.0, 1.15, fB);
var h = 0.0;
for (var i = 0; i < 2; i++) {
var d = mod_tgo_oct((uv + vec2f(st)) * freq, choppy);
d = d + mod_tgo_oct((uv - vec2f(st)) * freq, choppy);
h = h + d * amp;
uv = mat2x2f(1.6, 1.2, -1.2, 1.6) * uv;
freq = freq * 1.9;
amp = amp * mix(0.22, 0.11, fB);
choppy = mix(choppy, 1.0, 0.2);
}
return h + mod_tgo_surge(uv0, st);
}
// ═══ the bell-buoys: true marched bodies riding the swell ═══
// local frame: origin at the waterline, +y up. mats: 0 iron hull · 1 cage ·
// 2 the bell · 3 lantern head (emissive, glyph-colored)
fn mod_tgb_sdf(q0: vec3f, lean: f32) -> vec2f {
let cl = cos(lean); let sl = sin(lean);
let q = vec3f(cl * q0.x - sl * q0.y, sl * q0.x + cl * q0.y, q0.z);
// squat cone float with a waterline skirt
let ft = clamp((q.y + 0.40) / 0.60, 0.0, 1.0);
let fd = vec2f(length(q.xz) - mix(0.34, 0.52, ft) * step(q.y, 0.20) - mix(0.52, 0.10, clamp((q.y - 0.20) / 0.25, 0.0, 1.0)) * step(0.20, q.y), 0.0);
var d = max(length(q.xz) - mix(0.34, 0.52, ft), max(q.y - 0.42, -0.40 - q.y));
d = min(d, length(vec2f(length(q.xz) - 0.50, q.y - 0.02)) - 0.055); // skirt torus
var m = 0.0;
// tripod cage: three struts leaning to a collar
let pr = mod_tg_polar3(q);
let strut = mod_tg_seg3(pr, vec3f(0.34, 0.30, 0.0), vec3f(0.05, 1.30, 0.0)) - 0.030;
if (strut < d) { d = strut; m = 1.0; }
let collar = length(vec2f(length(q.xz) - 0.09, q.y - 1.30)) - 0.032;
if (collar < d) { d = collar; m = 1.0; }
// the bell hangs in the cage
let bell = max(length((q - vec3f(0.0, 0.88, 0.0)) * vec3f(1.0, 0.80, 1.0)) - 0.17, 0.62 - q.y);
if (bell < d) { d = bell; m = 2.0; }
// lantern head above the collar
let lant = mod_w3ish_box(q - vec3f(0.0, 1.46, 0.0), vec3f(0.062, 0.080, 0.062)) - 0.015;
if (lant < d) { d = lant; m = 3.0; }
return vec2f(d, m);
}
fn mod_tg_polar3(p: vec3f) -> vec3f {
let ang = 2.0943951;
let a = atan2(p.z, p.x);
let r = length(p.xz);
let a2 = (fract(a / ang + 0.5) - 0.5) * ang;
return vec3f(cos(a2) * r, p.y, sin(a2) * r);
}
fn mod_tg_seg3(p: vec3f, a: vec3f, b: vec3f) -> 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 mod_w3ish_box(p: vec3f, b: vec3f) -> f32 {
let d = abs(p) - b;
return length(max(d, vec3f(0.0))) + min(max(d.x, max(d.y, d.z)), 0.0);
}
// ═══ the island observatory: a real building, raymarched (ONE DAY treatment) ═══
// materials: 0 rock · 1 masonry · 2 iron · 3 glass dome · 4 roof · 5 cottage · 6 chimney · 7 beacon
fn mod_tgi_sdf(pw: vec3f) -> vec2f {
let q = pw - vec3f(6.5, 0.0, 16.0);
// the rock: noised mass with a leveled court
var d = length(q * vec3f(0.85, 1.45, 1.30)) - 2.85;
d = d + (vnoise3(q * 1.1) - 0.5) * 0.85 + (vnoise3(q * 3.1) - 0.5) * 0.28;
d = max(d, q.y - 2.08); // the court
var m = 0.0;
// tapered masonry tower on the court's east rise
let ty = clamp((q.y - 1.9) / 3.0, 0.0, 1.0);
let tr = mix(0.60, 0.40, pow(ty, 0.8)) * (1.0 + 0.45 * smoothstep(2.7, 1.9, q.y));
let dt = max(length(q.xz - vec2f(0.6, 0.0)) - tr, abs(q.y - 3.42) - 1.55);
if (dt < d) { d = dt; m = 1.0; }
// gallery deck + handrail ring + balusters
let rxz = length(q.xz - vec2f(0.6, 0.0));
let dDeck = max(rxz - 0.74, abs(q.y - 4.99) - 0.07);
if (dDeck < d) { d = dDeck; m = 2.0; }
let dRail = max(abs(rxz - 0.64) - 0.032, abs(q.y - 5.34) - 0.035);
if (dRail < d) { d = dRail; m = 2.0; }
let ph = atan2(q.z, q.x - 0.6);
let seg = 0.3927; // 2π/16
let aph = (glsl_mod(ph + seg * 0.5, seg) - seg * 0.5) * 0.64;
let dBal = max(length(vec2f(rxz - 0.64, aph)) - 0.028, abs(q.y - 5.16) - 0.17);
if (dBal < d) { d = dBal; m = 2.0; }
// the tide-glass itself: the great dome over the gallery
let dGl = max(length(q - vec3f(0.6, 5.02, 0.0)) - 0.56, 5.04 - q.y);
if (dGl < d) { d = dGl; m = 3.0; }
// finial beacon
let dFin = length(q - vec3f(0.6, 5.72, 0.0)) - 0.11;
if (dFin < d) { d = dFin; m = 7.0; }
// keeper's cottage on the west court: walls, gable roof, chimney
let qc = q - vec3f(-1.35, 2.36, 0.25);
let dW = sdBox(qc.xz, vec2f(0.78, 0.58));
let dWall = max(dW, abs(qc.y + 0.1) - 0.52);
if (dWall < d) { d = dWall; m = 5.0; }
let dRoof = max(max(abs(qc.z) * 0.85 + (qc.y - 0.85), -(qc.y - 0.32)), max(abs(qc.x) - 0.92, abs(qc.z) - 0.70));
if (dRoof < d) { d = dRoof; m = 4.0; }
let dCh = max(sdBox(qc.xz - vec2f(0.38, 0.0), vec2f(0.13, 0.13)), abs(qc.y - 1.02) - 0.38);
if (dCh < d) { d = dCh; m = 6.0; }
return vec2f(d, m);
}
// soft shadow: march from a surface point toward the sun through the island
fn mod_tgi_shadow(ro2: vec3f, ld: vec3f) -> f32 {
var t2 = 0.30;
var sh = 1.0;
for (var i = 0; i < 10; i++) {
let d = mod_tgi_sdf(ro2 + ld * t2).x;
sh = min(sh, 7.0 * d / t2);
t2 = t2 + clamp(d, 0.18, 1.4);
if (sh < 0.02 || t2 > 15.0) { break; }
}
return clamp(sh, 0.0, 1.0);
}
// bounded march for reflections: (t, material) or (-1,-1)
fn mod_tgi_march(ro2: vec3f, rd2: vec3f, tmax: f32) -> vec2f {
var t2 = 0.4;
for (var i = 0; i < 22; i++) {
let dm = mod_tgi_sdf(ro2 + rd2 * t2);
if (dm.x < 0.06) { return vec2f(t2, dm.y); }
t2 = t2 + max(dm.x * 0.9, 0.05);
if (t2 > tmax) { break; }
}
return vec2f(-1.0, -1.0);
}
fn mod_tgi_nrm(pw: vec3f) -> vec3f {
let e = 0.025;
let c = mod_tgi_sdf(pw).x;
return normalize(vec3f(
mod_tgi_sdf(pw + vec3f(e, 0.0, 0.0)).x - c,
mod_tgi_sdf(pw + vec3f(0.0, e, 0.0)).x - c,
mod_tgi_sdf(pw + vec3f(0.0, 0.0, e)).x - c));
}
fn mod_tgo_nrm(p: vec3f, eps: f32, st: f32) -> vec3f {
let hy = mod_tgo_map5(p, st);
let hx = mod_tgo_map5(p + vec3f(eps, 0.0, 0.0), st);
let hz = mod_tgo_map5(p + vec3f(0.0, 0.0, eps), st);
return normalize(vec3f(hx - hy, eps, hz - hy));
}
fn mod_tgo_spec(n: vec3f, l: vec3f, e: vec3f, s: f32) -> f32 {
let nrm = (s + 8.0) / (3.14159 * 8.0);
return pow(max(dot(reflect(e, n), l), 0.0), s) * nrm;
}
fn mod_tgo_seacol(p: vec3f, n: vec3f, sd: vec3f, md: vec3f, eye: vec3f, dist: vec3f, t: f32, st: f32, vault: f32) -> vec3f {
let el = sd.y;
let day = smoothstep(-0.10, 0.35, el);
let night = smoothstep(0.05, -0.18, el);
var fres = clamp(1.0 - dot(n, -eye), 0.0, 1.0);
fres = pow(fres, 3.0) * 0.5;
let reflected = mod_tgo_sky(reflect(eye, n), sd, md, t, vault);
let base = mix(vec3f(0.004, 0.008, 0.016), vec3f(0.030, 0.050, 0.085), day);
let waterCol = vec3f(0.16, 0.21, 0.20) * (0.25 + 0.75 * day);
let refracted = base + pow(dot(n, sd) * 0.4 + 0.6, 80.0) * waterCol * 0.12 * day;
var refl2 = reflected;
// ── the island stands in the mirror: march the reflected ray. The dome and
// beacon burn in the waves at night; the dark mass breaks the sky by day.
let rdir = reflect(eye, n);
if (p.x > 0.5 && p.z < 15.0 && rdir.z > 0.04) {
let hit = mod_tgi_march(p + vec3f(0.0, 0.03, 0.0), rdir, 17.0);
if (hit.x > 0.0) {
let hp2 = p + rdir * hit.x;
let mi2 = i32(hit.y + 0.5);
var rc = vec3f(0.016, 0.014, 0.018);
if (mi2 == 3) { rc = vec3f(0.08, 0.42, 0.46) * (0.6 + night * 1.4); }
else if (mi2 == 7) { rc = vec3f(0.30, 0.90, 0.95) * (1.3 + night * 2.0); }
else {
rc = rc * (0.8 + 0.4 * fbm(hp2.xy * 2.0, 2));
rc += mod_tgo_suncol(el) * clamp(hp2.y * 0.25, 0.0, 0.8) * (1.0 - night) * 0.35; // dusk-lit faces
// the tower's lit windows smear in the swell
let wy = smoothstep(0.25, 0.10, abs(hp2.y - 2.95)) + smoothstep(0.22, 0.09, abs(hp2.y - 3.95));
rc += vec3f(1.3, 0.85, 0.35) * wy * smoothstep(0.9, 0.2, abs(hp2.x - 7.1)) * (0.35 + night * 0.5);
}
refl2 = mix(refl2, rc, 0.85);
}
}
var col = mix(refracted, refl2, fres);
// the island's long dusk shadow lies across the water
if (p.x > 0.8 && p.z < 20.0 && sd.y > 0.01) {
let sh2 = mod_tgi_shadow(vec3f(p.x, 0.25, p.z), sd);
col *= 0.50 + 0.50 * sh2;
}
let atten = max(1.0 - dot(dist, dist) * 0.001, 0.0);
col = col + waterCol * (p.y - 0.6) * 0.18 * atten;
// sun glitter at dusk; at finale-night the vault-lamp takes the water
col = col + mod_tgo_suncol(el) * vec3f(1.7, 1.05, 0.55) * mod_tgo_spec(n, sd, eye, 150.0) * smoothstep(-0.08, 0.05, el);
col = col + vec3f(0.55, 1.05, 1.25) * mod_tgo_spec(n, md, eye, 140.0) * night * smoothstep(0.0, 0.15, md.y) * 1.7;
// crest foam
let foamN = vnoise(p.xz * 2.2 + vec2f(t * 0.5, -t * 0.35));
let crest = smoothstep(1.00, 1.50, p.y) * smoothstep(0.42, 0.85, foamN);
col = mix(col, vec3f(0.90, 0.82, 0.72) * (0.25 + 0.75 * day), crest * atten * 0.4);
// ── the rock's waterline: foam that follows the actual SDF, lapping with the swell ──
if (p.x > 2.2) {
let islD = mod_tgi_sdf(vec3f(p.x, p.y * 0.4, p.z)).x;
let lap = sin(st * 1.5 - islD * 5.5) * 0.5 + 0.5;
var lapFoam = smoothstep(0.85, 0.06, islD) * (0.30 + 0.70 * lap);
lapFoam *= 0.40 + 0.60 * vnoise(p.xz * 5.5 + vec2f(st * 0.9, -st * 0.6));
// fine bubble speckle right at the contact
let bub = step(0.965, hash21(floor(p.xz * 14.0 + vec2f(st * 1.2, 0.0))));
lapFoam += smoothstep(0.30, 0.03, islD) * bub * 0.6;
col = mix(col, vec3f(0.90, 0.89, 0.85) * (0.35 + 0.65 * day + 0.25 * night), clamp(lapFoam, 0.0, 1.0) * 0.85);
// wet reflection band hugging the rock
col += vec3f(0.25, 0.18, 0.10) * smoothstep(1.6, 0.2, islD) * (0.4 + 0.6 * day) * 0.25;
}
// ── buoy wakes: bob rings + worked collar, in the sea itself ──
for (var bk = 0; bk < 4; bk++) {
var bwx2 = -4.4;
if (bk == 1) { bwx2 = -2.45; } else if (bk == 2) { bwx2 = -0.6; } else if (bk == 3) { bwx2 = 1.45; }
let bd = length(p.xz - vec2f(bwx2, 11.0));
if (bd > 3.2) { continue; }
// expanding bob rings, born of the buoy's rhythm
let ring = sin(bd * 7.5 - st * 2.6 - f32(bk) * 1.9);
let rings = smoothstep(0.62, 0.97, ring) * exp(-bd * 1.35) * smoothstep(0.12, 0.35, bd) * 0.55;
// churned collar at the hull
let collar = smoothstep(0.34, 0.10, bd) * (0.45 + 0.55 * vnoise(p.xz * 9.0 + vec2f(st * 1.5, f32(bk) * 7.0)));
let bfoam = clamp(rings * 0.7 + collar, 0.0, 1.0);
col = mix(col, vec3f(0.88, 0.88, 0.86) * (0.35 + 0.65 * day + 0.22 * night), bfoam * 0.75);
// the hull's shadow in its own water
col *= 1.0 - smoothstep(0.30, 0.08, bd) * 0.14;
}
// ── THE FIRE SEA v2 (researched — Erta 'Ale lava-lake structure): a MOSAIC of
// dark crust plates drifting slowly, split by narrow INCANDESCENT SEAMS;
// the swell's folds crack the crust wider; sparse plates founder and
// fountain. Big fires flicker slow (~1.4Hz), per-plate phase. uni(82). ──
let fireB = clamp(uni(82), 0.0, 1.0);
if (fireB > 0.003) {
let lp = p.xz * vec2f(0.55, 0.62) + vec2f(t * 0.035, t * 0.02);
let vor = voronoi(lp);
let seamW = vor.y - vor.x;
let plateTone = 0.75 + 0.5 * vnoise(lp * 1.7 + vec2f(9.0));
var mc2 = vec3f(0.055, 0.022, 0.012) * plateTone;
mc2 *= 0.85 + 0.3 * fbm(p.xz * 3.0, 2);
let crest2 = smoothstep(0.9, 1.5, p.y);
let openW = 0.16 + crest2 * 0.30;
let seam = smoothstep(openW, 0.015, seamW);
let flick = 0.62 + 0.38 * sin(t * 8.5 + vor.x * 40.0);
let heart = smoothstep(0.06, 0.0, seamW);
mc2 += vec3f(1.3, 0.16, 0.02) * seam * 0.9;
mc2 += vec3f(2.9, 0.85, 0.08) * seam * seam * flick;
mc2 += vec3f(4.2, 2.3, 0.55) * heart * flick;
let cell2 = floor(lp * 2.0) + vec2f(floor(t * 0.45));
let burst = step(0.955, hash21(cell2)) * (0.5 + 0.5 * sin(t * 11.0 + hash21(cell2 + vec2f(3.0)) * 30.0));
mc2 += vec3f(3.0, 1.1, 0.15) * burst * seam * 1.6;
mc2 += vec3f(1.1, 0.42, 0.10) * fres * 1.2;
mc2 += vec3f(2.0, 0.85, 0.22) * mod_tgo_spec(n, sd, eye, 60.0) * 0.5;
col = mix(col, mc2, fireB);
}
return col;
}
// ═══ ancient machine-walls: height-field relief, engraved and fossiled ═══
// a toothed ring pressed into stone — the gear fossils of the tide-race
fn mod_tg_gearh(q: vec2f, r: f32, teeth: f32) -> f32 {
let a = atan2(q.y, q.x);
let tooth = smoothstep(0.30, 0.62, abs(fract(a * teeth * 0.15915) - 0.5) * 2.0);
let rr2 = r * (1.0 + tooth * 0.09);
let L = length(q);
let ring = smoothstep(0.022, 0.007, abs(L - rr2));
let spoke = smoothstep(0.030, 0.010, abs(fract(a * 0.9549 + 0.5) - 0.5) * max(L, 0.05)) * step(L, rr2 * 0.94) * step(rr2 * 0.30, L);
let hub = smoothstep(0.045, 0.018, abs(L - rr2 * 0.22));
return max(max(ring, spoke * 0.7), hub * 0.8);
}
// wall height by STYLE — every chamber speaks its own carved language:
// 0 GEARWORKS (the gate): fossil gear trains + conduit grooves
// 1 TIDE-SCRIPTORIUM (the record): engraved wave-charts + tally rows + strata
// 2 FLUTED WATERCOURSE (the hall): organ-rib fluting + band clamps + drip stains
fn mod_tg_wallh(p: vec2f, seed: f32, style: i32) -> f32 {
var h = fbm(p * 3.0 + vec2f(seed), 3) * 0.35;
h += fbm(p * 11.0 + vec2f(seed * 2.0), 2) * 0.15;
if (style == 0) {
h -= smoothstep(0.42, 0.5, abs(fract(p.y * 2.6 + fbm(p * 1.5, 2) * 0.25) - 0.5)) * 0.18;
let cell = floor(p * 1.15 + vec2f(seed));
let ch = hash21(cell);
if (ch > 0.48) {
let cq = (fract(p * 1.15 + vec2f(seed)) - 0.5) + (hash22(cell) - 0.5) * 0.26;
h += mod_tg_gearh(cq, 0.15 + ch * 0.16, floor(6.0 + ch * 9.0)) * 0.5;
}
let colId = floor(p.x * 0.9 + seed * 0.7);
let gx = abs(fract(p.x * 0.9 + seed * 0.7) - 0.5);
h -= smoothstep(0.055, 0.030, gx) * 0.22 * step(0.55, hash11(colId));
} else if (style == 1) {
// sediment strata, close-set
h -= smoothstep(0.40, 0.5, abs(fract(p.y * 4.2 + fbm(p * 1.2, 2) * 0.15) - 0.5)) * 0.14;
// engraved wave-chart lines: rows of carved sines, each row its own sea
let row = floor(p.y * 1.4 + seed);
let rh = hash11(row);
let wave = sin(p.x * (5.0 + rh * 7.0) + rh * 20.0) * (0.05 + rh * 0.05);
let chart = smoothstep(0.030, 0.010, abs(fract(p.y * 1.4 + seed) - 0.5 - wave));
h += chart * 0.42 * step(0.30, rh);
// tally strokes beneath some charts — the counted tides
let tq = vec2f(fract(p.x * 6.5 + rh * 5.0), fract(p.y * 1.4 + seed) - 0.72);
let tally = smoothstep(0.10, 0.04, abs(tq.x - 0.5)) * smoothstep(0.10, 0.05, abs(tq.y));
h -= tally * 0.30 * step(0.62, rh);
} else {
// vertical organ-rib fluting
h += smoothstep(0.5, 0.0, abs(fract(p.x * 3.4 + seed) - 0.5)) * 0.34;
// iron band clamps every few courses
h += smoothstep(0.045, 0.015, abs(fract(p.y * 1.1 + seed * 0.3) - 0.5)) * 0.30;
// drip stains running down from the bands
let dcol = floor(p.x * 5.0 + seed);
let dh2 = hash11(dcol);
h -= smoothstep(0.06, 0.02, abs(fract(p.x * 5.0 + seed) - 0.5)) * 0.12 * step(0.6, dh2) * fract(p.y * 0.7 + dh2);
}
return h;
}
// relief: (height, directional slope toward the key light, ridge01)
fn mod_tg_relief(p: vec2f, seed: f32, lite: vec2f, style: i32) -> vec3f {
let h0 = mod_tg_wallh(p, seed, style);
let h1 = mod_tg_wallh(p + lite * 0.012, seed, style);
return vec3f(h0, (h0 - h1) * 9.0, smoothstep(0.12, 0.55, h0));
}
// the full ancient-wall material: stone base, relief light, verdigris in the
// recesses, gold dust on the ridges — one call per wall pixel
fn mod_tg_ancient(p: vec2f, seed: f32, lite: vec2f, warm: f32, style: i32) -> vec3f {
let rel = mod_tg_relief(p, seed, lite, style);
var c = mix(vec3f(0.028, 0.025, 0.028), vec3f(0.080, 0.070, 0.062), rel.z);
c *= clamp(1.0 + rel.y * 0.6, 0.35, 1.8);
c = mix(c, vec3f(0.050, 0.082, 0.066), (1.0 - rel.z) * 0.40 * smoothstep(0.45, 0.15, rel.x)); // verdigris pools
c += vec3f(0.55, 0.42, 0.16) * smoothstep(0.60, 0.80, rel.x) * 0.20 * warm; // gilt ridges
c += vec3f(0.85, 0.55, 0.25) * max(rel.y, 0.0) * 0.10 * warm; // warm catch-light
return c;
}
// ── dusk→night sky, shared by shore + lens ──
fn mod_tg_sky(p: vec2f, t: f32, night: f32) -> vec3f {
let h = clamp(-p.y * 0.5 + 0.5, 0.0, 1.0); // 0 horizon → 1 zenith
let duskLo = vec3f(1.05, 0.52, 0.24);
let duskHi = vec3f(0.10, 0.14, 0.32);
let nightLo = vec3f(0.05, 0.08, 0.17);
let nightHi = vec3f(0.010, 0.016, 0.045);
var c = mix(mix(duskLo, duskHi, pow(h, 0.65)), mix(nightLo, nightHi, pow(h, 0.8)), night);
// banded clouds, advected slowly
let cl = fbm(vec2f(p.x * 1.6 - t * 0.008, p.y * 4.2), 3);
let band = smoothstep(0.45, 0.75, cl) * smoothstep(0.9, 0.2, h) * (1.0 - night * 0.75);
c = mix(c, vec3f(0.55, 0.26, 0.24) * (1.4 - h), band * 0.55);
// stars grow with night
let sc = floor(p * 190.0);
let sh = hash21(sc);
let tw = 0.6 + 0.4 * sin(t * (1.5 + sh * 3.0) + sh * 40.0);
c += vec3f(0.9, 0.95, 1.1) * step(0.9955, sh) * tw * (0.25 + night) * smoothstep(0.12, 0.5, h);
return c;
}
// ── the sea band, shared by shore views ──
fn mod_tg_sea(p: vec2f, t: f32, night: f32) -> vec3f {
let depth = clamp((p.y - 0.17) / 0.83, 0.0, 1.0); // 0 horizon → 1 near
var c = mix(mix(vec3f(0.35, 0.20, 0.20), vec3f(0.05, 0.09, 0.14), night),
mix(vec3f(0.05, 0.06, 0.10), vec3f(0.010, 0.022, 0.040), night), depth);
// swell lines compress toward the horizon
let sw = sin(p.x * 14.0 + t * 0.5 + sin(depth * 30.0) * 2.0) * sin(depth * 46.0 - t * 0.9);
c *= 1.0 + sw * 0.05 * (0.3 + depth);
// glitter path under the light (sun at dusk, risen vault at night)
let gx = p.x - mix(0.22, -0.15, night); // under the dusk sun; under the risen vault at night
let cell = floor(vec2f(p.x * 90.0, depth * 160.0 - t * 2.2));
let gh = hash21(cell);
let path = exp(-gx * gx * mix(22.0, 26.0, night)) * smoothstep(0.85, 0.25, depth);
let glit = step(0.976, gh) * path * max(0.3 + 0.7 * sin(t * 3.0 + gh * 50.0), 0.0);
c += mix(vec3f(1.3, 0.65, 0.25), vec3f(0.45, 0.85, 1.15), night) * glit * 0.7;
return c;
}
// ── carved rock body ──
fn mod_tg_rock(p: vec2f, tint: vec3f) -> vec3f {
var c = tint * (0.75 + 0.5 * fbm(p * 5.0, 3));
c *= 0.80 + 0.35 * fbm(p * 17.0 + vec2f(4.0), 2);
// damp streaks
c *= 1.0 - 0.22 * smoothstep(0.4, 0.9, fbm(vec2f(p.x * 9.0, p.y * 1.8), 2));
return c;
}
// ── nav chevron: dir 0 right, 1 left, 2 up, 3 down; hover glows ──
fn mod_tg_chev(px: vec2f, at: vec2f, dir: i32, hover: f32, t: f32) -> vec4f {
var q = (px - at) / 26.0;
if (dir == 0) { q = vec2f(-q.x, q.y); }
if (dir == 2) { q = vec2f(q.y, q.x); }
if (dir == 3) { q = vec2f(-q.y, q.x); }
// two nested arrows pointing -x after remap
let a1 = abs(abs(q.y) * 0.8 - q.x - 0.35) - 0.11;
let a2 = abs(abs(q.y) * 0.8 - q.x + 0.25) - 0.11;
let lim = step(abs(q.y), 0.75);
let m = max(smoothstep(0.06, -0.02, a1), smoothstep(0.06, -0.02, a2)) * lim;
let pulse = 0.55 + 0.25 * sin(t * 2.2) + hover * 0.9;
let col = mix(vec3f(0.85, 0.80, 0.62), vec3f(1.3, 1.15, 0.7), hover);
let halo = exp(-dot(q, q) * 1.4) * (0.10 + hover * 0.30);
return vec4f(col * (m * pulse + halo), max(m * 0.85, halo));
}
// ── small pixel-space SDF helpers, shared by the act-1 art modules (deck, engine,
// mirror, thicket) and the shore/gate retrofits. Kept in tg_lib so every later
// module can call them (modules concatenate in define-order). ──
fn tg_disc(px: vec2f, c: vec2f, r: f32) -> f32 { return smoothstep(r, r - 5.0, length(px - c)); }
fn tg_ring(px: vec2f, c: vec2f, ro: f32, ri: f32) -> f32 { return max(tg_disc(px, c, ro) - tg_disc(px, c, ri), 0.0); }
fn tg_seg(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 tg_backchev(px: vec2f, c: vec2f) -> f32 {
let a = tg_seg(px, c + vec2f(9.0, -12.0), c + vec2f(-7.0, 0.0));
let b = tg_seg(px, c + vec2f(-7.0, 0.0), c + vec2f(9.0, 12.0));
return smoothstep(3.0, 0.0, min(a, b));
}
// ═══ ARCANE SUN TECHNOLOGY — a caged miniature star (Galen: "gorgeous…
// arcane floating sun technology"). el: 0 water · 1 fire. ruling opens the
// gimbal cage wide and crowns the core; alt fades the whole machine in as
// the sun rises. Cheap by construction: 3 ring SDFs, one 2-octave fbm,
// fixed-count bead loops. Everything additive is built ≥ 0. ═══
fn px_to_r(px: vec2f, ctr: vec2f, R: f32) -> vec2f { return (px - ctr) / max(R, 1.0); }
fn tgsun_ring(d: vec2f, tilt: f32, spin: f32, R: f32, w: f32) -> f32 {
// an elliptical gimbal ring: rotate by spin, squash by tilt (precession =
// both animate). Returns a 0..1 stroke mask.
let q = rot2(spin) * d;
let e = vec2f(q.x, q.y / max(tilt, 0.14));
return smoothstep(w, w * 0.35, abs(length(e) - R));
}
fn tgsun_core(r: f32, a: f32, el: f32, t: f32) -> vec3f {
// the star itself. water: aqua-white, caustic shimmer. fire: furnace white-
// orange, short prominence licks breathing on the rim.
let licks = 0.5 + 0.5 * sin(a * 7.0 + t * (1.4 + el * 1.3) + fbm(vec2f(a * 2.2, t * 0.35), 2) * 4.0);
let rim = 1.0 + licks * mix(0.06, 0.22, el); // fire licks reach further
let body = smoothstep(rim, rim * 0.22, r);
let hot = smoothstep(0.55, 0.0, r);
var col = mix(vec3f(0.55, 0.95, 1.05), vec3f(1.25, 0.62, 0.20), el); // element voice
var white = mix(vec3f(0.95, 1.05, 1.15), vec3f(1.35, 1.15, 0.85), el);
// water caustic shimmer — light dapples crawling on the core
let dap = fbm(vec2f(r * 5.0 - t * 0.6, a * 3.0), 2) * (1.0 - el);
col += vec3f(0.10, 0.22, 0.26) * max(dap, 0.0);
return (col * body + white * hot * hot) * (0.55 + 0.45 * (0.5 + 0.5 * sin(t * (0.9 + el * 0.9))));
}
fn mod_tg_suntech(pxIn: vec2f, ctr: vec2f, Rbase: f32, el: f32, t: f32, ruling: f32, alt: f32, hov: f32) -> vec3f {
let d = px_to_r(pxIn, ctr, Rbase); // px_to_r hoisted above
let r = length(d);
if (r > 3.4) { return vec3f(0.0); } // cheap early-out beyond the halo
let a = atan2(d.y, d.x);
var c = vec3f(0.0);
let vis = clamp(alt, 0.0, 1.0);
// ── refraction ripple: faint concentric lensing of the sky around the machine ──
c += vec3f(0.06, 0.08, 0.10) * max(sin(r * 9.0 - t * 1.1), 0.0) * exp(-r * 1.4) * 0.35 * vis;
// ── god-ray shafts: soft angular spokes, slowly wheeling ──
let spokes = max(sin(a * 5.0 - t * 0.21), 0.0);
c += mix(vec3f(0.16, 0.30, 0.38), vec3f(0.55, 0.26, 0.10), el)
* spokes * spokes * exp(-max(r - 0.9, 0.0) * 2.6) * (0.35 + 0.45 * ruling) * vis;
// ── the caged star ──
c += tgsun_core(r, a, el, t) * vis;
// ── gimbal rings: brass-glass, precessing. ruling opens them wide; the
// lesser sun folds its cage tight around the core ──
let open = mix(0.55, 1.0, ruling);
let brass = mix(vec3f(0.72, 0.58, 0.30), vec3f(0.85, 0.55, 0.25), el);
let glass = vec3f(0.55, 0.75, 0.85);
let r1 = tgsun_ring(d, 0.34 + 0.22 * sin(t * 0.23), t * 0.11, 1.55 * open, 0.10);
let r2 = tgsun_ring(d, 0.30 + 0.24 * sin(t * 0.31 + 2.1), -t * 0.07 + 1.1, 1.95 * open, 0.085);
let r3 = tgsun_ring(d, 0.85, t * 0.045 + 0.6, 2.35 * open, 0.06) * ruling; // crown ring, ruling only
c += brass * r1 * (0.55 + 0.30 * max(sin(a * 2.0 + t * 0.7), 0.0)) * vis;
c += glass * r2 * 0.40 * vis;
c += mix(glass, brass, 0.5) * r3 * 0.50 * vis;
// ── bead satellites riding the outer ring ──
for (var i = 0; i < 3; i = i + 1) {
let ph = t * 0.33 + f32(i) * 2.094;
let bp = vec2f(cos(ph), sin(ph) * (0.30 + 0.24 * sin(t * 0.31 + 2.1)));
let bd = length(d - rot2(-t * 0.07 + 1.1) * bp * 1.95 * open);
c += mix(vec3f(0.5, 0.8, 0.9), vec3f(1.0, 0.6, 0.25), el) * exp(-bd * bd * 60.0) * 0.5 * vis;
}
// ── element rain: water sheds falling light-beads; fire sheds rising sparks ──
for (var i = 0; i < 3; i = i + 1) {
let fi = f32(i);
let cyc = glsl_mod(t * (0.16 + fi * 0.05) + fi * 0.37, 1.0);
let dir = mix(1.0, -1.0, el); // water falls, fire rises
let sp = vec2f((fi - 1.0) * 0.55 + 0.18 * sin(t * 0.5 + fi * 4.0), dir * (0.7 + cyc * 1.6));
let sd = length(d - sp);
c += mix(vec3f(0.35, 0.65, 0.80), vec3f(1.0, 0.5, 0.2), el)
* exp(-sd * sd * 90.0) * (1.0 - cyc) * 0.45 * vis;
}
// ── crown flare + hover invite (keeps the existing click affordance) ──
c += mix(vec3f(0.4, 0.7, 0.9), vec3f(1.1, 0.6, 0.2), el) * exp(-r * r * 0.8) * (0.18 * ruling + 0.30 * hov) * vis;
return max(c, vec3f(0.0));
}
module · tg_views
// px = pixel in 0..512 grid coords (y down), p = uv (-1..1, y down)
// ── THE AIRSHIP (top-left, emerges with dawn). PLACEHOLDER STUB — replaced by the
// graphics-swarm's synthesized mod_tg_airship. Defined before mod_tg_shore so
// the shore can call it. ──
fn mod_tg_shore(p: vec2f, px: vec2f, t: f32) -> vec3f {
let rawNight = uni(23);
let dawn = uni(44); // the orrery's gift: the night burns off, a new sun climbs
let night = rawNight * (1.0 - dawn * 0.92); // used for sky tint, buoy colour, glitter
let vault = uni(26);
let horizon = 0.17; // uv.y of the sea line (matches rd.y = 0)
// ── a real camera over a real sea ──
let pv = vec2f(p.x, -p.y);
let st = uni(83); // integrated sea clock from the hook — rate morphs WITHOUT phase-whip (Galen: the speed-up bug)
let ro = vec3f(0.0, 3.2 + sin(t * 0.4) * 0.05, 0.0);
let rd = normalize(vec3f(pv.x, pv.y * 0.72 + 0.122, 1.75));
// sun: sinks straight down over the sea at dusk, then at dawn CLIMBS straight
// back up the SAME azimuth — a clean vertical sunrise, no sideways drift. Only
// the elevation animates; the bearing is fixed so it never rises "at an angle".
let sel = mix(mix(0.055, -0.22, rawNight), 0.22, dawn);
let saz = 1.446; // the sun's bearing over the sea — set and rise in place
let sunv = normalize(vec3f(cos(saz) * cos(sel), sin(sel), sin(saz) * cos(sel)));
// the vault-lamp moon rises with the finale, then sets again as the dawn takes the sky
let mel = mix(-0.25, 0.50, rawNight) * (1.0 - dawn * 0.9);
let maz = 1.917; // the vault-lamp rises where the vault breaches
let mdv = normalize(vec3f(cos(maz) * cos(mel), sin(mel), sin(maz) * cos(mel)));
var c = mod_tgo_sky(rd, sunv, mdv, t, vault);
var seaT = 100000.0;
var seaHit = 0.0;
// THE TIDE (valve-driven, uni 60): at low water the whole sea SINKS — the
// beach widens, the island stands taller. High tide (0.65) = the shipped look.
let tdrop = vec3f(0.0, (0.65 - clamp(uni(60), 0.0, 1.0)) * 0.5, 0.0);
// ── the ocean: bisection-marched heightfield, analytic normals ──
if (rd.y < -0.003) {
var tm = 0.0;
var tx = 1000.0;
var hx = mod_tgo_map3(ro + rd * tx + tdrop, st);
if (hx < 0.0) {
var hm = mod_tgo_map3(ro + tdrop, st);
var tmid = 0.0;
for (var i = 0; i < 8; i++) {
tmid = mix(tm, tx, hm / (hm - hx));
let pm = ro + rd * tmid;
let hmid = mod_tgo_map3(pm + tdrop, st);
if (hmid < 0.0) { tx = tmid; hx = hmid; } else { tm = tmid; hm = hmid; }
}
let pt = ro + rd * tmid;
let dist = pt - ro;
let eps = max(dot(dist, dist) * 0.0002, 0.002);
let n = mod_tgo_nrm(pt + tdrop, eps, st);
let seaCol = mod_tgo_seacol(pt + tdrop, n, sunv, mdv, rd, dist, t, st, vault);
c = mix(c, seaCol, pow(1.0 - smoothstep(-0.02, 0.0, rd.y), 0.2));
seaT = tmid;
seaHit = 1.0;
}
}
// ── THE RISEN VAULT (finale): a glass dome breaching the sea ──
if (vault > 0.001) {
let vy = horizon + 0.02 - vault * 0.16; // rises out of the water
let vp = (p - vec2f(-0.15, vy + 0.16)) / 0.20;
let dome = length(vp * vec2f(1.0, 1.6)) - 1.0;
let waterY = vy + 0.115; // where the sea cuts across the dome
// ── SUBMERGED FOUNDATION: a widening base + caustics seen down through water ──
// (drawn first, so the emerged shell overlaps it at the breach line)
let subDepth = p.y - waterY; // >0 = below the surface
if (subDepth > 0.0 && subDepth < 0.30) {
let fx = abs(p.x - (-0.15));
let w = mix(0.075, 0.20, subDepth / 0.30); // the foundation flares as it descends
let inside = smoothstep(w, w * 0.55, fx);
let fade = smoothstep(0.30, 0.0, subDepth); // murkier the deeper it sinks
let caust = 0.55 + 0.45 * sin(p.x * 44.0 + t * 2.1) * sin(p.y * 30.0 - t * 1.6);
var bc = vec3f(0.05, 0.17, 0.21) * (0.4 + 0.7 * caust);
bc += vec3f(0.16, 0.46, 0.52) * exp(-fx * fx * 130.0) * (0.4 + 0.6 * fade); // lit central shaft
bc += vec3f(0.30, 0.72, 0.80) * pow(inside, 3.0) * fade * 0.5; // rim of the base
c = mix(c, bc, vault * inside * (0.55 + 0.35 * fade));
}
// ── the emerged glass shell — full dome, murked below the waterline ──
if (dome < 0.0) {
let nrm = clamp(-dome * 2.0, 0.0, 1.0);
var vc = mix(vec3f(0.045, 0.10, 0.14), vec3f(0.16, 0.34, 0.42), nrm);
vc += vec3f(0.55, 0.95, 1.05) * pow(1.0 - nrm, 2.5) * 1.3; // rim light
vc += vec3f(0.30, 0.75, 0.85) * exp(-dot(vp, vp) * 1.6) * (0.5 + 0.3 * sin(t * 0.9)); // inner lamp
vc += vec3f(1.15, 1.0, 0.62) * mod_tg_glyph(2, vp * 2.4) * (0.8 + 0.5 * sin(t * 1.7));
vc *= 1.0 - 0.22 * step(0.42, abs(fract(vp.y * 3.0) - 0.5)); // latitude ribs
vc *= 1.0 - 0.16 * step(0.44, abs(fract(vp.x * 2.6 + 0.5) - 0.5)); // meridian ribs — a built vault, not a bubble
// crown beacon: a hot point at the apex, throwing a soft upward shaft
let crown = exp(-dot(vp - vec2f(0.0, -1.02), vp - vec2f(0.0, -1.02)) * 90.0);
vc += vec3f(0.85, 1.05, 1.10) * crown * (1.2 + 0.5 * sin(t * 2.3));
// water SHEETS down the emerged shell while it rises — thin bright runnels
let emerged = smoothstep(waterY + 0.01, waterY - 0.06, p.y);
let runnel = pow(0.5 + 0.5 * sin(vp.x * 34.0 + sin(vp.y * 7.0 + t * 0.8) * 2.0), 6.0);
let sheetFall = fract(vp.y * 2.2 + t * 1.3);
vc += vec3f(0.55, 0.85, 0.92) * runnel * emerged * smoothstep(1.0, 0.55, sheetFall) * (0.55 * (1.0 - vault) + 0.10);
// below the waterline the shell dims, tints deep-teal, and its light wobbles
let below = smoothstep(waterY - 0.004, waterY + 0.03, p.y);
let caust2 = 0.5 + 0.5 * sin(vp.x * 8.0 + t * 2.0) * sin(vp.y * 6.0 - t * 1.5);
vc = mix(vc, mix(vc, vec3f(0.04, 0.15, 0.19), 0.62) * (0.42 + 0.4 * caust2), below);
c = mix(c, vc * (0.4 + vault), vault * smoothstep(0.03, -0.05, dome));
}
// near halo where it breaches
let lx = p.x + 0.15;
c += vec3f(0.30, 0.70, 0.90) * exp(-lx * lx * 22.0) * smoothstep(horizon, horizon + 0.22, p.y) * smoothstep(horizon + 0.45, horizon + 0.1, p.y) * vault * 0.32;
// water DISPLACED by the rising dome — stronger rings, a real push
let seaMask = smoothstep(horizon, horizon + 0.30, p.y) * smoothstep(horizon + 0.52, horizon + 0.06, p.y);
let db = length((p - vec2f(-0.15, waterY + 0.02)) * vec2f(1.0, 2.0));
let ring = sin(db * 13.0 - t * 3.4 - vault * 7.0) * exp(-db * 2.5);
let disp = ring * (vault * (1.0 - vault) * 5.0 + vault * 0.5);
c += vec3f(0.46, 0.86, 0.96) * max(disp, 0.0) * seaMask; // lifted crest
c -= vec3f(0.13, 0.21, 0.23) * max(-disp, 0.0) * seaMask; // trough behind it
// FOAM COLLAR — churning white right where the shell breaks the sea. This is
// what makes the displacement READ against a bright sea: hard, bright, alive.
let fxr = abs(p.x + 0.15);
let collarW = 0.075 + vault * 0.055;
let collar = exp(-pow((p.y - waterY) * 34.0, 2.0)) * smoothstep(collarW * 1.9, collarW * 0.4, fxr);
let churn = 0.55 + 0.45 * sin(p.x * 120.0 + t * 6.0) * sin(p.y * 90.0 - t * 4.2);
let boil = 0.5 + 0.5 * sin(fxr * 70.0 - t * 7.5);
c += vec3f(0.92, 1.0, 1.0) * collar * churn * (0.55 + 0.75 * vault * (1.0 - vault) * 4.0);
c += vec3f(0.55, 0.9, 0.95) * exp(-pow((p.y - waterY - 0.02) * 22.0, 2.0)) * smoothstep(collarW * 3.0, collarW, fxr) * boil * 0.35;
}
// ── RISING FLAMES (Galen): tongues lick UP off the burning sea — screen-space
// layer, upward-advected warped noise, sharp tips, fast flicker over slow magma ──
let fireS = clamp(uni(82), 0.0, 1.0);
if (fireS > 0.01 && p.y > horizon - 0.22) {
var fq = vec2f(p.x * 3.0, p.y * 5.0 + t * 2.1);
var fl = fbm(fq, 3);
fl = fl + 0.55 * fbm(fq * 2.3 + vec2f(fl * 1.7, t * 1.4), 2);
let nearness = smoothstep(horizon, 0.9, p.y); // near field burns taller
let above = smoothstep(horizon - 0.20, horizon + 0.05, p.y); // licks reach above the sea line
let flick2 = 0.55 + 0.45 * sin(t * 9.0 + p.x * 13.0 + fl * 8.0);
let tongue = pow(smoothstep(0.62, 1.05, fl + nearness * 0.22), 2.0) * above * flick2 * fireS;
// blackbody tongue: red skirt → orange body → yellow-white tip
c += vec3f(1.4, 0.18, 0.02) * tongue * 0.9;
c += vec3f(2.6, 0.95, 0.10) * tongue * tongue * 1.1;
c += vec3f(3.6, 2.4, 0.7) * pow(tongue, 3.0) * 0.9;
// heat haze: the sky just above the sea shivers warm
c += vec3f(0.45, 0.16, 0.04) * exp(-abs(p.y - horizon) * 9.0) * fireS * (0.5 + 0.3 * sin(t * 5.0 + p.x * 20.0));
}
// ── the island observatory: raymarched against the same sky and sun ──
if (p.x > 0.20 && p.y < 0.90 && rd.z > 0.0) {
var tI = 9.5;
var hitM = -1.0;
var hitP = vec3f(0.0);
for (var st2 = 0; st2 < 56; st2++) {
let pos = ro + rd * tI;
let dm = mod_tgi_sdf(pos);
if (dm.x < 0.02) { hitM = dm.y; hitP = pos; break; }
tI = tI + max(dm.x * 0.85, 0.015);
if (tI > 22.5) { break; }
}
if (hitM > -0.5 && (seaHit < 0.5 || tI < seaT)) {
let n = mod_tgi_nrm(hitP);
let q = hitP - vec3f(6.5, 0.0, 16.0);
let sunD = clamp(dot(n, sunv), 0.0, 1.0);
let skyA = 0.22 + 0.30 * clamp(n.y, 0.0, 1.0); // sky ambient
let suncol = mod_tgo_suncol(sel);
var alb = vec3f(0.05);
var emis = vec3f(0.0);
let mi = i32(hitM + 0.5);
if (mi == 0) { // rock
alb = vec3f(0.062, 0.055, 0.058) * (0.7 + 0.6 * fbm(hitP.xy * 2.2 + hitP.zz * 1.3, 3));
alb *= 1.0 - 0.3 * smoothstep(0.4, 0.0, hitP.y); // tide-dark base
} else if (mi == 1) { // masonry: weathered courses
alb = vec3f(0.135, 0.120, 0.105) * (0.75 + 0.4 * fbm(vec2f(atan2(q.z, q.x - 0.6) * 4.0, hitP.y * 7.0), 2));
alb *= 1.0 - 0.22 * smoothstep(0.35, 0.5, abs(fract(hitP.y * 3.6) - 0.5));
alb *= 1.0 - 0.25 * smoothstep(2.4, 1.9, hitP.y); // damp foot
} else if (mi == 2) { alb = vec3f(0.045, 0.040, 0.042); // ironwork
} else if (mi == 3) { // the tide-glass dome
let fres = pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 2.0);
alb = vec3f(0.035, 0.075, 0.085);
emis = vec3f(0.10, 0.55, 0.60) * (0.30 + night * 0.9 + fres * 0.5);
emis *= 1.0 - 0.35 * step(0.42, abs(fract(atan2(q.z, q.x - 0.6) * 1.273) - 0.5)); // ribs
emis += vec3f(0.9, 0.95, 1.0) * fres * 0.25;
} else if (mi == 4) { alb = vec3f(0.055, 0.085, 0.075) * (0.8 + 0.3 * fbm(hitP.xz * 6.0, 2)); // verdigris roof
} else if (mi == 5) { // cottage clapboard
alb = vec3f(0.140, 0.115, 0.090) * (0.8 + 0.3 * fbm(vec2f(hitP.x * 3.0, hitP.y * 14.0), 2));
alb *= 1.0 - 0.18 * smoothstep(0.35, 0.5, abs(fract(hitP.y * 7.0) - 0.5));
} else if (mi == 6) { alb = vec3f(0.110, 0.062, 0.048) * (0.8 + 0.35 * fbm(hitP.xy * 9.0, 2)); // brick
} else if (mi == 7) { // the beacon
alb = vec3f(0.03);
emis = vec3f(0.30, 0.90, 0.95) * (1.2 + night * 1.8) * (0.85 + 0.15 * sin(t * 2.1));
}
let selfSh = mod_tgi_shadow(hitP + n * 0.12, sunv);
var ic = alb * (skyA * mix(vec3f(0.55, 0.45, 0.55), vec3f(0.25, 0.30, 0.50), night) * 2.0 + suncol * sunD * selfSh * (1.15 - night * 0.75));
// dusk rim from the sunward edge
let rim = pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 3.0) * clamp(dot(n, sunv) + 0.4, 0.0, 1.0);
ic += suncol * rim * (0.5 - night * 0.3);
// lit windows: tower (two) + cottage (one), camera face
if (mi == 1 && q.z < 0.0) {
let wa = abs(atan2(q.z, q.x - 0.6) + 1.5708);
let wy1 = smoothstep(0.16, 0.10, abs(hitP.y - 2.95)) * smoothstep(0.30, 0.18, wa);
let wy2 = smoothstep(0.14, 0.09, abs(hitP.y - 3.95)) * smoothstep(0.30, 0.18, wa);
emis += vec3f(1.5, 0.95, 0.40) * (wy1 + wy2) * (0.5 + night * 0.9);
}
if (mi == 5 && q.z < 0.25) {
let wc = smoothstep(0.14, 0.08, abs(q.x + 1.05)) * smoothstep(0.16, 0.09, abs(hitP.y - 2.38));
emis += vec3f(1.5, 0.9, 0.35) * wc * (0.5 + night * 0.9);
}
ic += emis;
// aerial perspective into the dusk
let fogF = clamp((tI - 11.0) / 24.0, 0.0, 1.0);
ic = mix(ic, mod_tgo_sky(rd, sunv, mdv, t, vault) * 0.9, fogF * 0.35);
c = ic;
}
}
// ── a low mist bank drifts before the island, breathing with the hour ──
if (rd.z > 0.05) {
let mistT = 11.2 / rd.z;
let mp = ro + rd * mistT;
let seaBlocks = seaHit > 0.5 && seaT < mistT;
if (!seaBlocks && mp.y > -0.3 && mp.y < 3.4 && mp.x > -6.0 && mp.x < 12.0) {
var md = fbm(vec2f(mp.x * 0.30 - t * 0.035, mp.y * 0.75 + t * 0.008), 3);
md = md * (0.75 + 0.25 * vnoise(vec2f(mp.x * 1.3 + t * 0.05, mp.y * 2.0)));
let band = smoothstep(3.2, 0.5, mp.y) * smoothstep(0.40, 0.72, md);
let mistCol = mix(vec3f(0.80, 0.50, 0.34), vec3f(0.16, 0.24, 0.34), night)
+ mod_tgo_suncol(sel) * exp(-abs(mp.x - 4.0) * 0.25) * (1.0 - night) * 0.22;
c = mix(c, mistCol, band * 0.34);
}
}
// beacon halo + its wobbling light on the water (screen-space, projected)
{
let bs = vec2f(1.75 * 7.1 / 16.0, -((1.75 * 2.52 / 16.0) - 0.122) / 0.72);
let bd2 = p - bs;
c += vec3f(0.30, 0.85, 0.90) * exp(-dot(bd2, bd2) * 240.0) * (0.35 + night * 0.9);
if (p.y > horizon + 0.01) {
let sx2 = p.x - bs.x;
let wob = 0.72 + 0.28 * sin(p.y * 52.0 + t * 1.35);
c += vec3f(0.22, 0.60, 0.62) * exp(-sx2 * sx2 * 1600.0) * smoothstep(horizon + 0.42, horizon, p.y) * 0.20 * wob * (0.4 + night);
c += vec3f(0.85, 0.55, 0.22) * exp(-pow(p.x - 0.71, 2.0) * 900.0) * smoothstep(horizon + 0.30, horizon, p.y) * 0.14 * wob * (1.0 - night * 0.4);
}
}
// the CRYSTAL WORKS on the far left — drawn BEFORE the buoys so they pass in front
c = tg_cavehouse(c, p, t, night, dawn);
// ── four bell-buoys: real bodies riding the marched sea ──
for (var k = 0; k < 4; k++) {
let fk = f32(k);
var bwx = -4.4;
if (k == 1) { bwx = -2.45; } else if (k == 2) { bwx = -0.6; } else if (k == 3) { bwx = 1.45; }
let axk = 1.75 * bwx / 11.0;
if (abs(p.x - axk) > 0.34) { continue; }
// anchor on the actual wave field
let bh = mod_tgo_h2(vec2f(bwx, 11.0), st);
let base = vec3f(bwx, bh, 11.0);
let dirB = base - ro;
let bDist = length(dirB);
let occl = step(0.5, seaHit) * step(seaT, bDist - 0.45); // a crest stands in front
if (occl > 0.5) { continue; }
let su = 1.75 / dirB.z;
let sx = 1.75 * dirB.x / dirB.z;
let sy = -((1.75 * dirB.y / dirB.z - 0.122) / 0.72);
let lean = sin(t * 0.6 + fk * 1.9) * 0.10 + sin(t * 1.1 + fk * 3.1) * 0.03;
let gcol = mod_tg_gcol(k);
let flash = uni(19 + k);
// ── the buoy as it was: slim keeper of its lamp — hull, post, and light ──
{
var q0 = p - vec2f(sx, sy);
q0.x = q0.x + q0.y * lean;
// hull: half-sunk iron, a touch of dusk rim, seated in the swell
let hd = length((q0 - vec2f(0.0, -0.10 * su)) * vec2f(1.0, 2.2)) - 0.50 * su;
if (hd < 0.0 && q0.y < 0.06 * su) {
var bc = vec3f(0.034, 0.030, 0.032) * (0.85 + 0.4 * fbm(q0 * 30.0 + vec2f(fk * 7.0), 2));
bc += mix(vec3f(0.75, 0.34, 0.12), vec3f(0.10, 0.13, 0.22), night) * smoothstep(0.02 * su, -0.06 * su, hd) * clamp(0.5 - q0.x * 6.0, 0.0, 1.0) * 0.5;
bc += gcol * exp(-q0.y * q0.y * 300.0) * flash * 0.3;
// wet sheen at the waterline
bc += vec3f(0.4, 0.45, 0.5) * exp(-pow((q0.y - 0.02 * su) / (0.03 * su), 2.0)) * 0.25;
c = mix(c, bc, smoothstep(0.004, -0.004, hd));
}
// post: slender, warm-rimmed toward the sun
let pd2 = max(abs(q0.x) - 0.045 * su, max(q0.y - (-0.12 * su), -1.62 * su - q0.y));
if (pd2 < 0.0) {
var pc2 = vec3f(0.036, 0.032, 0.032) * (0.9 + 0.3 * fbm(vec2f(q0.x * 60.0, q0.y * 8.0), 2));
pc2 += mix(vec3f(0.7, 0.32, 0.12), vec3f(0.12, 0.15, 0.24), night) * smoothstep(0.0, -0.03 * su, abs(q0.x) - 0.02 * su) * 0.35;
// cross-brace collar where the lamp mounts
pc2 += vec3f(0.30, 0.24, 0.14) * smoothstep(0.016 * su, 0.006 * su, abs(q0.y + 1.52 * su)) * 0.8;
c = mix(c, pc2, smoothstep(0.003, -0.003, pd2));
}
// waterline seat shadow
let fq2 = q0 - vec2f(0.0, 0.035 * su);
c *= 1.0 - exp(-pow(fq2.y / (0.030 * su), 2.0)) * smoothstep(0.55 * su, 0.20 * su, abs(fq2.x)) * 0.30;
}
// the lantern-glyph, alive when its bell speaks (billboard over the head)
var q = p - vec2f(sx, sy);
q.x = q.x + q.y * lean;
let lp2 = (q - vec2f(0.0, -1.80 * su)) / (0.44 * su);
if (abs(lp2.x) < 1.6 && abs(lp2.y) < 1.6) {
// a dark lamp-glass backing makes the glyph legible against bright sky
let back = smoothstep(1.35, 0.95, length(lp2));
c = mix(c, vec3f(0.015, 0.014, 0.018), back * 0.72);
let g = mod_tg_glyph(k, lp2);
c = mix(c, gcol * (1.15 + flash * 2.6), g);
c += gcol * g * 0.4; // let bloom catch it
}
c += gcol * exp(-dot(q - vec2f(0.0, -1.78 * su), q - vec2f(0.0, -1.78 * su)) * (170.0 / (su * su))) * (0.10 + flash * 1.9) * su * 4.0;
// waterline seat: the hull settles INTO the sea (foam now lives in the sea shader)
let fq2 = q - vec2f(0.0, 0.035 * su);
c *= 1.0 - exp(-pow(fq2.y / (0.030 * su), 2.0)) * smoothstep(0.55 * su, 0.20 * su, abs(fq2.x)) * 0.30;
// its light wobbling down the water
if (q.y > 0.05 * su) {
let wob4 = 0.6 + 0.4 * sin(p.y * 46.0 + t * 1.6 + fk);
c += gcol * exp(-pow(q.x / (0.10 * su), 2.0)) * exp(-q.y * 2.2) * (0.12 + flash * 0.55) * wob4;
}
}
// ── the airship drifts in at the TOP-LEFT once the orrery is solved (uni(44)=
// dawn) — the world answering that the work is done. (built by the swarm) ──
// SUMMONED, not dawn-born (Galen Aug 10): the ship exists once the bloom has
// called it. During the cine it slides out of the portal while its own fade
// brings it up — the portal draws after, so its rim overlaps the emerging bow.
let smn = uni(115);
if (uni(116) > 0.5 && smn > 0.98) {
c = mod_tg_airship(c, p, t, 1.0, sel, night);
} else if (smn > 0.28) {
// THE SHIP COMES OUT OF THE HOLE (Galen): visible only INSIDE the aperture
// (seen against the elsewhere-sky) or on the EXIT side of the portal plane —
// never hanging in our sky behind the ring.
let em = smoothstep(0.30, 0.80, smn);
let PC = vec2f(-0.24, -0.66);
let slide = (PC - vec2f(-0.58, -0.50)) * (1.0 - em); // portal → mooring
let before = c;
c = mod_tg_airship(c, p - slide, t, em, sel, night);
let exitDir = normalize(vec2f(-0.58, -0.50) - PC);
let inHole = smoothstep(0.015, -0.015, length(p - PC) - tg_portalR(smn));
let outSide = smoothstep(-0.01, 0.03, dot(p - PC, exitDir));
c = mix(before, c, max(inHole, outSide));
}
if (smn > 0.0 && smn < 0.98) { c = tg_skybolt(c, p, t, smn); c = tg_portal(c, p, t, smn); }
return c;
}
// ═══ THE TIDE RECORD — the stele's own chamber, right of the gate disc ═══
fn mod_tg_stele(p: vec2f, px: vec2f, t: f32) -> vec3f {
// carved alcove: candle-lit ancient wall — gear fossils and tide-strata
let vign2 = 0.26 + 0.62 * exp(-dot(p * vec2f(0.9, 0.75), p * vec2f(0.9, 0.75)) * 0.9);
var c = mod_tg_ancient(p * 1.7 + vec2f(31.0), 7.0, vec2f(0.6, -0.7), vign2, 1) * vign2;
// archive pigeonholes flank the chamber: rows of scroll-filled recesses
if (abs(p.x) > 0.70 && p.y > -0.55 && p.y < 0.60) {
let hq = vec2f(fract(abs(p.x) * 7.0), fract((p.y + 0.55) * 5.2));
let hole = step(0.16, hq.x) * step(hq.x, 0.86) * step(0.14, hq.y) * step(hq.y, 0.84);
if (hole > 0.5) {
let hid = floor(vec2f(abs(p.x) * 7.0, (p.y + 0.55) * 5.2)) + vec2f(sign(p.x) * 9.0, 0.0);
let hh = hash21(hid);
var hc = vec3f(0.012, 0.010, 0.010); // deep recess
if (hh > 0.35) {
// a rolled scroll catches the candlelight — round end, wax seal fleck
let sq = (hq - vec2f(0.51, 0.49)) * vec2f(1.35, 1.8);
let scroll = smoothstep(0.30, 0.26, length(sq));
let paper = mix(vec3f(0.28, 0.22, 0.14), vec3f(0.42, 0.34, 0.22), hash21(hid + 4.0));
hc = mix(hc, paper * (0.35 + 0.65 * vign2) * (0.7 + 0.6 * smoothstep(0.3, -0.3, sq.x)), scroll);
hc += vec3f(0.55, 0.12, 0.08) * step(0.82, hh) * smoothstep(0.10, 0.05, length(sq - vec2f(0.12, 0.0))) * 0.8;
}
c = mix(c, hc, 0.92);
// worn sill light under each hole
c += vec3f(0.55, 0.34, 0.14) * smoothstep(0.14, 0.10, hq.y) * hole * vign2 * 0.15;
}
}
// floor line + flags
if (p.y > 0.66) {
let fv = p.y - 0.66;
let rc2 = log(1.0 + fv * 5.0) * 7.0;
let cc2 = p.x * (2.4 / (0.30 + fv)) + hash11(floor(rc2)) * 2.0;
var fc = vec3f(0.075, 0.066, 0.062) * (0.75 + 0.4 * hash21(vec2f(floor(cc2), floor(rc2))));
fc *= 1.0 - 0.4 * max(smoothstep(0.38, 0.5, abs(fract(rc2) - 0.5)), smoothstep(0.42, 0.5, abs(fract(cc2) - 0.5)));
c = fc * (0.5 + 0.6 * exp(-p.x * p.x * 2.0));
}
// two sconce lamps flanking the slab: iron, glass, a true flame
for (var sN = 0; sN < 2; sN++) {
let sxx = select(-0.62, 0.62, sN == 1);
let sp0 = p - vec2f(sxx, -0.02);
let fs = f32(sN);
let flick = 0.82 + 0.13 * sin(t * (6.3 + fs * 1.4) + fs * 9.0) + 0.05 * sin(t * 17.0 + fs * 5.0);
// the sconce: iron stem up the wall, a shallow dish, a wax candle
let stemD = sdBox(sp0 - vec2f(0.0, 0.235), vec2f(0.011, 0.085));
let dishD = max(length((sp0 - vec2f(0.0, 0.150)) * vec2f(1.0, 2.6)) - 0.058, -(sp0.y - 0.128));
let iron = min(stemD, dishD);
if (iron < 0.0) {
var ic2 = vec3f(0.050, 0.040, 0.032) * (0.85 + 0.5 * clamp(-(sp0.y - 0.15) * 8.0 + 0.4, 0.0, 1.0));
ic2 *= 0.8 + 0.4 * fbm(sp0 * 60.0 + vec2f(fs * 3.0), 2); // forged texture
ic2 += vec3f(1.0, 0.60, 0.22) * smoothstep(0.006, 0.0, abs(dishD + 0.008)) * 0.45 * flick; // rim catches the flame
c = mix(c, ic2, smoothstep(0.004, -0.004, iron));
}
// the candle: warm wax, dripped edges, a dark wick
let waxD = sdBox(sp0 - vec2f(0.0, 0.096), vec2f(0.021 + 0.004 * sin(sp0.y * 90.0 + fs * 5.0), 0.036));
if (waxD < 0.0) {
var wc2 = vec3f(0.62, 0.50, 0.34) * (0.55 + 0.45 * clamp(-(sp0.x) * 8.0 + 0.5, 0.0, 1.0));
wc2 += vec3f(1.0, 0.65, 0.28) * exp(-pow((sp0.y - 0.062) * 30.0, 2.0)) * 0.55 * flick; // translucent glow at the lip
c = mix(c, wc2, smoothstep(0.003, -0.003, waxD));
}
let wickD = sdBox(sp0 - vec2f(0.0, 0.052), vec2f(0.0035, 0.012));
c = mix(c, vec3f(0.05, 0.03, 0.02), smoothstep(0.003, -0.002, wickD));
// the flame: a living tongue — fbm-torn edge, layered heat, licking tip
{
var fq = sp0 - vec2f(0.0, -0.008);
// rising turbulence tears the profile; stronger toward the tip
let rise = clamp(-fq.y * 8.0 + 0.4, 0.0, 1.6);
let torn = fbm(vec2f(fq.x * 26.0, fq.y * 15.0 - t * 4.2 + fs * 9.0), 3) - 0.5;
fq.x = fq.x + torn * 0.030 * rise;
fq.x = fq.x + sin(t * 3.1 + fs * 2.2) * 0.006 * rise; // slow sway
// teardrop: wide at the wick, drawn to a licking point
let neck = 1.0 + clamp(fq.y + 0.035, 0.0, 0.4) * 5.5;
let tipFlick = 1.0 + 0.22 * sin(t * 11.0 + fs * 4.0) * smoothstep(0.0, -0.06, fq.y);
let flameD = length(vec2f(fq.x * 2.6 * neck, (fq.y + 0.008) * 1.30 / tipFlick)) - 0.075;
if (flameD < 0.012) {
let heat = clamp(-flameD * 11.0, 0.0, 1.0);
// stratified: transparent skin → orange → yellow → white heart
var fc2 = mix(vec3f(0.95, 0.30, 0.05), vec3f(1.55, 0.95, 0.25), smoothstep(0.15, 0.55, heat));
fc2 = mix(fc2, vec3f(2.0, 1.75, 1.25), smoothstep(0.62, 0.95, heat));
// the blue-violet root at the wick
fc2 = mix(fc2, vec3f(0.35, 0.25, 0.75), smoothstep(0.035, 0.075, fq.y) * smoothstep(0.9, 0.4, heat) * 0.7);
// inner darker vein above the wick (real candles hollow there)
fc2 *= 1.0 - 0.35 * exp(-dot(fq - vec2f(0.0, 0.030), fq - vec2f(0.0, 0.030)) * 900.0);
let edge = smoothstep(0.010, -0.004, flameD);
c = mix(c, fc2 * flick * 1.3, edge);
// heat shimmer just above the tip
c += vec3f(0.9, 0.55, 0.2) * exp(-pow((fq.y + 0.10) * 22.0, 2.0)) * exp(-fq.x * fq.x * 500.0) * 0.12 * flick;
}
}
// its light: near halo, wall gradient, pool on the floor
c += vec3f(1.0, 0.55, 0.18) * exp(-dot(sp0, sp0) * 5.0) * 0.36 * flick;
c += vec3f(0.75, 0.42, 0.14) * exp(-dot(sp0, sp0) * 1.1) * 0.14 * flick;
let poolD = (p - vec2f(sxx * 0.85, 0.86)) * vec2f(2.2, 8.0);
c += vec3f(0.85, 0.48, 0.16) * exp(-dot(poolD, poolD) * 1.2) * 0.16 * flick;
}
// ── the slab: large, enthroned, readable ──
let sp2 = p - vec2f(0.0, 0.10);
let steleD = sdRoundedBox(sp2, vec2f(0.34, 0.58), 0.05);
// plinth beneath
c = mix(c, vec3f(0.085, 0.075, 0.068) * (0.7 + 0.3 * fbm(p * 8.0, 2)), smoothstep(0.01, -0.01, sdBox(p - vec2f(0.0, 0.78), vec2f(0.42, 0.10))));
if (steleD < 0.0) {
var sc2 = mod_tg_rock(p * 2.6 + vec2f(9.0), vec3f(0.135, 0.122, 0.112));
sc2 *= 0.80 + 0.35 * smoothstep(0.0, -0.2, steleD);
// beveled edge catches candlelight
sc2 += vec3f(0.95, 0.60, 0.25) * smoothstep(0.030, 0.0, abs(steleD + 0.020)) * 0.35;
// carve: columns k=0..4, ticks = answer[k]+1 (answer 3,1,4,2,0)
for (var k = 0; k < 5; k++) {
var ticks = 1;
if (k == 0) { ticks = 4; } else if (k == 1) { ticks = 2; } else if (k == 2) { ticks = 5; } else if (k == 3) { ticks = 3; }
let cx = -0.232 + f32(k) * 0.116;
// faint column channel
sc2 *= 1.0 - 0.10 * smoothstep(0.040, 0.020, abs(sp2.x - cx)) * step(-0.30, sp2.y) * step(sp2.y, 0.50);
for (var j = 0; j < 5; j++) {
if (j >= ticks) { continue; }
let tickP = sp2 - vec2f(cx, 0.42 - f32(j) * 0.165);
let td = sdBox(tickP, vec2f(0.036, 0.026));
let carve = smoothstep(0.014, -0.006, td);
let inner = smoothstep(0.0, -0.014, td);
sc2 = mix(sc2, vec3f(0.040, 0.046, 0.048), carve * 0.9);
sc2 += vec3f(0.22, 0.80, 0.68) * inner * (0.30 + 0.12 * max(sin(t * 0.9 + f32(k)), -0.9));
}
}
// header: THE DROWNED-GATE COMBO (Galen Jul 30) — the top row IS the clue:
// four glyphs in dialing order + the notch-down ring. No decorative key row.
var cbh = array<i32, 4>(1, 3, 0, 2);
for (var k = 0; k < 4; k++) {
let gp2 = (sp2 - vec2f(-0.194 + f32(k) * 0.106, -0.470)) / 0.050;
if (abs(gp2.x) < 1.3 && abs(gp2.y) < 1.3) {
let g = mod_tg_glyph(cbh[k], gp2);
sc2 = mix(sc2, vec3f(0.045, 0.05, 0.05), g * 0.7);
sc2 += mod_tg_gcol(cbh[k]) * g * 0.55;
}
}
let nph = sp2 - vec2f(0.235, -0.470);
sc2 += vec3f(0.85, 0.66, 0.30) * smoothstep(0.006, 0.0, abs(length(nph) - 0.030)) * 0.7;
sc2 += vec3f(0.95, 0.75, 0.35) * smoothstep(0.009, 0.0, sdSegment(nph, vec2f(0.0, 0.030), vec2f(0.0, 0.052))) * 0.9;
// a graven wave-line beneath the header
sc2 *= 1.0 - 0.25 * smoothstep(0.012, 0.004, abs(sp2.y + 0.40 - sin(sp2.x * 9.0) * 0.012));
c = mix(c, sc2, smoothstep(0.006, -0.006, steleD));
}
// drifting dust in the candlelight
let dcell = hash21(floor(vec2f(p.x * 130.0, p.y * 80.0 - t * 1.1)));
c += vec3f(1.0, 0.8, 0.5) * step(0.9955, dcell) * exp(-dot(p, p) * 1.4) * 0.25;
return c;
}
// aged bronze plate: layered patina over near-black metal, lit from above-left
fn mod_tg_plate(p: vec2f, key: f32) -> vec3f {
var m = vec3f(0.030, 0.026, 0.022);
let pat1 = fbm(p * 5.0 + vec2f(7.0), 3);
let pat2 = fbm(p * 16.0 + vec2f(2.0), 2);
m = mix(m, vec3f(0.085, 0.065, 0.038), smoothstep(0.45, 0.75, pat1) * 0.8); // bronze bloom
m = mix(m, vec3f(0.040, 0.055, 0.045), smoothstep(0.55, 0.85, pat2) * 0.5); // verdigris fleck
m *= 0.65 + 0.6 * pat2 * pat1;
m *= 0.55 + key; // key light
return m;
}
fn mod_tg_gate(p: vec2f, px: vec2f, t: f32) -> vec3f {
let open = max(uni(8), uni(52)); // the gate opens for EITHER combo — it IS the portal (Galen Jul 30)
// the cliff is a MACHINE fossil: strata, gear imprints, conduit grooves —
// the observatory's dead works, pressed into the rock around the living door
let vign = 0.28 + 0.50 * exp(-dot(p - vec2f(0.0, -0.1), p - vec2f(0.0, -0.1)) * 1.0);
var c = mod_tg_ancient(p * 1.5, 3.0, vec2f(-0.7, -0.7), vign, 0) * vign;
c += vec3f(0.30, 0.16, 0.07) * exp(-(p.x + 0.9) * (p.x + 0.9) * 1.0) * exp(-(p.y + 0.9) * (p.y + 0.9) * 1.1) * 0.4;
// one great half-buried flywheel arcs behind the portal's upper right
{
let fq = (p - vec2f(0.94, -0.86)) / 0.52;
let fg = mod_tg_gearh(fq, 0.72, 14.0);
c = mix(c, vec3f(0.062, 0.052, 0.040) * (0.8 + vign), fg * 0.55 * smoothstep(1.6, 0.9, length(fq)));
c += vec3f(0.45, 0.32, 0.12) * fg * 0.20 * vign;
}
// ── the plate frame: aged bronze slab enthroning the disc ──
let fp = p - vec2f(0.0, 0.02);
let fr1 = abs(sdRoundedBox(fp, vec2f(0.80, 0.80), 0.10)) - 0.030;
let fr2 = abs(sdRoundedBox(fp, vec2f(0.90, 0.90), 0.12)) - 0.022;
let frame = min(fr1, fr2);
if (frame < 0.0) {
let key = clamp(0.5 - fp.x * 0.6 - fp.y * 0.8, 0.0, 1.1);
var kc = mod_tg_plate(p * 2.0, key);
// engraved thin border line inside each band
kc *= 1.0 - 0.35 * smoothstep(0.006, 0.002, abs(frame + 0.015));
kc += vec3f(0.75, 0.52, 0.22) * smoothstep(0.008, 0.0, abs(frame + 0.006)) * key * 0.35;
c = mix(c, kc, smoothstep(0.006, -0.006, frame));
}
// between the bands: dark plate with faint scattered etchings
let between = max(sdRoundedBox(fp, vec2f(0.90, 0.90), 0.12), -sdRoundedBox(fp, vec2f(0.80, 0.80), 0.10));
if (between < 0.0 && frame > 0.0) {
var bc2 = mod_tg_plate(p * 2.0 + vec2f(4.0), clamp(0.4 - fp.x * 0.5 - fp.y * 0.6, 0.0, 1.0) * 0.7);
let ecell = floor((fp + vec2f(2.0)) * 11.0);
let eh = hash21(ecell);
if (eh > 0.87) {
let ep = (fract((fp + vec2f(2.0)) * 11.0) - 0.5) / 0.30;
let gi2 = i32(eh * 37.0) % 4;
bc2 += vec3f(0.55, 0.42, 0.18) * mod_tg_glyph(gi2, ep) * 0.16; // ghost etchings
}
c = mix(c, bc2, smoothstep(0.006, -0.006, between));
}
// ── the threshold: worn steps and a puddle that remembers the door ──
if (p.y > 0.86) {
let sv = p.y - 0.86;
var gc2 = vec3f(0.070, 0.062, 0.058) * (0.75 + 0.4 * fbm(vec2f(p.x * 7.0, p.y * 18.0), 2));
gc2 *= 1.0 - 0.35 * smoothstep(0.35, 0.5, abs(fract(sv * 22.0) - 0.5)); // step treads
// puddle mirrors the door's warmth
let pd = length((p - vec2f(0.18, 0.965)) * vec2f(2.6, 9.0)) - 0.55;
if (pd < 0.0) {
let wob3 = 1.0 + sin(p.x * 60.0 + t * 2.0) * 0.15;
gc2 = mix(gc2, vec3f(0.05, 0.055, 0.075) + vec3f(0.55, 0.30, 0.10) * (0.25 + open * 0.9) * wob3, smoothstep(0.0, -0.15, pd));
}
c = mix(c, gc2 * (0.5 + 0.5 * exp(-p.x * p.x * 1.5)), smoothstep(0.0, 0.05, sv));
}
// drips
let dcol = floor(px.x / 24.0);
let dh = hash11(dcol);
if (dh > 0.55) {
let dy = fract(p.y * 0.5 - t * (0.05 + dh * 0.1) + dh * 7.0);
c += vec3f(0.35, 0.45, 0.5) * exp(-abs(fract(px.x / 24.0) - 0.5) * 14.0) * smoothstep(0.94, 1.0, dy) * 0.35;
}
// ── the great round door ──
let dp = p - vec2f(0.0, 0.02);
let R = 0.62;
let dd = length(dp) - R;
if (dd < 0.02) {
// opening: an iris — the door's inner disc pulls back and darkens
let irisR = R * (1.0 - open); // iris retracts FULLY — the 6% nub was 'the dot in the middle' (Galen's click found it)
let ird = length(dp) - irisR;
let rr = length(dp);
if (ird > 0.0) {
// ── the revealed passage: measured from the plate (zone read Jul 19) ──
// radial law: near-black rim, long amber falloff, small hot heart at the orb
let rn = rr / R;
let oc = vec2f(0.0, -0.28); // the ornament seat (upper-center)
// ── THE GATE IS A PORTAL (Galen Jul 30): the opened door SHOWS the zone it
// leads to — combo B reveals the drowned quarter, combo A the organ hall ──
let pp = dp / R;
// ── legible SKETCH previews (Galen: full scenes at this scale become blobs —
// the flywheel read as a black-edged dot, the water as a blue semicircle) ──
var pc = vec3f(0.005, 0.004, 0.004);
if (uni(52) > 0.5) {
// → the drowned quarter: dark flooded hall, three pillars, a living waterline
pc = vec3f(0.015, 0.035, 0.045) * (1.0 - pp.y * 0.3);
for (var pi = 0; pi < 3; pi++) {
let cx3 = -0.5 + f32(pi) * 0.5;
pc += vec3f(0.03, 0.06, 0.07) * smoothstep(0.10, 0.04, abs(pp.x - cx3));
}
let wl = 0.25 + 0.02 * sin(pp.x * 5.0 + t * 0.8);
pc += vec3f(0.10, 0.50, 0.55) * exp(-abs(pp.y - wl) * 18.0) * 0.5;
if (pp.y > wl) { pc = pc * vec3f(0.5, 0.8, 0.85) + vec3f(0.02, 0.08, 0.09); }
} else {
// → the organ hall, THE REAL ROOM through the door (Galen: the pipes must
// show as THEMSELVES) — the actual hall shader, zoomed onto the pipe row
let hp2 = pp * 0.55;
pc = mod_tg_hall(hp2, hp2 * 256.0 + 256.0, t);
pc = pc * 1.35 + vec3f(0.030, 0.022, 0.014); // lift the room out of shadow — the dim
// unsolved-hall level + the flywheel's dark center read as a blot at this size
}
pc *= open; // fades in as the iris pulls back
pc *= smoothstep(1.0, 0.82, rn); // rim to true black
// ── the ornament: chevrons and orb, the door's own enter-button ──
if (open > 0.5) {
let mm2 = vec2f(uni(27), uni(28));
let hov = smoothstep(95.0, 34.0, length(mm2 - vec2f(256.0, 184.0)));
let oq = dp - oc;
for (var ch = 0; ch < 2; ch++) {
let oy = select(0.056, -0.034, ch == 1);
let vd = abs(oq.y - oy - abs(oq.x) * 0.72) - 0.031;
let band = max(vd, abs(oq.x) - 0.165);
if (band < 0.0) {
// top-lit metal: bright upper face, bronze under-face, dark seam
let face = clamp(-(oq.y - oy - abs(oq.x) * 0.72) * 11.0 + 0.55, 0.0, 1.0);
var mc = mix(vec3f(0.34, 0.235, 0.095), vec3f(1.30, 1.06, 0.56), face);
mc *= 1.0 - 0.5 * smoothstep(0.004, 0.0, abs(band + 0.026)); // dark inner seam
mc += vec3f(1.3, 1.1, 0.65) * smoothstep(0.005, 0.0, abs(band + 0.006)) * 0.55;
pc = mix(pc, mc * (0.85 + hov * 0.45), smoothstep(0.004, -0.004, band));
}
}
// (orb removed — read as an artifact dot over the portal, Galen Jul 30)
pc += vec3f(1.0, 0.78, 0.42) * exp(-dot(oq, oq) * 44.0) * (0.22 + hov * 0.45);
}
// shadowed lip where the iris withdrew
pc *= smoothstep(0.0, 0.07, ird);
c = mix(c, pc, smoothstep(0.01, -0.01, dd));
} else {
// deep bronze: brushed rings, patina patches, one key light
let ang = atan2(dp.y, dp.x);
var bc = mix(vec3f(0.225, 0.140, 0.055), vec3f(0.085, 0.110, 0.095),
smoothstep(0.38, 0.78, fbm(dp * 6.0 + vec2f(3.0), 3)));
bc *= 0.72 + 0.48 * fbm(vec2f(ang * 6.0, rr * 22.0), 2); // brushed metal
bc *= 1.0 - 0.38 * smoothstep(0.32, 0.5, abs(fract(rr * 9.0) - 0.5)); // ring grooves
// engraved glyph ring
bc *= 1.0 - 0.30 * step(0.46, abs(fract(ang * 3.8197) - 0.5)) * step(abs(rr - 0.485), 0.030);
// eight spokes
bc *= 1.0 - 0.26 * smoothstep(0.035, 0.0, abs(fract(ang * 1.2732 + 0.5) - 0.5) * rr) * step(0.17, rr);
// key light upper-left, ambient falls off to near-black at the rim
bc *= 0.30 + 0.80 * clamp(0.55 - dp.x * 0.8 - dp.y * 0.8, 0.0, 1.2);
// center medallion: the vault star, waiting
if (rr < 0.135) {
bc = mix(vec3f(0.15, 0.092, 0.038), vec3f(0.26, 0.165, 0.07), clamp(1.0 - rr * 9.0, 0.0, 1.0));
bc += vec3f(0.85, 0.60, 0.28) * mod_tg_glyph(2, dp / 0.095) * 0.40;
bc *= 0.85 + 0.3 * clamp(-dp.y * 4.0, 0.0, 1.0);
}
// rim bevel light
bc += vec3f(1.0, 0.62, 0.3) * smoothstep(0.02, -0.03, abs(dd + 0.03)) * clamp(-dp.y - dp.x + 0.3, 0.0, 1.0) * 0.30;
c = mix(c, bc, smoothstep(0.01, -0.01, dd));
}
}
// carved arch ring around the door
c += vec3f(0.55, 0.4, 0.22) * smoothstep(0.025, 0.0, abs(length(dp) - (R + 0.05))) * 0.35;
// ── the four dials, low across the door ──
for (var k = 0; k < 4; k++) {
let dc = vec2f(-0.39 + f32(k) * 0.26, 0.60);
let q = p - dc;
let r = length(q);
if (r < 0.118) {
let mm = vec2f(uni(27), uni(28));
let mh = smoothstep(60.0, 22.0, length(mm - (dc * 0.5 + 0.5) * 512.0));
var kc: vec3f;
if (r > 0.082) {
// bezel: dark bronze, one worn top-arc highlight (zone read Jul 19)
kc = mod_tg_plate(q * 8.0 + vec2f(f32(k) * 5.0), 0.35);
let bang = atan2(q.y, q.x);
let topArc = exp(-pow((bang + 1.5708) * 1.1, 2.0));
kc += vec3f(0.55, 0.42, 0.20) * topArc * smoothstep(0.115, 0.095, r) * 0.7;
kc += vec3f(0.9, 0.68, 0.30) * smoothstep(0.009, 0.0, abs(r - 0.0855)) * (0.22 + topArc * 0.3);
kc *= 0.8 + 0.25 * fbm(q * 40.0 + vec2f(f32(k) * 9.0), 2);
kc *= 1.0 + mh * 0.4;
} else {
// the well: near-black, the glyph burning quiet inside
kc = vec3f(0.012, 0.011, 0.012) + vec3f(0.045, 0.032, 0.018) * clamp(1.0 - r * 10.0, 0.0, 1.0);
let gi = i32(uni(4 + k) + 0.5);
let gp = q / 0.055;
let g = mod_tg_glyph(gi, gp);
let gcol = mod_tg_gcol(gi);
kc = mix(kc, gcol * (1.05 + mh * 0.4), g * 0.95);
kc += gcol * exp(-r * r * 260.0) * (0.13 + mh * 0.12);
kc *= 0.55 + 0.45 * smoothstep(0.082, 0.055, r); // inner shadow at the lip
}
c = mix(c, kc, smoothstep(0.005, -0.005, r - 0.116));
}
}
// solved: gold seam light around everything
c += vec3f(1.2, 0.85, 0.35) * smoothstep(0.03, 0.0, abs(length(dp) - R * (1.0 - open * 0.94))) * open * (1.0 - open) * 2.2;
return c;
}
fn mod_tg_hall(p: vec2f, px: vec2f, t: f32) -> vec3f {
let lit = uni(31); // 0 dim → 1 solved blaze
let solved = uni(14);
// stone interior — the machine-wall again, and behind the organ a COLOSSAL
// flywheel entombed in the masonry, its teeth just breaking the surface
var c = mod_tg_ancient(p * 1.8 + vec2f(20.0), 13.0, vec2f(0.0, -0.9), 0.5 + lit * 0.5, 2) * (0.55 + 0.30 * lit);
{
let fq = (p - vec2f(0.0, 0.05)) / 1.15;
let fg = mod_tg_gearh(fq, 0.86, 18.0);
c = mix(c, vec3f(0.052, 0.045, 0.038) * (0.9 + lit * 0.8), fg * 0.45);
c += vec3f(0.50, 0.36, 0.14) * fg * (0.10 + lit * 0.22);
// its axle-boss behind the center pipe
let ax = length(p - vec2f(0.0, 0.05)) - 0.10;
c = mix(c, vec3f(0.070, 0.055, 0.038) * (1.0 + lit), smoothstep(0.015, -0.01, ax) * 0.8);
}
// back-wall arcade: shadowed arches between the pipes
for (var a = 0; a < 4; a++) {
let ax = -0.375 + f32(a) * 0.25;
let aq = p - vec2f(ax, 0.10);
var ad2 = 1.0;
if (aq.y > -0.22) { ad2 = max(abs(aq.x) - 0.072, aq.y - 0.40); }
else { ad2 = length(vec2f(aq.x, (aq.y + 0.22) * 1.1)) - 0.072; }
if (ad2 < 0.0) {
var av = vec3f(0.030, 0.028, 0.034) * (0.8 + 0.4 * fbm(aq * 9.0, 2));
av += vec3f(0.25, 0.16, 0.08) * exp(-aq.y * aq.y * 3.0) * lit * 0.4; // borrowed light
c = mix(c, av, smoothstep(0.008, -0.008, ad2) * 0.85);
}
// arch surround highlight
c += vec3f(0.30, 0.22, 0.13) * smoothstep(0.014, 0.0, abs(ad2 - 0.012)) * 0.35;
}
// vaulted ceiling ribs converging on the oculus
if (p.y < -0.45) {
let rib = abs(fract(atan2(p.x, -p.y - 0.30) * 2.546) - 0.5);
c *= 1.0 - 0.30 * smoothstep(0.10, 0.02, rib) * smoothstep(-0.45, -0.75, p.y);
c += vec3f(0.55, 0.42, 0.25) * smoothstep(0.05, 0.0, rib) * smoothstep(-0.5, -0.9, p.y) * (0.10 + lit * 0.35);
}
// oculus aperture ring
let oring = abs(length((p - vec2f(0.0, -0.98)) * vec2f(1.0, 2.2)) - 0.34) - 0.035;
c = mix(c, vec3f(0.14, 0.11, 0.06) * (0.8 + lit * 1.6), smoothstep(0.010, -0.010, oring));
// wall sconces between pipes: small ember lamps
for (var s = 0; s < 4; s++) {
let scx = -0.375 + f32(s) * 0.25;
let sq = p - vec2f(scx, -0.10);
let flick2 = 0.8 + 0.2 * sin(t * (6.0 + f32(s)) + f32(s) * 7.0);
let lamp = length(sq * vec2f(1.3, 1.0)) - 0.020;
if (lamp < 0.0) { c = vec3f(1.5, 0.85, 0.30) * flick2; }
c += vec3f(0.9, 0.50, 0.16) * exp(-dot(sq, sq) * 30.0) * 0.22 * flick2;
}
if (p.y > 0.62) {
c = mod_tg_rock(vec2f(p.x * 2.0, p.y * 6.0), vec3f(0.09, 0.082, 0.078)) * (1.1 - (p.y - 0.62) * 0.8);
// wet sheen strip where the pipes drip
c *= 1.0 + 0.25 * smoothstep(0.68, 0.63, p.y) * (0.5 + 0.5 * sin(p.x * 40.0 + t));
}
c *= 0.45 + 0.4 * exp(-dot(p, p) * 0.7);
// ── oculus shaft ──
let shaft = exp(-p.x * p.x * mix(14.0, 6.0, lit));
c += vec3f(0.9, 0.75, 0.5) * shaft * smoothstep(1.0, -1.0, p.y) * (0.09 + lit * 0.42);
let mcell = hash21(floor(vec2f(p.x * 160.0, p.y * 95.0 - t * 2.4)));
c += vec3f(1.1, 0.95, 0.7) * step(0.9945, mcell) * shaft * (0.05 + lit * 0.28);
// ── star-glyph mural, high center — ignites when the organ is solved ──
let mg = (p - vec2f(0.0, -0.72)) / 0.11;
if (abs(mg.x) < 1.4 && abs(mg.y) < 1.4) {
let g = mod_tg_glyph(2, mg);
let reveal = uni(29);
c = mix(c, mix(vec3f(0.10, 0.09, 0.10), mod_tg_gcol(2) * (1.1 + 1.3 * sin(t * 1.4) * 0.3), reveal), g * (0.35 + reveal * 0.65));
c += mod_tg_gcol(2) * exp(-dot(mg, mg) * 0.8) * reveal * 0.5;
}
// ── five organ pipes ──
for (var k = 0; k < 5; k++) {
let cx = -0.49 + f32(k) * 0.245;
let q = p - vec2f(cx, 0.14);
let bodyD = sdRoundedBox(q, vec2f(0.062, 0.46), 0.05);
if (bodyD < 0.012) {
// brass shell
var kc = mix(vec3f(0.34, 0.24, 0.11), vec3f(0.15, 0.19, 0.17), fbm(q * 12.0 + vec2f(f32(k) * 3.0), 2) * 0.7);
kc *= 0.75 + 0.5 * exp(-q.x * q.x * 160.0); // cylinder highlight
// glass gauge window
let gw = sdRoundedBox(q, vec2f(0.037, 0.40), 0.03);
if (gw < 0.0) {
let lvl = uni(9 + k); // 0..1 (level/4)
let fillTop = 0.40 - (lvl * 0.8 + 0.12) * 0.72; // liquid surface (y down)
var gc = vec3f(0.016, 0.022, 0.028);
if (q.y > fillTop) {
let glow = 0.55 + 0.45 * sin(q.y * 40.0 - t * 2.0);
gc = vec3f(0.05, 0.35, 0.33) * (0.8 + lit * 0.8);
gc += vec3f(0.10, 0.65, 0.58) * glow * 0.35;
let bub = hash21(floor(vec2f(q.x * 90.0, q.y * 60.0 - t * (1.0 + f32(k) * 0.2))));
gc += vec3f(0.3, 0.9, 0.8) * step(0.976, bub) * 0.5;
}
// liquid surface line
gc += vec3f(0.5, 1.1, 1.0) * exp(-abs(q.y - fillTop) * 140.0) * 0.8;
// tick marks — five stations
let tickY = fract((q.y + 0.40) / 0.144);
gc += vec3f(0.5, 0.45, 0.3) * smoothstep(0.06, 0.0, abs(tickY - 0.5)) * 0.14;
kc = mix(kc, gc, smoothstep(0.004, -0.004, gw));
}
// hover: the pipe knows the hand is near
let mm = vec2f(uni(27), uni(28));
let hd = abs(mm.x - (cx * 0.5 + 0.5) * 512.0);
kc += vec3f(0.9, 0.75, 0.4) * smoothstep(0.014, 0.0, abs(bodyD + 0.01)) * smoothstep(45.0, 12.0, hd) * 0.5;
c = mix(c, kc, smoothstep(0.006, -0.006, bodyD));
// floor reflection
let fy = 0.62 + (0.62 - p.y) * 0.85;
if (p.y > 0.62 && fy > -0.34 && fy < 0.60) {
let rq = vec2f(p.x - cx, fy - 0.14);
let rd = sdRoundedBox(rq, vec2f(0.062, 0.46), 0.05);
c = mix(c, vec3f(0.10, 0.16, 0.16) * (0.4 + lit * 0.5), smoothstep(0.01, -0.01, rd) * 0.30 * (1.0 - (p.y - 0.62) * 1.6));
}
}
}
// solved wash
c += vec3f(0.9, 0.8, 0.55) * solved * exp(-dot(p - vec2f(0.0, -0.4), p - vec2f(0.0, -0.4)) * 1.2) * 0.35;
return c;
}
fn mod_tg_lens(p: vec2f, px: vec2f, t: f32) -> vec3f {
// open night — the observatory crown, under the same physical sky as the shore
let lrd = normalize(vec3f(p.x, -p.y * 0.85 + 0.30, 1.45));
let lsd = normalize(vec3f(0.06, -0.30, 0.99)); // the sunk sun — deep night
let lmd = normalize(vec3f(-0.4, -0.5, 0.8)); // no moon before the finale
var c = mod_tgo_sky(lrd, lsd, lmd, t, uni(17));
// ── four constellations, each tagged with a glyph ──
// az from vertical: -0.90, -0.35, 0.25, 0.80 · glyphs 1,3,2,0 (answer: az 0.25 = glyph 2)
for (var k = 0; k < 4; k++) {
var az = -0.90; var gi = 1;
if (k == 1) { az = -0.35; gi = 3; } else if (k == 2) { az = 0.25; gi = 2; } else if (k == 3) { az = 0.80; gi = 0; }
let center = vec2f(sin(az), -cos(az)) * 0.72 + vec2f(0.0, 0.85);
// five stars per figure, deterministic offsets, joined by faint lines
var prev = vec2f(0.0);
for (var j = 0; j < 5; j++) {
let off = (hash22(vec2f(f32(k) * 7.0 + 2.0, f32(j) * 13.0 + 1.0)) - 0.5) * 0.30;
let spos = center + off;
let d = length(p - spos);
let tw = 0.7 + 0.3 * sin(t * 2.0 + f32(j * 3 + k) * 2.1);
c += vec3f(0.95, 1.0, 1.15) * exp(-d * d * 5200.0) * (1.6 + tw);
if (j > 0) {
let sd2 = sdSegment(p, prev, spos);
c += vec3f(0.35, 0.45, 0.6) * smoothstep(0.004, 0.0, sd2) * 0.30;
}
prev = spos;
}
// the tag-glyph beneath the figure
let gp = (p - (center + vec2f(0.0, 0.20))) / 0.045;
if (abs(gp.x) < 1.4 && abs(gp.y) < 1.4) {
c += mod_tg_gcol(gi) * mod_tg_glyph(gi, gp) * (0.30 + uni(29) * select(0.0, 0.55, gi == 2));
}
}
// ── the observatory crown: parapet arc, flanking pylons ──
// curved parapet — a dark arc the platform stands behind
let par = p.y - (0.64 + 0.06 * p.x * p.x);
if (par > 0.0) {
var pc2 = vec3f(0.028, 0.026, 0.034) * (0.8 + 0.4 * fbm(p * 7.0 + vec2f(40.0), 2));
// crenel notches along the rim
pc2 *= 1.0 - 0.30 * step(abs(fract(p.x * 6.0) - 0.5), 0.10) * smoothstep(0.05, 0.0, par);
// rim catch-light from the beam
pc2 += vec3f(0.45, 0.40, 0.55) * smoothstep(0.020, 0.0, par) * (0.25 + uni(16) * 0.5);
// engraved star-chart arcs + a rivet course along the parapet face
let arc = abs(length(vec2f(p.x, (p.y - 1.7) * 1.6)) - 1.30);
pc2 *= 1.0 - 0.25 * smoothstep(0.012, 0.004, min(arc, abs(arc - 0.09)));
let riv = length(vec2f(fract(p.x * 9.0) - 0.5, fract((par + 0.05) * 11.0) - 0.5));
pc2 += vec3f(0.40, 0.34, 0.28) * smoothstep(0.10, 0.04, riv) * smoothstep(0.10, 0.03, par) * 0.5;
c = mix(c, pc2, smoothstep(0.0, 0.015, par));
}
// two glyph-lamp pylons at the platform's edge
for (var s = 0; s < 2; s++) {
let sx = select(-0.80, 0.80, s == 1);
let pq = p - vec2f(sx, 0.72);
let pyl = sdBox(pq, vec2f(0.045, 0.26));
if (pyl < 0.0) {
var yc = vec3f(0.055, 0.048, 0.055) * (0.8 + 0.4 * fbm(pq * 11.0, 2));
yc += vec3f(0.5, 0.42, 0.28) * smoothstep(0.012, 0.0, abs(pq.x + 0.035)) * 0.4;
c = mix(c, yc, smoothstep(0.006, -0.006, pyl));
}
// lamp head: the star-glyph burns atop each pylon
let lg = (pq - vec2f(0.0, -0.315)) / 0.042;
if (abs(lg.x) < 1.3 && abs(lg.y) < 1.3) {
c += mod_tg_gcol(2) * mod_tg_glyph(2, lg) * (0.7 + 0.3 * sin(t * 1.3 + f32(s) * 3.0));
}
c += mod_tg_gcol(2) * exp(-dot(pq - vec2f(0.0, -0.315), pq - vec2f(0.0, -0.315)) * 160.0) * 0.35;
}
// ── the great lens, bottom center ──
let lc = vec2f(0.0, 0.84);
let lq = p - lc;
let ang = uni(15);
let dirv = vec2f(sin(ang), -cos(ang));
// beam — a cone from the lens along dir
let along = dot(lq, dirv);
let across = abs(dot(lq, vec2f(-dirv.y, dirv.x)));
if (along > 0.0) {
let width = 0.012 + along * 0.055;
let beam = exp(-pow(across / width, 2.0)) * exp(-along * 0.55);
let hold = uni(16);
c += mix(vec3f(1.0, 0.82, 0.45), vec3f(0.8, 0.62, 1.3), hold) * beam * (0.5 + hold * 1.5);
}
// mount pillar (behind the glass)
let mp = sdBox(p - vec2f(0.0, 1.06), vec2f(0.045, 0.20));
c = mix(c, vec3f(0.045, 0.040, 0.045), smoothstep(0.01, -0.01, mp));
c += vec3f(0.5, 0.35, 0.15) * smoothstep(0.012, 0.0, abs(mp)) * 0.4;
// lens body
let ld = length(lq) - 0.17;
if (ld < 0.06) {
var kc = mix(vec3f(0.30, 0.22, 0.11), vec3f(0.14, 0.18, 0.16), fbm(lq * 20.0, 2) * 0.6);
if (ld < -0.035) {
// the glass: sky refracted + inner glow
kc = mod_tg_sky(vec2f(p.x * 1.4, p.y * 1.4 - 0.9), t, 1.0) * 0.7 + vec3f(0.10, 0.14, 0.20);
kc += vec3f(0.7, 0.65, 1.0) * exp(-dot(lq, lq) * 60.0) * (0.4 + uni(16) * 1.2);
}
kc += vec3f(1.0, 0.9, 0.6) * smoothstep(0.012, 0.0, abs(ld + 0.035)) * 0.5;
c = mix(c, kc, smoothstep(0.008, -0.008, ld - 0.055));
}
// brass yoke: two arms cradle the glass, turning with the aim
for (var ya = 0; ya < 2; ya++) {
let side = select(-1.0, 1.0, ya == 1);
let yang = ang + side * 1.35;
let ydir = vec2f(sin(yang), -cos(yang));
let yd = sdSegment(lq, ydir * 0.16, ydir * 0.26) - 0.020;
if (yd < 0.0) {
var yc2 = vec3f(0.24, 0.165, 0.075) * (0.8 + 0.5 * clamp(-lq.y * 3.0, 0.0, 1.0));
yc2 += vec3f(0.9, 0.7, 0.35) * smoothstep(0.008, 0.0, abs(yd + 0.010)) * 0.4;
c = mix(c, yc2, smoothstep(0.005, -0.005, yd));
}
}
// hold-progress ring
let hold = uni(16);
if (hold > 0.001) {
let pr = polar(lq / 0.22);
let sweep = step(pr.y / 6.28318 + 0.5, hold);
c += vec3f(0.8, 0.7, 1.3) * smoothstep(0.05, 0.0, abs(pr.x - 1.0)) * sweep * 1.4;
}
return c;
}
// ── tg_views ends here ──
module · tg_views2
fn mod_tg_lattice(p: vec2f, px: vec2f, t: f32) -> vec3f {
// the observatory interior at night — weave the constellation the island keeps.
// whiteboard: uni(32)=bloom, uni(33..37)=the five anchor charges, uni(27),uni(28)=cursor px.
let bloom = uni(32);
let gold = uni(38); // locked in → the constellation turns gold
let cLine = mix(vec3f(0.30, 0.75, 0.85), vec3f(1.05, 0.72, 0.22), gold);
let cGlow = mix(vec3f(0.30, 0.70, 0.80), vec3f(1.05, 0.74, 0.26), gold);
let cCore = mix(vec3f(0.85, 0.95, 0.90), vec3f(1.0, 0.93, 0.62), gold);
let neb = fbm4(p * 1.5 + vec2f(t * 0.05, -t * 0.04));
var col = vec3f(0.015, 0.028, 0.05);
col = col + vec3f(0.04, 0.10, 0.16) * pow(neb, 2.0);
let s = 0.6;
var A = array<vec2f, 5>(vec2f(0.0, 0.0), vec2f(-s, 0.0), vec2f(s, 0.0), vec2f(0.0, -s), vec2f(0.0, s));
for (var i = 1; i < 5; i = i + 1) {
let pa = p - A[0]; let ba = A[i] - A[0];
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
let d = length(pa - ba * h);
col = col + vec3f(0.06, 0.10, 0.15) * exp(-d * d * 70.0) * (0.6 + 0.4 * sin(t * 3.0 + f32(i)));
}
for (var i = 0; i < 5; i = i + 1) {
let ci = uni(33 + i);
for (var j = i + 1; j < 5; j = j + 1) {
let cj = uni(33 + j);
let sm = min(ci, cj);
if (sm > 0.55) {
let pa = p - A[i]; let ba = A[j] - A[i];
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
let d = length(pa - ba * h);
col = col + cLine * exp(-d * d * 90.0) * (sm - 0.5) * 2.0;
}
}
}
for (var i = 0; i < 5; i = i + 1) {
let c = uni(33 + i);
let d = p - A[i]; let r = dot(d, d);
col = col + cGlow * exp(-r * 40.0) * (0.10 + c * 1.4);
col = col + cCore * exp(-r * 300.0) * (0.25 + c * 1.1);
}
let cur = (vec2f(uni(27), uni(28)) - 256.0) / 256.0;
let dc = p - cur;
col = col + vec3f(0.80, 0.95, 1.0) * exp(-dot(dc, dc) * 420.0) * 0.9;
let bloomCol = mix(vec3f(0.5, 0.85, 0.9), vec3f(1.0, 0.78, 0.34), gold);
col = mix(col, col + bloomCol * 0.7 + vec3f(0.12, 0.13, 0.16) + vec3f(0.08, 0.05, 0.0) * gold, bloom * 0.7);
return col;
}
fn mod_tg_core(p: vec2f, px: vec2f, t: f32) -> vec3f {
// INSIDE THE ORB — a deep well with three rings of light. Each ring carries a
// notch (uni(40..42), radians, 0 = up); the player turns them to the top gate.
// uni(39)=solved, uni(43)=ignition glow, uni(27),uni(28)=cursor px.
let solved = uni(39);
let glow = uni(43);
let rr = length(p);
var col = vec3f(0.015, 0.02, 0.035);
let neb = fbm4(p * 2.0 + vec2f(t * 0.03, t * 0.02));
col = col + vec3f(0.02, 0.05, 0.09) * pow(neb, 2.0) * (1.0 - smoothstep(0.4, 1.15, rr));
let warm = max(solved, glow);
let ringCol = mix(vec3f(0.30, 0.72, 0.85), vec3f(1.05, 0.72, 0.22), warm);
let notchCol = mix(vec3f(0.75, 0.95, 1.0), vec3f(1.0, 0.90, 0.55), warm);
// the gate at 12 o'clock — where notches must land
let ang = atan2(p.x, -p.y);
let gate = exp(-ang * ang * 24.0) * smoothstep(0.34, 0.9, rr) * smoothstep(1.05, 0.86, rr);
col = col + vec3f(0.95, 0.88, 0.52) * gate * 0.4;
var radii = array<f32, 3>(0.30, 0.55, 0.80);
for (var i = 0; i < 3; i = i + 1) {
let R = radii[i];
let band = exp(-(rr - R) * (rr - R) * 320.0);
col = col + ringCol * band * 0.16;
let a = uni(40 + i);
let nd = vec2f(sin(a), -cos(a));
let np = nd * R;
let dn = length(p - np);
let al = exp(-a * a * 20.0); // 1 when this notch is at the top
let bright = 0.8 + al * 1.0;
col = col + notchCol * exp(-dn * dn * 1500.0) * bright;
col = col + notchCol * exp(-dn * dn * 240.0) * 0.22 * bright;
}
// the heart of the orb — dim until woken, then a small sun
let heart = exp(-rr * rr * 24.0);
col = col + mix(vec3f(0.09, 0.15, 0.21), vec3f(2.3, 1.75, 0.95), glow) * heart * (0.22 + glow * 1.7);
col = col + vec3f(1.7, 1.25, 0.65) * exp(-rr * rr * 230.0) * glow;
if (glow > 0.001 && glow < 0.999) { // ignition ring blooming outward
let fr = glow * 1.15;
col = col + vec3f(1.0, 0.8, 0.4) * exp(-(rr - fr) * (rr - fr) * 110.0) * (1.0 - glow) * 0.85;
}
let cur = (vec2f(uni(27), uni(28)) - 256.0) / 256.0;
let dc = p - cur;
col = col + vec3f(0.8, 0.95, 1.0) * exp(-dot(dc, dc) * 520.0) * 0.6;
col = col * smoothstep(1.3, 0.5, rr);
return col;
}
// ═══ THE TIDE-ORRERY — the island's keystone, reached through the woken core ═══
// A brass armillary in the deep sanctum: a sun-bead on the outer ring, a moon-bead
// on the inner, the island fixed at the hub. An engraved horizon cuts the sphere —
// above is risen sky, below is the drowned. Set the sun BELOW and the moon ABOVE
// (the finale sky) and the tide-gauge surges to full: the spring tide the island
// remembers. Holding that alignment wakes the dawn.
// uni(45) sunA uni(46) moonA uni(47) tide uni(48) solved uni(49) glow uni(50) hold
fn mod_tg_orrery(p: vec2f, px: vec2f, t: f32) -> vec3f {
let sunA = uni(45);
let moonA = uni(46);
let tide = uni(47);
let solved = uni(48);
let glow = uni(49);
let hold = uni(50);
let warm = max(solved, glow);
let rr = length(p);
let mm = vec2f(uni(27), uni(28));
// ── the sanctum void: nebula + far stars, like the core it opens from ──
var c = vec3f(0.010, 0.016, 0.030);
let neb = fbm4(p * 1.7 + vec2f(t * 0.02, -t * 0.015));
c += vec3f(0.03, 0.06, 0.12) * pow(neb, 2.2) * (1.0 - smoothstep(0.45, 1.30, rr));
let sc = floor((p * 0.5 + 0.5) * 250.0);
let sh = hash21(sc);
c += vec3f(0.72, 0.80, 1.0) * step(0.9955, sh) * (0.35 + 0.65 * sin(t * 2.0 + sh * 40.0)) * 0.6 * smoothstep(1.25, 0.4, rr);
// ── the engraved horizon sea-line: sky above, drowned below ──
c += mix(vec3f(0.10, 0.16, 0.22), vec3f(0.55, 0.44, 0.22), warm) * smoothstep(0.006, 0.0, abs(p.y)) * (0.35 + 0.25 * sin(p.x * 22.0));
// a faint drowned wash below the line
c += vec3f(0.010, 0.030, 0.045) * smoothstep(0.0, 0.9, p.y) * (1.0 - warm * 0.5);
// BONES (Galen Jul 30): compass tide-glyphs DELETED — symbols with no puzzle meaning.
// ── the two armillary rings: engraved brass circles with graticule ticks ──
let sunR = 0.78;
let moonR = 0.52;
let ang = atan2(p.x, -p.y); // 0 at top, +cw
// outer sun ring
let dS = abs(rr - sunR);
var ringS = mix(vec3f(0.30, 0.22, 0.10), vec3f(0.95, 0.72, 0.34), warm);
let tickS = smoothstep(0.35, 0.5, abs(fract(ang * 3.8197) - 0.5)); // 24 ticks
c += ringS * smoothstep(0.016, 0.0, dS) * (0.45 + 0.35 * tickS + warm * 0.4);
c += ringS * smoothstep(0.006, 0.0, dS) * 0.5; // bright rail
// inner moon ring
let dM = abs(rr - moonR);
var ringM = mix(vec3f(0.24, 0.28, 0.36), vec3f(0.72, 0.82, 0.96), warm);
let tickM = smoothstep(0.35, 0.5, abs(fract(ang * 2.8648) - 0.5)); // 18 ticks
c += ringM * smoothstep(0.014, 0.0, dM) * (0.45 + 0.35 * tickM + warm * 0.3);
c += ringM * smoothstep(0.005, 0.0, dM) * 0.5;
// ── the island at the hub: a small dark rock with its beacon, the still center ──
{
let iq = p / 0.14;
let rock = length(iq * vec2f(1.0, 1.35)) - 0.7 - (vnoise(iq * 3.0) - 0.5) * 0.25;
if (rock < 0.0) {
var icol = vec3f(0.050, 0.046, 0.050) * (0.7 + 0.5 * fbm(iq * 3.0, 2));
icol += vec3f(0.30, 0.90, 0.95) * exp(-dot(iq - vec2f(0.0, -0.45), iq - vec2f(0.0, -0.45)) * 30.0) * (0.4 + warm); // beacon
c = mix(c, icol, smoothstep(0.05, -0.05, rock));
}
c += vec3f(0.20, 0.55, 0.60) * exp(-dot(p, p) * 26.0) * (0.10 + warm * 0.35); // hub halo
}
// ── the sun-bead on the outer ring ──
let sunPos = vec2f(sin(sunA), -cos(sunA)) * sunR;
let ds = length(p - sunPos);
// a hot disc with a shaded south face, a corona, and eight rays
let sunDisc = ds - 0.058;
if (sunDisc < 0.02) {
let sph = clamp(-sunDisc * 24.0, 0.0, 1.0);
var scol = mix(vec3f(0.55, 0.22, 0.06), vec3f(1.7, 1.05, 0.45), sph);
scol *= 0.7 + 0.3 * clamp(-(p.y - sunPos.y) * 10.0 + 0.5, 0.0, 1.0);
c = mix(c, scol, smoothstep(0.006, -0.006, sunDisc));
}
c += vec3f(1.5, 0.85, 0.35) * exp(-ds * ds * 130.0); // corona
c += vec3f(1.1, 0.55, 0.20) * exp(-ds * ds * 16.0) * 0.35;
let sray = abs(fract(atan2(p.y - sunPos.y, p.x - sunPos.x) * 1.2732) - 0.5);
c += vec3f(1.3, 0.75, 0.28) * smoothstep(0.42, 0.5, sray) * exp(-ds * 7.0) * smoothstep(0.05, 0.12, ds) * 0.5;
// ── the moon-bead on the inner ring, with a crescent terminator ──
let moonPos = vec2f(sin(moonA), -cos(moonA)) * moonR;
let dm = length(p - moonPos);
let moonDisc = dm - 0.046;
if (moonDisc < 0.014) {
let sph = clamp(-moonDisc * 30.0, 0.0, 1.0);
var mcol = mix(vec3f(0.12, 0.16, 0.24), vec3f(0.78, 0.86, 1.02), sph);
let term = smoothstep(-0.01, 0.02, (p.x - moonPos.x) + (p.y - moonPos.y) * 0.4); // lit limb
mcol *= 0.35 + 0.65 * term;
mcol += vec3f(0.05, 0.07, 0.10) * (1.0 - term);
c = mix(c, mcol, smoothstep(0.005, -0.005, moonDisc));
}
c += vec3f(0.55, 0.68, 0.95) * exp(-dm * dm * 220.0) * 0.8;
// ── THE TIDE ITSELF: a luminous sea flooding the drowned lower half, rising to
// the horizon as the alignment nears. The gauge is not a dial but the water —
// the sun sets INTO it, the moon lifts ABOVE it, and at the true syzygy the
// whole sea brims gold: the spring tide the island remembers. ──
{
let surf = 0.86 - tide * 0.86 + sin(p.x * 7.0 + t * 0.7) * 0.010 + sin(p.x * 17.0 - t * 1.1) * 0.005; // a gently waving waterline
if (p.y > surf) {
let depth = p.y - surf;
let caust = 0.5 + 0.5 * sin(p.x * 24.0 + t * 1.6 + sin(p.y * 9.0) * 1.5) * sin(p.y * 20.0 - t * 1.1);
var wc = mix(vec3f(0.04, 0.20, 0.24), vec3f(0.85, 0.68, 0.30), warm); // teal by night, gold once woken
wc *= 0.55 + 0.45 * caust;
wc += mix(vec3f(0.10, 0.55, 0.55), vec3f(1.0, 0.80, 0.40), tide) * exp(-depth * 3.0) * 0.6; // lit surface band
wc += vec3f(1.0, 0.85, 0.45) * smoothstep(0.78, 1.0, tide) * 0.5; // the spring-tide surge
// soft round glints drifting on the sea (not blocky — a jittered point per cell)
let cell = vec2f(p.x * 52.0, p.y * 52.0 - t * 1.1);
let ci = floor(cell);
let jit = (hash22(ci) - 0.5) * 0.6;
let glint = step(0.975, hash21(ci)) * smoothstep(0.30, 0.0, length(fract(cell) - 0.5 - jit));
wc += mix(vec3f(0.45, 0.95, 0.9), vec3f(1.0, 0.92, 0.55), tide) * glint * (0.5 + 0.6 * tide);
let a = smoothstep(0.0, 0.05, depth) * (0.32 + 0.55 * tide);
c = mix(c, wc, a);
}
// the meniscus — a bright waterline that warms from sea-glass to gold as it fills
c += mix(vec3f(0.4, 1.0, 1.0), vec3f(1.0, 0.85, 0.42), tide) * exp(-abs(p.y - surf) * 55.0) * (0.22 + 0.6 * tide);
}
// ── hold-progress arc sweeping the outer ring ──
if (hold > 0.001) {
let sweep = step(fract(ang * 0.15915 + 0.5), hold);
c += mix(vec3f(0.5, 0.85, 0.9), vec3f(1.0, 0.85, 0.4), hold) * smoothstep(0.03, 0.0, abs(rr - (sunR + 0.10))) * sweep * (0.7 + hold);
}
// ── ignition + solved bloom ──
if (glow > 0.001 && glow < 0.999) {
let fr = glow * 1.2;
c += vec3f(1.0, 0.82, 0.42) * exp(-(rr - fr) * (rr - fr) * 90.0) * (1.0 - glow) * 0.9;
}
c += vec3f(1.15, 0.92, 0.55) * solved * exp(-rr * rr * 1.3) * 0.55;
// cursor firefly
let cur = (mm - 256.0) / 256.0;
c += vec3f(0.8, 0.95, 1.0) * exp(-dot(p - cur, p - cur) * 460.0) * 0.6;
c *= smoothstep(1.45, 0.5, rr);
return c;
}
// ═══ THE SEA CAVE — a bioluminescent grotto in the rock, LEFT off the shore. Wet
// stone, a bright mouth open to the sea, a tide-pool that glows teal, glinting
// motes. Cool counterpoint to the warm puzzle rooms. Reuses rock/sky/sea/glyphs.
// ══ THE SEA-CAVE, raymarched (view 8). A wet grotto open to the ocean through a
// mouth in the back wall — the sky and the ruling moon show through it. At its
// heart a carved plinth with a tideglass SOCKET; seat the pearl and it blazes,
// firing a beam out the mouth to the crystal under the airship. Author: Fable 5.
const TGS_SOCK = vec3f(0.0, 0.96, 0.85); // socket bowl centre → projects ≈ px(256,300)
const TGS_MOUTH = vec3f(0.0, 1.85, -3.6); // the sea-mouth's heart (the one light) → px(256,224)
const TGS_STAL = array<vec3f, 5>( // (x, z, hang) — precomputed, map() is hot
vec3f(-1.800, -1.000, 0.500), vec3f(-0.748, -0.067, 1.048), vec3f(0.198, 1.036, 1.046),
vec3f(1.288, -0.495, 0.933), vec3f(2.256, -0.736, 0.516));
fn tgs_h1(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgs_hs(x: f32) -> f32 { return fract(sin(x * 91.3) * 43758.5453); }
fn tgs_n2(p: vec2f) -> f32 {
let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
return mix(mix(tgs_h1(i), tgs_h1(i + vec2f(1.0, 0.0)), u.x),
mix(tgs_h1(i + vec2f(0.0, 1.0)), tgs_h1(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgs_fbm(p: vec2f, oct: i32) -> f32 {
var v = 0.0; var a = 0.5; var q = p;
for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgs_n2(q); q = q * 2.03 + vec2f(5.0, 9.0); a *= 0.5; }
return v;
}
fn tgs_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 tgs_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 tgs_smin(a: f32, b: f32, k: f32) -> f32 { let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); }
// a warm→cool hue wheel for the ruling-moon tint (hue in ~[0,1))
fn tgs_hue(x: f32) -> vec3f {
let h = fract(x) * 6.28318;
return vec3f(0.6 + 0.4 * cos(h), 0.6 + 0.4 * cos(h - 2.094), 0.6 + 0.4 * cos(h - 4.188));
}
// SDF → (dist, matId). 1 rock · 2 wet floor · 3 plinth bronze · 4 pearl · 5 socket glass
fn tgs_map(p: vec3f, t: f32) -> vec2f {
var d = 1e5; var m = 1.0;
// ── the grotto, built from explicit walls so the back is genuinely OPEN to
// the sea (an inside-out box makes a closed shaft — this doesn't). ──
let ceil = (3.6 - p.y) + 0.12 * tgs_fbm(p.xz * 1.5, 2); // ceiling
d = ceil; m = 1.0;
let lw = (p.x + 2.9) + 0.14 * tgs_fbm(p.yz * 1.4, 2); // left wall
if (lw < d) { d = lw; m = 1.0; }
let rw = (2.9 - p.x) + 0.14 * tgs_fbm(p.yz * 1.4 + vec2f(4.0), 2); // right wall
if (rw < d) { d = rw; m = 1.0; }
// the back wall — a finite slab with the MOUTH punched out; rays through the
// hole pass the thin slab and escape to the open ocean beyond.
let back = tgs_box(p - vec3f(0.0, 1.5, -3.5), vec3f(3.2, 2.4, 0.3)) + 0.08 * tgs_fbm(p.xy * 1.4, 2);
let hole = tgs_box(p - vec3f(0.0, 1.85, -3.5), vec3f(1.2, 1.05, 1.2));
let wallH = max(back, -hole);
if (wallH < d) { d = wallH; m = 1.0; }
// ── the wet floor, silt-mounded — STOPS before the mouth so it never blocks
// the sea view (no infinite plane across the opening) ──
if (p.z > -2.2) {
let floorY = p.y + 0.28 + 0.06 * tgs_fbm(p.xz * 2.4, 2) - 0.06 * smoothstep(2.0, 0.0, length(p.xz - vec2f(0.4, 1.6)));
if (floorY < d) { d = floorY; m = 2.0; }
}
// ── stalactites down from the ceiling (positions precomputed — perf) ──
for (var k = 0; k < 5; k++) {
let st = TGS_STAL[k];
let sx = st.x; let sz = st.y; let hh = st.z;
let tq = p - vec3f(sx, 3.5, sz);
let taper = clamp(-tq.y / hh, 0.0, 1.0);
let stal = length(tq.xz) - mix(0.12, 0.01, taper) + max(0.0, tq.y) + max(0.0, -tq.y - hh);
if (stal < d) { d = stal; m = 1.0; }
}
// ── THE PLINTH: a carved bronze pedestal rising from the floor to the socket ──
let plc = TGS_SOCK - vec3f(0.0, 0.62, 0.0);
var plinth = tgs_cyl(p - plc, 0.60, 0.30);
plinth = tgs_smin(plinth, tgs_cyl(p - (TGS_SOCK - vec3f(0.0, 0.12, 0.0)), 0.05, 0.34), 0.06); // rim under the bowl
plinth = min(plinth, tgs_box(p - vec3f(0.0, -0.16, 0.85), vec3f(0.40, 0.12, 0.40))); // base on the floor
if (plinth < d) { d = plinth; m = 3.0; }
// the SOCKET bowl on top: a hemispherical hollow (glass-lined)
let bowl = max(length(p - (TGS_SOCK + vec3f(0.0, 0.10, 0.0))) - 0.20, -(p.y - (TGS_SOCK.y + 0.10)));
if (bowl < d) { d = bowl; m = 5.0; }
// ── the PEARL seated in the bowl (present once the socket is powered) ──
if (uni(109) > 0.5) {
let pd = length(p - (TGS_SOCK + vec3f(0.0, 0.10, 0.0))) - 0.13;
if (pd < d) { d = pd; m = 4.0; }
}
return vec2f(d, m);
}
fn tgs_nrm(p: vec3f, t: f32) -> vec3f {
let e = vec2f(0.006, 0.0);
return normalize(vec3f(
tgs_map(p + e.xyy, t).x - tgs_map(p - e.xyy, t).x,
tgs_map(p + e.yxy, t).x - tgs_map(p - e.yxy, t).x,
tgs_map(p + e.yyx, t).x - tgs_map(p - e.yyx, t).x));
}
fn tgs_march(ro: vec3f, rd: vec3f, t: f32) -> vec2f {
var tt = 0.02;
for (var i = 0; i < 64; i++) {
let h = tgs_map(ro + rd * tt, t);
if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
tt += h.x * 0.8; if (tt > 24.0) { break; }
}
return vec2f(-1.0, 0.0);
}
fn tgs_shadow(ro: vec3f, rd: vec3f, t: f32) -> f32 {
var res = 1.0; var tt = 0.04;
for (var i = 0; i < 12; i++) {
let h = tgs_map(ro + rd * tt, t).x;
if (h < 0.002) { return 0.15; }
res = min(res, 12.0 * h / tt); tt += clamp(h, 0.06, 0.5); if (tt > 6.0) { break; }
}
return clamp(res, 0.15, 1.0);
}
fn tgs_ao(p: vec3f, n: vec3f, t: f32) -> f32 {
var occ = 0.0; var w = 1.0;
for (var i = 1; i <= 3; i++) { let hh = 0.07 * f32(i); occ += w * (hh - tgs_map(p + n * hh, t).x); w *= 0.7; }
return clamp(1.0 - 1.9 * occ, 0.0, 1.0);
}
fn tgs_alb(m: f32, p: vec3f) -> vec3f {
if (m < 1.5) { // wet rock — cool, mineral-veined
var a = vec3f(0.055, 0.072, 0.082) * (0.7 + 0.6 * tgs_fbm(p.xy * 3.0 + p.zz, 3));
a = mix(a, vec3f(0.04, 0.09, 0.10), smoothstep(0.4, 0.8, tgs_fbm(p.xz * 5.0, 2)) * 0.5);
return a;
} else if (m < 2.5) { // wet silt floor
return vec3f(0.045, 0.06, 0.065) * (0.7 + 0.7 * tgs_fbm(p.xz * 4.0, 3));
} else if (m < 3.5) { // plinth bronze, sea-worn
var a = vec3f(0.16, 0.12, 0.06);
a = mix(a, vec3f(0.06, 0.13, 0.11), smoothstep(0.4, 0.75, tgs_fbm(p.xy * 6.0 + p.zz * 3.0, 3)));
return a;
} else if (m < 4.5) { // pearl (lit specially)
return vec3f(0.75, 0.82, 0.85);
}
return vec3f(0.10, 0.14, 0.16); // socket glass rim
}
// the view through the mouth: sky graded to sea, sun/moon by world state
fn tgs_ocean(rd: vec3f, t: f32, night: f32, dawn: f32, moonHue: f32, spill: vec3f) -> vec3f {
let up = clamp(rd.y * 0.5 + 0.5, 0.0, 1.0);
// sky: night indigo → day/dawn
let daySky = mix(vec3f(0.45, 0.62, 0.85), vec3f(1.0, 0.85, 0.6), dawn);
let nightSky = mix(vec3f(0.04, 0.07, 0.14), vec3f(0.10, 0.14, 0.26), up);
var col = mix(daySky, nightSky, night * (1.0 - dawn));
// horizon band
col = mix(mix(vec3f(0.10, 0.20, 0.30), vec3f(0.9, 0.7, 0.5), dawn), col, smoothstep(-0.02, 0.25, rd.y));
// the RULING MOON — a disc in the mouth's sky, tinted to its element
let mdir = normalize(vec3f(-0.55, 0.55, -1.0));
let md = distance(normalize(rd), mdir);
let moonDisc = smoothstep(0.11, 0.09, md);
let moonHalo = exp(-md * md * 20.0);
let moonTint = mix(vec3f(0.9, 0.93, 1.0), tgs_hue(moonHue), 0.7);
col = mix(col, moonTint, moonDisc * (0.5 + 0.5 * night));
col += moonTint * moonHalo * (0.25 + 0.35 * night);
// the sea below the horizon, catching the moon
if (rd.y < -0.01) {
let w = tgs_fbm(vec2f(rd.x * 6.0, rd.z * 6.0 - t * 0.3), 3);
var sea = mix(vec3f(0.03, 0.10, 0.15), vec3f(0.08, 0.20, 0.26), w);
sea += moonTint * smoothstep(0.15, 0.0, abs(rd.x + 0.4)) * (0.2 + 0.3 * night) * (0.5 + 0.5 * w); // moonpath
col = mix(col, sea, smoothstep(-0.01, -0.06, rd.y));
}
col += spill * 0.12; // the daylight that fills the cave leaks from here
return col;
}
// nearest distance from a ray to a segment [a,b] (for the volumetric beam)
fn tgs_segDist(ro: vec3f, rd: vec3f, a: vec3f, b: vec3f) -> f32 {
let ab = b - a; let ao = ro - a;
let rdd = dot(rd, rd); let abd = dot(ab, ab); let abrd = dot(ab, rd);
let aord = dot(ao, rd); let aoab = dot(ao, ab);
let den = rdd * abd - abrd * abrd;
var s = 0.0; var u = 0.0;
if (abs(den) > 1e-5) { s = (abrd * aoab - abd * aord) / den; }
u = (abrd * s + aoab) / abd;
s = max(s, 0.0); u = clamp(u, 0.0, 1.0);
let pr = ro + rd * s; let pl = a + ab * u;
return length(pr - pl);
}
fn mod_tg_seacave(p: vec2f, px: vec2f, t: f32) -> vec3f {
let mm = vec2f(uni(27), uni(28));
let pearlSet = uni(109); // the pearl is seated → socket glows, beam fires
let night = clamp(uni(23), 0.0, 1.0);
let dawn = clamp(uni(44), 0.0, 1.0);
let moonHue = uni(111); // ruling moon → the ocean view matches the sky
// ── camera: standing on the wet floor, the sea-mouth ahead-left ──
let uvx = px.x / 256.0 - 1.0;
let uvy = px.y / 256.0 - 1.0;
let ro = vec3f(0.0, 1.5, 4.0);
let rd = normalize(vec3f(uvx, -uvy - 0.06, -1.35));
// the daylight/moonlight spilling from the mouth, tinted by the ruling moon
let dayCol = mix(vec3f(0.55, 0.78, 0.92), vec3f(1.05, 0.95, 0.82), dawn);
let moonCol = mix(vec3f(0.40, 0.62, 0.85), tgs_hue(moonHue), 0.6);
let spill = mix(moonCol, dayCol, 1.0 - night * (1.0 - dawn));
let hit = tgs_march(ro, rd, t);
var c: vec3f;
if (hit.x > 0.0) {
let pw = ro + rd * hit.x;
let n = tgs_nrm(pw, t);
let lmouth = normalize(TGS_MOUTH - pw); // the mouth is the cave's one light
let dif = max(dot(n, lmouth), 0.0);
let sh = tgs_shadow(pw + n * 0.02, lmouth, t);
let ao = tgs_ao(pw, n, t);
var col = tgs_alb(hit.y, pw) * (0.16 + 1.05 * dif * sh) * ao;
col += spill * ao * (0.22 + 0.35 * dif) * 0.7; // cool ambient bounce from the mouth
// the tide-pool glow lifts everything near the floor
col += vec3f(0.06, 0.34, 0.40) * exp(-abs(pw.y + 0.18) * 2.2) * 0.5;
// ── THE SOCKET: a carved bowl; empty it's dark, seated it blazes ──
if (hit.y > 4.5 && hit.y < 5.5) {
col += tgs_hue(moonHue) * 0.10; // faint tideglass in the stone
if (pearlSet > 0.5) {
let fres = pow(1.0 - max(dot(-rd, n), 0.0), 2.0);
col = mix(vec3f(0.6, 0.75, 0.85), vec3f(1.2, 1.15, 1.0), fres);
col += vec3f(0.5, 0.8, 0.95) * (0.6 + 0.4 * sin(t * 3.0));
}
}
// pearl resonance on the plinth bronze after seating
if (pearlSet > 0.5 && hit.y > 3.5 && hit.y < 4.5) {
col += vec3f(0.3, 0.45, 0.55) * (0.4 + 0.4 * sin(t * 6.0 + pw.y * 12.0)) * 0.3;
}
c = col;
c = mix(c, vec3f(0.02, 0.045, 0.06), smoothstep(6.0, 14.0, hit.x) * 0.5); // depth haze
} else {
// ── the ray escaped through the MOUTH: the sea, the sky, the ruling moon ──
c = tgs_ocean(rd, t, night, dawn, moonHue, spill);
}
// ── the LASER: once the pearl is seated, a bright beam climbs from the socket
// and out the mouth — the line that powers the airship's crystal ──
if (pearlSet > 0.5) {
let a = TGS_SOCK + vec3f(0.0, 0.14, 0.0);
let b = TGS_MOUTH;
let bd = tgs_segDist(ro, rd, a, b);
let core = exp(-bd * bd * 900.0);
let glow = exp(-bd * bd * 60.0);
let beam = tgs_hue(moonHue);
let pulse = 0.7 + 0.3 * sin(t * 18.0 - px.y * 0.05); // energy climbing the beam
c += (vec3f(1.2) * core + (beam * 0.6 + vec3f(0.3, 0.6, 0.8)) * glow) * pulse;
}
// ── screen-space affordances ──
// the socket: a ring inviting the pearl (only while you carry it, before seating)
if (uni(108) > 0.5 && pearlSet < 0.5) {
let sc = vec2f(256.0, 300.0);
c += vec3f(0.7, 0.85, 0.95) * tg_ring(px, sc, 40.0, 33.0) * (0.3 + 0.3 * sin(t * 2.6)) * (0.5 + 0.5 * smoothstep(56.0, 24.0, length(mm - sc)));
}
// the 16t gear under the loose plank (unchanged clue — reads invMask bit 8)
{
let m8 = uni(114); // 1 once the 8t gear was EVER taken (seating it must not resurrect the plank gear)
c += vec3f(0.95, 0.80, 0.42) * mod_tg_icon_gear(px, vec2f(150.0, 420.0), 15.0, 8.0, t) * (1.0 - m8) * (0.75 + 0.25 * sin(t * 2.5));
c += vec3f(0.9, 0.75, 0.4) * tg_ring(px, vec2f(150.0, 420.0), 24.0, 21.0) * (1.0 - m8) * (0.18 + 0.18 * sin(t * 2.5));
}
// drifting spores + the cursor's cool glow
let mote = hash21(floor(vec2f(px.x * 0.28, px.y * 0.28 - t * 8.0)));
c += vec3f(0.4, 0.8, 0.9) * step(0.9965, mote) * (0.4 + 0.6 * sin(t * 2.0 + mote * 40.0)) * 0.35;
let cur = (mm - 256.0) / 256.0;
c += vec3f(0.3, 0.7, 0.8) * exp(-dot(p - cur, p - cur) * 12.0) * 0.10;
// click-confirm ripple
let cfx = uni(84);
if (cfx > 0.003) { let cd = length(px - vec2f(uni(85), uni(86))); c += vec3f(0.8, 0.9, 0.95) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4; }
c = tgs_obelisk(c, px, t);
return max(c, vec3f(0.0));
}
fn mod_tg_scene(view: i32, p: vec2f, px: vec2f, t: f32) -> vec3f {
if (uni(113) > 0.0005) { return mod_tg_flycine(p, px, t); } // THE CROSSING — Act I finale owns the frame
if (view == 1) { return mod_tg_gate(p, px, t); }
if (view == 2) { return mod_tg_hall(p, px, t); }
if (view == 3) { return mod_tg_lens(p, px, t); }
if (view == 4) { return mod_tg_stele(p, px, t); }
if (view == 5) { return mod_tg_lattice(p, px, t); }
if (view == 6) { return mod_tg_core(p, px, t); }
if (view == 7) { return mod_tg_orrery(p, px, t); }
if (view == 8) { return mod_tg_seacave(p, px, t); }
if (view == 9) { return mod_tg_deck(p, px, t); }
if (view == 10) { return mod_tg_engine(p, px, t); }
if (view == 11) { return mod_tg_mirror(p, px, t); }
if (view == 12) { return mod_tg_thicket(p, px, t); }
if (view == 13) { return mod_tg_drowned(p, px, t); }
if (view == 14) { return mod_tg_kiln(p, px, t); }
return mod_tg_shore(p, px, t);
}
// ═══ THE OBELISK — a slim glass-stone monolith in the grotto, px(430,300),
// tide-ring engravings lit by the cave's own bioluminescence. uni(117)
// irises the slot; the SPANNER waits inside until uni(53) says taken.
// (Self-contained spanner SDF — the satchel's icon fns are visualType-side
// and unreachable from modules.) ═══
fn tgs_spannerSdf(d: vec2f, s: f32) -> f32 {
let h = sdSegment(d, vec2f(-s, s * 0.55), vec2f(s * 0.35, -s * 0.30));
var m = smoothstep(0.20 * s, 0.12 * s, h);
let hc = d - vec2f(0.62 * s, -0.50 * s);
m = max(m, smoothstep(0.13 * s, 0.06 * s, abs(length(hc) - 0.33 * s)) * step(-hc.y, 0.30 * s));
return clamp(m, 0.0, 1.0);
}
fn tgs_obelisk(cIn: vec3f, px: vec2f, t: f32) -> vec3f {
var c = cIn;
let O = vec2f(430.0, 300.0);
let d = px - O;
if (abs(d.x) > 46.0 || d.y < -118.0 || d.y > 96.0) { return c; }
let openA = clamp(uni(117), 0.0, 1.0);
let taken = uni(53);
// the stone: a tall tapered slab, glassy-dark, strata catching the grotto light
let hw = 20.0 - d.y * 0.055; // taper toward the crown
let slab = max(abs(d.x) - hw, max(d.y - 88.0, -d.y - 112.0));
if (slab < 0.0) {
var oc = vec3f(0.030, 0.042, 0.052) * (0.8 + 0.45 * fbm(d * 0.16 + vec2f(11.0), 2));
// tide-ring engravings: pale horizontal bands, worn at the edges
let ring = max(sin(d.y * 0.30 + 1.3), 0.0);
oc += vec3f(0.10, 0.20, 0.24) * ring * ring * smoothstep(hw, hw - 7.0, abs(d.x)) * 0.35;
// bioluminescent rim light along the left edge (the water side)
oc += vec3f(0.08, 0.30, 0.30) * smoothstep(4.0, 0.0, abs(d.x + hw)) * 0.5;
c = mix(c, oc, smoothstep(1.5, -1.5, slab));
}
// THE SLOT — a vertical lens aperture at the heart; stone leaves withdraw as
// openA rises. Interior glows; the spanner rests on a pedestal of light.
let sd = vec2f(d.x, (d.y + 14.0) / 2.1);
let mouth = length(sd) - 15.0;
let leaves = abs(d.x) - 15.0 * openA; // the leaves' retreating inner edge
let slot = max(mouth, -leaves); // open where leaves have withdrawn
if (mouth < 0.0 && leaves < 0.0) {
var ic = vec3f(0.012, 0.020, 0.026);
ic += vec3f(0.10, 0.30, 0.34) * (0.5 + 0.5 * sin(t * 1.7)) * 0.5; // breathing inner light
c = mix(c, ic, smoothstep(1.0, -1.0, mouth));
if (taken < 0.5) {
let sm = tgs_spannerSdf(d - vec2f(0.0, -14.0), 11.0);
c = mix(c, vec3f(0.68, 0.66, 0.60), sm * 0.9); // old bright steel
c += vec3f(0.35, 0.55, 0.60) * sm * (0.4 + 0.3 * sin(t * 2.3));
c += vec3f(0.12, 0.30, 0.34) * exp(-dot(d - vec2f(0.0, -2.0), d - vec2f(0.0, -2.0)) * 0.004) * 0.6; // pedestal glow
} else {
c += vec3f(0.30, 0.14, 0.06) * exp(-dot(sd, sd) * 0.02) * 0.35; // the ember it leaves behind
}
}
// the unlock flare: while the leaves are mid-travel the seam burns
c += vec3f(0.55, 0.85, 0.85) * smoothstep(3.0, 0.0, abs(leaves)) * step(mouth, 0.0)
* openA * (1.0 - openA) * 4.0 * (0.6 + 0.4 * sin(t * 6.0));
return c;
}
module · tg_thicket
// ═══════════════════════════════════════════════════════════════════════════
// mod_tg_thicket — VIEW 12, THE THICKET (synthesized module)
// Tideglass Act 1, design doc sec 12 "THE THICKET — an eastward room, fire's
// first home". Composed by the thicket synthesizer from the five hand-built
// aspect parts in tg-build/thicket/ (backlight, beyond_shape, roots, tangle,
// wet_green_readout), following the airship/ parts->synthesized pattern.
//
// THE SCENE: a dense overgrown tangle choking an eastward passage. A shape
// (an arched stone threshold) is visible but unreachable deep beyond the wet
// branches, backlit from somewhere past the canopy. Wet green everywhere that
// reads as NOT-yet-burnable (glossy, sodden, beaded). Ground roots grip the
// soil. In Act 1 the thicket is an impassable "not yet"; once the fire moon
// exists and BURN is chosen it chars away and the passage opens.
//
// EXPOSED FN (matches the cartridge view-fn signature, cf. mod_tg_shore):
// fn mod_tg_thicket(p: vec2f, px: vec2f, t: f32) -> vec3f
// p : view uv in [-1,1], y-DOWN (top of screen = negative y).
// px : pixel coords 0..512 (unused here; nav chevrons are drawn by the
// cartridge's separate nav pass, not inside the view fn).
// t : scene seconds.
//
// UNIFORMS THIS MODULE READS (documented per the shader/hook-consistency law):
// uni(76) thicketBurn01 in [0,1]. 0 = wet, living, impassable Act-1 thicket
// (cool green backlight, wet-green material, breathing sway). ->1 the
// fire moon has dried and charred the tangle: green desaturates to
// charcoal, ember cracks flare then die, roots/branches thin so the
// way past clears, the backlight floods warm and open. The step hook
// MUST write U[76]; until it does, uni() returns 0 (the correct
// Act-1 default), so the module is safe before the hook is wired.
// (uni(76) is the ONLY uniform read here. All burn behaviour is forwarded to
// the aspect parts as their `burn` argument.)
//
// SHARED HELPERS RELIED ON (from the shaders.ts frame preamble + cartridge lib,
// in scope when this module is concatenated): fbm, rot2, hash21, vnoise,
// sdSegment, mod_tgo_suncol. No baked pixel data / no const arrays
// (freeze-quarantine law); pure procedural SDF + noise throughout.
//
// COMPOSITION (back -> front), each part in its documented role:
// 0. base : a dim wet-forest depth gradient so gaps read as depth.
// 1. beyond_shape : the arched threshold silhouette + its glow/haze (backdrop).
// 2. backlight : the breach halo, god-ray shafts and drifting motes leaking
// from beyond — drawn BEFORE the branches so they occlude it
// into slivers (its header asks to be composited early).
// 3. roots : the detailed ground-root tangle gripping the soil, before
// the tall branch weave overhangs it.
// 4. tangle : the dominant branch weave, wet foliage clumps, burn front,
// embers and smoke — composited over the lit backdrop.
// 5. wet_green : a wet-green material enrichment (beads, sheen, sick-green)
// masked to the darker on-branch pixels, held out of the
// bright breach and receding as the burn takes over so the
// tangle part owns the char.
// ═══════════════════════════════════════════════════════════════════════════
// ── aspect: backlight ──────────────────────────────────────────────────────
// The light from BEYOND the tangle: a wet luminous glow leaking through a gap in
// the canopy, god-ray shafts raking down, drifting dust motes. Light ONLY (no
// branches / silhouette) so it is drawn early and later occluded into slivers.
// Additive over cIn. Reads no uniform directly; `burn` is passed in from uni(76).
fn mod_tg_thicket_backlight(cIn: vec3f, p: vec2f, t: f32, burn: f32) -> vec3f {
var c = cIn;
// ── the gap in the canopy: the one bright breach the light pours from ──
// it breathes a touch (wind stirring the far leaves) so the beam is never dead-still.
let sx = 0.02 * sin(t * 0.23) + 0.015 * sin(t * 0.61 + 1.3);
let s = vec2f(sx, -0.30); // high-centre (y-down: negative = up)
let d = p - s;
let r = length(d);
let ang = atan2(d.y, d.x); // angle around the breach for the shafts
// ── burn palette: wet green-gold daylight → hot ember flood ──
// beyond the wet thicket the light reads as a cool, sunless forest glow;
// once fire has taken the tangle the same breach roars warm and much brighter.
let coolBeyond = mix(vec3f(0.34, 0.62, 0.40), vec3f(0.86, 0.92, 0.66), 0.35); // green-gold
let hotBeyond = mod_tgo_suncol(0.10); // low warm ember key (kin to the world sun)
let beamCol = mix(coolBeyond, hotBeyond, burn);
let gain = mix(1.0, 2.6, burn); // the passage floods as it chars open
// ── (1) the breach halo: soft radial bloom from the gap, warm core ──
let halo = exp(-r * 2.4) + 0.55 * exp(-r * 6.5);
let core = exp(-r * 11.0); // near-white heart of the opening
c += beamCol * halo * (0.28 * gain);
c += mix(vec3f(0.82, 0.95, 0.78), vec3f(1.10, 0.86, 0.52), burn) * core * (0.55 * gain);
// ── (2) god-ray shafts: streaks raking out of the breach, down through the tangle ──
let drift = t * 0.06;
let streak = fbm(vec2f(ang * 3.2 + drift, r * 1.4 - drift * 0.5), 3);
var shafts = smoothstep(0.42, 0.86, streak);
// bias the shafts downward (light falls INTO the room, y-down = +y is down)
let downBias = smoothstep(-0.55, 0.65, p.y);
shafts *= downBias;
let reach = exp(-r * 1.7); // fade with distance from the breach
c += beamCol * shafts * reach * (0.34 * gain);
// ── (3) dust motes adrift in the beam: sparse, slow, only where light reaches ──
{
let mp = p * vec2f(9.0, 9.0) + vec2f(-drift * 1.5, drift * 3.0);
let cell = floor(mp);
let h = hash21(cell);
let twinkle = 0.5 + 0.5 * sin(t * (0.9 + h * 1.4) + h * 40.0);
let mote = step(0.965, h) * smoothstep(0.34, 0.03, length(fract(mp) - 0.5));
c += beamCol * mote * twinkle * reach * downBias * (0.5 + 0.6 * burn);
}
// ── (4) a faint vertical breath of light down the throat of the passage ──
let throat = exp(-(p.x - sx) * (p.x - sx) / (0.010 + burn * 0.05));
c += beamCol * throat * downBias * (0.06 + 0.20 * burn);
return c;
}
// ── aspect: beyond_shape ───────────────────────────────────────────────────
// The arched threshold / gateway deep past the tangle: the "shape visible but
// unreachable". Composited EARLY as the far backdrop; the branches draw over it.
// Act 1: wet green-grey silhouette against a cold hazed glow. Burn: clears/warms.
// Self-contained hash so it survives any synthesis order.
fn mod_tg_thicket_bs_hash(q: vec2f) -> f32 {
return fract(sin(dot(q, vec2f(41.317, 289.113))) * 43758.5453);
}
fn mod_tg_thicket_beyond_shape(cIn: vec3f, p: vec2f, t: f32, burn01: f32) -> vec3f {
var c = cIn;
let burn = clamp(burn01, 0.0, 1.0);
// ── local arch coords: O at frame centre, a.y points UP (p.y is screen-down) ──
let O = vec2f(0.0, 0.0);
let a = vec2f(p.x - O.x, O.y - p.y);
// arch opening = rounded-top gateway (legs box unioned with a semicircle cap)
let hw = 0.15; // half width of the passage
let base = -0.30; // bottom of the opening (a.y)
let cap = 0.14; // where the legs stop and the arch turns
// box SDF for the straight legs
let bc = vec2f(0.0, (base + cap) * 0.5);
let bh = vec2f(hw, (cap - base) * 0.5);
let bd = abs(a - bc) - bh;
let dLegs = length(max(bd, vec2f(0.0))) + min(max(bd.x, bd.y), 0.0);
// circle SDF for the arch cap
let dCap = length(a - vec2f(0.0, cap)) - hw;
let dOp = min(dLegs, dCap); // <0 inside the passage, >0 in the stone / outside
// ── the light from beyond: a soft warm shaft climbing out of the opening ──
let coldGlow = vec3f(0.14, 0.30, 0.28); // dim verdigris-teal cast
let warmGlow = vec3f(1.15, 0.62, 0.24); // ember / amber from beyond
let glowCol = mix(coldGlow, warmGlow, burn);
let colX = smoothstep(hw + 0.10, 0.0, abs(a.x)); // narrows the beam to the gap
let colY = smoothstep(0.55, -0.20, a.y); // fades upward into depth
let breathe = 0.85 + 0.15 * sin(t * 0.6);
let shaft = colX * colY * breathe;
c += glowCol * shaft * (0.10 + 0.55 * burn);
// bright core seen THROUGH the opening (the "somewhere it leads")
let coreMask = smoothstep(0.012, -0.05, dOp);
let coreCol = mix(vec3f(0.20, 0.42, 0.40), vec3f(1.30, 0.78, 0.34), burn);
let shim = mod_tg_thicket_bs_hash(floor(vec2f(a.x * 40.0, a.y * 22.0 - t * 1.3)));
c = mix(c, coreCol * (0.55 + 0.45 * shim), coreMask * (0.35 + 0.45 * burn));
// ── the stone threshold frame: a wet, mossy silhouette (the shape you can't reach) ──
let wall = 0.085; // frame thickness
let frameMask = smoothstep(-0.010, 0.006, dOp)
* smoothstep(wall + 0.012, wall - 0.012, dOp);
let stoneWet = vec3f(0.070, 0.115, 0.090);
let stoneDry = vec3f(0.150, 0.120, 0.088);
var stone = mix(stoneWet, stoneDry, burn);
let rim = smoothstep(wall, wall - 0.030, dOp) * smoothstep(-0.010, 0.010, dOp);
stone += glowCol * rim * (0.20 + 0.40 * burn);
c = mix(c, stone, frameMask * 0.92);
// ── atmospheric depth haze: the beyond sits far back behind the tangle ──
let hazeCol = mix(vec3f(0.10, 0.17, 0.16), vec3f(0.34, 0.22, 0.15), burn);
let haze = (0.42 - 0.34 * burn) * smoothstep(0.9, -0.4, a.y);
c = mix(c, hazeCol, clamp(haze, 0.0, 1.0));
return c;
}
// ── aspect: roots ──────────────────────────────────────────────────────────
// A ground-root tangle gripping the soil at the base of the thicket. Composited
// after the backlit backdrop and before the tall branch tangle. Ground horizon
// yG = 0.42; roots sprawl at and below it. burn passed from uni(76).
fn mod_tg_thicket_roots(cIn: vec3f, p: vec2f, t: f32, burn: f32) -> vec3f {
var c = cIn;
let yG = 0.42; // ground horizon for view 12
// GNARL: warp the sample point so every root reads hand-twisted, not a clean capsule.
let warp = vec2f(
vnoise(p * 3.1 + vec2f(11.0, 3.0)),
vnoise(p * 3.4 + vec2f(-5.0, 7.0))) - vec2f(0.5);
let pw = p + warp * 0.045;
var cov = 0.0; // root coverage (0..1)
var lit = 0.0; // top / backlit edge accumulation (moss + wet sheen)
var deep = 0.0; // crevice overlap -> occlusion darkening
// Fan of gnarled roots emerging from the ground line and crawling outward.
let N = 7;
for (var i = 0; i < N; i = i + 1) {
let fi = f32(i);
let s = hash21(vec2f(fi, 4.0)); // per-root jitter
let s2 = hash21(vec2f(fi, 9.0));
// base anchored along the ground line, spread across the frame
let bx = mix(-0.95, 0.95, fi / f32(N - 1)) + (s - 0.5) * 0.10;
let base = vec2f(bx, yG + 0.02 + s2 * 0.04);
let dir = sign(bx + 1e-3); // crawl away from centre
// knee + tip: root reaches sideways, then dives into the soil (y down)
let knee = base + vec2f(dir * (0.10 + s * 0.14), 0.14 + s2 * 0.12);
let tip = knee + vec2f(dir * (0.06 + s2 * 0.20), 0.20 + s * 0.16);
// slow breathing sway, the WET tangle is alive; killed as it chars
let sway = (1.0 - burn) * 0.010 * sin(t * 0.6 + fi * 1.7);
let kA = knee + vec2f(sway, 0.0);
let tB = tip + vec2f(sway * 1.8, 0.0);
// two tapered segments per root
let d = min(sdSegment(pw, base, kA), sdSegment(pw, kA, tB));
// taper: thick where it emerges, thinning as it spreads; thins more as it burns.
let alongY = clamp((pw.y - (yG - 0.10)) / 0.50, 0.0, 1.0);
let wid = (0.028 + 0.020 * (1.0 - alongY)) * (1.0 - 0.35 * s) * (1.0 - 0.45 * burn);
let rc = smoothstep(wid, wid * 0.40, d);
cov = max(cov, rc);
// lit upper edge, light from beyond catches the top of each root
lit = max(lit, smoothstep(wid * 1.4, 0.0, d) * smoothstep(0.0, 0.02, kA.y - pw.y));
deep += rc; // count overlaps for occlusion
}
if (cov <= 0.001) { return c; } // let backlit gaps pass through
// ---- WET, LIVING colour ---------------------------------------------
let woodDk = vec3f(0.055, 0.070, 0.050); // near-black damp bark
let woodMid = vec3f(0.110, 0.140, 0.100);
let moss = vec3f(0.160, 0.340, 0.170); // wet tideglass green on lit edges
let grain = 0.5 + 0.5 * vnoise(pw * 22.0 + vec2f(0.0, t * 0.05));
var rootCol = mix(woodDk, woodMid, grain);
rootCol = mix(rootCol, moss, clamp(lit, 0.0, 1.0) * 0.60 * (1.0 - burn));
// cold wet sheen along the lit edges, reads as damp, un-burnable
let sheen = pow(clamp(lit, 0.0, 1.0), 3.0) * (1.0 - burn);
rootCol += vec3f(0.20, 0.30, 0.26) * sheen * 0.50;
// crevice occlusion where roots pile over one another
rootCol *= 1.0 - 0.35 * smoothstep(1.2, 2.4, deep);
// ---- CHAR / EMBER (burn state) --------------------------------------
if (burn > 0.001) {
rootCol = mix(rootCol, vec3f(0.030, 0.028, 0.030), smoothstep(0.0, 0.70, burn));
let crack = smoothstep(0.62, 0.72, fbm(pw * 30.0 + vec2f(t * 0.6, 0.0), 3));
let emberLife = smoothstep(0.05, 0.35, burn) * (1.0 - smoothstep(0.60, 1.00, burn));
rootCol += vec3f(1.20, 0.42, 0.10) * crack * emberLife * cov;
}
c = mix(c, rootCol, cov);
return c;
}
// ── aspect: tangle ─────────────────────────────────────────────────────────
// The overgrown branch weave that chokes the passage: layered wavy strands, wet
// foliage clumps, a burn front that chars then ashes to open the way, embers and
// smoke. Composites over cIn and lets the backlight leak through the gaps.
// burn passed from uni(76).
fn mod_tg_thicket_tangle(cIn: vec3f, p: vec2f, t: f32, burn: f32) -> vec3f {
// ── (1) BACKLIGHT FROM BEYOND ─────────────────────────────────────────────
// A pale, faintly warm glow rises from the unreachable space past the branches.
// Sits BEHIND the tangle; revealed as the branches burn away (alpha drops below).
let gap = p - vec2f(0.0, -0.12);
let beyond = exp(-dot(gap * vec2f(1.05, 1.35), gap * vec2f(1.05, 1.35)) * 1.9);
let arch = smoothstep(0.34, 0.0, abs(p.x)) * smoothstep(0.55, -0.35, p.y);
let backCol = mix(vec3f(0.30, 0.42, 0.44), vec3f(0.78, 0.72, 0.55), 0.35 + beyond * 0.4);
var back = mix(cIn, backCol, clamp(beyond * 0.72 + arch * 0.18, 0.0, 0.9));
// ── (2) THE TANGLE — layered wavy strands, far→near ───────────────────────
var T = vec3f(0.0);
var A = 0.0;
let breathe = 0.012 * sin(t * 0.23); // barely-there sway; the wet green holds
for (var i = 0; i < 3; i = i + 1) {
let fi = f32(i);
let ang = 0.62 - fi * 0.66; // each layer crosses the last
let u = rot2(ang) * (p + vec2f(breathe * (fi - 1.0), 0.0));
let sp = 0.150 + fi * 0.046; // strand spacing (near = looser)
let wob = 0.085 * sin(u.x * 2.7 + fi * 2.1) + (fbm(u * 1.5 + vec2f(fi * 6.3), 3) - 0.5) * 0.28;
let uy = (u.y + wob) / sp;
let cell = fract(uy) - 0.5; // signed across the strand
let thick = 0.30 + 0.16 * sin(u.x * 5.3 + fi * 1.7); // knotty thickening
let cov = smoothstep(thick, thick * 0.34, abs(cell)) * step(0.06, thick);
if (cov > 0.001) {
let lit = 0.34 + 0.66 * (fi / 2.0); // nearer layers catch more light
let moss = fbm(u * 5.5 + vec2f(fi * 3.1), 2);
var bcol = mix(vec3f(0.020, 0.030, 0.022), vec3f(0.060, 0.100, 0.058), moss) * lit;
bcol = mix(bcol, vec3f(0.050, 0.082, 0.066), 0.35); // cartridge verdigris key
let sheen = smoothstep(0.0, thick * 0.9, cell) * smoothstep(thick, thick * 0.2, abs(cell));
bcol += vec3f(0.42, 0.60, 0.52) * sheen * 0.30 * lit;
let bead = step(0.86, fbm(u * 22.0, 2)) * smoothstep(thick, thick * 0.4, abs(cell));
bcol += vec3f(0.55, 0.70, 0.62) * bead * 0.35 * lit;
T = mix(T, bcol, cov);
A = max(A, cov * (0.55 + 0.45 * lit));
}
}
// ── (3) WET FOLIAGE CLUMPS clinging to the weave ──────────────────────────
let leaf = fbm(p * 3.1 + vec2f(t * 0.015, 1.7), 4);
let clump = smoothstep(0.52, 0.82, leaf) * (0.35 + A); // leaves cling to branches
if (clump > 0.001) {
let lcol = mix(vec3f(0.035, 0.085, 0.045), vec3f(0.100, 0.190, 0.085), fbm(p * 8.0, 3));
T = mix(T, lcol, clamp(clump, 0.0, 1.0));
A = max(A, clamp(clump * 0.9, 0.0, 1.0));
T += vec3f(0.50, 0.68, 0.55) * step(0.90, leaf) * 0.30; // glossy wet dots
}
// ── (4) GROUND ROOTS fanning up from the floor ────────────────────────────
let rq = p - vec2f(0.0, 1.25);
let ra = atan2(rq.x, -rq.y); // fan angle from below
let rootField = abs(fract(ra * 3.8 + (fbm(p * 2.2, 2) - 0.5) * 1.6) - 0.5);
let rootCov = smoothstep(0.11, 0.03, rootField) * smoothstep(-0.1, 0.9, p.y);
if (rootCov > 0.001) {
let rcol = mix(vec3f(0.030, 0.028, 0.022), vec3f(0.070, 0.075, 0.052), fbm(p * 6.0, 2));
T = mix(T, rcol, rootCov);
A = max(A, rootCov);
}
// ── (5) BURN STATE (uni(76)) — chars away, opens the passage ──────────────
let A0 = A; // material before it ashes
let bn = fbm(p * 2.3 + vec2f(4.1, 2.6), 4); // the burn front's shape
let bd = burn * 1.30; // let it fully consume at 1.0
let charAmt = smoothstep(bn - 0.10, bn + 0.12, bd);
let ashAmt = smoothstep(bn + 0.04, bn + 0.32, bd);
T = mix(T, vec3f(0.045, 0.040, 0.045), charAmt); // charred to black bark
A = A * (1.0 - ashAmt); // burns through → gap opens
var col = mix(back, T, A);
// glowing embers along the front, only where material still exists
let front = smoothstep(0.15, 0.0, abs(bd - bn)) * step(0.02, burn) * (1.0 - ashAmt * 0.6);
col += vec3f(1.25, 0.48, 0.13) * front * A0 * (0.7 + 0.5 * fbm(p * 9.0 + vec2f(t * 0.9), 2));
// a warm smoke lift above the char while it burns
let smoke = smoothstep(0.0, 0.5, bd) * (1.0 - smoothstep(0.7, 1.15, bd)) * A0
* smoothstep(0.35, -0.6, p.y) * (0.4 + 0.6 * fbm(p * vec2f(3.0, 6.0) + vec2f(0.0, -t * 0.5), 3));
col = mix(col, vec3f(0.16, 0.14, 0.15), smoke * 0.30);
return col;
}
// ── aspect: wet_green_readout ──────────────────────────────────────────────
// Re-skins an on-branch colour as glossy, sodden, deep-green growth: mottled
// leaf/moss, hard wet specular from the backlight, beaded droplets, deep knot
// shadow. Preserves cIn's shading via luminance so it composes over any branch
// shape. As burn rises the green desaturates to charcoal and ember cracks flare.
// burn passed from uni(76).
fn mod_tg_thicket_wet_green_readout(cIn: vec3f, p: vec2f, t: f32, burn: f32) -> vec3f {
let lum = clamp(dot(cIn, vec3f(0.299, 0.587, 0.114)), 0.0, 1.0);
// ── mottled wet-green albedo ──────────────────────────────────────────────
let grain = fbm(p * 7.3, 3); // broad leaf/moss clumps
let fine = fbm(p * 23.0 + vec2f(4.1, 1.7), 2); // damp speckle
let mottle = clamp(grain * 0.75 + fine * 0.25, 0.0, 1.0);
let barkGreen = vec3f(0.030, 0.066, 0.038); // sodden shadowed growth
let leafGreen = vec3f(0.115, 0.255, 0.095); // lit wet leaf
let sickGreen = vec3f(0.190, 0.270, 0.070); // yellow-green highlight (young, unburnable)
var col = mix(barkGreen, leafGreen, smoothstep(0.15, 0.85, mottle));
col = mix(col, sickGreen, smoothstep(0.60, 0.95, mottle) * 0.5);
col *= 0.45 + 0.85 * lum; // carry the branch's own shading
// ── backlight sheen (light from beyond, through the gaps) ─────────────────
let gx = fbm(p * 7.3 + vec2f(0.02, 0.0), 3) - grain;
let gy = fbm(p * 7.3 + vec2f(0.0, 0.02), 3) - grain;
let edge = clamp(length(vec2f(gx, gy)) * 42.0, 0.0, 1.0);
let backlit = vec3f(0.55, 0.95, 0.62); // cold wet-green glow leaking from beyond
col += backlit * edge * (0.30 + 0.35 * mottle);
// ── glossy wet specular: tight hotspots on upper-facing wet surfaces ──────
let sheen = pow(clamp(lum * (0.5 + 0.5 * mottle), 0.0, 1.0), 6.0);
col += vec3f(0.80, 0.95, 0.78) * sheen * 0.28;
// ── beaded water droplets: sparse procedural highlights, slow shimmer ─────
let bcell = floor(p * 34.0);
let bh = hash21(bcell);
let bl = length(fract(p * 34.0) - 0.5);
let bead = step(0.972, bh) * smoothstep(0.34, 0.06, bl);
let twinkle = 0.55 + 0.45 * sin(t * 1.6 + bh * 40.0);
col += vec3f(0.85, 1.0, 0.92) * bead * twinkle * 0.55;
// ── deep self-shadow in the densest knot (low-lum, low-mottle cores) ──────
let knot = (1.0 - smoothstep(0.05, 0.35, lum)) * (1.0 - mottle);
col *= 1.0 - knot * 0.40;
// ═══ BURN STATE (uni(76)) — chars the wet green away ══════════════════════
if (burn > 0.001) {
let flick = fbm(p * 9.0 + vec2f(0.0, -t * 0.6), 3);
let heat = smoothstep(0.55, 0.30, abs(flick - (0.35 + 0.30 * burn)));
let emberWin = smoothstep(0.05, 0.35, burn) * (1.0 - smoothstep(0.75, 1.0, burn));
let ember = vec3f(1.15, 0.42, 0.10) * heat * emberWin;
let charc = vec3f(0.030, 0.024, 0.020);
let ash = vec3f(0.070, 0.066, 0.062);
var burnt = mix(charc, ash, smoothstep(0.6, 1.0, burn));
burnt += ember; // embers glow through the char
burnt += vec3f(0.30, 0.10, 0.02) * heat * emberWin * 0.5; // heat bleed around cracks
col = mix(col, burnt, smoothstep(0.0, 0.9, burn));
}
return col;
}
// ═══════════════════════════════════════════════════════════════════════════
// THE VIEW FN — layers the aspects into the thicket scene.
// ═══════════════════════════════════════════════════════════════════════════
fn mod_tg_thicket(p: vec2f, px: vec2f, t: f32) -> vec3f {
let burn = clamp(uni(76), 0.0, 1.0); // U[76] thicketBurn01 (0 wet Act1, 1 charred)
// ── 0. base: a dim wet-forest depth gradient so the gaps read as depth,
// not void. Cool green-black up near the canopy breach, darker damp earth
// below. Warms a touch as the far light floods on burn.
let deep = mix(vec3f(0.055, 0.100, 0.090), vec3f(0.020, 0.038, 0.036),
smoothstep(-1.0, 1.0, p.y));
var c = mix(deep, mix(deep, vec3f(0.10, 0.075, 0.050), 0.5), burn * 0.5);
// ── 1. the arched threshold beyond (the shape you cannot reach) — backdrop.
c = mod_tg_thicket_beyond_shape(c, p, t, burn);
// ── 2. backlight from beyond: halo, god-ray shafts, motes — drawn before the
// branches so they occlude it into slivers of glow between the boughs.
c = mod_tg_thicket_backlight(c, p, t, burn);
// ── 3. the ground-root tangle gripping the soil, before the branches overhang.
c = mod_tg_thicket_roots(c, p, t, burn);
// ── 4. the dominant branch weave / foliage / burn front over the lit backdrop.
c = mod_tg_thicket_tangle(c, p, t, burn);
// ── 5. wet-green material enrichment (the "not-yet-burnable" read): beads,
// sheen and sick-green skinned onto the DARKER on-branch pixels, held out of
// the bright breach and receding as the burn takes over so the tangle owns
// the char. lum-keyed mask keeps it off the glow, so the beyond stays clear.
{
let lum = dot(c, vec3f(0.299, 0.587, 0.114));
let branchProxy = smoothstep(0.42, 0.10, lum); // dark = likely branch
let breach = exp(-length(p - vec2f(0.0, -0.30)) * 2.0); // hold out of the beyond glow
let wetMask = branchProxy * (1.0 - breach * 0.7)
* (1.0 - smoothstep(0.25, 0.65, burn)) * 0.55;
let wet = mod_tg_thicket_wet_green_readout(c, p, t, burn);
c = mix(c, wet, clamp(wetMask, 0.0, 1.0));
}
return max(c, vec3f(0.0));
}
module · tg_drowned
// THE DROWNED QUARTER (view 13) — combo B's payoff, rebuilt raymarched. A stone
// undercroft beneath the gate: fluted columns, silt floor, the trapdoor's light
// shaft. Its FLOODWATER is the island's tide (uni 60): valve closed → black
// water over everything, the bell's crown a drowned glimmer; sluice open → the
// quarter DRAINS → THE DROWNED BELL stands in the silt. Strike it (bell tolls,
// uni 107) → the crown opens → THE TIDEGLASS PEARL (uni 108). Anchors (px, LAW):
// bell (256,316) r70 · pearl (256,226) r34. Author: Claude Fable 5.
fn tgw_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgw_n2(p: vec2f) -> f32 {
let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
return mix(mix(tgw_h(i), tgw_h(i + vec2f(1.0, 0.0)), u.x),
mix(tgw_h(i + vec2f(0.0, 1.0)), tgw_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgw_fbm(p: vec2f, oct: i32) -> f32 {
var v = 0.0; var a = 0.5; var q = p;
for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgw_n2(q); q = q * 2.03 + vec2f(7.0, 3.0); a *= 0.5; }
return v;
}
fn tgw_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 tgw_cylY(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 tgw_smin(a: f32, b: f32, k: f32) -> f32 { let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); }
const TGW_BELL = vec3f(0.0, 0.62, 0.8); // bell centre → projects ≈ px(256,316)
// SDF → (dist, matId). 1 stone · 2 silt · 3 bell bronze · 4 pearl · 5 crown ring
fn tgw_map(p: vec3f, rung: f32, pearl: f32, t: f32) -> vec2f {
var d = 1e5; var m = 1.0;
// ── the undercroft: back wall + two column rows + a low vault hint ──
let wall = tgw_box(p - vec3f(0.0, 1.6, -3.4), vec3f(4.6, 2.2, 0.4));
if (wall < d) { d = wall; m = 1.0; }
for (var k = 0; k < 4; k++) {
let fx = select(-1.0, 1.0, (k % 2) == 0) * (1.55 + 0.9 * f32(k / 2));
let fz = -0.4 - 1.1 * f32(k / 2);
var col = tgw_cylY(p - vec3f(fx, 1.3, fz), 1.5, 0.26);
// flutes
let ang = atan2(p.z - fz, p.x - fx);
col += 0.012 * sin(ang * 9.0);
// capital + base
col = min(col, tgw_box(p - vec3f(fx, 2.72, fz), vec3f(0.36, 0.10, 0.36)));
col = min(col, tgw_box(p - vec3f(fx, -0.12, fz), vec3f(0.38, 0.12, 0.38)));
if (col < d) { d = col; m = 1.0; }
}
// ── the silt floor, mounded ──
let floorY = p.y + 0.28 + 0.07 * tgw_fbm(p.xz * 2.2, 3) - 0.05 * smoothstep(1.2, 0.0, length(p.xz - TGW_BELL.xz));
if (floorY < d) { d = floorY; m = 2.0; }
// ── THE DROWNED BELL: a great bronze bell, leaning in the silt so its flared
// MOUTH shows on the high side — unmistakably a bell, not a boulder ──
let lean = 0.16 + rung * 0.05; // settled lean (+ the toll's nudge)
let bp0 = p - TGW_BELL;
let bp = vec3f(bp0.x * cos(lean) - bp0.y * sin(lean), bp0.x * sin(lean) + bp0.y * cos(lean), bp0.z);
// classic profile by revolution: r grows toward the mouth, flares at the lip
let q = vec2f(length(bp.xz), bp.y);
let h01 = clamp((q.y + 0.40) / 1.05, 0.0, 1.0); // 0 at mouth … 1 at shoulder
let prof = 0.62 - 0.30 * h01 * h01 + 0.10 * exp(-h01 * 9.0); // waist + flare
var bell = max(abs(q.x - prof) - 0.055, abs(q.y - 0.12) - 0.55); // shell wall
// the LIP: a heavy torus right at the mouth — the read that says BELL
let lip = length(vec2f(q.x - (prof + 0.02), q.y + 0.43)) - 0.075;
bell = min(bell, lip);
// shoulder cap + the CROWN loop and yoke bar on top
let cap = length(bp - vec3f(0.0, 0.66, 0.0)) - 0.20;
bell = tgw_smin(bell, cap, 0.10);
if (bell < d) { d = bell; m = 3.0; }
// the CROWN: a bright brass cradle-ring mounted WORLD-UP on the bell (so it
// reads clean whatever the bell's lean) — held off the shoulder by two posts.
let btop = TGW_BELL + vec3f(0.0, 0.72, 0.0);
let cq = p - btop;
let crown = length(vec2f(length(cq.xz) - 0.16, cq.y)) - 0.04; // flat ring ø0.32
if (crown < d) { d = crown; m = 5.0; }
let post = tgw_cylY(vec3f(abs(cq.x) - 0.16, cq.y + 0.09, cq.z), 0.09, 0.028);
if (post < d) { d = post; m = 5.0; }
// ── THE PEARL — seated in the ring once the toll opens it, until taken ──
if (rung > 0.5 && pearl < 0.5) {
let pd = length(p - (btop + vec3f(0.0, 0.05, 0.0))) - 0.13;
if (pd < d) { d = pd; m = 4.0; }
}
return vec2f(d, m);
}
fn tgw_nrm(p: vec3f, rung: f32, pearl: f32, t: f32) -> vec3f {
let e = vec2f(0.006, 0.0);
return normalize(vec3f(
tgw_map(p + e.xyy, rung, pearl, t).x - tgw_map(p - e.xyy, rung, pearl, t).x,
tgw_map(p + e.yxy, rung, pearl, t).x - tgw_map(p - e.yxy, rung, pearl, t).x,
tgw_map(p + e.yyx, rung, pearl, t).x - tgw_map(p - e.yyx, rung, pearl, t).x));
}
fn tgw_march(ro: vec3f, rd: vec3f, rung: f32, pearl: f32, t: f32) -> vec2f {
var tt = 0.02;
for (var i = 0; i < 80; i++) {
let h = tgw_map(ro + rd * tt, rung, pearl, t);
if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
tt += h.x * 0.85; if (tt > 20.0) { break; }
}
return vec2f(-1.0, 0.0);
}
fn tgw_ao(p: vec3f, n: vec3f, rung: f32, pearl: f32, t: f32) -> f32 {
var occ = 0.0; var w = 1.0;
for (var i = 1; i <= 4; i++) { let hh = 0.05 * f32(i); occ += w * (hh - tgw_map(p + n * hh, rung, pearl, t).x); w *= 0.72; }
return clamp(1.0 - 2.1 * occ, 0.0, 1.0);
}
fn tgw_alb(m: f32, p: vec3f) -> vec3f {
if (m < 1.5) { // old stone — cold, mineral-streaked, tide-stained
var a = vec3f(0.075, 0.082, 0.090) * (0.75 + 0.5 * tgw_fbm(p.xy * 3.0 + p.zz, 3));
a *= 1.0 - 0.22 * smoothstep(0.35, 0.5, abs(fract(p.y * 1.8) - 0.5)); // courses
a = mix(a, vec3f(0.055, 0.075, 0.070), smoothstep(1.6, 0.4, p.y) * 0.6); // tide stain
return a;
} else if (m < 2.5) { return vec3f(0.060, 0.056, 0.048) * (0.7 + 0.6 * tgw_fbm(p.xz * 4.0, 3)); } // silt
else if (m < 3.5) { // bell bronze — verdigris blooms, barnacle crust low
var a = vec3f(0.17, 0.125, 0.06);
a = mix(a, vec3f(0.07, 0.14, 0.11), smoothstep(0.4, 0.75, tgw_fbm(p.xy * 6.0 + p.zz * 3.0, 3)));
a = mix(a, vec3f(0.10, 0.095, 0.085), smoothstep(0.45, 0.1, p.y) * 0.5);
return a;
} else if (m < 4.5) { return vec3f(0.75, 0.82, 0.85); } // pearl (lit specially)
return vec3f(0.55, 0.42, 0.16); // crown ring brass — bright
}
fn mod_tg_drowned(pp: vec2f, px: vec2f, t: f32) -> vec3f {
let mm = vec2f(uni(27), uni(28));
let tide = clamp(uni(60), 0.0, 1.0);
let rung = uni(107);
let pearl = uni(108);
// the water table: flooded (tide .65) drowns the bell crown; drained (.05) = ankle pools
let wl = mix(0.02, 1.55, smoothstep(0.05, 0.65, tide)); // flooded stays below the EYE — you stand above the black water
// ── camera: standing at the foot of the ladder, looking into the quarter ──
let ro = vec3f(0.0, 1.95, 4.2);
let uvx = px.x / 256.0 - 1.0;
let uvy = px.y / 256.0 - 1.0;
let rd = normalize(vec3f(uvx, -uvy - 0.12, -1.0));
// ── ambient dark; the trapdoor's light shaft falls from above ──
var c = vec3f(0.012, 0.020, 0.030);
let hit = tgw_march(ro, rd, rung, pearl, t);
if (hit.x > 0.0) {
let p = ro + rd * hit.x;
let n = tgw_nrm(p, rung, pearl, t);
// key: the shaft of day from the trapdoor (cool-silver), angled from high right
let ldir = normalize(vec3f(0.35, 0.9, 0.2));
let key = vec3f(0.75, 0.85, 0.95);
let dif = max(dot(n, ldir), 0.0);
let ao = tgw_ao(p, n, rung, pearl, t);
var col = tgw_alb(hit.y, p) * (0.14 + 1.5 * dif) * ao;
col += vec3f(0.05, 0.075, 0.09) * ao * 0.6; // cavern fill
// the shaft itself brightens what stands in its column (x ≈ 0.9, z ≈ 0.6)
let inShaft = smoothstep(1.3, 0.25, length(p.xz - vec2f(0.9, 0.6)));
col += key * inShaft * 0.55 * (0.5 + 0.5 * dif);
// pearl: lit from within — mother-of-glass
if (hit.y > 3.5 && hit.y < 4.5) {
let fres = pow(1.0 - max(dot(-rd, n), 0.0), 2.0);
col = mix(vec3f(0.55, 0.68, 0.75), vec3f(1.1, 1.05, 0.95), fres);
col += vec3f(0.5, 0.75, 0.8) * (0.4 + 0.3 * sin(t * 2.2));
}
// bell after the toll: a live resonance shiver on the bronze
if (rung > 0.5 && hit.y > 2.5 && hit.y < 3.5) {
col += vec3f(0.25, 0.20, 0.10) * (0.5 + 0.5 * sin(t * 9.0 + p.y * 14.0)) * 0.25;
}
// the CROWN always gleams — a warm brass so the cradle is never lost in the dark
if (hit.y > 4.5) {
col += vec3f(0.45, 0.34, 0.13) * (0.45 + 0.3 * sin(t * 2.0));
}
// ── underwater: everything below the water table drinks the light ──
if (p.y < wl) {
let depth = clamp((wl - p.y) * 0.8, 0.0, 1.0);
col *= mix(vec3f(1.0), vec3f(0.28, 0.5, 0.55), depth);
// caustic shimmer near the surface
col += vec3f(0.10, 0.22, 0.24) * (1.0 - depth) * tgw_n2(p.xz * 5.0 + vec2f(t * 0.7, 0.0)) * 0.4 * step(0.1, wl - 0.05);
}
c = col;
c = mix(c, vec3f(0.010, 0.016, 0.024), smoothstep(6.0, 16.0, hit.x) * 0.5);
}
// ── the water SURFACE: where the view ray crosses the table before a solid ──
if (rd.y < -0.001 || ro.y > wl) {
let ts = (wl - ro.y) / rd.y
;
if (ts > 0.0 && (hit.x < 0.0 || ts < hit.x)) {
let sp = ro + rd * ts;
if (abs(sp.x) < 4.6 && sp.z > -3.4 && sp.z < 4.4) {
let rip = tgw_n2(sp.xz * 3.0 + vec2f(t * 0.5, t * 0.3)) - 0.5;
let fres = pow(1.0 - max(dot(-rd, vec3f(rip * 0.3, 1.0, rip * 0.3)), 0.0), 2.0);
var wcol = mix(vec3f(0.020, 0.045, 0.055), vec3f(0.10, 0.16, 0.19), fres);
wcol += vec3f(0.35, 0.5, 0.55) * smoothstep(0.42, 0.5, rip) * 0.25; // glints
// THE DROWNED GLIMMER: while flooded, the bell's crown gleams below —
// the "something down there" that pulls you to find the valve
if (wl > 0.8) {
let over = length(sp.xz - TGW_BELL.xz);
wcol += vec3f(0.35, 0.65, 0.6) * smoothstep(0.9, 0.1, over) * (0.5 + 0.4 * sin(t * 1.8)) * 0.8;
}
c = mix(c, wcol, 0.82);
// surface meets stone: a faint bathtub line
c += vec3f(0.30, 0.42, 0.45) * smoothstep(0.05, 0.0, abs(rd.y * (hit.x - ts))) * 0.15;
}
}
}
// ── drained: wet-glisten pools in the silt hollows ──
if (wl < 0.4) {
let g = smoothstep(0.55, 0.85, tgw_fbm(px * 0.02 + vec2f(3.0, 7.0), 3));
c += vec3f(0.10, 0.16, 0.18) * g * smoothstep(330.0, 470.0, px.y) * 0.5;
}
// ── affordances (screen-space light only) ──
let bc = vec2f(256.0, 316.0);
let hovB = smoothstep(84.0, 34.0, length(mm - bc));
if (wl < 0.4 && rung < 0.5) {
c += vec3f(0.85, 0.70, 0.35) * tg_ring(px, bc, 64.0, 58.0) * (0.25 + 0.3 * sin(t * 2.2)) * (0.5 + 0.5 * hovB);
}
if (rung > 0.5 && pearl < 0.5) {
let pc = vec2f(256.0, 266.0); // over the pearl where it truly projects
c += vec3f(0.65, 0.85, 0.9) * tg_ring(px, pc, 34.0, 28.0) * (0.3 + 0.3 * sin(t * 2.8)) * (0.5 + 0.5 * smoothstep(52.0, 22.0, length(mm - pc)));
}
// click-confirm ripple
let cfx = uni(84);
if (cfx > 0.003) { let cd = length(px - vec2f(uni(85), uni(86))); c += vec3f(0.8, 0.85, 0.8) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4; }
return c;
}
— STEP HOOKS (JAVASCRIPT) —
hook · tideglass_core
tideglass core — Act-I finale plays once & holds on the stay-tuned card (no replay)
try {
const wd = sim.worldData
// ── R-RESET (Galen Jul 31): on a cc-reset reload the engine sets wd.__fresh
// (space path sets it ONLY on reset). The host owns __fresh — a hook reply
// can't clear it — so latch once per session. The server autosave restore
// lands ~1s later and would resurrect the run we just wiped ("resets a
// moment then the save comes back"), so hold a wipe window: a RESTORED run
// (its clock is far ahead of a fresh one) appearing inside the window is
// wiped too; the autosave then persists the fresh start. ONLY wd.save.tg —
// this world's branch — is touched; nothing else in the player's save.
// NEVER delete a __key here: a deletion does not propagate back to the host
// (the reply applies only PRESENT keys), so a deleted window key leaves the
// host stuck at its last positive value → the window never closes → any run
// older than the age gate gets wiped forever (the "random resets", Jul 31).
// Set 0 instead — zero round-trips fine.
if (wd.__fresh && !wd.__tgFreshSeen) { wd.__tgFreshSeen = 1; wd.__tgWipe = 12 }
else if (!wd.__fresh && wd.__tgFreshSeen) wd.__tgFreshSeen = 0 // no reset this session → clear the latch (a snapshot capture may carry a stale 1, which would silently eat the NEXT legit R)
if (typeof wd.__tgWipe === 'number' && wd.__tgWipe > 0) {
// wipe ONLY while __fresh is present (reset sessions — it is stripped on every
// load, so an inherited window can never fire) and only in the window's first
// seconds (a hot-reload merge into a long session can't tail-gate a wipe).
// the WHOLE wd.save is this world's slot (tideglass:__autosave) — wipe it all:
// trigger/edge latches (__trig/__edge) and chapters live BESIDE tg, and a
// surviving 'tg-lens' latch bricks the lens on the next run (Galen, Jul 31)
if (wd.__fresh && wd.save && wd.save.tg && (wd.__tgWipe >= 12 || (wd.__tgWipe > 8 && (wd.save.tg.t || 0) > 20))) wd.save = {}
wd.__tgWipe = Math.max(0, wd.__tgWipe - dt)
}
// ═══ PER-PLAYER SAVES (cutover v2 — sandbox 'save' wire deployed 3f27555):
// progress lives in wd.save.tg, engine-persisted per-user/per-anon.
wd.save = wd.save || {}
if (!wd.save.tg || wd.save.tg.v !== 2) wd.save.tg = {
v: 2, view: 0, pv: 0, fade: 0, t: 0,
dials: [0, 0, 0, 0], pipes: [0, 0, 0, 0, 0],
door: 0, organ: 0, star: 0, lensA: 0, hold: 0, fin: 0, night: 0, vault: 0,
clickP: 0, flash: [0, 0, 0, 0], lastToll: -1, act1music: false, musicAct: 0
}
const G = wd.save.tg
if (typeof G.kilnBloom !== "number") G.kilnBloom = 0
if (typeof G.sluiceOpen !== "number") G.sluiceOpen = 0 // the freed wheel is a TIDE VALVE
if (typeof G.bellRung !== "number") G.bellRung = 0
if (typeof G.pearl !== "number") G.pearl = 0
if (typeof G.pearlSet !== "number") G.pearlSet = 0 // pearl seated in the cave receptacle
if (typeof G.crystalPowered !== "number") G.crystalPowered = 0 // the airship's crystal, lit by the beam
if (typeof G.wheelAnim !== "number") G.wheelAnim = 0
if (typeof G.summon !== "number") G.summon = 0 // summoning cinematic clock 0..1 (Aug 10)
if (typeof G.airshipArrived !== "number") G.airshipArrived = 0
if (typeof G.obeliskOpen !== "number") G.obeliskOpen = 0
if (typeof G.obeliskUnlocked !== "number") G.obeliskUnlocked = 0
if (typeof G.summonMigrated !== "number") G.summonMigrated = 0
if (typeof G.flyCine !== "number") G.flyCine = 0 // Act-I finale cinematic timeline (0 off · 0..1 playing · 1 = card)
const inSeat = (id) => { const s = G.seats || []; return s[0] === id || s[1] === id || s[2] === id; };
G.t += dt
// the observatory's constellation puzzle (view 5) — folded in from LATTICE
if (!Array.isArray(G.lat)) { G.lat = [0, 0, 0, 0, 0]; G.latBloom = 0 }
// THE CORE (view 6) — reached only after the constellation locks gold. Three
// rings of light; click a ring to turn its notch 30°; land all three at the
// top gate to wake the core. Steps 0..11; solved when all are 0 (notch up).
if (!Array.isArray(G.core)) { G.core = [5, 8, 2]; G.coreSolved = 0; G.coreGlow = 0 }
// THE ORRERY (view 7) — the deepest room, opened by the woken core. Grab the
// sun-bead (outer ring) or the moon-bead (inner) and swing it round; set the sun
// BELOW the horizon and the moon ABOVE — the finale sky — and hold. The spring
// tide the island remembers wakes the dawn. Answer: sunA=π (down), moonA=0 (up).
if (typeof G.sunA !== 'number') { G.sunA = 2.5; G.moonA = -2.3; G.orrerySolved = 0; G.orreryGlow = 0; G.orreryHold = 0; G.tide = 0; G.dawn = 0; G.grab = null }
// THE SEA-CAVE (view 8) — a bioluminescent grotto in the rock, LEFT off the
// shore (left chevron, always open). Not a puzzle — a place to wander.
// ── ACT 1 v2 state (additive guard — does NOT reset existing progress) ──
if (typeof G.gate2 === 'undefined') {
G.gate2 = false // the drowned-gate combo (B) is dialed at low tide, notches down
G.itemA = false // legacy spanner flag, kept as a mirror of inv.spanner (old saves)
G.act = 1 // the island is Act 1; the flight opens Act 2
}
if (typeof G.inv === 'undefined') {
G.inv = { spanner: false, gear8: false, gear12: false, gear16: false, gear20: false }
G.held = 0 // 0 none ·1 spanner ·2 g8 ·3 g12 ·4 g16 ·5 g20
G.seats = [0, 0, 0] // gear id per engine seat (0 empty; 2..5)
G.notch = 0 // 0 = UP/12 o'clock · 1 = DOWN/6 o'clock (drowned gate)
G.moons = [{ el: 'glass', omega: 0.06, amp: 1.0, phase: 0, hue: 0, risen: 1, alt: 1 }]
G.activeMoon = -1 // -1 = none clicked (natural argmax); else moon index
G.tideLevel = 1 // current tide 0..1 from tide()
G.tideBaseline = 0 // climbs each act (the world floods a little)
G.thicketBurn = 0 // 0 impassable · 1 burned open (fire moon)
G.engineOK = 0 // gear train hits target cadence + direction
G.enginePhase = 0 // crank phase accumulator (re-gearing never pops)
G.fly = 0 // helm pulled — act-boundary anim
G.fireMoon = false // Mirror Island grants it (section 13)
}
if (G.itemA && !G.inv.spanner) G.inv.spanner = true // migrate the old spanner flag
// GALEN'S RULE (Aug 10, overriding the earlier grandfather): the airship comes
// ONLY from the kiln bloom's summons — for everyone, old saves included. The
// one-shot reversal below un-grandfathers any save the deleted forward pass
// touched (flag 1 = migrated without a real summon; nobody with flag 1 can
// have legitimately summoned, since summon=1 blocked the trigger).
if (G.summonMigrated === 1) { G.airshipArrived = 0; G.summon = 0; G.summonMigrated = 2 }
if (G.act >= 2 && !G.crystalPowered) G.crystalPowered = 1 // a save that flew Act I before the pearl-beam existed keeps its power — the new chain gates FRESH runs, it doesn't brick earned wheels (Galen Jul 31)
// STALE-LATCH HEAL (Galen Jul 31): an earlier reset wiped save.tg but left
// __trig beside it — a latch without its truth-state can never fire again
// (the bricked lens). Truth and latch are set in the same tick, so a latch
// whose truth is absent is ALWAYS stale: clear it and the puzzle works again.
if (wd.save.__trig) {
const T = wd.save.__trig
if (T['tg-lens'] && !(G.fin > 0)) delete T['tg-lens']
if (T['tg-organ'] && !(G.organ >= 1)) delete T['tg-organ']
if (T['tg-gate'] && !G.gateChaptered) delete T['tg-gate']
if (T['tg-orrery'] && !G.orrerySolved) delete T['tg-orrery']
if (T['tg-lattice'] && !G.latSolved) delete T['tg-lattice']
}
sim.defineChapters(['THE SHORE', 'THE GATE', 'THE ORGAN', 'THE LIGHT'])
// pin the canvas
for (const f of sim.fields.values()) {
if ((f.name || '') === 'Tideglass') { const T = f.transform; T.x = 256; T.y = 256; T.vx = 0; T.vy = 0 }
}
const ANSWER_DIALS = [2, 0, 3, 1] // the toll order (opens the door)
// ── ACT 1 v2 LOCKED puzzle constants (unit-tested: scenes/tideglass-tests.mjs) ──
const MIRROR_MAP = [1, 0, 3, 2] // vertical-mirror glyph pairs (an involution)
const COMBO_B = [1, 3, 0, 2] // the drowned-gate combo (dial @6 o'clock, low tide)
const CAVE_GLYPHS = [0, 2, 1, 3] // = mirror(COMBO_B); the carving the cave pool reflects
const LOW_THRESH = 0.25 // tide below this = the drowned gate is engageable
const FIXED_PINION = [10, 12, 8] // per-seat fixed pinion teeth (product 960)
const GEAR_TEETH = { 2: 8, 3: 12, 4: 16, 5: 20 } // item id -> tooth count
const CRANK_RATE = 1.0 // boiler crank turns/sec, CW
const TARGET_CADENCE = 0.5 // shaft: one turn per two bell strikes
const TARGET_DIR = -1 // CCW after 3 meshes
const CAD_TOL = 0.02 // 2% relative tolerance on the cadence match
const MOON_TABLE = { fire: { el: 'fire', omega: 0.09, amp: 0.6, phase: 1.7, hue: 0.05, risen: 1, alt: 0 } }
const ANSWER_PIPES = [3, 1, 4, 2, 0] // the stele columns (ticks - 1)
const TONES = [220.0, 293.66, 329.63, 392.0] // glyph voices
const PIPE_TONES = [146.83, 164.81, 196.0, 220.0, 246.94]
const sounds = []
// ═══ SCENE TABLE v1 (Galen Jul 30): edge exits are DATA — one table drives the
// hit-zones AND the drawn chevrons (uni 88..103). Diegetic doors stay coded. ═══
// INSPECT MODE (Galen Jul 30): press I — every click gets documented on a HUD
// console: coords · view · the named node/zone it hit · owning module · saved.
const VIEWN = { 0:['SHORE','tg_views'],1:['GATE','tg_views'],2:['ORGAN HALL','tg_views'],3:['LENS','tg_views'],4:['TABLET','tg_views'],5:['CONSTELLATION','tg_views'],6:['CORE','tg_views2'],7:['ORRERY','tg_views2'],8:['SEA-CAVE','tg_views2'],9:['DECK','tg_deck'],10:['ENGINE','tg_engine'],11:['MIRROR ISLE','tg_mirror'],12:['THICKET','tg_air'],13:['DROWNED','tg_drowned'],14:['KILN','tg_kiln'] }
const ZONES = {
0: [[400,96,40,'moon (ruling)'],[338,62,28,'moon (other)'],[108,128,92,'airship (summoned)'],[214,318,80,'dome (vault-gated)'],[440,300,62,'gate building']],
1: [[156,409,30,'dial 1'],[222,409,30,'dial 2'],[289,409,30,'dial 3'],[355,409,30,'dial 4'],[432,360,26,'notch control (12↔6)'],[256,261,140,'the doorway (enter when open)']],
2: [[131,300,32,'pipe 1'],[193,300,32,'pipe 2'],[256,300,32,'pipe 3'],[319,300,32,'pipe 4'],[381,300,32,'pipe 5']],
5: [[256,256,46,'constellation heart (latSolved-gated)']],
6: [[256,256,50,'core heart (coreSolved-gated)']],
8: [[150,420,36,'8t gear plank'],[256,300,46,'the tideglass socket (seat the pearl → the beam fires)'],[430,300,50,'the obelisk'],[430,300,36,'obelisk slot — the spanner']],
9: [[432,256,44,'hatch → engine'],[256,120,52,'helm (engineOK-gated)']],
10: [[176,256,46,'gear seat 1'],[256,256,46,'gear seat 2'],[336,256,46,'gear seat 3'],[90,404,38,'seized 12t gear (needs spanner held)']],
11: [[346,250,54,'sluice wheel — spanner frees it; then the TIDE VALVE (click = open/close)']],
13: [[256,316,70,'the drowned bell (needs the sluice open — low water)'],[256,266,38,'the pearl (after the bell tolls)']],
14: [[256,290,60,'kiln mouth — anneal the glass bloom']],
}
const NAV = {
0: [{ x: 28, y: 256, d: 1, to: 8 }, { x: 487, y: 256, d: 0, to: 1 }, { x: 108, y: 128, d: 2, to: 9, when: () => !!G.airshipArrived }],
1: [{ x: 25, y: 256, d: 1, to: 0 }, { x: 487, y: 256, d: 0, to: 4 }, { x: 256, y: 210, d: 3, to: 13, when: () => !!G.gate2 }],
2: [{ x: 25, y: 470, d: 1, to: 1 }, { x: 256, y: 30, d: 2, to: 3, when: () => G.organ > 0.5 }],
3: [{ x: 60, y: 470, d: 3, to: 2 }, { x: 487, y: 256, d: 0, to: 12 }],
4: [{ x: 25, y: 256, d: 1, to: 1 }, { x: 487, y: 256, d: 0, to: 12 }],
5: [{ x: 25, y: 256, d: 1, to: 0 }],
6: [{ x: 25, y: 256, d: 1, to: 5 }],
7: [{ x: 25, y: 256, d: 1, to: 6 }],
8: [{ x: 487, y: 256, d: 0, to: 0 }, { x: 25, y: 256, d: 1, to: 11 }],
9: [{ x: 25, y: 256, d: 1, to: 0 }, { x: 432, y: 256, d: 3, to: 10, when: () => G.held === 1 }],
10: [{ x: 25, y: 256, d: 1, to: 9 }],
11: [{ x: 487, y: 256, d: 0, to: 8 }],
12: [{ x: 25, y: 256, d: 1, to: 4 }, { x: 487, y: 256, d: 0, to: 14, when: () => (G.thicketBurn || 0) > 0.6 }],
13: [{ x: 256, y: 40, d: 2, to: 1 }],
14: [{ x: 25, y: 256, d: 1, to: 12 }],
}
const mx = wd.mouse_x, my = wd.mouse_y
const hasMouse = typeof mx === 'number' && typeof my === 'number'
const inView = v => G.view === v && G.fade < 0.35
// ── travel ──
const go = v => { if (v === G.view) return; G.pv = G.view; G.view = v; G.fade = 1; sounds.push({ frequency: 90, duration: 0.35, volume: 0.18, type: 'sine' }) }
G.fade = Math.max(0, G.fade - dt * 1.8)
// ── ACT 1 v2 hook-local helpers (G stores only plain data — these are its "methods";
// functions can NOT live on G because G is serialized into worldData). ──
const mirror = arr => arr.map(g => MIRROR_MAP[g])
const dialsMatch = target => G.dials.every((v, i) => v === target[i])
const tide = () => {
// pre-dawn the tide sits high (natural); AFTER dawn the orrery moon-handle goes
// live and drives moon 1's altitude — drag the moon low to pull the tide out (§2).
// pre-dawn the glass moon still ORBITS, so the tide ebbs and flows on a ~20s cycle —
// wait for the ebb (moonHeight → 0) to engage the drowned gate. After dawn the orrery
// moon-handle takes over. (Fix: pinning moonHeight=1 pre-dawn made low tide unreachable.)
const moonHeight = ((G.dawn || 0) > 0.3) ? (1 + Math.cos(G.moonA || 0)) / 2 : (0.5 + 0.5 * Math.cos((G.t || 0) * 0.32))
if (G.moons[0]) G.moons[0].alt = moonHeight
let s = G.tideBaseline || 0
for (const m of G.moons) s += (m.amp || 0) * (m.alt || 0)
return Math.max(0, Math.min(1, s))
}
const dominantMoon = () => {
if (G.activeMoon >= 0) return G.activeMoon
let best = 0, bestAlt = -2
for (let i = 0; i < G.moons.length; i++) { const a = G.moons[i].alt || 0; if (a > bestAlt) { bestAlt = a; best = i } }
return best
}
const raiseMoon = spec => { G.moons.push(spec) }
const onActComplete = n => {
const next = MOON_TABLE[n + 1] // Act 2+ moons arrive as acts ship
if (next && !G.moons.some(m => m.el === next.el)) raiseMoon(next)
G.act = n + 1
G.tideBaseline = Math.min(0.6, (G.tideBaseline || 0) + 0.12) // the water climbs as you leave
}
const gearCadence = seats => {
let r = CRANK_RATE, meshes = 0
for (let i = 0; i < 3; i++) {
const id = seats[i]
if (!id) return { cadence: 0, dir: 0, full: false } // an empty seat: the train is broken
r *= FIXED_PINION[i] / GEAR_TEETH[id]
meshes++
}
return { cadence: r, dir: Math.pow(-1, meshes), full: true }
}
// THE TIDE VALVE (Galen Jul 31): the freed sluice wheel DRIVES the tide.
// Valve open → the island drains to low water; closed → the sea stands high.
// Legible cause → effect (the old ambient tide cycle stayed deleted for good).
// Progression is untouched: tideLow stays const true for the shipped gates;
// the NEW dependency is the drowned quarter's floodwater, read from U[60].
{
const tideTarget = (G.inv.gear20 && G.sluiceOpen) ? 0.05 : 0.65
G.tideLevel = (G.tideLevel ?? 0.65) + (tideTarget - (G.tideLevel ?? 0.65)) * Math.min(1, dt * 0.5)
}
const tideLow = true // BONES: low-tide requirement deleted (never legible to players)
// ── the bells: cycle 11s, four tolls in the answer order ──
const cyc = G.t % 11
const slot = Math.floor((cyc - 1.0) / 2.2)
const inToll = cyc >= 1.0 && (cyc - 1.0) % 2.2 < 0.1
if (inToll && slot >= 0 && slot < 4 && G.lastToll !== slot) {
G.lastToll = slot
const g = ANSWER_DIALS[slot] // which buoy speaks
G.flash[g] = 1
const vol = inView(0) ? 0.5 : (inView(1) ? 0.16 : 0.05)
if (vol > 0.06 || G.view === 0) sounds.push(
{ frequency: TONES[g], duration: 1.7, volume: vol, type: 'sine' },
{ frequency: TONES[g] * 2.0, duration: 0.9, volume: vol * 0.3, type: 'sine' })
}
if (cyc < 1.0) G.lastToll = -1
for (let i = 0; i < 4; i++) G.flash[i] = Math.max(0, G.flash[i] - dt * 0.8)
// ── clicks ──
G.clickP = Math.max(0, G.clickP - dt * 2.2)
G.wheelAnim = Math.max(0, (G.wheelAnim || 0) - dt * 0.8)
if (G.flyCine > 0 && G.flyCine < 1) G.flyCine = Math.min(1, G.flyCine + dt / 10) // the crossing plays ~10s, then HOLDS on the Act I card // the valve turn animation
if (sim.edge('tg-inspect', !!wd.key_i)) { G.inspect = !G.inspect; G.clickLog = G.clickLog || []; sounds.push({ frequency: G.inspect ? 880 : 440, duration: 0.12, volume: 0.2, type: 'sine' }) }
if (G.dawnCine) G.dawnCine = Math.max(0, G.dawnCine - dt)
if (hasMouse && sim.edge('tg-click', !!wd.mouse_down)) {
G.clickP = 1
const hit = (x, y, r) => Math.hypot(mx - x, my - y) < r
const deny = (x, y) => { G.deny = { x, y, t: 1 }; sounds.push({ frequency: 82, duration: 0.18, volume: 0.3, type: 'square' }) } // a locked thing ANSWERS the click
// INSPECTOR CHANNEL (Galen Jul 30): every click is recorded — the AI reads
// G.clicks over the bridge and decodes which shader block owns those pixels.
G.clicks = G.clicks || []
G.clicks.push({ x: Math.round(mx), y: Math.round(my), v: G.view, t: Math.round(G.t) })
if (G.clicks.length > 8) G.clicks.shift()
wd.__clicks = G.clicks // mirror world-global: the AI reads clicks over the bridge
G.clickFx = { x: mx, y: my, t: 1 } // the game CONFIRMS it heard the click (Galen)
if (G.inspect) {
const vi = VIEWN[G.view] || ['?', '?']
let what = null
for (const e of (NAV[G.view] || [])) { if (hit(e.x, e.y, 45)) { what = 'chevron → ' + (VIEWN[e.to] || ['?'])[0] + ((!e.when || e.when()) ? '' : ' (locked)'); break } }
if (!what) { for (const z of (ZONES[G.view] || [])) { if (hit(z[0], z[1], z[2])) { what = z[3]; break } } }
if (!what && Math.abs(my - 490) < 15 && mx > 100 && mx < 412) what = 'satchel strip'
if (!what) what = 'empty ground'
G.clickLog = G.clickLog || []
G.clickLog.push('(' + Math.round(mx) + ',' + Math.round(my) + ') ' + vi[0] + ' · ' + what + ' · ' + vi[1] + ' · saved ✓')
if (G.clickLog.length > 6) G.clickLog.shift()
}
// ── THE SATCHEL: click a slot to take/replace the held item (any view). Slots at
// x 128/192/256/320/384, y 486; ids 1 spanner ·2 g8 ·3 g12 ·4 g16 ·5 g20. ──
let satchelClicked = false
if (!G.inspect && Math.abs(my - 490) < 15 && mx > 100 && mx < 412) { // tight band: clears the y=470 trapdoor
const slotX = [128, 192, 256, 320, 384]
const owned = [G.inv.spanner, G.inv.gear8 && !inSeat(2), G.inv.gear12 && !inSeat(3), G.inv.gear16 && !inSeat(4), G.inv.gear20 && !inSeat(5)]
for (let s = 0; s < 5; s++) {
if (Math.abs(mx - slotX[s]) < 26 && owned[s]) {
satchelClicked = true
G.held = (G.held === s + 1) ? 0 : s + 1
sounds.push({ frequency: 330 + s * 30, duration: 0.16, volume: 0.22, type: 'triangle' })
break
}
}
}
if (satchelClicked) { /* consumed by the satchel — no world click this frame */ }
else if (G.inspect) { /* INSPECT MODE: clicks are documentation, never gameplay (Galen) */ }
else if (G.dawnCine > 0) { /* dawn transition: every chevron and zone is locked until the sky settles */ }
else if (G.flyCine > 0) { /* Act-I finale plays once, then HOLDS on the stay-tuned card — no click-dismiss, no replay (Galen: stop with just text) */ } // the FINALE owns the screen: mid-flight clicks pass like wind; a tap on the Act I card returns you to the deck
else if ((NAV[G.view] || []).some(e => (!e.when || e.when()) && hit(e.x, e.y, 45) && (go(e.to) || true))) { /* the table took the click */ }
else if (inView(0)) {
// MUTUALLY EXCLUSIVE (else-if) — the dome zone (214,318) sits below the
// building/right-chevron zones. The right chevron (487,256) and building
// (440,300) both mean "into the gate", so they share one branch; the dome
// (constellation) wins only when it's actually risen and clicked.
if (!!G.airshipArrived && hit(108, 128, 92)) go(9) // the moored airship (top-left) → its deck
else if (G.vault > 0.03 && hit(214, 318, 80)) go(5) // the risen dome → the constellation room
else if (hit(440, 300, 62)) go(1) // the gate building (diegetic; right-edge chevron is table-driven)
if (hit(400, 96, 40)) { G.activeMoon = ((G.activeMoon + 2) % (G.moons.length + 1)) - 1; sounds.push({ frequency: 528 + Math.max(0, G.activeMoon) * 66, duration: 0.5, volume: 0.22, type: 'sine' }) } // the RULING moon — cycle rule
else if (G.moons.length > 1 && hit(338, 62, 28)) { const curM = dominantMoon(); G.activeMoon = G.moons.findIndex((m, i) => i !== curM); sounds.push({ frequency: 594, duration: 0.5, volume: 0.22, type: 'sine' }) } // the OTHER moon — click it to hand it the rule
} else if (inView(5)) {
if (G.latSolved && hit(256, 256, 46)) go(6) // the gold heart → dive into the orb
} else if (inView(6)) {
if (G.coreSolved && (G.coreGlow || 0) > 0.9 && hit(256, 256, 50)) go(7) // the woken heart → the orrery
else if (!G.coreSolved && hasMouse) {
// pick a ring by how far the click is from the centre, then turn its
// notch one step (30°). Bands match the draw radii (0.30/0.55/0.80).
const rn = Math.hypot(mx - 256, my - 256) / 256
let ring = -1
if (rn < 0.425) ring = 0
else if (rn < 0.675) ring = 1
else if (rn < 0.98) ring = 2
if (ring >= 0) {
G.core[ring] = (G.core[ring] + 1) % 12
sounds.push({ frequency: 330 + ring * 110, duration: 0.18, volume: 0.28, type: 'triangle' })
if (G.core[0] === 0 && G.core[1] === 0 && G.core[2] === 0) {
G.coreSolved = 1 // all three notches at the gate — the core wakes
sounds.push({ frequency: 264, duration: 2.0, volume: 0.40, type: 'sine' },
{ frequency: 396, duration: 2.0, volume: 0.34, type: 'sine' },
{ frequency: 528, duration: 2.4, volume: 0.30, type: 'sine' })
}
}
}
} else if (inView(1)) {
if (hit(432, 360, 26)) { G.notch = G.notch ? 0 : 1; sounds.push({ frequency: 120 + G.notch * 70, duration: 0.4, volume: 0.3, type: 'square' }) } // swing the notch targets 12 o'clock <-> 6 o'clock
if (hit(256, 261, 140)) { if (G.gate2) { go(13) } else if (G.door > 0.9) { go(2) } else { deny(256, 261) } } // the WHOLE doorway is the door (Galen's inspector clicks found the dead center); shut → it answers
// dials at grid (156,409) (222,409) (288,409) (354,409)
// dials always turn — you can dial in NEW combinations even after the gate
// has settled open (the door stays open once first solved; a re-match re-chimes)
for (let k = 0; k < 4; k++) {
if (hit(156 + k * 66.5, 409, 30)) {
G.dials[k] = (G.dials[k] + 1) % 4
sounds.push({ frequency: TONES[G.dials[k]], duration: 0.35, volume: 0.3, type: 'triangle' })
// the gate is a live lock: combo A opens the door, combo B opens the
// trapdoor, any wrong code throws the bolts. Sounds fire on transitions.
const mA = ANSWER_DIALS.every((v, i) => G.dials[i] === v)
const mB = dialsMatch(COMBO_B) && tideLow && G.notch === 1 // the real drowned-gate open
if (mA && !G.wasMatchA) sounds.push({ frequency: 55, duration: 2.2, volume: 0.7, type: 'sine' },
{ frequency: 110, duration: 1.6, volume: 0.35, type: 'sine' },
{ frequency: TONES[0], duration: 1.8, volume: 0.2, type: 'sine' })
if (!mA && G.wasMatchA) sounds.push({ frequency: 70, duration: 0.7, volume: 0.4, type: 'sine' }) // the bolts throw — the door closes
// combo-B feedback is the drowned-gate shudder (a wrong-but-down code at low
// tide sheds silt, rendered), not a typed-equality chime; only the true open rings
if (mB && !G.wasMatchB) sounds.push({ frequency: 174.61, duration: 1.8, volume: 0.5, type: 'sine' },
{ frequency: 261.63, duration: 1.6, volume: 0.35, type: 'sine' })
G.wasMatchA = mA; G.wasMatchB = mB
}
}
} else if (inView(2)) {
// pipes at x 130.5 + k*62.7, wide zone
if (G.organ < 0.5) for (let k = 0; k < 5; k++) {
const cx = 130.5 + k * 62.7
if (Math.abs(mx - cx) < 28 && my > 160 && my < 430) {
G.pipes[k] = (G.pipes[k] + 1) % 5
sounds.push({ frequency: PIPE_TONES[G.pipes[k]], duration: 0.8, volume: 0.35, type: 'triangle' },
{ frequency: PIPE_TONES[G.pipes[k]] * 2, duration: 0.4, volume: 0.10, type: 'sine' })
if (ANSWER_PIPES.every((v, i) => G.pipes[i] === v)) {
sounds.push({ frequency: 146.83, duration: 2.6, volume: 0.5, type: 'sine' },
{ frequency: 220.0, duration: 2.6, volume: 0.4, type: 'sine' },
{ frequency: 293.66, duration: 2.6, volume: 0.35, type: 'sine' },
{ frequency: 369.99, duration: 2.6, volume: 0.3, type: 'sine' })
G.organGo = true
}
}
}
} else if (inView(3)) {
} else if (inView(4)) {
} else if (inView(7)) {
} else if (inView(8)) {
if (!G.inv.gear8 && hit(150, 420, 36)) { G.inv.gear8 = true; sounds.push({ frequency: 523.25, duration: 0.4, volume: 0.32, type: 'triangle' }, { frequency: 784.0, duration: 0.25, volume: 0.2, type: 'sine' }) } // the creaking plank lifts → the 8t gear
else if (G.pearl && !G.pearlSet && hit(256, 300, 46)) { G.pearlSet = 1; G.crystalPowered = 1; sounds.push({ frequency: 174.61, duration: 1.4, volume: 0.4, type: 'sine' }, { frequency: 880.0, duration: 0.5, volume: 0.3, type: 'sawtooth' }, { frequency: 1760.0, duration: 1.6, volume: 0.22, type: 'sine' }) } // seat the pearl → the socket wakes and fires the beam up to the airship's crystal
else if (!G.pearl && !G.pearlSet && hit(256, 300, 46)) { deny(256, 300); sounds.push({ frequency: 130.81, duration: 0.4, volume: 0.2, type: 'sine' }) } // the empty socket — it waits for the tideglass pearl
else if (!G.obeliskUnlocked && hit(430, 300, 50)) { G.obeliskUnlocked = 1; sounds.push({ frequency: 55, duration: 1.2, volume: 0.3, type: 'sawtooth' }, { frequency: 73.42, duration: 1.2, volume: 0.25, type: 'sawtooth' }) } // the obelisk grinds — its slot leaves withdraw
else if (G.obeliskOpen > 0.8 && !G.inv.spanner && hit(430, 300, 36)) { G.inv.spanner = true; G.itemA = true; sounds.push({ frequency: 523.25, duration: 0.3, volume: 0.35, type: 'triangle' }, { frequency: 784.0, duration: 0.25, volume: 0.2, type: 'sine' }, { frequency: 98.0, duration: 0.8, volume: 0.25, type: 'sine' }) } // take the spanner from the obelisk slot
} else if (inView(9)) { // AIRSHIP DECK
if (hit(432, 256, 44)) { deny(432, 256) } // the hatch opens only with the SPANNER HELD — the nav chevron (shown when held) takes it; otherwise deny
else if (!G.engineOK && hit(256, 120, 52)) { deny(256, 120) }
else if (G.engineOK && !G.crystalPowered && hit(256, 120, 52)) { deny(256, 120); sounds.push({ frequency: 87.31, duration: 0.6, volume: 0.24, type: 'sine' }) } // the gears turn but the ship is dark — the crystal beneath the hull needs the pearl's beam
else if (G.act < 2 && G.engineOK && G.crystalPowered && hit(256, 120, 52)) { G.fly = 1; G.flyCine = 0.001; if (G.act < 2) onActComplete(1); sounds.push({ frequency: 130.81, duration: 2.4, volume: 0.5, type: 'sine' }, { frequency: 196.0, duration: 2.4, volume: 0.35, type: 'sine' }, { frequency: 261.63, duration: 2.8, volume: 0.25, type: 'sine' }) } // take the wheel — THE CROSSING plays (act advances only the first time; the wheel always flies)
} else if (inView(10)) { // ENGINE ROOM — THE GEARHEART
if (!G.inv.gear12 && G.held !== 1 && hit(90, 404, 38)) { deny(90, 404) } // seized — needs the spanner HELD
else if (!G.inv.gear12 && G.held === 1 && hit(90, 404, 38)) { G.inv.gear12 = true; sounds.push({ frequency: 196.0, duration: 0.5, volume: 0.35, type: 'sawtooth' }, { frequency: 392.0, duration: 0.3, volume: 0.2, type: 'sine' }) } // the spanner frees the 12t gear seized in old grease
else {
// the three gear seats: drop a held gear in (empty hand + filled seat lifts it back out)
const seatX = [176, 256, 336]
for (let s = 0; s < 3; s++) {
if (hit(seatX[s], 256, 46)) {
if (G.seats[s] === 0 && G.held < 2) { deny(seatX[s], 256) } // an empty seat wants a HELD gear
else if (G.seats[s] === 0 && G.held >= 2) { G.seats[s] = G.held; G.held = 0; sounds.push({ frequency: 220 + s * 40, duration: 0.3, volume: 0.3, type: 'triangle' }) }
else if (G.seats[s] !== 0 && G.held === 0) { G.held = G.seats[s]; G.seats[s] = 0; sounds.push({ frequency: 180 + s * 40, duration: 0.25, volume: 0.25, type: 'triangle' }) }
const gc = gearCadence(G.seats)
const ok = gc.full && Math.abs(gc.cadence - TARGET_CADENCE) <= TARGET_CADENCE * CAD_TOL && gc.dir === TARGET_DIR
if (ok && !G.engineOK) sounds.push({ frequency: 110, duration: 1.6, volume: 0.5, type: 'sine' }, { frequency: 220, duration: 1.4, volume: 0.3, type: 'sine' }, { frequency: 330, duration: 1.2, volume: 0.2, type: 'sine' })
G.engineOK = ok ? 1 : 0
break
}
}
}
} else if (inView(11)) { // THE MIRROR ISLAND
if (!G.inv.gear20 && G.held !== 1 && hit(346, 250, 54)) { deny(346, 250) } // the sluice wants the spanner HELD
else if (!G.inv.gear20 && G.held === 1 && hit(346, 250, 54)) { G.inv.gear20 = true; sounds.push({ frequency: 174.61, duration: 0.9, volume: 0.4, type: 'sawtooth' }, { frequency: 392.0, duration: 1.2, volume: 0.4, type: 'sine' }, { frequency: 587.33, duration: 1.0, volume: 0.25, type: 'sine' }) } // the mirrored sluice (reflection) → the 20t gear (the fire sun now comes from the orrery, Aug 10)
else if (G.inv.gear20 && hit(346, 250, 54)) { G.sluiceOpen = G.sluiceOpen ? 0 : 1; G.wheelAnim = 1; sounds.push({ frequency: 98, duration: 0.7, volume: 0.35, type: 'sawtooth' }, { frequency: G.sluiceOpen ? 220 : 147, duration: 1.1, volume: 0.25, type: 'sine' }) } // the freed wheel is the TIDE VALVE — click turns it: open drains the island, closed floods it
} else if (inView(13)) { // THE DROWNED QUARTER
// flooded while the tide stands high — the sluice valve (mirror isle) drains it
const drained = G.tideLevel < 0.2
if (!drained && hit(256, 316, 70)) { deny(256, 316); sounds.push({ frequency: 55, duration: 0.5, volume: 0.22, type: 'sine' }) } // a muffled glub — the water still owns it
else if (drained && !G.bellRung && hit(256, 316, 70)) { G.bellRung = 1; sounds.push({ frequency: 65.4, duration: 3.5, volume: 0.6, type: 'sine' }, { frequency: 130.8, duration: 3.0, volume: 0.4, type: 'sine' }, { frequency: 196.0, duration: 2.2, volume: 0.22, type: 'sine' }) } // THE DROWNED BELL tolls — heard through the whole island
else if (drained && G.bellRung && !G.pearl && hit(256, 266, 38)) { G.pearl = 1; sounds.push({ frequency: 1046.5, duration: 0.6, volume: 0.3, type: 'sine' }, { frequency: 1568, duration: 0.8, volume: 0.2, type: 'sine' }) } // the crown opens: THE TIDEGLASS PEARL, taken
} else if (inView(12)) { // THE THICKET / BRAMBLES
} else if (inView(14)) { // THE KILN — anneal the glass
if (!G.kilnBloom && hit(256, 290, 60)) { G.kilnBloom = 1; sounds.push({ frequency: 261.63, duration: 1.3, volume: 0.4, type: "sine" }, { frequency: 392.0, duration: 1.5, volume: 0.32, type: "sine" }, { frequency: 523.25, duration: 1.7, volume: 0.26, type: "sine" }) }
else if (G.kilnBloom && !G.summon && hit(256, 224, 44)) { G.summon = 0.001; sounds.push({ frequency: 110, duration: 0.9, volume: 0.3, type: "sine" }, { frequency: 880, duration: 0.9, volume: 0.25, type: "sine" }, { frequency: 60, duration: 0.14, volume: 0.5, type: "square" }, { frequency: 2200, duration: 0.12, volume: 0.3, type: "sawtooth" }) } // click the risen bloom → it fires the summons skyward
}
}
// obelisk slot easing (Aug 10): once clicked open, the stone leaves withdraw
G.obeliskOpen = Math.max(0, Math.min(1, (G.obeliskOpen || 0) + (G.obeliskUnlocked ? dt / 1.5 : -dt)))
// summoning clock: the bloom's call → bolt → portal → the ship emerges & moors
if (G.summon > 0 && G.summon < 1) {
G.summon = Math.min(1, G.summon + dt / 12)
if (G.summon >= 0.8 && !G.airshipArrived) { G.airshipArrived = 1; sounds.push({ frequency: 196.0, duration: 1.5, volume: 0.3, type: 'sine' }, { frequency: 294.0, duration: 1.5, volume: 0.22, type: 'sine' }) } // the distant horn — she has arrived
}
// ── the gate reflects the CURRENT code — combo A opens the door, a wrong code
// closes it (re-lockable); combo B opens the trapdoor to the drowned quarter ──
const gateMatchA = ANSWER_DIALS.every((v, i) => G.dials[i] === v)
G.gate2 = tideLow && dialsMatch(COMBO_B) && G.notch === 1 // low tide + combo B + notches DOWN
G.door = gateMatchA ? Math.min(1, G.door + dt * 0.4) : Math.max(0, G.door - dt * 0.5)
G.doorB = G.gate2 ? Math.min(1, (G.doorB || 0) + dt * 0.4) : Math.max(0, (G.doorB || 0) - dt * 0.5) // B-side opens on the same glide as A
if (G.door >= 1 && !G.gateChaptered) { G.gateChaptered = true; if (sim.trigger('tg-gate', true)) sim.completeChapter() }
if (G.organGo) { G.organ = Math.min(1, G.organ + dt * 0.5); G.star = Math.min(1, G.star + dt * 0.35) }
if (sim.trigger('tg-organ', G.organ >= 1)) sim.completeChapter()
// ── the lens ──
if (inView(3) && G.fin < 0.5) {
if (hasMouse && wd.mouse_down) {
const dx = mx - 256, dyUp = 430 - my
if (dyUp > 20) {
const az = Math.max(-1.15, Math.min(1.15, Math.atan2(dx, dyUp)))
G.lensA += (az - G.lensA) * Math.min(1, dt * 5)
}
}
const aligned = Math.abs(G.lensA - 0.25) < 0.075
if (aligned) {
const was = Math.floor(G.hold * 5)
G.hold = Math.min(1, G.hold + dt / 2.4)
if (Math.floor(G.hold * 5) > was) sounds.push({ frequency: 440 + G.hold * 440, duration: 0.15, volume: 0.2, type: 'sine' })
} else G.hold = Math.max(0, G.hold - dt * 0.6)
if (sim.trigger('tg-lens', G.hold >= 1)) {
G.fin = 0.0001; sim.completeChapter()
sounds.push({ frequency: 73.42, duration: 4.0, volume: 0.6, type: 'sine' },
{ frequency: 146.83, duration: 4.0, volume: 0.45, type: 'sine' },
{ frequency: 220.0, duration: 4.0, volume: 0.4, type: 'sine' },
{ frequency: 293.66, duration: 4.0, volume: 0.35, type: 'sine' },
{ frequency: 440.0, duration: 3.0, volume: 0.2, type: 'sine' })
}
}
// ── the orrery: grab a luminary, swing it round its ring; the tide answers ──
if (inView(7) && !G.orrerySolved) {
const sunR = 0.78, moonR = 0.52
if (hasMouse && wd.mouse_down) {
const ux = (mx - 256) / 256, uy = (my - 256) / 256
const r = Math.hypot(ux, uy)
if (!G.grab) G.grab = (Math.abs(r - sunR) <= Math.abs(r - moonR)) ? 'sun' : 'moon'
const ang = Math.atan2(ux, -uy) // 0 = up (matches vec2(sin,-cos) in the shader)
if (G.grab === 'sun') G.sunA = ang; else G.moonA = ang
} else G.grab = null
const wrap = a => Math.atan2(Math.sin(a), Math.cos(a))
const sunErr = Math.abs(wrap(G.sunA - Math.PI)) // 0 when the sun is set (bottom)
const moonErr = Math.abs(wrap(G.moonA)) // 0 when the moon is risen (top)
const sunDown = (1 - Math.cos(G.sunA)) / 2
const moonUp = (1 + Math.cos(G.moonA)) / 2
G.tide = Math.max(0, Math.min(1, 0.55 * (0.5 * sunDown + 0.5 * moonUp) + 0.45 * sunDown * moonUp))
const aligned = sunErr < 0.26 && moonErr < 0.26
if (aligned) {
const was = Math.floor((G.orreryHold || 0) * 5)
G.orreryHold = Math.min(1, (G.orreryHold || 0) + dt / 1.3)
if (Math.floor(G.orreryHold * 5) > was) sounds.push({ frequency: 330 + G.orreryHold * 500, duration: 0.16, volume: 0.22, type: 'sine' })
} else G.orreryHold = Math.max(0, (G.orreryHold || 0) - dt * 0.7)
if (sim.trigger('tg-orrery', (G.orreryHold || 0) >= 1)) {
G.orrerySolved = 1
if (!G.fireMoon) { raiseMoon(MOON_TABLE.fire); G.fireMoon = true } // the tide puzzle brings the FIRE SUN (Galen Aug 10)
G.dawnCine = 8 // the dawn holds the island still — nav locked while the sky turns (Galen Jul 30)
sounds.push({ frequency: 65.41, duration: 4.0, volume: 0.55, type: 'sine' },
{ frequency: 130.81, duration: 4.0, volume: 0.42, type: 'sine' },
{ frequency: 196.0, duration: 4.0, volume: 0.36, type: 'sine' },
{ frequency: 329.63, duration: 3.4, volume: 0.28, type: 'sine' },
{ frequency: 523.25, duration: 3.0, volume: 0.20, type: 'sine' })
}
} else if (inView(7)) {
// post-dawn the moon handle stays LIVE — drag it to pull the tide out/in (design §2 aha1)
if (G.orrerySolved && hasMouse && wd.mouse_down) {
const ux = (mx - 256) / 256, uy = (my - 256) / 256
G.moonA = Math.atan2(ux, -uy)
}
G.tide = 1
}
// ── finale: return to a transformed shore ──
if (G.fin > 0) {
G.fin = Math.min(1, G.fin + dt / 3.5)
// ONE-TIME whisk: when the finale blooms, sweep the player out of the lens
// to the shore to watch the world turn to night. After that, act two is free
// to roam — DON'T re-pin every frame (that yanked building→gate back to sea).
if (G.fin > 0.35 && !G.finReturned) { if (G.view !== 0) go(0); G.finReturned = true }
G.night = Math.min(1, G.night + dt / 6)
if (G.fin > 0.6) G.vault = Math.min(1, G.vault + dt / 8)
}
// ── music: one score per era, brightness follows the place ──
const era = (G.dawn || 0) > 0.3 ? 3 : (G.fin > 0.5 ? 2 : (G.organ > 0.5 ? 1 : 0))
if (G.musicAct !== era + 1) {
G.musicAct = era + 1
if (era === 0) wd.__play_music = { score: { bpm: 54, loop: true, gain: 0.30, tracks: [
{ inst: 'sine', gain: 0.5, cutoff: 320, a: 1.2, d: 2.5, notes: 'A2 . . . . . . . E2 . . . . . . . F2 . . . . . . . E2 . . . . . . .' },
{ inst: 'triangle', gain: 0.14, cutoff: 700, a: 0.8, d: 2.0, notes: '. . . . A3+C4 . . . . . . . . . . . . . . . G3+B3 . . . . . . . . . . .' },
] } }
if (era === 1) wd.__play_music = { score: { bpm: 58, loop: true, gain: 0.34, tracks: [
{ inst: 'sine', gain: 0.5, cutoff: 380, a: 1.0, d: 2.2, notes: 'D2 . . . . . . . A2 . . . . . . . B2 . . . . . . . A2 . . . . . . .' },
{ inst: 'triangle', gain: 0.18, cutoff: 900, a: 0.5, d: 1.6, notes: '. . D4 . . . F#4 . . . . . A4 . . . . . B3 . . . D4 . . . . . . . . .' },
] } }
if (era === 2) wd.__play_music = { score: { bpm: 64, loop: true, gain: 0.38, swing: 0.05, tracks: [
{ inst: 'sine', gain: 0.5, cutoff: 420, a: 0.8, d: 2.0, notes: 'D2 . . . A2 . . . B2 . . . F#2 . . . G2 . . . D2 . . . G2 . . . A2 . . .' },
{ inst: 'triangle', gain: 0.2, cutoff: 1200, a: 0.3, d: 1.2, notes: 'D4 . F#4 . A4 . B4 . A4 . F#4 . D4 . E4 . D4 . F#4 . A4 . D5 . A4 . F#4 . E4 . D4 .' },
{ inst: 'sawtooth', gain: 0.06, cutoff: 600, a: 1.5, d: 2.5, notes: 'D3+F#3+A3 . . . . . . . G3+B3+D4 . . . . . . . B2+D3+F#3 . . . . . . . A2+C#3+E3 . . . . . . .' },
] } }
if (era === 3) wd.__play_music = { score: { bpm: 60, loop: true, gain: 0.36, tracks: [ // the dawn — warm, major, rising
{ inst: 'sine', gain: 0.5, cutoff: 460, a: 1.0, d: 2.4, notes: 'G2 . . . . . . . D3 . . . . . . . C3 . . . . . . . D3 . . . . . . .' },
{ inst: 'triangle', gain: 0.2, cutoff: 1300, a: 0.4, d: 1.6, notes: 'G4 . B4 . D5 . B4 . C5 . E5 . D5 . B4 . A4 . C5 . B4 . G4 . D5 . B4 . G4 . D4 .' },
{ inst: 'sine', gain: 0.12, cutoff: 820, a: 1.6, d: 2.5, notes: 'G3+B3+D4 . . . . . . . C4+E4+G4 . . . . . . . D4+F#4+A4 . . . . . . . G3+B3+D4 . . . . . . .' },
] } }
}
// ── the observatory constellation (view 5): weave near a star to kindle it ──
if (G.latSolved) {
for (let i = 0; i < 5; i++) G.lat[i] = 1 // locked in — it holds, gold, forever
G.latBloom = 1
} else if (G.view === 5) {
const AX = [0, -0.6, 0.6, 0, 0]
const AY = [0, 0, 0, -0.6, 0.6]
const cux = hasMouse ? (mx - 256) / 256 : -99
const cuy = hasMouse ? (my - 256) / 256 : -99
let lit = 0
for (let i = 0; i < 5; i++) {
const d = Math.hypot(cux - AX[i], cuy - AY[i])
if (hasMouse && d < 0.22) G.lat[i] = Math.min(1, G.lat[i] + 0.06)
else G.lat[i] = Math.max(0, G.lat[i] - 0.0035)
if (G.lat[i] > 0.6) lit++
}
G.latBloom = lit >= 5 ? Math.min(1, (G.latBloom || 0) + 0.02) : Math.max(0, (G.latBloom || 0) - 0.01)
if (sim.trigger('tg-lattice', lit >= 5)) {
G.latSolved = 1 // it locks
sounds.push({ frequency: 264, duration: 1.6, volume: 0.34, type: 'sine' })
sounds.push({ frequency: 528, duration: 1.8, volume: 0.30, type: 'sine' }) // a fifth — the vault answers
}
} else {
// fade the weave when you leave the room, so re-entry starts calm
for (let i = 0; i < 5; i++) G.lat[i] = Math.max(0, G.lat[i] - dt * 0.5)
G.latBloom = Math.max(0, (G.latBloom || 0) - dt * 0.5)
}
// solving the constellation raises the vault-dome from the sea — a space opens
if (G.latSolved) G.vault = Math.min(1, (G.vault || 0) + dt / 7)
// waking the core lights it, and the light holds — it blooms once, forever
if (G.coreSolved) { for (let i = 0; i < 3; i++) G.core[i] = 0; G.coreGlow = Math.min(1, (G.coreGlow || 0) + dt / 2.5) }
// waking the orrery brings the dawn — the island's final turning, held forever
if (G.orrerySolved) {
G.sunA = Math.PI // the sun settles set; the moon stays LIVE for the tide
G.orreryGlow = Math.min(1, (G.orreryGlow || 0) + dt / 2.5)
G.dawn = Math.min(1, (G.dawn || 0) + dt / 9)
// one-time whisk out to the shore to witness the sunrise, then free to roam
if (G.dawn > 0.28 && !G.dawnReturned) { if (G.view !== 0) go(0); G.dawnReturned = true }
}
wd.music_mod = { brightness: 0.25 + (G.view === 0 ? 0.35 : G.view === 8 ? 0.28 : G.view === 3 ? 0.3 : (G.view === 6 || G.view === 7) ? 0.24 : 0.12) + G.fin * 0.3 + (G.coreGlow || 0) * 0.2 + (G.dawn || 0) * 0.3, gain: 1 }
if (sounds.length) wd.__play_sound = sounds
// ── publish the whiteboard ──
// gear-train: accumulate the crank phase hook-side (re-gearing never pops the spin),
// recompute the honest cadence, refresh the tide + thicket-burn from the moons.
G.enginePhase = (G.enginePhase || 0) + dt * CRANK_RATE
const gcNow = gearCadence(G.seats)
const shaftCadence = gcNow.cadence
G.engineOK = (gcNow.full && Math.abs(gcNow.cadence - TARGET_CADENCE) <= TARGET_CADENCE * CAD_TOL && gcNow.dir === TARGET_DIR) ? 1 : 0
const domFire = (G.moons[dominantMoon()] || {}).el === 'fire' // thicket burns only under the fire moon
G.thicketBurn = domFire ? Math.min(1, (G.thicketBurn || 0) + dt * 0.5) : Math.max(0, (G.thicketBurn || 0) - dt * 0.4)
// (tide is computed once, above — the sluice valve drives it)
const U = new Array(118).fill(0)
U[0] = G.view; U[1] = G.pv; U[2] = G.fade; U[3] = G.t
for (let i = 0; i < 4; i++) U[4 + i] = G.dials[i]
U[8] = G.door
for (let i = 0; i < 5; i++) U[9 + i] = G.pipes[i] / 4
U[14] = G.organ; U[15] = G.lensA; U[16] = G.hold; U[17] = G.fin
U[18] = G.door > 0 ? 1 : 0
for (let i = 0; i < 4; i++) U[19 + i] = G.flash[i]
U[23] = G.night; U[24] = sim.act; U[25] = G.clickP; U[26] = G.vault
U[27] = hasMouse ? mx : -100; U[28] = hasMouse ? my : -100
U[29] = G.star; U[30] = (hasMouse && wd.mouse_down) ? 1 : 0
U[31] = Math.max(G.organ, G.door * 0.25)
U[32] = G.latBloom || 0
for (let i = 0; i < 5; i++) U[33 + i] = G.lat[i]
U[38] = G.latSolved ? 1 : 0
U[39] = G.coreSolved ? 1 : 0
for (let i = 0; i < 3; i++) U[40 + i] = (G.core[i] / 12) * Math.PI * 2 // ring notch angles (rad)
U[43] = G.coreGlow || 0
U[44] = G.dawn || 0
U[45] = G.sunA; U[46] = G.moonA; U[47] = G.tide || 0
U[48] = G.orrerySolved ? 1 : 0
U[49] = G.orreryGlow || 0
U[50] = G.orreryHold || 0
// ── ACT 1 v2 flags (52–59 kept; 55 retired; 59 repurposed to sluice-freed) ──
U[51] = G.enginePhase // crank phase accumulator (gear-train spin, no-pop)
U[52] = G.doorB || 0 // drowned-gate opening, EASED (was a binary snap)
U[53] = G.inv.spanner ? 1 : 0 // spanner in the satchel (deck render)
U[54] = G.gate2 ? 1 : 0 // Mirror-Island route open
U[55] = 0 // RETIRED (was keyB) — reserved, do not reuse
U[56] = G.engineOK ? 1 : 0 // helm / deck power visual
U[57] = G.airshipArrived ? 1 : 0 // airship moored (summoned & arrived — Aug 10)
U[58] = G.act || 1 // 1 = island, 2 = flight begun
U[59] = G.inv.gear20 ? 1 : 0 // sluice-freed (the mirrored wheel has turned)
// ── the moons / gears / satchel / tide / thicket board (uni 60–77) ──
U[60] = G.tideLevel // tide01
U[61] = (G.inv.spanner ? 1 : 0) + (G.inv.gear8 && !inSeat(2) ? 2 : 0) + (G.inv.gear12 && !inSeat(3) ? 4 : 0) + (G.inv.gear16 && !inSeat(4) ? 8 : 0) + (G.inv.gear20 && !inSeat(5) ? 16 : 0) // invMask
U[62] = G.held // heldItem 0..5
U[63] = G.seats[0]; U[64] = G.seats[1]; U[65] = G.seats[2] // seat gear ids
U[66] = shaftCadence // true shaft cadence
U[67] = TARGET_CADENCE // the tide-bell target
U[68] = G.engineOK ? 1 : 0 // engineOK01
U[69] = (G.inv.spanner || G.inv.gear8 || G.inv.gear12 || G.inv.gear16 || G.inv.gear20) ? 1 : 0 // satchelOpen01
U[70] = G.inv.gear12 ? 1 : 0 // gear12free01 (grease broken)
U[71] = G.fly || 0 // fly01 (helm-pull anim)
U[72] = dominantMoon() // dominantMoonId
U[73] = G.moons.length // moonCount
U[74] = G.tideBaseline || 0 // tideBaseline01 (climbs per act)
U[75] = G.activeMoon >= 0 ? 1 : 0 // activeMoonSet01
U[76] = G.thicketBurn || 0 // thicketBurn01
U[77] = G.notch || 0 // drowned-gate notch (0 up · 1 down)
U[78] = ({ glass: 0, fire: 1 })[(G.moons[dominantMoon()] || {}).el] || 0 // ruling ELEMENT — sky + sea read it
G.elBlend = (G.elBlend || 0) + (U[78] - (G.elBlend || 0)) * Math.min(1, dt * 0.7)
U[82] = G.elBlend // the eased element morph — ONE DAY-style conversion driver
if (G.seaClock === undefined) G.seaClock = 1.0 + G.t * 0.55 // seamless takeover of the old clock
G.seaClock += dt * 0.55 * (1 - 0.78 * G.elBlend) // magma runs at 22% — rate morphs, phase NEVER jumps
U[83] = G.seaClock
if (G.deny) { G.deny.t = Math.max(0, G.deny.t - dt * 2.2); if (G.deny.t <= 0) G.deny = null }
U[79] = G.deny ? G.deny.t : 0; U[80] = G.deny ? G.deny.x : 0; U[81] = G.deny ? G.deny.y : 0 // deny flash
if (G.clickFx) { G.clickFx.t = Math.max(0, G.clickFx.t - dt * 2.8); if (G.clickFx.t <= 0) G.clickFx = null }
U[84] = G.clickFx ? G.clickFx.t : 0; U[85] = G.clickFx ? G.clickFx.x : 0; U[86] = G.clickFx ? G.clickFx.y : 0 // click-confirm
U[104] = G.kilnBloom ? 1 : 0 // KILN — the glass has bloomed
U[105] = (G.sluiceOpen ? 1.05 : 0) + G.wheelAnim * 2.5 // wheel angle: rest↔turned + the turning kick
U[106] = G.sluiceOpen ? 1 : 0 // TIDE VALVE open
U[107] = G.bellRung ? 1 : 0 // the drowned bell has tolled
U[108] = G.pearl ? 1 : 0 // the tideglass pearl is taken
U[109] = G.pearlSet ? 1 : 0 // pearl seated in the cave socket → it glows, the beam rises
U[110] = G.crystalPowered ? 1 : 0 // the airship crystal is lit (shore beam + deck helm live)
U[115] = G.summon || 0 // summoning clock 0..1 (bolt→portal→emerge→seal)
U[116] = G.airshipArrived ? 1 : 0 // the airship persists after the cine
U[117] = G.obeliskOpen || 0 // the sea-cave obelisk slot iris
U[113] = G.flyCine // Act-I finale cinematic timeline (0 off · 0..1 crossing · 1 card)
U[114] = G.inv.gear8 ? 1 : 0 // 8t gear EVER taken (satchel bit clears when seated — the cave plank must not haunt)
{ const _rm = (G.moons && G.moons[dominantMoon()]) || {} // ruling moon → the cave's ocean view matches the sky
U[111] = _rm.hue || 0
U[112] = (_rm.alt || 0) }
U[87] = G.inspect ? 1 : 0 // inspect-mode skin (blue cast + grid)
const navRows = NAV[G.view] || []
for (let ni = 0; ni < 4; ni++) {
const e = navRows[ni]
U[88 + ni * 4] = e ? e.x : 0; U[89 + ni * 4] = e ? e.y : 0
U[90 + ni * 4] = e ? e.d : 0; U[91 + ni * 4] = (e && (!e.when || e.when()) && !(G.flyCine > 0)) ? 1 : 0
}
if (G.inspect) {
const lines = [{ id: 'insp0', type: 'text', x: '14px', y: '14px', text: '◉ INSPECT ON (I) — clicks are documented for the AI', fontSize: '13px', color: '#7fe8d8' }]
const lg = G.clickLog || []
for (let li = 0; li < lg.length; li++) lines.push({ id: 'insp' + (li + 1), type: 'text', x: '14px', y: (34 + li * 18) + 'px', text: lg[lg.length - 1 - li], fontSize: '12px', color: li === 0 ? '#ffd9a0' : '#b8a888' })
wd.hud = lines
} else if (G.inspectWasOn) { wd.hud = [] }
G.inspectWasOn = !!G.inspect
// ACT I card subtitle (Galen): a quiet line under the mark while the card holds
if (!G.inspect) {
const showCard = (G.flyCine || 0) >= 0.82
if (showCard && !G.hudCard) wd.hud = [{ id: 'act1sub', type: 'text', x: '30%', y: '63%', text: 'stay tuned for updates', fontSize: '17px', color: '#e8c98a', css: { fontFamily: 'Georgia, "Times New Roman", serif', fontStyle: 'italic', letterSpacing: '0.34em', textShadow: '0 0 14px rgba(255,190,90,0.45)', opacity: '0.92' } }]
else if (!showCard && G.hudCard) wd.hud = []
G.hudCard = showCard ? 1 : 0
}
wd.gpuUniforms = U
} catch (e) { /* the island keeps its silence */ }