diff --git a/app/HistoryTransport.zig b/app/HistoryTransport.zig index 96c838cda..9a792cfcb 100644 --- a/app/HistoryTransport.zig +++ b/app/HistoryTransport.zig @@ -289,7 +289,10 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica }; const stepped = if (self.grip_steps) |was| opts.steps > was else false; self.grip_steps = opts.steps; - self.liquid.setLevel(fill); + // Out of a corner vial, the liquid has poured into the line: the vial's own drains as the + // bar comes out and fills again as it goes back, the one liquid all the way, so the vial at + // rest takes over from the bar with no change of surface. + self.liquid.setLevel(if (opts.corner != null) fill * (1 - dome.smooth(p / vial_drain)) else fill); if (opts.corner) |c| { self.liquid.step(now, c.point()); // Into a vial, the new step splashes the surface, as a drop landing would: after the @@ -298,7 +301,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica // splashes for when a vial came. if (stepped) self.liquid.splashIn(); } - const glow: f32 = if (self.stepped_at) |t| (if (now - t < step_glow_ns) 1 - @as(f32, @floatCast(@as(f64, @floatFromInt(now - t)) / @as(f64, @floatFromInt(step_glow_ns)))) else 0) else 0; + const glow = self.stepGlow(now); if (glow == 0) self.stepped_at = null; const in_use = on_grip or self.stepped_at != null or !self.slosh.settled(now); const alpha = self.presence.ease(in_use, now); @@ -497,6 +500,9 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica // once they are most of the way out. Without a grip, it fades. const from: ?dvui.Rect.Physical = if (!opts.history or p >= 1) null else if (opts.corner) |c| c.box(s) else if (gripRect(area, dome.radius, opts.edge)) |g| wrs.rectToPhysical(g) else null; const out = dome.smooth(p); + // What the bubbles bud out of and change back into: a dome's bud, or a corner vial's own circle. + const home: ?dvui.Rect.Physical = if (from) |f| if (opts.corner) |c| c.home(s) else dome.bud(f) else null; + const travel: f32 = if (from) |f| dome.bud(f).h else 0; // Out of a corner vial, which is whole at rest, the glass is whole throughout: eased toward a // dome's quiet presence, it was glass at partial alpha — a hole — and the vial at full alpha // the frame it went back in, a flash. @@ -509,7 +515,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica // strip grows, so the pictures sit inside them. const line_radius = std.math.lerp(phys[1].h / 2, core.corners.scaled(core.corners.card) * s, @min(1, grown * 1.5)); for (phys, bubbles[0..5], 0..) |r, *b, i| b.* = .{ - .rect = if (from) |f| dome.pull(f, r, p, bud_delays[i], bud_arcs[i]) else r, + .rect = if (home) |h| dome.pullFrom(h, travel, r, p, bud_delays[i], bud_arcs[i]) else r, .radius = if (i == 1 and grown > 0) line_radius else null, // Lit under the pointer only once the bar is out: moving, a lit bubble was a lighter drop // among the rest, its light running across the necks as they joined. @@ -528,7 +534,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica if (self.tab) { tab_index = count; bubbles[count] = .{ - .rect = if (from) |f| dome.pull(f, tab_p, p, bud_delays[5], bud_arcs[5]) else tab_p, + .rect = if (home) |h| dome.pullFrom(h, travel, tab_p, p, bud_delays[5], bud_arcs[5]) else tab_p, .light = if (p >= 1 and tab_p.contains(pt)) core.dialogs.bubble_hover else 0, }; count += 1; @@ -549,8 +555,18 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica // (the tab into the bar). The one soft shadow of the merged shape is the orb's // (`plans/LIQUID_EDGE_PLAN.md`); the rim and lens are what set the glass off meanwhile. // Out of a corner vial, cut by the card's outline, curve and all, as the vial is at rest. - core.dialogs.glassBubbles(fid, shapes[0..count], s, .{ .merge = dome.merge(p, 14), .shadow = 0, .edge = if (opts.corner) |c| c.place() else null }); + core.dialogs.glassBubbles(fid, shapes[0..count], s, .{ .merge = if (opts.corner != null) dome.cornerMerge(p, 14) else dome.merge(p, 14), .shadow = 0, .edge = if (opts.corner) |c| c.place() else null }); } + // A corner vial's contents change with it rather than fade: its liquid drains as the stream + // pours out into the line and fills again as it pours back, and its icon shrinks into the + // corner with the vial and grows back with it, so the vial the bar ends as is the vial at rest. + if (opts.corner) |c| if (from != null) { + const t = 1 - dome.smooth(p); + // The vial's own liquid (drained by `setLevel` above) and its icon lit as at rest. + const look: dome.Look = .{ .fill = fill, .tilt = tilt, .liquid = &self.liquid, .lit = on_grip, .glow = self.stepGlow(now) }; + dome.paintCornerLiquid(c, t, look); + dome.cornerIcon(c, t, look, float.data().borderRectScale().r); + }; // What is on the bubbles comes once they are most of the way out; the liquid comes with them. const shape_alpha = dvui.alpha(1); if (from != null) { @@ -627,6 +643,17 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica const bud_delays = [6]f32{ 0.3, 0, 0.16, 0.08, 0.23, 0.36 }; const bud_arcs = [6]f32{ 1.3, 0.6, 1.0, 0.8, 1.15, 1.6 }; +/// How far out of a corner vial the bar is when the vial's liquid has all poured out into the line +/// (and, coming back, when it starts to fill again). +const vial_drain: f32 = 0.45; + +/// How bright the grip is from the step it took last, 1 as it lands to 0 after `step_glow_ns`. +fn stepGlow(self: *const HistoryTransport, now: i128) f32 { + const t = self.stepped_at orelse return 0; + if (now - t >= step_glow_ns) return 0; + return 1 - @as(f32, @floatCast(@as(f64, @floatFromInt(now - t)) / @as(f64, @floatFromInt(step_glow_ns)))); +} + /// macOS's trim yellow. const yellow: dvui.Color = .{ .r = 0xF5, .g = 0xC0, .b = 0x2E }; diff --git a/app/dome.zig b/app/dome.zig index 4ca357302..e8b607abd 100644 --- a/app/dome.zig +++ b/app/dome.zig @@ -69,17 +69,21 @@ pub fn emerge(p: f32, delay: f32) f32 { return core.motion.enterFull(std.math.clamp((p - delay) / (1 - delay), 0, 1)); } -/// Where a bubble of the bar is, `p` of the way out (0 in its grip `from`, 1 at its place `to`), -/// budding at `delay` (a share of the growing): a drop pulled out of the grip rather than a button -/// sliding. Small while it travels, inflating as it nears its place; along an arc, up and then -/// across, rather than straight, `arc` times the usual height (so bubbles leaving together fan out -/// rather than travel stacked); on one smooth curve (`smooth`) both ways, so going back is coming -/// played backwards — in the reverse order, as the later a bubble buds, the sooner it is in. -pub fn pull(from: dvui.Rect.Physical, to: dvui.Rect.Physical, p: f32, delay: f32, arc: f32) dvui.Rect.Physical { +/// Where a bubble of the bar is, `p` of the way out (0 at its `home` in the grip, 1 at its place +/// `to`), budding at `delay` (a share of the growing): a drop pulled out of the grip rather than a +/// button sliding. It changes from `home`'s shape to a drop up to `size` across as it sets off, +/// travels small and inflates as it nears its place; along an arc, up and then across, rather than +/// straight, `arc` times the usual height (so bubbles leaving together fan out rather than travel +/// stacked); on one smooth curve (`smooth`) both ways, so going back is coming played backwards — +/// in the reverse order, as the later a bubble buds, the sooner it is in — and one arriving takes +/// `home`'s shape. A dome's home is its bud (`bud`); a corner vial's is the vial's own circle +/// (`Corner.home`), so the bubbles going back become the vial, and the glass the bar ends as is the +/// vial at rest. All physical. +pub fn pullFrom(home: dvui.Rect.Physical, size: f32, to: dvui.Rect.Physical, p: f32, delay: f32, arc: f32) dvui.Rect.Physical { const k = std.math.clamp((p - delay) / (1 - delay), 0, 1); const t = smooth(k); - if (t <= 0) return bud(from); - const a = bud(from); + if (t <= 0) return home; + const a = home; const ac: [2]f32 = .{ a.x + a.w / 2, a.y + a.h / 2 }; const bc: [2]f32 = .{ to.x + to.w / 2, to.y + to.h / 2 }; // Up off the straight line, most of the way out at the middle of the way. @@ -87,7 +91,9 @@ pub fn pull(from: dvui.Rect.Physical, to: dvui.Rect.Physical, p: f32, delay: f32 const lift = dist * pull_arc * arc * @sin(std.math.pi * std.math.clamp(t, 0, 1)); const c: [2]f32 = .{ std.math.lerp(ac[0], bc[0], t), std.math.lerp(ac[1], bc[1], t) - lift }; // A drop while it travels, as big as the grip's bud at most; its place's size once near it. - const drop = @min(a.h, @min(to.w, to.h) * pull_drop); + // Leaving home it goes from home's size to the drop's, so one arriving takes home's. + const sink = std.math.clamp(t / pull_sink, 0, 1); + const drop = std.math.lerp(a.h, @min(size, @min(to.w, to.h) * pull_drop), sink * sink * (3 - 2 * sink)); const grow = std.math.clamp((t - pull_inflate) / (1 - pull_inflate), 0, 1); const g = grow * grow * (3 - 2 * grow); const w = std.math.lerp(drop, to.w, g); @@ -103,6 +109,8 @@ const pull_arc: f32 = 0.14; // smallest shape, and a tiny drop is all rim — a darker drop among the rest. const pull_drop: f32 = 0.7; const pull_inflate: f32 = 0.55; +/// How far along a bubble has gone from its home's size to its drop's. +const pull_sink: f32 = 0.3; /// The dome at `from` itself in the bar's glass while the bar is `p` of the way out, shrinking into /// its edge as the bubbles leave it. Clip the glass to the edge (`aboveEdge`). @@ -118,6 +126,15 @@ pub fn merge(p: f32, settled: f32) f32 { return std.math.lerp(28, settled, smooth((p - 0.3) / 0.6)); } +/// `merge` for a bar growing out of a corner vial: none at all at the very start and end, so the +/// glass the bar goes back into is the vial's own outline, not the vial swollen by the drops +/// sinking into it, and the vial at rest takes over from it without a step. +pub fn cornerMerge(p: f32, settled: f32) f32 { + return merge(p, settled) * smooth(p / corner_merge_in); +} +/// How far along the growing a corner vial's glass reaches out to the drops leaving it in full. +const corner_merge_in: f32 = 0.25; + /// The clip that keeps the glass growing out of the dome at `from` from reaching past the edge it /// stands on; set it round the glass, and put the previous one back after. pub fn aboveEdge(from: dvui.Rect.Physical) dvui.Rect.Physical { @@ -343,6 +360,14 @@ pub const Corner = struct { return .{ .x = p[0] - r, .y = p[1] - r, .w = r, .h = r }; } + /// What the bar's bubbles bud out of and change back into (`pullFrom`): the vial's own circle + /// on the corner point, so a bubble arriving is the vial. + pub fn home(self: Corner, s: f32) dvui.Rect.Physical { + const r = self.radiusPx(s); + const p = self.point(); + return .{ .x = p[0] - r, .y = p[1] - r, .w = 2 * r, .h = 2 * r }; + } + /// The orb, `t` of its size (1 whole, 0 nothing), on the corner point. pub fn shape(self: Corner, s: f32, t: f32) core.widgets.orb.Shape { return .circle(self.point(), self.radiusPx(s) * @max(0, t)); @@ -392,21 +417,32 @@ pub fn drawCorner(c: Corner, id_extra: usize, look: Look, comptime anchor_fmt: [ } } paintCorner(float.data().id, c, 1, look); - // The history icon over the liquid: grey at rest, the text colour while it is in use. + cornerIcon(c, 1, look, float.data().borderRectScale().r); + return pressed; +} + +/// The history icon over the vial's liquid at `c`, the vial `t` of its size (1 whole), in the +/// widget whose border rect is `bounds` (physical): grey at rest, the text colour while it is in +/// use. It shrinks into the corner with the vial as the bar grows out of it, and grows back with +/// it (`HistoryTransport`), rather than fading. +pub fn cornerIcon(c: Corner, t: f32, look: Look, bounds: dvui.Rect.Physical) void { + const s = dvui.windowRectScale().s; + const k = std.math.clamp(t, 0, 1); + if (k * corner_icon < 1) return; const theme = dvui.themeGet(); const grey = theme.color(.window, .fill).lerp(if (theme.dark) dvui.Color.white else dvui.Color.black, 0.3).opacity(1); const lit: f32 = if (look.lit) 1 else std.math.clamp(@max(look.glow, look.held), 0, 1); - const ic = c.iconCenter(s); - const d = corner_icon * s; - const bounds = float.data().borderRectScale().r; + const p = c.point(); + const full = c.iconCenter(s); + const ic: [2]f32 = .{ std.math.lerp(p[0], full[0], k), std.math.lerp(p[1], full[1], k) }; + const d = corner_icon * k * s; core.icon.icon(@src(), "history_grip", icons.tvg.lucide.history, .{ .stroke_color = .white, .fill_color = .white, }, .{ - .rect = .{ .x = (ic[0] - d / 2 - bounds.x) / s, .y = (ic[1] - d / 2 - bounds.y) / s, .w = corner_icon, .h = corner_icon }, + .rect = .{ .x = (ic[0] - d / 2 - bounds.x) / s, .y = (ic[1] - d / 2 - bounds.y) / s, .w = corner_icon * k, .h = corner_icon * k }, .color_text = .{ .color = grey.lerp(theme.color(.content, .text), lit) }, }); - return pressed; } /// The vial's pixels at `c`, `t` of its size (1 whole; less while the bar grows out of it): its @@ -422,6 +458,17 @@ pub fn paintCorner(id: dvui.Id, c: Corner, t: f32, look: Look) void { // Lit under the pointer, lighter still as the wait before the bar runs. .light = if (look.lit) std.math.lerp(core.dialogs.bubble_hover, core.dialogs.bubble_press, look.held) else 0, }); + paintCornerLiquid(c, t, look); +} + +/// The liquid in the vial at `c`, `t` of its size, up to its level and tipped by its slosh, without +/// its glass: the vial's own, and the bar going back draws it filling again inside the bar's glass. +pub fn paintCornerLiquid(c: Corner, t: f32, look: Look) void { + const s = dvui.windowRectScale().s; + const prev = dvui.clip(c.card); + defer dvui.clipSet(prev); + const shape = c.shape(s, t); + if (shape.half[0] < 1) return; if (look.fill <= 0) return; // The vessel: the glass a little in from its rim, cut by the card a little in from its edge. const inset = liquid_inset * s; @@ -522,3 +569,17 @@ fn pourStream(lip: [2]f32, end: [2]f32, tail: f32, head: f32, th: f32, color: dv const r: dvui.Rect.Physical = .{ .x = h[0] - hw, .y = h[1] - hw, .w = 2 * hw, .h = 2 * hw }; r.fill(.all(hw), .{ .color = .{ .color = color }, .fade = 1 }); } + +test "a bar going back into its corner vial ends as the vial, with no glass between" { + const c: Corner = .{ .card = .{ .x = 10, .y = 20, .w = 800, .h = 600 }, .radii = .{ 12, 12, 12, 12 } }; + const s: f32 = 2; + const home = c.home(s); + const to: dvui.Rect.Physical = .{ .x = 300, .y = 500, .w = 68, .h = 68 }; + // Every bubble arrives as the home, whatever it budded at. + for ([_]f32{ 0, 0.16, 0.36 }) |delay| try std.testing.expectEqual(home, pullFrom(home, bud(c.box(s)).h, to, 0, delay, 1)); + // Its home is the vial whole: the bubbles arriving are the vial's own outline. + try std.testing.expectEqual(home, core.widgets.orb.rectOf(c.shape(s, 1))); + // And no glass runs between them and the vial at the end, which would bulge it. + try std.testing.expectEqual(@as(f32, 0), cornerMerge(0, 14)); + try std.testing.expectApproxEqAbs(@as(f32, 14), cornerMerge(1, 14), 0.001); +}