summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gui/gui.zig5
-rw-r--r--src/pardes.zig75
-rw-r--r--src/tag_layer.zig4
3 files changed, 65 insertions, 19 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index acb8d729..dd74ae58 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -7311,7 +7311,10 @@ fn appendPaneChrome(
// comes with it, and shows only once it is below that cut.
const rule_y = top + (1 + layer.slide) * layout.h;
if (rule_y - 1 < top) continue;
- builder.addRect(x0, rule_y - 1, rule_x1, rule_y, overlayRgb(chrome.border));
+ // ...and fades up and dissolves with it rather than standing alone.
+ var rule = overlayRgb(chrome.border);
+ rule.a = layer.fade;
+ builder.addRect(x0, rule_y - 1, rule_x1, rule_y, rule);
}
appendColumnSpines(core, surface.rows, layout, builder);
}
diff --git a/src/pardes.zig b/src/pardes.zig
index 42729d50..4335ffde 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -2746,19 +2746,32 @@ test "a message falls in, lingers past the input that dismisses it, and dissolve
try std.testing.expect(p.executeBuiltinLine(p.active, "Msg hello"));
try std.testing.expectEqual(Pane.MessagePhase.entering, pane.msg_life.phase);
try std.testing.expect(p.animationActive());
- // An ease-in fall: starts a whole row up, lands exactly on it.
+ // An ease-out fall: starts a whole row up and unseen, covers most of the
+ // way at once and settles into its row, landing exactly on it without
+ // passing it. Its colour is whole by half way.
const start = p.messageMotion(pane.msg_life);
try std.testing.expectEqual(@as(f32, -1), start.slide);
- var last = start.slide;
- for (0..Pardes.messageFrames(p.settings.message_fall_ms)) |_| {
+ try std.testing.expectEqual(@as(f32, 0), start.alpha);
+ const fall = Pardes.messageFrames(p.settings.message_fall_ms);
+ var last = start;
+ var first_step: f32 = 0;
+ var last_step: f32 = 0;
+ for (0..fall) |frame| {
p.needs_frame = false;
p.update(.tick);
// Every tick that moves it owes a frame, the landing one included.
try std.testing.expect(p.needs_frame);
const now = p.messageMotion(pane.msg_life);
- try std.testing.expect(now.slide >= last);
- last = now.slide;
+ try std.testing.expect(now.slide >= last.slide and now.slide <= 0);
+ try std.testing.expect(now.alpha >= last.alpha);
+ if (frame == 0) first_step = now.slide - last.slide;
+ // Never slower to colour up than to arrive, and solid by half way.
+ try std.testing.expect(now.alpha >= 1 + now.slide);
+ if (2 * (frame + 1) >= fall) try std.testing.expectEqual(@as(f32, 1), now.alpha);
+ last_step = now.slide - last.slide;
+ last = now;
}
+ try std.testing.expect(first_step > 4 * last_step);
try std.testing.expectEqual(Pane.MessagePhase.shown, pane.msg_life.phase);
try std.testing.expectEqual(@as(f32, 0), p.messageMotion(pane.msg_life).slide);
try std.testing.expect(!p.animationActive());
@@ -2776,16 +2789,25 @@ test "a message falls in, lingers past the input that dismisses it, and dissolve
}
try std.testing.expectEqual(Pane.MessagePhase.lingering, pane.msg_life.phase);
p.update(.tick);
- // ...then an ease-out dissolve toward nothing, and gone.
+ // ...then an ease-in dissolve: it lets go slowly and goes faster and
+ // faster, in place, its last frame showing it gone.
try std.testing.expectEqual(Pane.MessagePhase.leaving, pane.msg_life.phase);
+ const dissolve = Pardes.messageFrames(p.settings.message_dissolve_ms);
var alpha: f32 = 1;
- for (0..Pardes.messageFrames(p.settings.message_dissolve_ms)) |_| {
- const now = p.messageMotion(pane.msg_life).alpha;
- try std.testing.expect(now <= alpha);
- alpha = now;
+ var first_drop: f32 = 0;
+ var last_drop: f32 = 0;
+ for (0..dissolve) |frame| {
+ const now = p.messageMotion(pane.msg_life);
+ try std.testing.expect(now.alpha <= alpha);
+ try std.testing.expectEqual(@as(f32, 0), now.slide);
+ if (frame == 1) first_drop = alpha - now.alpha;
+ last_drop = alpha - now.alpha;
+ alpha = now.alpha;
p.needs_frame = false;
p.update(.tick);
}
+ try std.testing.expectEqual(@as(f32, 0), alpha);
+ try std.testing.expect(first_drop > 0 and first_drop < last_drop);
try std.testing.expectEqual(@as(u16, 0), pane.msg_len);
try std.testing.expect(!p.animationActive());
// The tick that clears it still owes the frame that shows it gone.
@@ -7335,7 +7357,8 @@ pub const Pardes = struct {
}
/// How present a message is (1 = fully) and how far above its row it
- /// still is (rows), eased: an ease-in fall, an ease-out dissolve.
+ /// still is (rows): a fall that decelerates into place with its colour
+ /// arriving ahead of it, and a dissolve.
const MessageMotion = struct { alpha: f32 = 1, slide: f32 = 0 };
/// Animation frames a message timing lasts; at least one.
@@ -7350,12 +7373,26 @@ pub const Pardes = struct {
.shown, .lingering => .{ .alpha = 1, .slide = 0 },
.entering => blk: {
const t = @as(f32, @floatFromInt(@min(life.frame, fall_frames))) / @as(f32, @floatFromInt(fall_frames));
- const eased = t * t;
- break :blk .{ .alpha = eased, .slide = eased - 1 };
+ // Out of the gate fast and easing into its row (a cubic
+ // ease-out), so it is there the moment it is said and the
+ // last frames only settle it. No overshoot: at a row's height
+ // and a dozen frames, a follow-through is a pixel or two that
+ // reads as a jitter, not a bounce.
+ const left = 1 - t;
+ // The colour leads the motion: solid by half way, so what
+ // lands is already legible rather than still fading up.
+ const faded = 1 - @min(1, 2 * t);
+ break :blk .{ .alpha = 1 - faded * faded, .slide = -(left * left * left) };
},
.leaving => blk: {
- const t = @as(f32, @floatFromInt(@min(life.frame, dissolve_frames))) / @as(f32, @floatFromInt(dissolve_frames));
- break :blk .{ .alpha = (1 - t) * (1 - t), .slide = 0 };
+ // Counted to its last frame rather than past it, so that frame
+ // shows it gone and it never blinks out from part way.
+ const last = @max(1, dissolve_frames - 1);
+ const t = @as(f32, @floatFromInt(@min(life.frame, last))) / @as(f32, @floatFromInt(last));
+ // An exit eases in: still readable for a beat, then away
+ // faster and faster. It stays where it is -- the lines
+ // stacked around it do not move either.
+ break :blk .{ .alpha = 1 - t * t, .slide = 0 };
},
};
}
@@ -14585,10 +14622,10 @@ pub const Pardes = struct {
const line = try arena.alloc(u8, pad + kept.len);
@memset(line[0..pad], ' ');
@memcpy(line[pad..], kept);
- // A pixel shell slides the message in whole and only fades
- // it on the way out.
+ // A pixel shell slides the message in as it fades up, and
+ // fades it on the way out.
const motion: MessageMotion = if (noticeLife(pane, i)) |life| p.messageMotion(life) else .{};
- const fade = if (motion.slide == 0) motion.alpha else 1;
+ const fade = motion.alpha;
const page = p.theme().bg orelse chrome.tag_bg;
const layer_index = NOTICE_LAYER_BASE + id * Pane.Notices.max + i;
try p.renderHeaderLayer(arena, layer_index, .notice, @intCast(id), .{
@@ -14598,6 +14635,7 @@ pub const Pardes = struct {
.h = BOX_H,
}, line, blendRgb(chrome.tag_bg, page, fade), blendRgb(chrome.tag_fg, page, fade));
p.surface.tag_layers[layer_index].slide = motion.slide;
+ p.surface.tag_layers[layer_index].fade = fade;
// The caret rides the band, where the prompt's glyphs are,
// at the same place in the text the grid pass puts it.
if (kind != .prompt or id != p.active) continue;
@@ -14634,6 +14672,7 @@ pub const Pardes = struct {
layer.bg = bg;
layer.cursor = null;
layer.slide = 0;
+ layer.fade = 1;
var surface: Surface = .{ .cols = columns, .rows = 1, .cells = layer.cells };
// A notice band carries a PANE id, not a column index, so it must not
// answer to the column being edited -- that would scroll and select
diff --git a/src/tag_layer.zig b/src/tag_layer.zig
index f8cc6eca..52da297d 100644
--- a/src/tag_layer.zig
+++ b/src/tag_layer.zig
@@ -30,6 +30,10 @@ pub const Layer = struct {
/// that is moving into place. A pixel shell draws it offset and clipped
/// to the rows below the viewport's top; the grid has no such thing.
slide: f32 = 0,
+ /// How present a notice band is (1 = fully) while it fades up or
+ /// dissolves. `bg` and the cells are already blended by it; a pixel
+ /// shell uses it for what it draws around the band, like its rule.
+ fade: f32 = 1,
pub fn capacity(layer: *const Layer, metrics: pardes.RowMetrics) u16 {
return @intCast(@min(std.math.maxInt(u16), @max(1, @as(u32, layer.viewport.w) * @max(1, metrics.body_w) / @max(1, metrics.tagline_w))));