diff options
| -rw-r--r-- | docs/config.md | 2 | ||||
| -rw-r--r-- | docs/effects.md | 1 | ||||
| -rw-r--r-- | src/animation.zig | 41 | ||||
| -rw-r--r-- | src/draw.zig | 20 |
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; |
