diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-16 15:49:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:44:42 -0300 |
| commit | 1551e409c31992437cb2fa864f576d45c8433801 (patch) | |
| tree | e2fae8451f87b735a1360c7c2e383fdc40165789 /src/gui/crt.zig | |
| parent | be2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff) | |
| download | pardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz pardes-1551e409c31992437cb2fa864f576d45c8433801.zip | |
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'src/gui/crt.zig')
| -rw-r--r-- | src/gui/crt.zig | 108 |
1 files changed, 83 insertions, 25 deletions
diff --git a/src/gui/crt.zig b/src/gui/crt.zig index 9cf81593..2a5ea7e4 100644 --- a/src/gui/crt.zig +++ b/src/gui/crt.zig @@ -6,38 +6,96 @@ const std = @import("std"); -/// MUST match BARREL in shaders/crt.frag.glsl -pub const barrel: f32 = 0.012; +/// MUST match BARREL in shaders/crt.frag.glsl. +pub const barrel: f32 = 0.016; pub const Point = struct { x: f32, y: f32 }; +pub const Effects = struct { crt: bool = false, ripple: bool = false, glitch: bool = false }; +pub const Frame = struct { effects: Effects = .{}, time_seconds: f32 = 0 }; -/// physical window pixels -> the scene pixels the shader displays there, -/// clamped to the window. The shader's phosphor quantization and chromatic -/// offset displace at most a few pixels — well under a cell — and are -/// ignored here. -pub fn map(x: f32, y: f32, w: f32, h: f32) Point { - const ccx = x / w * 2.0 - 1.0; - const ccy = y / h * 2.0 - 1.0; - const s = 1.0 + barrel * (ccx * ccx + ccy * ccy); +fn fract(x: f32) f32 { + return x - @floor(x); +} + +fn smoothstep(lo: f32, hi: f32, value: f32) f32 { + const t = std.math.clamp((value - lo) / (hi - lo), 0, 1); + return t * t * (3 - 2 * t); +} + +/// Match `sceneUv` in crt.frag.glsl. This is a forward screen→sample mapping: +/// a physical pointer is sent to the logical scene pixel displayed under it. +pub fn mapScene(x: f32, y: f32, w: f32, h: f32, effects: Effects, time_seconds: f32) ?Point { + const safe_w = @max(1, w); + const safe_h = @max(1, h); + const ccx = x / safe_w * 2.0 - 1.0; + const ccy = y / safe_h * 2.0 - 1.0; + const s = 1.0 + barrel * (ccx * ccx + ccy * ccy) * @as(f32, @floatFromInt(@intFromBool(effects.crt))); + var u = ccx * s * 0.5 + 0.5; + var v = ccy * s * 0.5 + 0.5; + // The CRT shader paints no scene when this barrel-mapped coordinate lies + // on or beyond the tube edge. There is no logical edge cell under those + // black pixels, so input must be inert there rather than clamped. + if (effects.crt and (u <= 0 or u >= 1 or v <= 0 or v >= 1)) return null; + if (effects.ripple) { + const cx = u - 0.5; + const cy = v - 0.5; + const radius = @sqrt(cx * cx + cy * cy); + const envelope = 1.0 - smoothstep(0.04, 0.72, radius); + const wave = @sin(radius * 42.0 - time_seconds * 5.2); + const len = @max(radius, 0.00001); + u += cx / len * wave * envelope * 0.0032; + v += cy / len * wave * envelope * 0.0032; + } + if (effects.glitch) { + const band = @floor(v * 96.0); + const tick = @floor(time_seconds * 13.0); + const seed = noiseAt(band, tick); + const tear = smoothstep(0.955, 1.0, seed); + u += (noiseAt(seed, band) - 0.5) * tear * 0.020; + } return .{ - .x = std.math.clamp((ccx * s + 1.0) * 0.5 * w, 0, w - 1), - .y = std.math.clamp((ccy * s + 1.0) * 0.5 * h, 0, h - 1), + .x = std.math.clamp(u * safe_w, 0, safe_w - 1), + .y = std.math.clamp(v * safe_h, 0, safe_h - 1), }; } +fn noiseAt(x: f32, y: f32) f32 { + var qx = fract(x * 0.1031); + var qy = fract(y * 0.1031); + var qz = fract(x * 0.1031); + const d = qx * (qy + 33.33) + qy * (qz + 33.33) + qz * (qx + 33.33); + qx += d; + qy += d; + qz += d; + return fract((qx + qy) * qz); +} + test "map matches the shader's barrel formula" { // center is a fixed point - try std.testing.expectApproxEqAbs(@as(f32, 500), map(500, 250, 1000, 500).x, 0.001); - try std.testing.expectApproxEqAbs(@as(f32, 250), map(500, 250, 1000, 500).y, 0.001); - // corners: cc=(±1,±1), r2=2, scale 1.024 pushes past the tube -> clamped - try std.testing.expectEqual(@as(f32, 0), map(0, 0, 1000, 500).x); - try std.testing.expectEqual(@as(f32, 0), map(0, 0, 1000, 500).y); - try std.testing.expectEqual(@as(f32, 999), map(1000, 500, 1000, 500).x); - try std.testing.expectEqual(@as(f32, 499), map(1000, 500, 1000, 500).y); - // (100,100) of 1000x500: cc=(-0.8,-0.6), r2=1.0, s=1.012 -> - // cc'=(-0.8096,-0.6072) -> (95.2, 98.2) - try std.testing.expectApproxEqAbs(@as(f32, 95.2), map(100, 100, 1000, 500).x, 0.01); - try std.testing.expectApproxEqAbs(@as(f32, 98.2), map(100, 100, 1000, 500).y, 0.01); - // (900,250): cc=(0.8,0), r2=0.64, s=1.00768 -> x'=903.072 - try std.testing.expectApproxEqAbs(@as(f32, 903.072), map(900, 250, 1000, 500).x, 0.01); + const center = mapScene(500, 250, 1000, 500, .{ .crt = true }, 0).?; + try std.testing.expectApproxEqAbs(@as(f32, 500), center.x, 0.001); + try std.testing.expectApproxEqAbs(@as(f32, 250), center.y, 0.001); + // Corners barrel beyond the tube and are black/inert in the shader. + try std.testing.expect(mapScene(0, 0, 1000, 500, .{ .crt = true }, 0) == null); + try std.testing.expect(mapScene(1000, 500, 1000, 500, .{ .crt = true }, 0) == null); + // (100,100) of 1000x500: cc=(-0.8,-0.6), r2=1.0, s=1.016 -> + // cc'=(-0.8128,-0.6096) -> (93.6, 97.6) + const near_edge = mapScene(100, 100, 1000, 500, .{ .crt = true }, 0).?; + try std.testing.expectApproxEqAbs(@as(f32, 93.6), near_edge.x, 0.01); + try std.testing.expectApproxEqAbs(@as(f32, 97.6), near_edge.y, 0.01); + // (900,250): cc=(0.8,0), r2=0.64, s=1.01024 -> x'=904.096 + const side = mapScene(900, 250, 1000, 500, .{ .crt = true }, 0).?; + try std.testing.expectApproxEqAbs(@as(f32, 904.096), side.x, 0.01); +} + +test "scene mapping composes ripple and deterministic glitch" { + const plain = mapScene(300, 200, 800, 600, .{}, 2.5).?; + try std.testing.expectApproxEqAbs(@as(f32, 300), plain.x, 0.001); + try std.testing.expectApproxEqAbs(@as(f32, 200), plain.y, 0.001); + const ripple = mapScene(300, 200, 800, 600, .{ .ripple = true }, 2.5).?; + try std.testing.expect(ripple.x != plain.x or ripple.y != plain.y); + try std.testing.expectEqual( + mapScene(300, 200, 800, 600, .{ .glitch = true }, 2.5).?, + mapScene(300, 200, 800, 600, .{ .glitch = true }, 2.5).?, + ); } |
