summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/config.md2
-rw-r--r--docs/effects.md1
-rw-r--r--src/animation.zig41
-rw-r--r--src/draw.zig20
4 files changed, 48 insertions, 16 deletions
diff --git a/docs/config.md b/docs/config.md
index 8b995e8a..189c5382 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -513,7 +513,7 @@ pair, and are composed in the core the same way:
- `PanelEdges` — whole rows slide in from alternating screen edges.
- `PanelFall` — columns rain down into place, each with its own head start.
- `PanelWave` — a vertical ripple travels across the pane and decays.
-- `PanelCurtain` — a curtain of glyphs marches column by column, left to right.
+- `PanelCurtain` — a wipe, left to right, behind a soft edge two cells wide.
- `PanelScramble` — every cell churns through printable ASCII and locks onto
its final glyph at its own stable noise threshold.
- `PanelType` — reading-order reveal with a caret sitting on the write head.
diff --git a/docs/effects.md b/docs/effects.md
index f5588e93..6685b577 100644
--- a/docs/effects.md
+++ b/docs/effects.md
@@ -267,6 +267,7 @@ it does in a pixel shell.
| T1 chip shadow | `ChipShadow on\|off` (off) | a notice chip casts a shadow a cell right and down in its body: half blocks (▌ ▀ ▘) in a darker ground on blank cells, the ground 35% darker (OKLab) under text; never the cursor, a selection, a tag or another chip; truecolor only | .scratch/render/tty/shadow/ (forge, acme; zooms) | pending |
| T4 ordered swap | `PanelDissolve` (off) | the terminal's dissolve swaps each changed cell once at its place in a 4x4 Bayer matrix (shifted per pane), an even patterned screen instead of noise; hit-testing follows the same order | .scratch/render/tty/dissolve/ (ordered-swap-sim: the matrix applied to two real frames; see note) | pending |
| T7 thumb flash | `ThumbFlash on\|off` (off) | a pane whose view moves has its scroll thumb drawn toward the text's colour, 60% at once and back over 250 ms (quadratic); the thumb only, never a focus indicator; truecolor only; a few cells a frame while it fades | .scratch/render/tty/thumb-flash.mp4, thumb-flash-strip.png (pyte replay, forge) | pending |
+| T3 wipe | `PanelCurtain` (off) | the curtain becomes a wipe, left to right: new grid behind the head, old ahead, and a soft edge of two columns whose ink and ground sit 2/3 and 1/3 of the way to the old ground (OKLab; a palette colour steps at the half); only the head's columns change a frame; the head's cells are not hit-testable until settled | .scratch/render/tty/wipe/ (curtain-gui.mp4, curtain-edge-zoom.png) | pending |
Note on T4's material: a pane opening in a tty session recorded through
pyte did not animate (PanelSlide did not either there), so the review
diff --git a/src/animation.zig b/src/animation.zig
index b682c770..840412f3 100644
--- a/src/animation.zig
+++ b/src/animation.zig
@@ -359,6 +359,9 @@ pub const CharSource = union(enum) {
/// that cell holds — a caret marching over empty space is still a caret.
byte: u8,
churn: u8,
+ /// The destination cell with its colours still this far (0..1) toward
+ /// the frozen old cell's ground: a soft edge between the two grids.
+ edge: f32,
pub const Offset = struct { cols: i32 = 0, rows: i32 = 0 };
@@ -390,12 +393,16 @@ pub fn charSource(track: Track, col: u16, row: u16, area: CellArea) CharSource {
.wave => .{ .at = .{
.rows = cellsOf(remaining * @min(4.0, h) * @sin(c * 0.55 - t * 9.0)),
} },
- // A curtain of glyphs marches in from the right, column by column, left
- // to right; each column still has a short slide of its own.
+ // A wipe, left to right: the new grid behind the head, the old one
+ // ahead of it, and between them a soft edge two cells wide whose
+ // colours step from the old ground to the new. Only the head's
+ // columns change from one frame to the next.
.curtain => blk: {
- const lead = t * (w + 1.0) - c;
+ const lead = t * (w + 2.0) - c;
if (lead <= 0.0) break :blk .old;
- break :blk .{ .at = .{ .cols = -cellsOf(@max(0.0, 3.0 - lead)) } };
+ if (lead <= 1.0) break :blk .{ .edge = 2.0 / 3.0 };
+ if (lead <= 2.0) break :blk .{ .edge = 1.0 / 3.0 };
+ break :blk .settled;
},
// Every cell churns through printable ASCII and locks onto its final
// glyph at its own stable threshold: the pane resolves out of noise.
@@ -524,7 +531,7 @@ test "each character effect moves glyphs along its own axis" {
var col: u16 = 0;
while (col < area.cols) : (col += 1) switch (charSource(fall, col, 0, area)) {
.old => {},
- .byte, .churn => return error.FallShouldNotChurn,
+ .byte, .churn, .edge => return error.FallShouldNotChurn,
.at => |offset| {
try std.testing.expectEqual(@as(i32, 0), offset.cols);
try std.testing.expect(offset.rows >= 0);
@@ -546,20 +553,24 @@ test "each character effect moves glyphs along its own axis" {
}
try std.testing.expect(above and below);
- // The curtain has a head: columns behind it hold the old grid, columns the
- // head has passed are settled, and the head itself is still sliding.
+ // The curtain wipes: columns ahead of its head hold the old grid,
+ // columns it has passed are settled, and between them a soft edge of
+ // exactly two columns steps from the old ground to the new.
const curtain: Track = .{ .effect = .curtain, .frame = 5, .to = box };
try std.testing.expectEqual(CharSource.settled, charSource(curtain, 0, 0, area));
try std.testing.expectEqual(CharSource{ .old = {} }, charSource(curtain, 29, 0, area));
- var sliding = false;
+ var wiped: [2]f32 = undefined;
+ var at_edge: usize = 0;
col = 0;
while (col < area.cols) : (col += 1) switch (charSource(curtain, col, 0, area)) {
- .at => |offset| if (offset.cols < 0) {
- sliding = true;
+ .edge => |weight| {
+ wiped[at_edge] = weight;
+ at_edge += 1;
},
- .old, .byte, .churn => {},
+ .old, .at, .byte, .churn => {},
};
- try std.testing.expect(sliding);
+ try std.testing.expectEqual(@as(usize, 2), at_edge);
+ try std.testing.expect(wiped[0] < wiped[1]);
var scramble: Track = .{ .effect = .scramble, .frame = 3, .to = box };
var churning: usize = 0;
@@ -573,12 +584,12 @@ test "each character effect moves glyphs along its own axis" {
scramble.frame = 4;
switch (charSource(scramble, col, 0, area)) {
.churn => |next| changed = changed or next != byte,
- .old, .at, .byte => {},
+ .old, .at, .byte, .edge => {},
}
scramble.frame = 3;
},
.at => locked += 1,
- .old, .byte => return error.ScrambleShouldNotFreeze,
+ .old, .byte, .edge => return error.ScrambleShouldNotFreeze,
};
try std.testing.expect(churning > 0 and locked > 0 and changed);
@@ -598,7 +609,7 @@ test "each character effect moves glyphs along its own axis" {
try std.testing.expectEqual(@as(u8, '_'), byte);
carets += 1;
},
- .old, .at, .churn => {},
+ .old, .at, .churn, .edge => {},
};
}
try std.testing.expectEqual(@as(usize, 1), carets);
diff --git a/src/draw.zig b/src/draw.zig
index 93d5b2d9..b0b16e12 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -723,6 +723,7 @@ fn composedCell(
return switch (animation.charSource(track, col - area.x0, row - area.y0, area)) {
.old => canonical.previous_cells[index],
.byte => |byte| withByte(canonical.cells[index], byte),
+ .edge => |weight| towardGround(canonical.cells[index], canonical.previous_cells[index].style.bg, weight),
// Churn belongs on the glyph, not on the pane's empty space, and it
// borrows whichever side of the diff actually has one.
.churn => |byte| blk: {
@@ -749,6 +750,25 @@ fn composedCell(
};
}
+/// A cell with its ink and ground drawn `weight` of the way toward another
+/// ground, in OKLab. A palette or default colour cannot be mixed, so the
+/// ground steps over at the halfway point instead.
+fn towardGround(cell: Cell, ground: Color, weight: f32) Cell {
+ var out = cell;
+ const to = switch (ground) {
+ .rgb => |rgb| rgb,
+ .default, .index => return if (weight >= 0.5) blk: {
+ out.style.bg = ground;
+ break :blk out;
+ } else out,
+ };
+ inline for (.{ &out.style.fg, &out.style.bg }) |color| switch (color.*) {
+ .rgb => |rgb| color.* = .{ .rgb = colors.mixOklab(rgb, to, weight) },
+ .default, .index => {},
+ };
+ return out;
+}
+
fn withByte(cell: Cell, byte: u8) Cell {
var out = cell;
out.text[0] = byte;