diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 75 |
1 files changed, 57 insertions, 18 deletions
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 |
