summaryrefslogtreecommitdiff
path: root/src/animation.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/animation.zig')
-rw-r--r--src/animation.zig62
1 files changed, 45 insertions, 17 deletions
diff --git a/src/animation.zig b/src/animation.zig
index 713a8a30..19f7ce0b 100644
--- a/src/animation.zig
+++ b/src/animation.zig
@@ -313,11 +313,18 @@ pub fn cellNoise(serial: u32, col: u16, row: u16) f32 {
/// Whether a changed dissolve cell has crossed from the frozen old grid to
/// the new one. Exact endpoints are part of the presentation contract.
+/// T4 (§9.2): an ordered swap, each cell at its place in a 4x4 Bayer
+/// matrix (shifted by the pane's serial so two panes differ), so the new
+/// content comes in as an even, patterned screen rather than noise, and
+/// each cell swaps once.
pub fn dissolveRevealed(serial: u32, col: u16, row: u16, raw_progress: f32) bool {
const t = std.math.clamp(raw_progress, 0.0, 1.0);
if (t <= 0) return false;
if (t >= 1) return true;
- return cellNoise(serial, col, row) < t;
+ const bayer = [4][4]u8{ .{ 0, 8, 2, 10 }, .{ 12, 4, 14, 6 }, .{ 3, 11, 1, 9 }, .{ 15, 7, 13, 5 } };
+ const x = (@as(u32, col) +% serial) % 4;
+ const y = (@as(u32, row) +% serial / 4) % 4;
+ return (@as(f32, @floatFromInt(bayer[y][x])) + 0.5) / 16 < t;
}
pub const CellArea = struct {
@@ -352,6 +359,9 @@ pub const CharSource = union(enum) {
/// that cell holds — a caret marching over empty space is still a caret.
byte: u8,
churn: u8,
+ /// The destination cell with its colours still this far (0..1) toward
+ /// the frozen old cell's ground: a soft edge between the two grids.
+ edge: f32,
pub const Offset = struct { cols: i32 = 0, rows: i32 = 0 };
@@ -383,12 +393,16 @@ pub fn charSource(track: Track, col: u16, row: u16, area: CellArea) CharSource {
.wave => .{ .at = .{
.rows = cellsOf(remaining * @min(4.0, h) * @sin(c * 0.55 - t * 9.0)),
} },
- // A curtain of glyphs marches in from the right, column by column, left
- // to right; each column still has a short slide of its own.
+ // A wipe, left to right: the new grid behind the head, the old one
+ // ahead of it, and between them a soft edge two cells wide whose
+ // colours step from the old ground to the new. Only the head's
+ // columns change from one frame to the next.
.curtain => blk: {
- const lead = t * (w + 1.0) - c;
+ const lead = t * (w + 2.0) - c;
if (lead <= 0.0) break :blk .old;
- break :blk .{ .at = .{ .cols = -cellsOf(@max(0.0, 3.0 - lead)) } };
+ if (lead <= 1.0) break :blk .{ .edge = 2.0 / 3.0 };
+ if (lead <= 2.0) break :blk .{ .edge = 1.0 / 3.0 };
+ break :blk .settled;
},
// Every cell churns through printable ASCII and locks onto its final
// glyph at its own stable threshold: the pane resolves out of noise.
@@ -517,7 +531,7 @@ test "each character effect moves glyphs along its own axis" {
var col: u16 = 0;
while (col < area.cols) : (col += 1) switch (charSource(fall, col, 0, area)) {
.old => {},
- .byte, .churn => return error.FallShouldNotChurn,
+ .byte, .churn, .edge => return error.FallShouldNotChurn,
.at => |offset| {
try std.testing.expectEqual(@as(i32, 0), offset.cols);
try std.testing.expect(offset.rows >= 0);
@@ -539,20 +553,24 @@ test "each character effect moves glyphs along its own axis" {
}
try std.testing.expect(above and below);
- // The curtain has a head: columns behind it hold the old grid, columns the
- // head has passed are settled, and the head itself is still sliding.
+ // The curtain wipes: columns ahead of its head hold the old grid,
+ // columns it has passed are settled, and between them a soft edge of
+ // exactly two columns steps from the old ground to the new.
const curtain: Track = .{ .effect = .curtain, .frame = 5, .to = box };
try std.testing.expectEqual(CharSource.settled, charSource(curtain, 0, 0, area));
try std.testing.expectEqual(CharSource{ .old = {} }, charSource(curtain, 29, 0, area));
- var sliding = false;
+ var wiped: [2]f32 = undefined;
+ var at_edge: usize = 0;
col = 0;
while (col < area.cols) : (col += 1) switch (charSource(curtain, col, 0, area)) {
- .at => |offset| if (offset.cols < 0) {
- sliding = true;
+ .edge => |weight| {
+ wiped[at_edge] = weight;
+ at_edge += 1;
},
- .old, .byte, .churn => {},
+ .old, .at, .byte, .churn => {},
};
- try std.testing.expect(sliding);
+ try std.testing.expectEqual(@as(usize, 2), at_edge);
+ try std.testing.expect(wiped[0] < wiped[1]);
var scramble: Track = .{ .effect = .scramble, .frame = 3, .to = box };
var churning: usize = 0;
@@ -566,12 +584,12 @@ test "each character effect moves glyphs along its own axis" {
scramble.frame = 4;
switch (charSource(scramble, col, 0, area)) {
.churn => |next| changed = changed or next != byte,
- .old, .at, .byte => {},
+ .old, .at, .byte, .edge => {},
}
scramble.frame = 3;
},
.at => locked += 1,
- .old, .byte => return error.ScrambleShouldNotFreeze,
+ .old, .byte, .edge => return error.ScrambleShouldNotFreeze,
};
try std.testing.expect(churning > 0 and locked > 0 and changed);
@@ -591,7 +609,7 @@ test "each character effect moves glyphs along its own axis" {
try std.testing.expectEqual(@as(u8, '_'), byte);
carets += 1;
},
- .old, .at, .churn => {},
+ .old, .at, .churn, .edge => {},
};
}
try std.testing.expectEqual(@as(usize, 1), carets);
@@ -624,6 +642,16 @@ test "opening presets separate geometry and content transitions" {
try std.testing.expect(closing.amount() < progress(.vertical, closing.frame));
}
+test "the ordered dissolve reveals a 4x4 block evenly, each cell once" {
+ for ([_]f32{ 0.25, 0.5, 0.75 }) |t| {
+ var revealed: usize = 0;
+ for (0..4) |y| for (0..4) |x| {
+ if (dissolveRevealed(7, @intCast(x), @intCast(y), t)) revealed += 1;
+ };
+ try std.testing.expectEqual(@as(usize, @intFromFloat(t * 16)), revealed);
+ }
+}
+
test "dissolve has exact stable endpoints" {
for (0..64) |col| {
const x: u16 = @intCast(col);
@@ -974,7 +1002,7 @@ pub const CursorGlide = struct {
}
}
- fn centreAt(glide: *const CursorGlide, now_ns: u64) [2]f32 {
+ pub fn centreAt(glide: *const CursorGlide, now_ns: u64) [2]f32 {
var c: [2]f32 = .{ 0, 0 };
for (glide.corners) |corner| for (&c, corner) |*sum, axis| {
sum.* += axis.value(now_ns) / 4;