summaryrefslogtreecommitdiff
path: root/src/animation.zig
diff options
context:
space:
mode:
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);
- }
-};