summaryrefslogtreecommitdiff
path: root/src/animation.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 13:15:41 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commite0b5564f04aa0ae6666f29ef29c768d800fbabce (patch)
tree3477e8e37ab752e883a8442e97df0701a93af836 /src/animation.zig
parent30c2c8a154e770b3920bcfd7d553825fe9182bf4 (diff)
downloadpardes-e0b5564f04aa0ae6666f29ef29c768d800fbabce.tar.gz
pardes-e0b5564f04aa0ae6666f29ef29c768d800fbabce.zip
Step core animation by the shell's clock and sleep to the next wake
A shell now answers Host.now, its monotonic clock in ns, and the pump advances core animation to it: one .tick per whole 16 ms frame since core time last stood still. The same animation therefore takes the same time at 60, 120 and 144 Hz, over ssh and after a slow frame (the GUI's re-armed clock ran them ~25% slow at 144 Hz; the tty's post-frame sleep drifted). nextWake() lists every core animation in one place: a frame from now while anything moves, the end of the wait while something only waits (a message lingering, the look-hover delay), null when idle. Shells sleep exactly that long, and a wait is jumped to its end in one step, so an 800 ms linger costs one frame instead of fifty. An overshoot under 1.5 ms after a step is let go: at 60 and 120 Hz every display frame takes exactly one step (a 16 ms frame against a 16.67 ms vsync would otherwise double-step every 24th). The six tick drivers are gone: tty's timer thread only times the wait, interruptibly (a newer, shorter request cuts short a sleep still timing a longer one); the GUI's AnimationClock, web's JS tick bank (pardes_tick now takes rAF's timestamp), the detached server's and the board's ticks; grid mode steps a virtual clock straight to each wake. PARDES_TEST_CLOCK, set by the snapshot harness, gives tty and the detached server the same virtual clock. Every stepped frame is drawn. The old pump never drew the last frame of a fade (a .tick asks for no frame, and the fade was over by the check), so a theme switch stopped at 9/10 of the way until the next input: theme.golden, themesel.golden and the GUI's acme-light scene move to the theme's true colours, and nothing else changes. The frozen previous grid is captured only while a panel transition is chosen, not on every idle frame.
Diffstat (limited to 'src/animation.zig')
-rw-r--r--src/animation.zig251
1 files changed, 126 insertions, 125 deletions
diff --git a/src/animation.zig b/src/animation.zig
index b190c1a1..d6ef1ec3 100644
--- a/src/animation.zig
+++ b/src/animation.zig
@@ -588,157 +588,158 @@ test "dissolve has exact stable endpoints" {
}
}
-pub const Animation = struct {
- pub const frame_ms: u32 = 16;
- pub const frame_ns: u64 = frame_ms * std.time.ns_per_ms;
- pub const transition_steps: u16 = 10;
+pub const frame_ms: u32 = 16;
+pub const frame_ns: u64 = frame_ms * std.time.ns_per_ms;
+pub const transition_steps: u16 = 10;
- pub fn Transition(comptime Value: type) type {
- return struct {
- const Self = @This();
+/// A displayed value that fades from one target to the next over
+/// `transition_steps` frames; the chrome colours are one.
+pub fn Fade(comptime Value: type) type {
+ return struct {
+ const Self = @This();
- from: Value,
- to: Value,
- displayed: Value,
- step: u16 = transition_steps,
+ from: Value,
+ to: Value,
+ displayed: Value,
+ step: u16 = transition_steps,
- pub fn init(value: Value) Self {
- return .{ .from = value, .to = value, .displayed = value };
- }
-
- pub fn isActive(a: *const Self) bool {
- return a.step < transition_steps;
- }
+ pub fn init(value: Value) Self {
+ return .{ .from = value, .to = value, .displayed = value };
+ }
- pub fn retarget(a: *Self, target: Value) void {
- a.from = a.displayed;
- a.to = target;
- a.step = if (std.meta.eql(a.from, target)) transition_steps else 0;
- if (a.step == transition_steps) a.displayed = target;
- }
+ pub fn isActive(a: *const Self) bool {
+ return a.step < transition_steps;
+ }
- pub fn advance(a: *Self) void {
- if (!a.isActive()) return;
- a.step += 1;
- a.displayed = if (a.step == transition_steps)
- a.to
- else
- Value.interpolate(a.from, a.to, a.step, transition_steps);
- }
+ pub fn retarget(a: *Self, target: Value) void {
+ a.from = a.displayed;
+ a.to = target;
+ a.step = if (std.meta.eql(a.from, target)) transition_steps else 0;
+ if (a.step == transition_steps) a.displayed = target;
+ }
- pub fn snap(a: *Self, value: Value) void {
- a.* = init(value);
- }
- };
- }
+ pub fn advance(a: *Self) void {
+ if (!a.isActive()) return;
+ a.step += 1;
+ a.displayed = if (a.step == transition_steps)
+ a.to
+ else
+ Value.interpolate(a.from, a.to, a.step, transition_steps);
+ }
- pub fn Immediate(comptime Value: type) type {
- return struct {
- const Self = @This();
+ pub fn snap(a: *Self, value: Value) void {
+ a.* = init(value);
+ }
+ };
+}
- displayed: Value,
+pub fn Immediate(comptime Value: type) type {
+ return struct {
+ const Self = @This();
- pub fn init(value: Value) Self {
- return .{ .displayed = value };
- }
+ displayed: Value,
- pub fn isActive(_: *const Self) bool {
- return false;
- }
+ pub fn init(value: Value) Self {
+ return .{ .displayed = value };
+ }
- pub fn retarget(a: *Self, target: Value) void {
- a.displayed = target;
- }
+ pub fn isActive(_: *const Self) bool {
+ return false;
+ }
- pub fn advance(_: *Self) void {}
+ pub fn retarget(a: *Self, target: Value) void {
+ a.displayed = target;
+ }
- pub fn snap(a: *Self, value: Value) void {
- a.displayed = value;
- }
- };
- }
+ pub fn advance(_: *Self) void {}
- pub fn interpolateRgb(from: [3]u8, to: [3]u8, step: u16, steps: u16) [3]u8 {
- if (step == 0) return from;
- if (step >= steps) return to;
- var out: [3]u8 = undefined;
- for (&out, from, to) |*dst, a, b| {
- const numerator = @as(u32, a) * (steps - step) + @as(u32, b) * step;
- dst.* = @intCast((numerator + steps / 2) / steps);
+ pub fn snap(a: *Self, value: Value) void {
+ a.displayed = value;
}
- return out;
+ };
+}
+
+pub fn interpolateRgb(from: [3]u8, to: [3]u8, step: u16, steps: u16) [3]u8 {
+ if (step == 0) return from;
+ if (step >= steps) return to;
+ var out: [3]u8 = undefined;
+ for (&out, from, to) |*dst, a, b| {
+ const numerator = @as(u32, a) * (steps - step) + @as(u32, b) * step;
+ dst.* = @intCast((numerator + steps / 2) / steps);
}
+ return out;
+}
- const TestColor = struct {
- rgb: [3]u8,
+const TestColor = struct {
+ rgb: [3]u8,
- pub fn interpolate(from: TestColor, to: TestColor, step: u16, steps: u16) TestColor {
- return .{ .rgb = interpolateRgb(from.rgb, to.rgb, step, steps) };
- }
- };
+ pub fn interpolate(from: TestColor, to: TestColor, step: u16, steps: u16) TestColor {
+ return .{ .rgb = interpolateRgb(from.rgb, to.rgb, step, steps) };
+ }
+};
- test "Immediate lands where a completed Transition lands" {
- const from: TestColor = .{ .rgb = .{ 240, 3, 90 } };
- const to: TestColor = .{ .rgb = .{ 5, 222, 90 } };
+test "Immediate lands where a completed Transition lands" {
+ const from: TestColor = .{ .rgb = .{ 240, 3, 90 } };
+ const to: TestColor = .{ .rgb = .{ 5, 222, 90 } };
- var faded = Animation.Transition(TestColor).init(from);
- faded.retarget(to);
- for (0..transition_steps) |_| faded.advance();
+ var faded = Fade(TestColor).init(from);
+ faded.retarget(to);
+ for (0..transition_steps) |_| faded.advance();
- var instant = Immediate(TestColor).init(from);
- try std.testing.expect(!instant.isActive());
- instant.retarget(to);
- try std.testing.expectEqual(faded.displayed, instant.displayed);
+ var instant = Immediate(TestColor).init(from);
+ try std.testing.expect(!instant.isActive());
+ instant.retarget(to);
+ try std.testing.expectEqual(faded.displayed, instant.displayed);
- // Never active, so a frontend that renders only while something is animating stops immediately
- // rather than spending ten frames discovering there is nothing to draw.
- try std.testing.expect(!instant.isActive());
- instant.advance();
- try std.testing.expectEqual(to, instant.displayed);
+ // Never active, so a frontend that renders only while something is animating stops immediately
+ // rather than spending ten frames discovering there is nothing to draw.
+ try std.testing.expect(!instant.isActive());
+ instant.advance();
+ try std.testing.expectEqual(to, instant.displayed);
- instant.snap(from);
- try std.testing.expectEqual(from, instant.displayed);
- }
+ instant.snap(from);
+ try std.testing.expectEqual(from, instant.displayed);
+}
- test "fixed-step interpolation has exact monotonic endpoints" {
- const Tween = Animation.Transition(TestColor);
- const from: TestColor = .{ .rgb = .{ 240, 3, 90 } };
- const to: TestColor = .{ .rgb = .{ 5, 222, 90 } };
- var tween = Tween.init(from);
- tween.retarget(to);
- try std.testing.expectEqual(from, tween.displayed);
+test "fixed-step interpolation has exact monotonic endpoints" {
+ const Tween = Fade(TestColor);
+ const from: TestColor = .{ .rgb = .{ 240, 3, 90 } };
+ const to: TestColor = .{ .rgb = .{ 5, 222, 90 } };
+ var tween = Tween.init(from);
+ tween.retarget(to);
+ try std.testing.expectEqual(from, tween.displayed);
- var previous = tween.displayed;
- for (0..transition_steps) |_| {
- tween.advance();
- try std.testing.expect(tween.displayed.rgb[0] <= previous.rgb[0]);
- try std.testing.expect(tween.displayed.rgb[1] >= previous.rgb[1]);
- try std.testing.expectEqual(@as(u8, 90), tween.displayed.rgb[2]);
- previous = tween.displayed;
- }
- try std.testing.expect(!tween.isActive());
- try std.testing.expectEqual(to, tween.displayed);
+ var previous = tween.displayed;
+ for (0..transition_steps) |_| {
tween.advance();
- try std.testing.expectEqual(to, tween.displayed);
+ try std.testing.expect(tween.displayed.rgb[0] <= previous.rgb[0]);
+ try std.testing.expect(tween.displayed.rgb[1] >= previous.rgb[1]);
+ try std.testing.expectEqual(@as(u8, 90), tween.displayed.rgb[2]);
+ previous = tween.displayed;
}
+ try std.testing.expect(!tween.isActive());
+ try std.testing.expectEqual(to, tween.displayed);
+ tween.advance();
+ try std.testing.expectEqual(to, tween.displayed);
+}
- test "retarget starts at the currently displayed value" {
- const Tween = Animation.Transition(TestColor);
- const first: TestColor = .{ .rgb = .{ 0, 40, 200 } };
- const second: TestColor = .{ .rgb = .{ 200, 140, 0 } };
- const third: TestColor = .{ .rgb = .{ 20, 10, 250 } };
- var tween = Tween.init(first);
- tween.retarget(second);
- tween.advance();
- tween.advance();
- tween.advance();
- const on_screen = tween.displayed;
+test "retarget starts at the currently displayed value" {
+ const Tween = Fade(TestColor);
+ const first: TestColor = .{ .rgb = .{ 0, 40, 200 } };
+ const second: TestColor = .{ .rgb = .{ 200, 140, 0 } };
+ const third: TestColor = .{ .rgb = .{ 20, 10, 250 } };
+ var tween = Tween.init(first);
+ tween.retarget(second);
+ tween.advance();
+ tween.advance();
+ tween.advance();
+ const on_screen = tween.displayed;
+
+ tween.retarget(third);
+ try std.testing.expectEqual(on_screen, tween.from);
+ try std.testing.expectEqual(on_screen, tween.displayed);
+ try std.testing.expect(tween.isActive());
+ for (0..transition_steps) |_| tween.advance();
+ try std.testing.expectEqual(third, tween.displayed);
+}
- tween.retarget(third);
- try std.testing.expectEqual(on_screen, tween.from);
- try std.testing.expectEqual(on_screen, tween.displayed);
- try std.testing.expect(tween.isActive());
- for (0..transition_steps) |_| tween.advance();
- try std.testing.expectEqual(third, tween.displayed);
- }
-};