diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 20:31:56 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | c5c2baf21bba0fb8f66e7d2ee096818d6bd5a8e0 (patch) | |
| tree | 6cd19cfb7fbd076433fbb965e8a52ac1800949fb /src/draw.zig | |
| parent | 242467eb9f066f80164d534be55c2c4ebc9b5604 (diff) | |
| download | pardes-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/draw.zig')
| -rw-r--r-- | src/draw.zig | 48 |
1 files changed, 45 insertions, 3 deletions
diff --git a/src/draw.zig b/src/draw.zig index 1f30a40b..9df76cfd 100644 --- a/src/draw.zig +++ b/src/draw.zig @@ -134,11 +134,12 @@ pub fn place(p: *Pardes, s: *Surface) void { // snaps (it has no in-between), and so does nothing that shows it. const shown = lift != .off or inactiveDim(p) != 0; const motion = animation.Motion.of(if (shown and builtins.capabilities.lift) p.settings.motion else .off); - spring.retarget(if (id == p.active) 1 else 0, p.now_ns, motion); + // A flavour exaggerates what is only a few pixels (Motion.gain). + spring.retarget(if (id == p.active) animation.Motion.of(p.settings.motion).gain else 0, p.now_ns, motion); _ = spring.step(p.now_ns); } for (s.regions[0..s.nregions]) |*region| switch (region.kind) { - .grip, .tag, .body, .rail => region.lift = if (lift != .off) p.lifts[region.owner].value(p.now_ns) else 0, + .grip, .tag, .body, .rail => region.lift = if (lift != .off) std.math.clamp(p.lifts[region.owner].value(p.now_ns), -1, 2) else 0, .notice => region.lift = if (lift != .off) 1 else 0, else => {}, }; @@ -416,7 +417,8 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface { for (p.panes, p.lifts, 0..) |slot, spring, id| { if (slot == null or id == p.active) continue; - const amount = @as(f32, @floatFromInt(inactiveDim(p))) / 100 * std.math.clamp(1 - spring.value(p.now_ns), 0, 1); + const focus = spring.value(p.now_ns) / animation.Motion.of(p.settings.motion).gain; + const amount = @as(f32, @floatFromInt(inactiveDim(p))) / 100 * std.math.clamp(1 - focus, 0, 1); if (amount > 0.001) recede(p, s, id, amount); } @@ -1604,6 +1606,46 @@ test "with no lift and no dim a focus switch asks for no frames" { try std.testing.expect(!p.continuous()); } +test "each Motion flavour reaches the lift as its own trajectory" { + // A grid snaps; the flavours are a pixel shell's. + if (!builtins.capabilities.lift) return error.SkipZigTest; + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + _ = try p.setTestFile("text\n"); + p.newScratchBelow(0); + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + p.settings.lift = .shadow; + var peaks: [5]f32 = undefined; + const ms = std.time.ns_per_ms; + var start: u64 = std.time.ns_per_s; + for (std.enums.values(animation.Motion.Flavour), &peaks) |flavour, *peak| { + p.settings.motion = flavour; + p.active = 1; + p.advance(start); + _ = arena.reset(.retain_capacity); + _ = try p.render(arena.allocator()); + start += 3 * std.time.ns_per_s; + p.advance(start); + _ = try p.render(arena.allocator()); + p.active = 0; + peak.* = 0; + var t: u64 = 0; + while (t <= 1500 * ms) : (t += 4 * ms) { + p.advance(start + t); + _ = arena.reset(.retain_capacity); + const s = try p.render(arena.allocator()); + const lift = for (s.regionList()) |region| { + if (region.kind == .grip and region.owner == 0) break region.lift; + } else unreachable; + peak.* = @max(peak.*, lift); + } + start += 3 * std.time.ns_per_s; + } + // Bouncy and playful pass their (exaggerated) mark; smooth never does. + try std.testing.expect(peaks[2] <= 1.001 and peaks[3] > 1.25 and peaks[4] > peaks[3]); +} + test "a monkey over notices, prompts, tags and tiny panes at a gui's metrics never panics" { // The snapshot harness runs the tty, whose tagline pitch is the body's, // so it never reaches what a narrower tagline does to a notice's band: |
