summaryrefslogtreecommitdiff
path: root/src/panel_animation.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-17 16:12:56 -0300
committerGabriel Schneider <[email protected]>2026-08-18 23:45:04 -0300
commit31fece62f56aa2311e2325de83659edbc9e641db (patch)
tree13eee1e542d9670ad3fb7badad353265121d668f /src/panel_animation.zig
parent1551e409c31992437cb2fa864f576d45c8433801 (diff)
downloadpardes-31fece62f56aa2311e2325de83659edbc9e641db.tar.gz
pardes-31fece62f56aa2311e2325de83659edbc9e641db.zip
animation: core publishes transition records; gui evaluates via shaders, tty over grid cells
Diffstat (limited to 'src/panel_animation.zig')
-rw-r--r--src/panel_animation.zig101
1 files changed, 41 insertions, 60 deletions
diff --git a/src/panel_animation.zig b/src/panel_animation.zig
index 045d2a87..0451213d 100644
--- a/src/panel_animation.zig
+++ b/src/panel_animation.zig
@@ -1,13 +1,17 @@
//! Backend-neutral vocabulary and math for pane transitions.
//!
-//! The core publishes plain transition data. GUI shells evaluate that data in
-//! shaders; the TTY shell evaluates the same progress over cells in its grid.
-//! There are deliberately no callbacks or backend objects here.
+//! The core publishes plain transition data and composes semantic PanelAscii
+//! bytes itself. GUI shells evaluate geometry/dissolve data in shaders; the
+//! TTY shell evaluates the same records over cells in its grid. There are
+//! deliberately no callbacks or backend objects here.
const std = @import("std");
+pub const ascii_max_movement_frames: u16 = 12;
+
pub const Easing = enum(u8) {
linear,
smooth,
+ in_cubic,
out_cubic,
out_back,
};
@@ -38,7 +42,10 @@ pub const Transition = enum(u8) {
.slide => 12,
.zoom => 14,
.dissolve => 10,
- .ascii => 16,
+ // Frame zero is the exact old byte. Core's AsciiDiff caps a long
+ // byte walk at twelve eased movement samples, including the exact
+ // destination; nearby bytes still move one value at a time.
+ .ascii => ascii_max_movement_frames + 1,
.vertical => 12,
};
}
@@ -89,16 +96,27 @@ pub const Track = extern struct {
effect: Transition = .off,
_padding: u8 = 0,
frame: u16 = 0,
- _frame_padding: u16 = 0,
+ /// Core-computed duration for data-dependent effects. Zero selects the
+ /// effect preset; PanelAscii fills this from the longest eased byte walk
+ /// in the pane's semantic cell diff.
+ frame_count: u16 = 0,
from: Box = .{},
to: Box = .{},
pub fn active(track: Track) bool {
- return track.effect != .off and track.frame < track.effect.frames();
+ return track.effect != .off and track.frame < track.frames();
+ }
+
+ pub fn frames(track: Track) u16 {
+ return if (track.frame_count != 0) track.frame_count else track.effect.frames();
}
pub fn amount(track: Track) f32 {
- return progress(track.effect, track.frame);
+ // Opening rises quickly and settles; closing reverses that motion and
+ // accelerates down out of the fixed clip.
+ if (track.phase == .closing and track.effect == .vertical)
+ return progressEased(.in_cubic, track.frames(), track.frame);
+ return progressEased(track.effect.easing(), track.frames(), track.frame);
}
pub fn presented(track: Track) Box {
@@ -153,14 +171,15 @@ pub fn openingBox(effect: Transition, target: Box, screen_width: u16) Box {
};
}
-/// Destination for a lifecycle-only closing track. The old panel rises out
-/// through its own fixed clip; surviving panes are already at canonical
-/// geometry underneath it and never receive tracks for this effect.
+/// Destination for a lifecycle-only closing track. The old panel drops out
+/// through its own fixed clip, reversing the opening path; surviving panes
+/// are already at canonical geometry underneath it and never receive tracks
+/// for this effect.
pub fn closingBox(effect: Transition, source: Box) Box {
return switch (effect) {
.vertical => blk: {
var to = source;
- to.y -= source.h;
+ to.y += source.h;
break :blk to;
},
else => source,
@@ -172,6 +191,7 @@ pub fn sample(easing: Easing, raw: f32) f32 {
return switch (easing) {
.linear => t,
.smooth => t * t * (3.0 - 2.0 * t),
+ .in_cubic => t * t * t,
.out_cubic => 1.0 - (1.0 - t) * (1.0 - t) * (1.0 - t),
// Robert Penner's ease-out-back polynomial. It intentionally travels
// a little past one before settling exactly on the endpoint.
@@ -185,12 +205,15 @@ pub fn sample(easing: Easing, raw: f32) f32 {
}
pub fn progress(effect: Transition, frame: u16) f32 {
- const frames = effect.frames();
+ return progressEased(effect.easing(), effect.frames(), frame);
+}
+
+fn progressEased(easing: Easing, frames: u16, frame: u16) f32 {
if (frames <= 1 or frame >= frames - 1) return 1.0;
// `frames` is the number of presented samples, including both exact
// endpoints. This makes the last active frame the real final image rather
// than 15/16 followed by an unrendered snap to canonical content.
- return sample(effect.easing(), @as(f32, @floatFromInt(frame)) / @as(f32, @floatFromInt(frames - 1)));
+ return sample(easing, @as(f32, @floatFromInt(frame)) / @as(f32, @floatFromInt(frames - 1)));
}
pub fn lerpBox(from: Box, to: Box, t: f32) Box {
@@ -227,39 +250,13 @@ pub fn dissolveRevealed(serial: u32, col: u16, row: u16, raw_progress: f32) bool
return cellNoise(serial, col, row) < t;
}
-pub const AsciiCell = struct {
- /// When true the pane's real cell has arrived.
- reveal: bool,
- /// A transient one-byte glyph while `reveal` is false. Space means the
- /// cell has not entered the animation yet.
- glyph: u8,
-};
-
-/// ASCII-materialization preset: a diagonal wave perturbed by stable noise.
-/// It is content animation rather than geometry; TTY replaces the cell glyph,
-/// while GUI shaders use the same threshold to dissolve glyph coverage.
-pub fn asciiCell(serial: u32, col: u16, row: u16, raw_progress: f32) AsciiCell {
- const t = std.math.clamp(raw_progress, 0.0, 1.0);
- // These endpoints are real presented frames: even a cell whose stable
- // threshold is zero starts as old data, and every cell ends as new data.
- if (t <= 0) return .{ .reveal = false, .glyph = ' ' };
- if (t >= 1) return .{ .reveal = true, .glyph = ' ' };
- const noise = cellNoise(serial, col, row);
- const diagonal = @as(f32, @floatFromInt((@as(u32, col) + @as(u32, row) * 2) % 17)) / 17.0;
- const threshold = noise * 0.72 + diagonal * 0.28;
- if (t >= threshold) return .{ .reveal = true, .glyph = ' ' };
- if (t + 0.22 < threshold) return .{ .reveal = false, .glyph = ' ' };
- const glyphs = ".:+*#%@";
- const index: usize = @intFromFloat(@floor(noise * @as(f32, @floatFromInt(glyphs.len))));
- return .{ .reveal = false, .glyph = glyphs[@min(index, glyphs.len - 1)] };
-}
-
test "easing presets have exact endpoints and intended shapes" {
inline for (std.enums.values(Easing)) |easing| {
try std.testing.expectEqual(@as(f32, 0), sample(easing, 0));
try std.testing.expectEqual(@as(f32, 1), sample(easing, 1));
}
try std.testing.expectEqual(@as(f32, 0.5), sample(.linear, 0.5));
+ try std.testing.expect(sample(.in_cubic, 0.5) < sample(.linear, 0.5));
try std.testing.expect(sample(.out_cubic, 0.5) > sample(.linear, 0.5));
try std.testing.expect(sample(.out_back, 0.8) > 1.0);
}
@@ -287,7 +284,7 @@ test "opening presets separate geometry and content transitions" {
try std.testing.expectEqual(@as(f32, 0), openingBox(.zoom, target, 80).w);
try std.testing.expectEqual(@as(f32, 80), openingBox(.slide, target, 80).x);
try std.testing.expectEqual(@as(f32, target.y + target.h), openingBox(.vertical, target, 80).y);
- try std.testing.expectEqual(@as(f32, target.y - target.h), closingBox(.vertical, target).y);
+ try std.testing.expectEqual(@as(f32, target.y + target.h), closingBox(.vertical, target).y);
var track: Track = .{ .effect = .slide, .from = target, .to = target };
try std.testing.expect(track.active());
@@ -297,31 +294,15 @@ test "opening presets separate geometry and content transitions" {
track = .{ .effect = .dissolve, .frame = 3, .from = .{}, .to = target };
try std.testing.expectEqual(target, track.visualBox());
- const closing: Track = .{
+ var closing: Track = .{
.phase = .closing,
.effect = .vertical,
.from = target,
.to = closingBox(.vertical, target),
};
try std.testing.expectEqual(target, closing.contentBox());
-}
-
-test "ASCII materialization is stable and finishes as real content" {
- const a = asciiCell(42, 11, 7, 0.4);
- const b = asciiCell(42, 11, 7, 0.4);
- try std.testing.expectEqual(a, b);
- try std.testing.expect(!asciiCell(42, 11, 7, 0).reveal);
- try std.testing.expectEqual(@as(u8, ' '), asciiCell(42, 11, 7, 0).glyph);
- try std.testing.expect(asciiCell(42, 11, 7, 1).reveal);
-
- var changed = false;
- for (0..32) |col| {
- if (!std.meta.eql(asciiCell(42, @intCast(col), 3, 0.35), asciiCell(43, @intCast(col), 3, 0.35))) {
- changed = true;
- break;
- }
- }
- try std.testing.expect(changed);
+ closing.frame = 2;
+ try std.testing.expect(closing.amount() < progress(.vertical, closing.frame));
}
test "dissolve has exact stable endpoints" {