summaryrefslogtreecommitdiff
path: root/src/animation.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/animation.zig')
-rw-r--r--src/animation.zig65
1 files changed, 65 insertions, 0 deletions
diff --git a/src/animation.zig b/src/animation.zig
index aef6fe6d..c22396b8 100644
--- a/src/animation.zig
+++ b/src/animation.zig
@@ -60,6 +60,44 @@ pub fn Transition(comptime Value: type) type {
};
}
+/// `Transition`'s interface with the animation taken OUT: a value that is only ever the one it was
+/// last set to.
+///
+/// This exists so that a build which never fades does not carry the machinery for fading. A runtime
+/// flag around the same `Transition` cannot achieve that - the endpoints stay in the struct and
+/// `Value.interpolate` stays in the binary, reachable and therefore emitted. Selecting a different
+/// type at comptime is what makes the interpolator genuinely unreachable, and on a target whose whole
+/// display is a 115200-baud serial line, absent code and unspent frames are the same saving twice.
+///
+/// Every method here is the trivial one, and `retarget` is deliberately `snap` rather than an error:
+/// callers ask for a new palette and get it, on the next frame, in one step. Nothing about the
+/// interface says how many frames the arrival takes.
+pub fn Immediate(comptime Value: type) type {
+ return struct {
+ const Self = @This();
+
+ displayed: Value,
+
+ pub fn init(value: Value) Self {
+ return .{ .displayed = value };
+ }
+
+ pub fn isActive(_: *const Self) bool {
+ return false;
+ }
+
+ pub fn retarget(a: *Self, target: Value) void {
+ a.displayed = target;
+ }
+
+ pub fn advance(_: *Self) void {}
+
+ pub fn snap(a: *Self, value: Value) void {
+ a.displayed = value;
+ }
+ };
+}
+
/// Linear RGB interpolation with nearest-integer rounding. The weighted-sum
/// form stays unsigned for both rising and falling channels.
pub fn interpolateRgb(from: [3]u8, to: [3]u8, step: u16, steps: u16) [3]u8 {
@@ -81,6 +119,33 @@ const TestColor = struct {
}
};
+// The substitute has to be interchangeable, and the property that matters is the one a caller could
+// otherwise get wrong: it must arrive at the SAME palette a completed fade arrives at. A fade whose
+// endpoint differed by a rounding step would make the build option a visible change of colors rather
+// than a change of how long they take.
+test "Immediate lands where a completed Transition lands" {
+ const from: TestColor = .{ .rgb = .{ 240, 3, 90 } };
+ const to: TestColor = .{ .rgb = .{ 5, 222, 90 } };
+
+ var faded = Transition(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);
+
+ // 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);
+}
+
test "fixed-step interpolation has exact monotonic endpoints" {
const Tween = Transition(TestColor);
const from: TestColor = .{ .rgb = .{ 240, 3, 90 } };