diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-26 12:00:20 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-26 12:00:20 -0300 |
| commit | 3b8ee10301e0b83013a38d93516a345622a93aaa (patch) | |
| tree | 5e024ce79f1d470cddb63a4fc23ef72dbbc38328 /src/animation.zig | |
| parent | 18a8e39c551e08eca9ed26e2c13580ccdd36b955 (diff) | |
| download | pardes-3b8ee10301e0b83013a38d93516a345622a93aaa.tar.gz pardes-3b8ee10301e0b83013a38d93516a345622a93aaa.zip | |
Make the chrome fade a build option, and compile it out for the board
A theme change moves the anchored chrome palette - taglines, boxes, line numbers, scroll
bars - from the old colors to the new ones over ten display frames. On a screen that
repaints in microseconds that is a short legible transition, and it is why the code exists:
a palette that teleports reads as a glitch.
On a 115200 serial line it is not a fade. Each of the ten steps recolors every anchored
cell, so the diff finds the whole chrome dirty and spends a frame's worth of wire on it, ten
times over, with nothing else on screen to look at. Measured on the die, one `NextColor`:
fade on 12,593 bytes 1,097 ms of saturated wire
fade off 2,425 bytes 215 ms
A second of the editor talking to itself about a color, on the one transport where a second
is noticeable, for a gradient nobody can watch arrive at 11.5 KB/s.
## Comptime, so the code is not there
`ChromeAnimation` now selects between `animation.Transition` and a new `animation.Immediate`
- the same interface with the animation taken out, a value that is only ever what it was
last set to. That is what makes `ChromeTheme.interpolate` unreachable, and unreachable is
what makes it absent: the flashed image drops 2,336 bytes, and the object 13,180.
A bool tested at runtime would have kept every one of those bytes and still paid the
branch. It also would have needed a second meaning bolted onto `animate_theme_changes`,
whose job is the startup window and nothing else; that field is untouched here.
The option is `-Dtheme-animation`, defaulting to off for `p4` and on everywhere else, and it
is an option rather than a platform test because "is a frame expensive" is a property of the
transport: a P4 driven over something faster than a UART would want the fade back, and
`-Dtheme-animation=true` gives it to them.
## What was checked
`Immediate` is new code with one contract worth pinning, and it is the one a caller could
get wrong: it must arrive at the SAME palette a completed fade arrives at. An endpoint that
differed by a rounding step would make the option a change of colors rather than a change of
how long they take. Tested against a fully advanced `Transition` in `animation.zig`.
Full suite: unit-test, snap 95/95, hxdiff 481/0, hxparity 561/0, image-harness, pdf-harness,
mupdf-check. Builds: tty, p4, gui, and tty/gui with the fade forced off. On the die the
canonical verifier reports the screen IDENTICAL across both arms - the workload contains no
theme change, so this is the check that ordinary rendering was not perturbed - and
`p4-bench --check` stays 4/4.
Diffstat (limited to 'src/animation.zig')
| -rw-r--r-- | src/animation.zig | 65 |
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 } }; |
