1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
//! The Crt builtin's screen geometry, shared between the gui shell's input
//! mapping and its shader: crt.frag.glsl shows the scene point
//! uv' = cc*(1 + barrel*r2) at screen point cc, so a physical mouse position
//! must pass through the SAME forward transform before it becomes a cell.
//! Pure std, so the unit-test step can pin it against the shader formula.
const std = @import("std");
/// 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 };
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(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
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).?,
);
}
|