summaryrefslogtreecommitdiff
path: root/src/Presentation.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 20:31:56 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitc5c2baf21bba0fb8f66e7d2ee096818d6bd5a8e0 (patch)
tree6cd19cfb7fbd076433fbb965e8a52ac1800949fb /src/Presentation.zig
parent242467eb9f066f80164d534be55c2c4ebc9b5604 (diff)
downloadpardes-c5c2baf21bba0fb8f66e7d2ee096818d6bd5a8e0.tar.gz
pardes-c5c2baf21bba0fb8f66e7d2ee096818d6bd5a8e0.zip
Motion flavours differ by design, and drive notice drops and pane moves
The flavours looked alike because Lift, the only motion that read them, moves a shadow a few pixels: overshoot and wind-up were sub-pixel. By design now: crisp leaves at full speed and never passes its mark; smooth (the default) eases in and out in twice crisp's time, a slide arriving in 208 ms (inside ยง8.1's 220); bouncy passes its mark by ~30% and settles twice; playful winds up, passes by ~40%, and stretches and squashes, text included (the user's choice). Small motions are exaggerated by the flavour's gain. Motion gains launch, gain, span and norm, and ease(u)/speed(u) for moves of fixed length. The notice drop follows ease() over its fall (alpha never behind its position) and stays inside its pane's body past its row. Panel tracks carry the flavour in Track.motion (the old padding byte; macOS header name follows); opening and moving slides, zooms and rises follow it over span x their frames; landed, a pane is exactly its target; closing keeps its own exit. A pane past its mark never takes a neighbour's click; a rising one stays in its box. Tests: each flavour's lift trajectory distinct; flavour character; exact landing bit for bit; a stretched pane's drawn cells map back; the overshoot hit rule; the vertical clip at peak; legacy tests pinned to crisp. GUI golden 18-mid-transition changes (the retuned smooth slide at frame 6). Shared files: src/animation.zig, src/Presentation.zig, src/Messages.zig, src/macos.zig, src/config.zig, src/pardes.zig tests, src/ninep/screen.zig test, docs/effects.md, docs/config.md.
Diffstat (limited to 'src/Presentation.zig')
-rw-r--r--src/Presentation.zig59
1 files changed, 55 insertions, 4 deletions
diff --git a/src/Presentation.zig b/src/Presentation.zig
index 3b5bb3e0..15ed9ea6 100644
--- a/src/Presentation.zig
+++ b/src/Presentation.zig
@@ -183,7 +183,7 @@ pub fn sync(self: *Presentation, p: *Pardes) void {
prior.?.from
else
old.box;
- const next: Track = .{
+ const next = stamp(p, .{
.serial = pane.serial,
.pane = @intCast(id),
.phase = if (!same_lifetime or
@@ -191,19 +191,19 @@ pub fn sync(self: *Presentation, p: *Pardes) void {
.effect = effect,
.from = from,
.to = target,
- };
+ });
self.tracks[id] = next;
changed = true;
animated_change = true;
} else {
- const next: Track = .{
+ const next = stamp(p, .{
.serial = pane.serial,
.pane = @intCast(id),
.phase = .opening,
.effect = effect,
.from = openingBox(effect, target, p.screen_w),
.to = target,
- };
+ });
self.tracks[id] = next;
changed = true;
animated_change = true;
@@ -215,6 +215,21 @@ pub fn sync(self: *Presentation, p: *Pardes) void {
if (changed and self.acknowledged) self.pending = true;
}
+/// A pane that moves (slides, zooms or rises) follows the Motion flavour:
+/// its curve, and a length in proportion (animation.Motion.span).
+fn stamp(p: *const Pardes, track: Track) Track {
+ var stamped = track;
+ switch (track.effect) {
+ .slide, .zoom, .vertical => {
+ const motion = animation.Motion.of(p.settings.motion);
+ stamped.motion = @intFromEnum(p.settings.motion) + 1;
+ stamped.frame_count = @intFromFloat(@max(2, @round(@as(f32, @floatFromInt(track.effect.frames())) * motion.span)));
+ },
+ else => {},
+ }
+ return stamped;
+}
+
fn appendClosing(self: *Presentation, track: Track) bool {
std.debug.assert(track.phase == .closing);
const baseline = self.previous_layout[track.pane] orelse return false;
@@ -388,6 +403,10 @@ pub fn pointerFractional(self: *const Presentation, cols: u16, rows: u16, x: f32
.slide, .zoom, .vertical => {
const target = track.contentBox();
if (track.effect == .vertical and !boxContainsPoint(target, x, y)) continue;
+ // Past its mark (a flavour's overshoot or wind-up) a pane
+ // is drawn over a neighbour; a point there is the
+ // neighbour's, never the moving pane's.
+ if (pastMark(track) and !boxContainsPoint(target, x, y)) continue;
const shown = track.presented();
if (!boxContainsPoint(shown, x, y)) {
if (track.effect == .vertical) return null;
@@ -410,6 +429,13 @@ pub fn pointerFractional(self: *const Presentation, cols: u16, rows: u16, x: f32
return .{ .x = x, .y = y };
}
+/// A flavour's overshoot or wind-up: the pane is drawn beyond the path from
+/// where it was to where it goes.
+fn pastMark(track: Track) bool {
+ const t = track.amount();
+ return t < 0 or t > 1;
+}
+
fn boxContainsPoint(box: Box, x: f32, y: f32) bool {
return box.w > 0 and box.h > 0 and x >= box.x and y >= box.y and x < box.x + box.w and y < box.y + box.h;
}
@@ -436,6 +462,8 @@ pub fn pointer(self: *const Presentation, cols: u16, rows: u16, col: u16, row: u
switch (track.effect) {
.slide, .zoom => {
const shown = track.presented();
+ // Past its mark, the neighbour's cells stay the neighbour's.
+ if (pastMark(track) and !boxContainsCell(track.to, col, row)) continue;
if (shown.w <= 0 or shown.h <= 0 or !boxContainsCell(shown, col, row)) continue;
const x = @as(f32, @floatFromInt(col)) + 0.5;
const y = @as(f32, @floatFromInt(row)) + 0.5;
@@ -688,6 +716,29 @@ test "presentation submission failure preserves shown geometry until retry is ac
try std.testing.expectEqual(CellPosition{ .col = 5, .row = 3 }, p.presentation.pointer(p.screen_w, p.screen_h, 5, 3).?);
}
+test "a pane past its mark never takes a neighbour's click" {
+ // Bouncy, at its peak: pane 0 slides right to 0..40 and swings past it,
+ // over the neighbour at 40..80.
+ var presentation: Presentation = .{};
+ var track: Track = .{ .serial = 7, .effect = .slide, .motion = @intFromEnum(animation.Motion.Flavour.bouncy) + 1, .frame_count = 20, .from = .{ .x = -40, .y = 0, .w = 40, .h = 20 }, .to = .{ .x = 0, .y = 0, .w = 40, .h = 20 } };
+ var peak: u16 = 0;
+ var furthest: f32 = -1e9;
+ while (track.frame < track.frames()) : (track.frame += 1) {
+ if (track.presented().x > furthest) {
+ furthest = track.presented().x;
+ peak = track.frame;
+ }
+ }
+ track.frame = peak;
+ try std.testing.expect(track.presented().x > 2);
+ presentation.shown_tracks[0] = track;
+ // A cell of the neighbour under the overshoot is the neighbour's own.
+ const col: u16 = 41;
+ try std.testing.expectEqual(CellPosition{ .col = col, .row = 5 }, presentation.pointer(80, 20, col, 5).?);
+ const point = presentation.pointerFractional(80, 20, 41.5, 5.5).?;
+ try std.testing.expectApproxEqAbs(@as(f32, 41.5), point.x, 1e-4);
+}
+
test "fractional panel inverse preserves compact header rows through slide and zoom" {
const layer: pardes.Layer = .{ .serial = 7, .viewport = .{ .x = 4, .y = 3, .w = 30, .h = 10 }, .cols = 30, .rows = 15, .context_rows = 4 };
for ([_]Transition{ .slide, .zoom, .vertical }) |effect| {