From 09ec515c5e94de741104038cd61f18b2b997e78c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 06:04:28 -0300 Subject: G5: lapis, 0x4200.cafe's manuscript, as a theme with its ornament A native theme `lapis` (src/themes/lapis.zig) from web/theme/lapis.css: a lapis-deep page, lapis tags with gold names, vellum text, vermilion rubrics, gold rules and a vermilion tag rule, gold selection in lapis ink. Its ornament is the theme's new `decor` (colors.Decor), drawn by a pixel shell inside the chrome it already has (decision 1), all of it off every focus indicator: - page_dots: the site's gold dot grid (#e8c46a22, 22 px), drawn by the cell shader on page-coloured cells alone (flag 0x10000000), so it lies under the text and never under a selection, a tag or the cursor; fixed to the window, a page the panes lie on. - rail_checker: the dither frame's gold and lapis checker in the scroll track (a decor kind, flag 0x08000000), anchored to its rail; the thumb stays solid gold. - tag plaques (tag_border, tag_shadow, tag_stripe): every tag a raised plaque inside its own band, the site's trail: a 2px gold frame and a hard 5px vermilion shadow down and right, both within the band's rows and columns (down, what the slack under the text leaves), so no shadow reaches a body, a grip or the next pane. The focused pane's plaque is the code window's title bar, gold and lapis stripes of 2px, its words on plates of the focused ground; unfocused ones are plain; column and workspace tags take the object-label language (1px frame, 2px shadow toward the page). Drawn as decor under the cells (a group's under range), with blank tag cells see-through and the cells blended premultiplied (pipeline_over) on such frames; opaque windows only. - title_shadow: the titles' vermilion offset shadow under a file name's letters, sampled in the cell shader inside the letters' own cells (flag 0x08000000), none on a cursor. Chrome carries the decor (decor_*, name_ink, active_name_ink); a theme file folds a nested decor literal field by field. Left out, with why, in docs/effects.md: the fonts, the ornaments, a decor crossfade, and in the tty anything but the colours (a tag row has no spare cell for a shadow). An empty column is the page. The effect id is masked to 27 bits (ui.vert/ui.frag) for the fifth flag. Tests: the cell flags land only on page ground and file-name cells, never a cursor, nothing without decor; lapis's plaques stay inside their tag's band, clear of every grip, only the focused one striped; its checker is in the track; no other theme draws any; the separator and focus tests cover lapis. Goldens: theme and themesel (the ring gains lapis after acme). Gates as G4. Feel review: .scratch/render/lapis/ (lapis.png, lapis-2x.png, tags-focused-vs-unfocused-2x.png, a PanelSlide at 60 fps and a quarter). --- shaders/ui.frag.glsl | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'shaders/ui.frag.glsl') diff --git a/shaders/ui.frag.glsl b/shaders/ui.frag.glsl index dca224ba..62dbbfa2 100644 --- a/shaders/ui.frag.glsl +++ b/shaders/ui.frag.glsl @@ -10,6 +10,12 @@ layout(set = 2, binding = 0) uniform sampler2D u_atlas; layout(std140, set = 3, binding = 0) uniform UiUniforms { float background_opacity; + // A theme's ornament (colors.Decor): the page's dots (rgb, strength), + // their geometry (period, solid radius, faded radius, in pixels), and a + // file name's shadow (rgb, offset in pixels). + vec4 decor_dots; + vec4 decor_dot_geom; + vec4 decor_shadow; } u_ui; layout(location = 0) in vec2 v_uv; @@ -43,7 +49,7 @@ void main() { float bgA = (v_effect.x & 0x20000000u) != 0u ? 1.0 : (v_effect.x & 0x40000000u) != 0u ? 0.0 : clamp(u_ui.background_opacity, 0.0, 1.0); - uint effect = v_effect.x & 0x0fffffffu; + uint effect = v_effect.x & 0x07ffffffu; float progress = clamp(uintBitsToFloat(v_effect.y), 0.0, 1.0); uint cellCol = v_effect.w & 0xffffu; uint cellRow = v_effect.w >> 16; @@ -52,7 +58,29 @@ void main() { // Ink over ground, both premultiplied: v_fg*a + (v_bg*bgA)*(1-a), which is // mix() with the ground already premultiplied. At bgA = 1 that is exactly // the mix(v_bg, v_fg, a) at alpha 1 this shader always wrote. - vec4 cell = vec4(mix(v_bg * bgA, v_fg, a), mix(bgA, 1.0, a)); + vec3 ground = v_bg; + // 0x10000000: the page's ground under a pane body, dotted (lapis): a + // dot every period pixels, fixed to the window, under the text. + if ((v_effect.x & 0x10000000u) != 0u) { + float period = u_ui.decor_dot_geom.x; + vec2 d = mod(gl_FragCoord.xy, period) - 0.5 * period; + float dot_a = 1.0 - smoothstep(u_ui.decor_dot_geom.y, u_ui.decor_dot_geom.z, length(d)); + ground = mix(ground, u_ui.decor_dots.rgb, u_ui.decor_dots.a * dot_a); + } + vec4 under = vec4(ground * bgA, bgA); + // Derivatives outside any branch: a pixel's step in the atlas and in + // the cell, for the shadow's offset. + vec2 step_uv = vec2(dFdx(v_uv.x), dFdy(v_uv.y)) * u_ui.decor_shadow.a; + vec2 step_cell = vec2(dFdx(v_cell_uv.x), dFdy(v_cell_uv.y)) * u_ui.decor_shadow.a; + // 0x08000000: a file name's letters cast a shadow down and right onto + // their own cell's ground (lapis), under the ink; what would fall past + // the cell's edge is not drawn. + if ((v_effect.x & 0x08000000u) != 0u) { + vec2 from = v_cell_uv - step_cell; + float s = all(greaterThanEqual(from, vec2(0.0))) ? texture(u_atlas, v_uv - step_uv).r : 0.0; + under = mix(under, vec4(u_ui.decor_shadow.rgb, 1.0), s); + } + vec4 cell = vec4(mix(under.rgb, v_fg, a), mix(under.a, 1.0, a)); // 3 = data dissolve. Changed cells retain the frozen old cell until their // stable threshold, then atomically become the new cell. Exact endpoints -- cgit v1.3 From c1dbb16d069a54f52533f346655b0fc49026c9a4 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 06:58:19 -0300 Subject: G7: a moving pane casts a shadow, a retargeted move keeps its speed, a dissolve crossfades - Track.launch: a move retargeted mid-way (Presentation.sync) starts at the old move's speed along the new one's longest side (carriedLaunch, Motion.easeFrom/speedFrom), so a second change during a slide bends the motion instead of stopping it dead. The macOS track ABI grows the float (pardes.h, 48 bytes; its reserved byte is named motion, as in Zig). - A pane that slides, zooms or rises casts Lift's soft shadow at sin(pi x progress) of its strength, on the other panes' bodies and rails alone, under the same contrast ceiling, carried by its track. - The GUI's PanelDissolve is a crossfade: the new cell drawn first, the old one over it at 1 - progress, premultiplied, on the blending cell pipeline for such groups (new cells now emitted before old ones, as the old layers already came after the new). sRGB blend, not linear light (documented). The tty keeps its per-cell swap. Presentation.zig's and animation.zig's tests were not in any test root; pardes.zig's test block now takes them in. Tests: a retargeted move carries its speed and still lands exactly; a moving pane's shadow is clipped off every tag, grip and its own body, none at the start or the landing, most mid-way. GUI golden 18 changes by the shadow (frame 6 of a slide). Gates as G6. Feel review: .scratch/render/transitions/. --- docs/effects.md | 15 ++++++ shaders/prebuilt/ui.frag.glsl | 17 ++++--- shaders/prebuilt/ui.frag.spv | Bin 8448 -> 8176 bytes shaders/ui.frag.glsl | 17 ++++--- src/Presentation.zig | 39 +++++++++++++++- src/animation.zig | 33 ++++++++++++-- src/gui/gui.zig | 103 ++++++++++++++++++++++++++++++++++++------ src/macos.zig | 2 +- src/macos/pardes.h | 3 +- src/pardes.zig | 2 + 10 files changed, 199 insertions(+), 32 deletions(-) (limited to 'shaders/ui.frag.glsl') diff --git a/docs/effects.md b/docs/effects.md index 04757aca..58c0098e 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -40,6 +40,7 @@ why. The verdict is the user's. | G4 bloom, vignette, grain | `Bloom`, `Vignette`, `Grain` `0..3` (off) | bundled post passes; none touches a tag, grip, notice or the cursor (pardesSpare); each capped per theme (pardesCap) | .scratch/render/{bloom,vignette,grain}/ (levels 0-3 on forge, dusk, acme; crops; mp4 stepping through the levels) | pending | | G5 lapis | `Theme lapis` (its `decor`) | a native theme with ornament: tag plaques with hard shadows inside their bands (the focused one striped), dots, rail checker, title shadow | .scratch/render/lapis/ (whole window at 1x and 2x, crops, a slide mp4) | pending | | G6 smooth scroll | follows `Motion` (on through the default smooth; `Motion off` lands at once) | a mouse wheel's notch glides, a touchpad follows the finger | .scratch/render/scroll/ (glide-.mp4, quarter speed, strips) | pending | +| G7 panel transitions | `PanelSlide`, `PanelZoom`, `PanelVertical`, `PanelDissolve` (off) with `Motion` | a moving pane casts a shadow; a retargeted move keeps its speed; the GUI's dissolve is a crossfade | .scratch/render/transitions/ (slide smooth, bouncy, retargeted; dissolve) | pending | ## Motion flavours @@ -161,3 +162,17 @@ continuous path (Pardes.shell_continuous), so the shell draws every loop while it moves and a virtual clock moves with it; idle, it costs nothing. A PDF page scrolls as before. The test feed takes wheel events (ESC]777;mouse;wheel;;;BEL). + +Notes on G7: the geometry already followed the Motion flavour's spring +(sxrrwplk); a move retargeted mid-way now carries its speed on +(Track.launch, from the old move's speed along the new one's longest +side), so a second Newcol during a slide bends the motion instead of +stopping it dead. A pane that slides, zooms or rises stands off the page +while it moves: a soft shadow as Lift's, at sin(pi x progress) of its +strength (none as it sets out or lands), on the other panes' bodies and +rails alone and under the same contrast ceiling, moving with the pane. The +GUI's PanelDissolve is a crossfade: a changed cell's new form drawn whole, +its old one over it at 1 - progress; the blend is in sRGB, not linear +light (a two-colour mix in linear light needs both in one shader; open +point 3). The tty keeps its per-cell swap. GUI golden 18-mid-transition +changes (the slide's shadow at frame 6). diff --git a/shaders/prebuilt/ui.frag.glsl b/shaders/prebuilt/ui.frag.glsl index 62dbbfa2..cb38fff1 100644 --- a/shaders/prebuilt/ui.frag.glsl +++ b/shaders/prebuilt/ui.frag.glsl @@ -82,13 +82,18 @@ void main() { } vec4 cell = vec4(mix(under.rgb, v_fg, a), mix(under.a, 1.0, a)); - // 3 = data dissolve. Changed cells retain the frozen old cell until their - // stable threshold, then atomically become the new cell. Exact endpoints - // mirror panel_animation.dissolveRevealed, including the zero hash. + // 3 = dissolve, a crossfade (G7): a changed cell's new form is drawn + // first, whole, and its frozen old one over it at 1 - progress, + // premultiplied (the frame's cells blend over, pipeline_over), so the + // old fades out as the new comes in; at the ends, exactly one of them. + // The tty keeps its per-cell swap (panel_animation.dissolveRevealed). if (effect == 3u) { - bool reveal = progress >= 1.0 || (progress > 0.0 && noise < progress); - if (oldLayer ? reveal : !reveal) discard; - o_col = cell; + if (!oldLayer) { + o_col = cell; + return; + } + if (progress >= 1.0) discard; + o_col = cell * (1.0 - progress); return; } diff --git a/shaders/prebuilt/ui.frag.spv b/shaders/prebuilt/ui.frag.spv index d83d9c55..15318214 100644 Binary files a/shaders/prebuilt/ui.frag.spv and b/shaders/prebuilt/ui.frag.spv differ diff --git a/shaders/ui.frag.glsl b/shaders/ui.frag.glsl index 62dbbfa2..cb38fff1 100644 --- a/shaders/ui.frag.glsl +++ b/shaders/ui.frag.glsl @@ -82,13 +82,18 @@ void main() { } vec4 cell = vec4(mix(under.rgb, v_fg, a), mix(under.a, 1.0, a)); - // 3 = data dissolve. Changed cells retain the frozen old cell until their - // stable threshold, then atomically become the new cell. Exact endpoints - // mirror panel_animation.dissolveRevealed, including the zero hash. + // 3 = dissolve, a crossfade (G7): a changed cell's new form is drawn + // first, whole, and its frozen old one over it at 1 - progress, + // premultiplied (the frame's cells blend over, pipeline_over), so the + // old fades out as the new comes in; at the ends, exactly one of them. + // The tty keeps its per-cell swap (panel_animation.dissolveRevealed). if (effect == 3u) { - bool reveal = progress >= 1.0 || (progress > 0.0 && noise < progress); - if (oldLayer ? reveal : !reveal) discard; - o_col = cell; + if (!oldLayer) { + o_col = cell; + return; + } + if (progress >= 1.0) discard; + o_col = cell * (1.0 - progress); return; } 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(); -- cgit v1.3