diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Presentation.zig | 39 | ||||
| -rw-r--r-- | src/animation.zig | 33 | ||||
| -rw-r--r-- | src/gui/gui.zig | 103 | ||||
| -rw-r--r-- | src/macos.zig | 2 | ||||
| -rw-r--r-- | src/macos/pardes.h | 3 | ||||
| -rw-r--r-- | src/pardes.zig | 2 |
6 files changed, 162 insertions, 20 deletions
diff --git a/src/Presentation.zig b/src/Presentation.zig index 15ed9ea6..5cec73e0 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 = stamp(p, .{ + var next = stamp(p, .{ .serial = pane.serial, .pane = @intCast(id), .phase = if (!same_lifetime or @@ -192,6 +192,10 @@ pub fn sync(self: *Presentation, p: *Pardes) void { .from = from, .to = target, }); + // Retargeted mid-way from where it shows: it keeps its speed. + if (same_lifetime and shown != null) if (prior) |old_move| { + next.launch = carriedLaunch(old_move, next); + }; self.tracks[id] = next; changed = true; animated_change = true; @@ -230,6 +234,23 @@ fn stamp(p: *const Pardes, track: Track) Track { return stamped; } +/// A retargeted move's starting speed, in its own lengths per unit of +/// progress, from the old move's speed now (cells per frame), along the +/// new move's longest side; within four lengths per unit either way. +fn carriedLaunch(old_move: Track, next: Track) f32 { + const v = old_move.velocity(); + if (next.frames() <= 1) return 0; + const steps: f32 = @floatFromInt(next.frames() - 1); + const d: [4]f32 = .{ next.to.x - next.from.x, next.to.y - next.from.y, next.to.w - next.from.w, next.to.h - next.from.h }; + const vs: [4]f32 = .{ v.x, v.y, v.w, v.h }; + var axis: usize = 0; + for (d, 0..) |value, i| if (@abs(value) > @abs(d[axis])) { + axis = i; + }; + if (@abs(d[axis]) < 1e-3) return 0; + return std.math.clamp(vs[axis] * steps / d[axis], -4, 4); +} + fn appendClosing(self: *Presentation, track: Track) bool { std.debug.assert(track.phase == .closing); const baseline = self.previous_layout[track.pane] orelse return false; @@ -784,3 +805,19 @@ test "presentation snapshots own compact body and tag rows across later edits" { try std.testing.expectEqual(@as(u8, 'B'), state.previous_body_layers[0].cells[0].text[0]); try std.testing.expectEqual(@as(u8, 'U'), state.previous_tag_layers[0].cells[0].text[0]); } + +test "a move retargeted mid-way carries its speed into the new one" { + const smooth = @intFromEnum(animation.Motion.Flavour.smooth) + 1; + const first: Track = .{ .serial = 1, .phase = .moving, .effect = .slide, .motion = smooth, .frame = 4, .frame_count = 14, .from = .{ .x = 0, .y = 1, .w = 40, .h = 20 }, .to = .{ .x = 40, .y = 1, .w = 40, .h = 20 } }; + const was = first.velocity(); + try std.testing.expect(was.x > 0.5); + var next: Track = .{ .serial = 1, .phase = .moving, .effect = .slide, .motion = smooth, .frame_count = 14, .from = first.presented(), .to = .{ .x = 60, .y = 1, .w = 40, .h = 20 } }; + next.launch = carriedLaunch(first, next); + try std.testing.expect(next.launch > 0); + // It sets off at the speed the old one had, not from a stop. + const now = next.velocity(); + try std.testing.expectApproxEqRel(was.x, now.x, 0.15); + // And still lands exactly. + next.frame = 13; + try std.testing.expectEqual(next.to.x, next.presented().x); +} diff --git a/src/animation.zig b/src/animation.zig index 12d352d1..855fef03 100644 --- a/src/animation.zig +++ b/src/animation.zig @@ -136,6 +136,10 @@ pub const Track = extern struct { frame_count: u16 = 0, from: Box = .{}, to: Box = .{}, + /// The move's speed as it starts, in its lengths per unit of progress: + /// 0 sets off as the flavour does; a move retargeted mid-way carries + /// the old one's speed on (Presentation.sync), so it never stops dead. + launch: f32 = 0, pub fn active(track: Track) bool { return track.effect != .off and track.frame < track.frames(); @@ -151,7 +155,7 @@ pub const Track = extern struct { if (track.phase == .closing and track.effect == .vertical) return progressEased(.in_cubic, track.frames(), track.frame); if (track.flavour()) |motion| - return motion.ease(track.progress()); + return motion.easeFrom(track.progress(), track.launch); return progressEased(track.effect.easing(), track.frames(), track.frame); } @@ -163,6 +167,16 @@ pub const Track = extern struct { return Motion.of(which); } + /// How fast its presented box moves now, in cells per frame of its + /// own (x, y, w, h); zero when it follows no flavour or has landed. + pub fn velocity(track: Track) Box { + const motion = track.flavour() orelse return .{}; + const u = track.progress(); + if (u >= 1 or track.frames() <= 1) return .{}; + const rate = motion.speedFrom(u, track.launch) / @as(f32, @floatFromInt(track.frames() - 1)); + return .{ .x = (track.to.x - track.from.x) * rate, .y = (track.to.y - track.from.y) * rate, .w = (track.to.w - track.from.w) * rate, .h = (track.to.h - track.from.h) * rate }; + } + fn progress(track: Track) f32 { if (track.frames() <= 1 or track.frame >= track.frames() - 1) return 1; return @as(f32, @floatFromInt(track.frame)) / @as(f32, @floatFromInt(track.frames() - 1)); @@ -180,7 +194,7 @@ pub const Track = extern struct { // box, so a click lands on the glyph it is drawn as). const motion = track.flavour() orelse return box; if (motion.stretch == 0) return box; - const factor = 1 + motion.stretch * std.math.clamp(motion.speed(track.progress()) / 4, -1, 1); + const factor = 1 + motion.stretch * std.math.clamp(motion.speedFrom(track.progress(), track.launch) / 4, -1, 1); if (@abs(track.to.x - track.from.x) >= @abs(track.to.y - track.from.y)) { const w = box.w * factor; box.x -= (w - box.w) / 2; @@ -685,16 +699,27 @@ pub const Motion = struct { /// flavour's spring (wind-up, overshoot and all), landing exactly on 1 /// at its end. `instant` is there at once. pub fn ease(motion: Motion, u: f32) f32 { + return motion.easeFrom(u, 0); + } + + /// `ease` for a move that starts already moving at `launch` lengths + /// per unit of `u` (a retarget): no wind-up then, the speed carried on. + pub fn easeFrom(motion: Motion, u: f32, launch: f32) f32 { if (motion.instant or u >= 1) return 1; if (u <= 0) return 0; - const spring: Spring = .{ .omega = motion.norm, .zeta = motion.zeta, .target = 1, .from = 0, .velocity = (motion.launch - 2 * motion.anticipate) * motion.norm, .settled = false }; + const kick = if (launch != 0) launch else (motion.launch - 2 * motion.anticipate) * motion.norm; + const spring: Spring = .{ .omega = motion.norm, .zeta = motion.zeta, .target = 1, .from = 0, .velocity = kick, .settled = false }; return spring.value(@intFromFloat(u * std.time.ns_per_s)); } /// The same move's speed at `u`, in lengths per unit of `u`. pub fn speed(motion: Motion, u: f32) f32 { + return motion.speedFrom(u, 0); + } + + pub fn speedFrom(motion: Motion, u: f32, launch: f32) f32 { const h: f32 = 1.0 / 256.0; - return (motion.ease(@min(1, u + h)) - motion.ease(@max(0, u - h))) / (@min(1, u + h) - @max(0, u - h)); + return (motion.easeFrom(@min(1, u + h), launch) - motion.easeFrom(@max(0, u - h), launch)) / (@min(1, u + h) - @max(0, u - h)); } }; diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 449cae15..b5da9ad6 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -5963,7 +5963,9 @@ fn renderFrame( const track = groups.items[group_index].track; const data_effect = if (track) |active| active.effect == .dissolve else false; const data_diff = data_effect and gripCellChanged(surface, covered, previous_covered, col, row); - if (data_diff) emitSurfaceCell( + // The new cell, then (a dissolve) the old one fading out over + // it, as the old layers come after the new ones. + emitSurfaceCell( g, chrome, instances, @@ -5973,16 +5975,16 @@ fn renderFrame( layout, win_w, win_h, - track, - surface.previous_cells[@as(usize, row) * surface.cols ..][0..surface.cols], - previous_covered[@as(usize, row) * surface.cols ..][0..surface.cols], + if (data_effect and !data_diff) null else track, + line, + line_cover, col, - null, - true, + line_cursor, false, + cursors_shown and at == cursor_idx and !cursor_bar and bodyLayerAt(surface, col, row) == null, page, ); - emitSurfaceCell( + if (data_diff) emitSurfaceCell( g, chrome, instances, @@ -5992,13 +5994,13 @@ fn renderFrame( layout, win_w, win_h, - if (data_effect and !data_diff) null else track, - line, - line_cover, + track, + surface.previous_cells[@as(usize, row) * surface.cols ..][0..surface.cols], + previous_covered[@as(usize, row) * surface.cols ..][0..surface.cols], col, - line_cursor, + null, + true, false, - cursors_shown and at == cursor_idx and !cursor_bar and bodyLayerAt(surface, col, row) == null, page, ); } @@ -6104,7 +6106,10 @@ fn renderFrame( } const has_shifted = index == 0 and shifted != 0; if ((group.cell_count != 0 or has_shifted) and g.vbuf != null) { - c.SDL_BindGPUGraphicsPipeline(rp, if (cell_decor.nplaques != 0) g.pipeline_over else g.pipeline); + // Blended over what is under: a theme's plaques show through + // blank tag cells; a dissolve's old cells fade over its new. + const dissolving = if (group.track) |active| active.effect == .dissolve else false; + c.SDL_BindGPUGraphicsPipeline(rp, if (cell_decor.nplaques != 0 or dissolving) g.pipeline_over else g.pipeline); const samp_binding = c.SDL_GPUTextureSamplerBinding{ .texture = g.atlas_tex, .sampler = g.atlas_sampler }; c.SDL_BindGPUFragmentSamplers(rp, 0, &samp_binding, 1); if (group.cell_count != 0) { @@ -8715,6 +8720,34 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g } else null; try addLift(g, gpa, lift_style, ceilings, box, rim_edge, bodies, grip.lift, track, win_w, win_h); } + // G7: a pane on the move stands off the page, rising as it sets + // out and settling as it lands, its shadow on the other panes' + // bodies only (never a tag, a grip or its own text), moving + // with it. + if (track) |active| if (active.phase != .closing and (active.effect == .slide or active.effect == .zoom or active.effect == .vertical)) { + const steps: f32 = @floatFromInt(@max(1, active.frames() -| 1)); + const u = std.math.clamp(@as(f32, @floatFromInt(active.frame)) / steps, 0, 1); + const lift = @sin(std.math.pi * u); + var box: [4]f32 = .{ std.math.inf(f32), std.math.inf(f32), -std.math.inf(f32), -std.math.inf(f32) }; + var others: [pardes.MAX_PANES][4]f32 = undefined; + var nothers: usize = 0; + for (placed) |part| { + if (part.kind != .tag and part.kind != .rail and part.kind != .body and part.kind != .grip) continue; + const r = bodyPixels(part, layout); + if (part.owner == active.pane) { + box = .{ @min(box[0], r[0]), @min(box[1], r[1]), @max(box[2], r[2]), @max(box[3], r[3]) }; + } else if ((part.kind == .body or part.kind == .rail) and nothers < others.len) { + others[nothers] = r; + nothers += 1; + } + } + if (box[2] > box[0] and lift > 0.01) { + const h: f32 = @floatFromInt(g.cell_h); + const first = g.decor.items.len; + try addSoft(g, gpa, box, Shadow.sigma * h * lift, Shadow.side * h * lift, Shadow.drop * h * lift, @min(Shadow.strength * lift, ceilings.shadow), .{ 0, 0, 0 }, track, win_w, win_h); + try clipToBodies(g, gpa, first, others[0..nothers]); + } + }; } // A notice chip is tagline hung over the body's top rows, so it is // ruled off from the text beneath it the way a tag is: the same @@ -9287,6 +9320,50 @@ test "decor is read from the regions, each pane's in its own group" { try std.testing.expectEqual(@as(u32, 0), groups.items[groups.ink].decor_count); } +test "a pane on the move casts a shadow on the other panes' bodies alone, most at mid-way" { + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 }); + defer core.deinit(); + _ = try core.newShell(1, ""); + try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false)); + try std.testing.expect(core.executeBuiltinLine(0, "Theme acme")); + for (0..40) |_| core.update(.tick); + pardes.test_api.sync(core); + const s = try core.render(core.scratch.allocator()); + var g: Gui = undefined; + g.decor = .empty; + defer g.decor.deinit(gpa); + g.cell_w = 10; + g.cell_h = 20; + g.tagline_width = 6; + g.tagline_height = 12; + const layout = fixedCellLayout(&g); + const serial = core.panes[1].?.serial; + var strengths: [3]f32 = @splat(0); + for ([_]u16{ 0, 6, 12 }, 0..) |frame, i| { + g.decor.clearRetainingCapacity(); + const moving: pardes.animation.Track = .{ .pane = 1, .serial = serial, .phase = .moving, .effect = .slide, .frame = frame, .frame_count = 13, .from = .{ .x = 0, .y = 1, .w = 40, .h = 23 }, .to = .{ .x = 40, .y = 1, .w = 40, .h = 23 } }; + var groups = makeGroups(&.{moving}, true); + if (!moving.active()) groups = makeGroups(&.{}, true); + try buildDecor(&g, gpa, s, &groups, layout, 800, 480, false); + for (g.decor.items) |item| { + if (item.effect & soft_shadow_bit == 0) continue; + strengths[i] = @max(strengths[i], item.fr); + // Clipped to pane 0's body or rail, never pane 1's, never a tag. + try std.testing.expect(item.clip_x1 >= item.clip_x0); + for (s.regionList()) |region| { + if (region.kind != .tag and region.kind != .grip and !(region.owner == 1 and (region.kind == .body or region.kind == .rail))) continue; + const r = bodyPixels(region, layout); + try std.testing.expect(item.clip_x1 <= r[0] or item.clip_x0 >= r[2] or item.clip_y1 <= r[1] or item.clip_y0 >= r[3]); + } + } + } + // Rising as it sets out, gone as it lands. + try std.testing.expectEqual(@as(f32, 0), strengths[0]); + try std.testing.expect(strengths[1] > 0.05); + try std.testing.expectEqual(@as(f32, 0), strengths[2]); +} + /// What a bundled post pass leaves alone (Post.Frame.spare), in window /// pixels: every tag with its grip, the column and workspace tags, and the /// notices, the chrome the focus indicators live in. A grip, the gap after diff --git a/src/macos.zig b/src/macos.zig index d6daf944..4ff13b23 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -2995,7 +2995,7 @@ test "mac panel mask is a literal normalized grayscale texture" { test "mac panel ABI has the documented compact byte layout" { try std.testing.expectEqual(@as(usize, 16), @sizeOf(PanelBox)); - try std.testing.expectEqual(@as(usize, 44), @sizeOf(PanelTrack)); + try std.testing.expectEqual(@as(usize, 48), @sizeOf(PanelTrack)); try std.testing.expectEqual(@as(usize, 0), @offsetOf(PanelTrack, "serial")); try std.testing.expectEqual(@as(usize, 4), @offsetOf(PanelTrack, "pane")); try std.testing.expectEqual(@as(usize, 5), @offsetOf(PanelTrack, "phase")); diff --git a/src/macos/pardes.h b/src/macos/pardes.h index 29f8a37d..46453429 100644 --- a/src/macos/pardes.h +++ b/src/macos/pardes.h @@ -108,11 +108,12 @@ typedef struct { uint8_t pane; uint8_t phase; uint8_t effect; - uint8_t reserved0; + uint8_t motion; uint16_t frame; uint16_t frame_count; pardes_panel_box_s from; pardes_panel_box_s to; + float launch; } pardes_panel_track_s; #define PARDES_PANEL_OPENING 0u diff --git a/src/pardes.zig b/src/pardes.zig index bbd8e7cd..1f2c3175 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -471,6 +471,8 @@ test { _ = @import("body_layer.zig"); _ = @import("draw.zig"); _ = @import("tag_layer.zig"); + _ = @import("Presentation.zig"); + _ = @import("animation.zig"); } pub const Builtin = builtins.registry.Builtin(); |
