diff options
| -rw-r--r-- | docs/config.md | 2 | ||||
| -rw-r--r-- | docs/effects.md | 10 | ||||
| -rw-r--r-- | shaders/prebuilt/ui.frag.glsl | 18 | ||||
| -rw-r--r-- | shaders/prebuilt/ui.frag.spv | bin | 8176 -> 6968 bytes | |||
| -rw-r--r-- | shaders/ui.frag.glsl | 18 | ||||
| -rw-r--r-- | src/config.zig | 12 | ||||
| -rw-r--r-- | src/detached/wire.zig | 4 | ||||
| -rw-r--r-- | src/draw.zig | 5 | ||||
| -rw-r--r-- | src/gui/gui.zig | 57 | ||||
| -rw-r--r-- | src/surface.zig | 6 |
10 files changed, 96 insertions, 36 deletions
diff --git a/docs/config.md b/docs/config.md index 44f37079..97bf95a1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -578,6 +578,8 @@ HoverGlow on a soft glow under the word a look-hover would open, fading in over 80 ms Occlusion on pane bodies darken faintly toward their edges (2%), never over the cursor +Parallax on a theme's page pattern (lapis's dots) moves with the text + at a quarter of its speed CursorBlink on the cursor blinks, solid while typing and half a second after, eased at each edge, solid after 10 idle seconds GripWidth 150 the grip's button and the scrollbar under it, percent of the diff --git a/docs/effects.md b/docs/effects.md index ad454775..fbb44dfa 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -46,6 +46,7 @@ why. The verdict is the user's. | G10 theme crossfade | always (a theme change) | the chrome's colours fade through OKLab, easing in and out (out when retargeted) | .scratch/render/crossfade/ (forge, lapis, acme cut short by dusk) | pending | | G11 look-hover glow | `HoverGlow on\|off` (off) | a soft underline of the theme's accent under the word a look-hover would open, fading in over 80 ms | .scratch/render/hover/ (forge, acme, lapis; zooms) | pending | | G12 ambient occlusion | `Occlusion on\|off` (off) | pane bodies darken toward their edges, at most 2% (§9.1: 1 to 2%) | .scratch/render/occlusion/ (on/off on forge, acme, lapis; acme-diff-x40.png) | pending | +| G13 parallax | `Parallax on\|off` (off) | a theme's page pattern under the bodies (lapis's dots) moves with the text at a quarter of its speed, glides included | .scratch/render/parallax/ (lapis, notches down and up) | pending | ## Motion flavours @@ -227,3 +228,12 @@ under the lift's contrast ceiling, in the pane's own group so it moves with it. At §9.1's strength it is all but invisible on a dark page (a level of 255 on forge) and faint on a light one (acme's page steps one level toward the edges); acme-diff-x40.png shows where it falls. + +Notes on G13: a body region now carries its first line (Region.line: a +file's scroll, a terminal's grid offset; on the wire, a u32 more a +region). Under Parallax the dots under each body are offset by a quarter +of its line times the row height, plus G6's glide offset while one runs, +so they drift with the text as it glides and settle with it (measured: 4 +lines at 35 px move the 22 px grid 13 px, 35 mod 22). The cell shader +reads the offset from the instance's serial slot, free since G7 took the +dissolve's per-cell noise out. Without a pattern, nothing happens. diff --git a/shaders/prebuilt/ui.frag.glsl b/shaders/prebuilt/ui.frag.glsl index cb38fff1..51ec5b8c 100644 --- a/shaders/prebuilt/ui.frag.glsl +++ b/shaders/prebuilt/ui.frag.glsl @@ -27,16 +27,6 @@ layout(location = 5) flat in vec4 v_clip; layout(location = 0) out vec4 o_col; -float cellNoise(uint serial, uint col, uint row) { - uint x = serial ^ (col * 0x9e3779b9u) ^ (row * 0x85ebca6bu); - x ^= x >> 16; - x *= 0x7feb352du; - x ^= x >> 15; - x *= 0x846ca68bu; - x ^= x >> 16; - return float(x & 0xffffu) / 65535.0; -} - void main() { if (v_clip.z >= v_clip.x && (gl_FragCoord.x < v_clip.x || gl_FragCoord.x >= v_clip.z || gl_FragCoord.y < v_clip.y || gl_FragCoord.y >= v_clip.w)) discard; @@ -51,9 +41,6 @@ void main() { : clamp(u_ui.background_opacity, 0.0, 1.0); 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; - float noise = cellNoise(v_effect.z, cellCol, cellRow); // 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 @@ -63,7 +50,10 @@ void main() { // 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; + // v_effect.z: how far its pane's text has moved the dots (G13's + // parallax), in pixels, as a float's bits (0 without). + vec2 at = gl_FragCoord.xy - vec2(0.0, uintBitsToFloat(v_effect.z)); + vec2 d = mod(at, 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); } diff --git a/shaders/prebuilt/ui.frag.spv b/shaders/prebuilt/ui.frag.spv Binary files differindex 15318214..02e3edc4 100644 --- a/shaders/prebuilt/ui.frag.spv +++ b/shaders/prebuilt/ui.frag.spv diff --git a/shaders/ui.frag.glsl b/shaders/ui.frag.glsl index cb38fff1..51ec5b8c 100644 --- a/shaders/ui.frag.glsl +++ b/shaders/ui.frag.glsl @@ -27,16 +27,6 @@ layout(location = 5) flat in vec4 v_clip; layout(location = 0) out vec4 o_col; -float cellNoise(uint serial, uint col, uint row) { - uint x = serial ^ (col * 0x9e3779b9u) ^ (row * 0x85ebca6bu); - x ^= x >> 16; - x *= 0x7feb352du; - x ^= x >> 15; - x *= 0x846ca68bu; - x ^= x >> 16; - return float(x & 0xffffu) / 65535.0; -} - void main() { if (v_clip.z >= v_clip.x && (gl_FragCoord.x < v_clip.x || gl_FragCoord.x >= v_clip.z || gl_FragCoord.y < v_clip.y || gl_FragCoord.y >= v_clip.w)) discard; @@ -51,9 +41,6 @@ void main() { : clamp(u_ui.background_opacity, 0.0, 1.0); 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; - float noise = cellNoise(v_effect.z, cellCol, cellRow); // 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 @@ -63,7 +50,10 @@ void main() { // 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; + // v_effect.z: how far its pane's text has moved the dots (G13's + // parallax), in pixels, as a float's bits (0 without). + vec2 at = gl_FragCoord.xy - vec2(0.0, uintBitsToFloat(v_effect.z)); + vec2 d = mod(at, 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); } diff --git a/src/config.zig b/src/config.zig index 22e65c46..d11cbe76 100644 --- a/src/config.zig +++ b/src/config.zig @@ -125,6 +125,7 @@ pub const leader_path = paths: { if (builtins.capabilities.lift) table.set(.SelectionGlow, null); if (builtins.capabilities.lift) table.set(.HoverGlow, null); if (builtins.capabilities.lift) table.set(.Occlusion, null); + if (builtins.capabilities.lift) table.set(.Parallax, null); if (builtins.capabilities.lift) table.set(.Lift, null); table.set(.Motion, null); table.set(.InactiveDim, null); @@ -678,6 +679,9 @@ pub const Runtime = struct { /// G12: pane bodies darken faintly toward their edges (ambient /// occlusion, at most 2%), a pixel shell's. occlusion: bool = false, + /// G13: a theme's page pattern under the bodies (lapis's dots) moves + /// with the text at a quarter of its speed, a pixel shell's. + parallax: bool = false, /// How long a message stays after the key or click that dismisses it. message_linger_ms: u16 = 800, /// How long its fall into the row takes, easing out (§8.1: arriving, @@ -901,7 +905,7 @@ pub const Runtime = struct { /// The message timings a setting word sets, each in milliseconds. pub const MessageTime = enum { linger, fall, dissolve }; - pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, syntax_bold, verbose, message_animation, ligatures, cursor_blink, selection_glow, hover_glow, occlusion }; + pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, syntax_bold, verbose, message_animation, ligatures, cursor_blink, selection_glow, hover_glow, occlusion, parallax }; /// A bundled post pass, a Shadertoy file in shaders/post/: the CRT, and /// G4's bloom, vignette and grain (docs/effects.md). pub const Scene = enum { crt, bloom, vignette, grain }; @@ -963,6 +967,7 @@ pub const Runtime = struct { .{ .word = "SelectionGlow", .action = .{ .toggle = .selection_glow }, .availability = .lift, .doc = "A soft halo of the selection's colour round it in the body, on or off." }, .{ .word = "HoverGlow", .action = .{ .toggle = .hover_glow }, .availability = .lift, .doc = "A soft underline of light under the word a look-hover would open, on or off." }, .{ .word = "Occlusion", .action = .{ .toggle = .occlusion }, .availability = .lift, .doc = "Pane bodies darken faintly toward their edges, on or off." }, + .{ .word = "Parallax", .action = .{ .toggle = .parallax }, .availability = .lift, .doc = "A theme's page pattern moves with the text at a quarter of its speed, on or off." }, .{ .word = "Lift", .action = .lift, .availability = .lift, .doc = "How the focused pane stands off the page: off, shadow, rim or auto." }, .{ .word = "Motion", .action = .motion, .doc = "How animations move: off, crisp, smooth, bouncy or playful." }, .{ .word = "InactiveDim", .action = .inactive_dim, .doc = "Fade the unfocused panes' text by a percentage, 0 through 100, 0 off." }, @@ -1039,9 +1044,9 @@ pub const Runtime = struct { .toggle => |field| if (field == .ligatures) { if (setting.availability != .ligatures) @compileError("the ligatures toggle must use the ligatures capability"); - } else if (field == .cursor_blink or field == .selection_glow or field == .hover_glow or field == .occlusion) { + } else if (field == .cursor_blink or field == .selection_glow or field == .hover_glow or field == .occlusion or field == .parallax) { if (setting.availability != .lift) - @compileError("CursorBlink, SelectionGlow, HoverGlow and Occlusion must use the lift capability (a pixel shell's)"); + @compileError("CursorBlink, SelectionGlow, HoverGlow, Occlusion and Parallax must use the lift capability (a pixel shell's)"); } else if (setting.availability != null) @compileError("unconditional toggles cannot carry a backend capability"), else => if (setting.availability != null) @@ -1699,6 +1704,7 @@ pub const Runtime = struct { .selection_glow => "SelectionGlow on\n", .hover_glow => "HoverGlow on\n", .occlusion => "Occlusion on\n", + .parallax => "Parallax on\n", }, .shell => "Shell requested (new panes) fish\n", .tagline_size => "TaglineSize 73\n", diff --git a/src/detached/wire.zig b/src/detached/wire.zig index 3a914e42..6903a1e8 100644 --- a/src/detached/wire.zig +++ b/src/detached/wire.zig @@ -270,6 +270,7 @@ pub const Frame = struct { region.notice = try r.getByte(); region.clip = try r.getU16(); region.lift = @bitCast(try r.getU32()); + region.line = @bitCast(try r.getU32()); // A springy flavour overshoots, and a wind-up dips below. if (!(region.lift >= -1 and region.lift <= 2)) return error.BadValue; if (region.rect.x > f.cols or region.rect.w > f.cols - region.rect.x or @@ -754,7 +755,7 @@ pub fn layersBound(layers: []const pardes.Layer, tags: []const pardes.Layer, reg return n + regions.len * region_len + chrome_len; } -const region_len = 1 + 1 + 4 + 8 + 1 + 2 + 2 + 1 + 2 + 4; +const region_len = 1 + 1 + 4 + 8 + 1 + 2 + 2 + 1 + 2 + 4 + 4; /// The palette: each colour three bytes, a theme's optional ones a flag /// before them. const chrome_len = @typeInfo(pardes.Chrome).@"struct".fields.len * 4 + 8 * 4; @@ -839,6 +840,7 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point try w.putByte(region.notice); try w.putU16(region.clip); try w.putU32(@bitCast(region.lift)); + try w.putU32(@bitCast(region.line)); } inline for (@typeInfo(pardes.Chrome).@"struct".fields) |field| { const rgb = @field(chrome, field.name); diff --git a/src/draw.zig b/src/draw.zig index 027326fa..aef93f51 100644 --- a/src/draw.zig +++ b/src/draw.zig @@ -63,7 +63,8 @@ pub fn place(p: *Pardes, s: *Surface) void { // A folded pane is its tag rows and nothing else. if (pane.collapsed) continue; const body_h = r.h -| pane.tag_rows; - s.addRegion(.{ .kind = .body, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x + config.GUTTER, .y = body_y, .w = r.w - config.GUTTER, .h = body_h } }); + const line: i32 = if (pane.file) |f| @intCast(@min(f.scroll, std.math.maxInt(i32))) else if (pane.isTerminal()) panes.Terminal.gridOffset(pane) else 0; + s.addRegion(.{ .kind = .body, .owner = owner, .serial = pane.serial, .active = active, .line = line, .rect = .{ .x = r.x + config.GUTTER, .y = body_y, .w = r.w - config.GUTTER, .h = body_h } }); var rail: Region = .{ .kind = .rail, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x, .y = body_y, .w = config.GUTTER, .h = body_h } }; // An image's rail has no thumb; a native PDF's is measured as its // pages are placed (Pdf.draw); every other pane's is here. @@ -472,7 +473,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface { .rail_px = @intCast(@min(255, @as(u32, p.theme().rail_px orelse 12) * p.settings.grip_width / 100)), .name_ink = chrome.tag_name_fg, .active_name_ink = if (p.settings.focus_tint) chrome.tag_active_name_fg else chrome.tag_name_fg, - .fx = @as(u8, @intFromBool(p.settings.selection_glow)) | @as(u8, @intFromBool(p.settings.hover_glow)) << 1 | @as(u8, @intFromBool(p.settings.occlusion)) << 2, + .fx = @as(u8, @intFromBool(p.settings.selection_glow)) | @as(u8, @intFromBool(p.settings.hover_glow)) << 1 | @as(u8, @intFromBool(p.settings.occlusion)) << 2 | @as(u8, @intFromBool(p.settings.parallax)) << 3, }; if (p.theme().decor) |decor| { s.chrome.decor_dots = decor.page_dots; diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 8d9c29ab..dc58ee3f 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -5827,6 +5827,17 @@ fn renderFrame( if (surface.chrome.fx & 2 == 0 or surface.panelTracks().len != 0) g.hover_glow = .{ .now_ns = g.sel_glow.now_ns }; if (g.glide.moving()) g.glide.offset = g.glide.spring.value(g.glide.now_ns); cell_decor = CellDecor.of(chrome); + // G13: a body's dots follow its text at a quarter of its speed: its + // first line, and a glide's picture offset, in rows. + if (chrome.fx & 8 != 0 and chrome.decor_dots != null) for (surface.regionList()) |region| { + if (region.kind != .body or cell_decor.nbodies == cell_decor.bodies.len) continue; + var rows: f32 = @floatFromInt(region.line); + if (g.glide.moving() and g.glide.pane == region.owner) rows += g.glide.offset; + const period = cell_decor.uniforms[4]; + cell_decor.bodies[cell_decor.nbodies] = bodyPixels(region, layout); + cell_decor.shifts[cell_decor.nbodies] = @mod(-rows * layout.h * 0.25, period); + cell_decor.nbodies += 1; + }; // Plaques need an opaque window: with WindowOpacity under 100 the cells // are drawn as they are, and the plaques not at all. cell_decor.plaques_off = backgroundOpacity(g.applied_window_opacity) < 1; @@ -7004,7 +7015,13 @@ fn emitInstance( (cell_decor.inPlaque(px0 + 0.5, py0 + 0.5) or cell_decor.inPlaque(px0 + layout.w - 0.5, py0 + 0.5))) instances[idx].effect |= clear_bg_bit; if (cell_decor.dots) |dotted| { - if (!resolved.clear_bg and std.mem.eql(u8, &resolved.bg, &dotted)) instances[idx].effect |= dotted_bit; + if (!resolved.clear_bg and std.mem.eql(u8, &resolved.bg, &dotted)) { + instances[idx].effect |= dotted_bit; + // G13: the dots under a body move with its text, a quarter + // as far; the serial slot carries it (the shader reads it + // as the dots' offset alone). + instances[idx].serial = @bitCast(cell_decor.parallaxAt(px0 + 0.5, py0 + 0.5)); + } } if (cell_decor.title) |inks| { if (resolved.role == .tagline and (std.mem.eql(u8, &resolved.fg, &inks[0]) or std.mem.eql(u8, &resolved.fg, &inks[1]))) instances[idx].effect |= title_bit; @@ -8954,6 +8971,17 @@ const CellDecor = struct { grounds: [2][3]u8 = .{ .{ 0, 0, 0 }, .{ 0, 0, 0 } }, plaques_off: bool = false, + /// G13: each body's rectangle and how far its dots have moved. + bodies: [pardes.MAX_PANES][4]f32 = undefined, + shifts: [pardes.MAX_PANES]f32 = undefined, + nbodies: usize = 0, + + fn parallaxAt(decor: *const CellDecor, x: f32, y: f32) f32 { + for (decor.bodies[0..decor.nbodies], decor.shifts[0..decor.nbodies]) |band, shift| + if (x >= band[0] and x < band[2] and y >= band[1] and y < band[3]) return shift; + return 0; + } + fn inPlaque(decor: *const CellDecor, x: f32, y: f32) bool { for (decor.plaques[0..decor.nplaques]) |band| if (x >= band[0] and x < band[2] and y >= band[1] and y < band[3]) return true; @@ -9740,6 +9768,33 @@ test "occlusion shades each body's edges, inside it, never a tag, a grip or the try std.testing.expect(shades >= 4); } +test "a body tells its first line, and lapis's dots under it move a quarter as far" { + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 }); + defer core.deinit(); + const pane = try core.setTestFile("line\n" ** 80); + pane.scrollBy(8); + try std.testing.expect(core.executeBuiltinLine(0, "Theme lapis")); + core.settings.parallax = true; + for (0..40) |_| core.update(.tick); + pardes.test_api.sync(core); + const s = try core.render(core.scratch.allocator()); + const body = for (s.regionList()) |region| { + if (region.kind == .body) break region; + } else return error.NoBody; + try std.testing.expectEqual(@as(i32, 8), body.line); + try std.testing.expect(s.chrome.fx & 8 != 0); + // The shift the cell shader is handed: a quarter of 8 rows of 20 px, + // upward, within the dots' period. + var decor = CellDecor.of(&s.chrome); + const period = decor.uniforms[4]; + decor.bodies[0] = .{ 0, 0, 800, 480 }; + decor.shifts[0] = @mod(-8.0 * 20.0 * 0.25, period); + decor.nbodies = 1; + try std.testing.expectApproxEqAbs(@mod(-40.0, period), decor.parallaxAt(10, 10), 1e-4); + try std.testing.expectEqual(@as(f32, 0), decor.parallaxAt(900, 10)); +} + /// 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/surface.zig b/src/surface.zig index 02e4bb5d..d1ea78b1 100644 --- a/src/surface.zig +++ b/src/surface.zig @@ -370,6 +370,10 @@ pub const Region = struct { /// focus (Lift, on a spring), a notice's while Lift is on. A pixel shell /// casts a soft shadow from it; the tier it is drawn in never changes. lift: f32 = 0, + /// A body's view: the first line it shows (a file's scroll, a + /// terminal's grid offset), for what a pixel shell moves with the text + /// (G13's parallax). + line: i32 = 0, /// Cells the column-move rail takes off the right edge while it runs /// there: layers and pixel shells stop short of them, the grid does not. clip: u16 = 0, @@ -451,7 +455,7 @@ pub const Chrome = struct { /// A pixel shell's small effects: bit 0 a glow round the body's /// selection (G9, SelectionGlow), bit 1 one under the word a look-hover /// would open (G11, HoverGlow), bit 2 the bodies' edge shade (G12, - /// Occlusion). + /// Occlusion), bit 3 the page pattern's parallax (G13, Parallax). fx: u8 = 0, }; |
