summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Presentation.zig39
-rw-r--r--src/animation.zig33
-rw-r--r--src/gui/gui.zig103
-rw-r--r--src/macos.zig2
-rw-r--r--src/macos/pardes.h3
-rw-r--r--src/pardes.zig2
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();