diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/animation.zig | 65 | ||||
| -rw-r--r-- | src/pardes.zig | 23 |
2 files changed, 87 insertions, 1 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 } }; diff --git a/src/pardes.zig b/src/pardes.zig index 691ee345..9f20e0a3 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -68,6 +68,17 @@ pub const frameMark = tracy.frameMark; pub const Platform = enum { tty, gui, web, macos, p4 }; pub const platform: Platform = @field(Platform, @tagName(@import("pardes_config").platform)); +/// Whether a theme change FADES the anchored chrome palette or replaces it. Ten display frames +/// either way (`animation.transition_steps`), and on every screen but one that is a short legible +/// transition rather than a glitch. +/// +/// The exception is a screen reached through a UART. 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, for a fade nobody can see arrive gradually anyway. Off by default for `p4` and settable +/// either way from the build, because the thing that makes it wrong is the transport rather than the +/// target - see `build.zig`. +pub const theme_animation = @import("pardes_config").theme_animation; + /// A build with no host but its display: the embedded source filesystem, the /// in-process clipboard, silent ptys. Comptime, and its own option module /// rather than a `pardes_config` field, because it is the one setting that @@ -2436,7 +2447,13 @@ pub const ChromeTheme = struct { } }; -const ChromeAnimation = animation.Transition(ChromeTheme); +/// The fade, or its absence, decided at comptime so that `-Dtheme-animation=false` leaves +/// `ChromeTheme.interpolate` unreachable and therefore out of the binary entirely. Every call site +/// below is written against the shared interface and needs no condition of its own. +const ChromeAnimation = if (theme_animation) + animation.Transition(ChromeTheme) +else + animation.Immediate(ChromeTheme); const initial_chrome = ChromeTheme.fromTheme(&themes[0]); // ---- the boundary types ---- @@ -5548,6 +5565,10 @@ pub const Pardes = struct { chrome_animation: ChromeAnimation = ChromeAnimation.init(initial_chrome), /// False only while startup configuration or a dump restore is selecting /// its first theme. No frame is rendered in that interval. + /// + /// Nothing to do with `-Dtheme-animation`: that is decided by the type of + /// `chrome_animation`, so a build without the fade does not carry a flag + /// saying so. animate_theme_changes: bool = false, /// Native pixel attachments supported by the shell (Kitty graphics in a /// terminal, GPU textures in SDL). Image panes dynamically fall back to |
