diff options
Diffstat (limited to 'src/animation.zig')
| -rw-r--r-- | src/animation.zig | 251 |
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); - } -}; |
