dark glass gets a surface ladder, and the 200-750ms stalls are dead - two sessions of uncommitted work landing together
SURFACES: panes add black, surfaces INSIDE panes add white. Every pane stands on 0.7075 black over the acrylic (.content 0.35 over .app 0.55), so a translucent black card nets 0.81 - darker than its own pane and inside the ~0.85 band already rejected once on the artist page. Solid mode's ladder is measured UPWARD from #121212 (+6/+18/+24), so glass elevates with WHITE: new --glass-surface/-hover/-press/-hairline/-well/-ring under :root.glass (0.03/0.08/0.11/0.10/0.18), which is independently the vocabulary .btn-ghost:hover and .ax-ghost:hover already shipped. The four solid tokens are NEVER redefined - they are consumed inside color-mix() and gradient stops where an alpha multiplies through and kills the call site (0.74 x 0.03 = an invisible chip; the album row's text-overflow fade works by BEING opaque), and they paint modal chrome that has to stay solid. 129 additive :global(:root.glass) declarations across 18 files, all 129 verified inside a glass block by walking the diff - solid mode byte-identical, confirmed structurally, by computed style with the class off, and visually. Three of them are not colour swaps: the album row's .rowbtns shim was OCCLUDING the last ~19px of the listeners bar and nothing translucent can occlude, so .tcount masks out just before the buttons instead; Playlist's banner scrim and the discography's pinned ambience both landed on opaque --bg-base and now dissolve like the album banner; and the 0.9 black hero tint on Home/CatalogSearch/Discography paints straight onto the pane when there is no artwork, so :first-child (the {#if} renders no element) flips it to white. Menus, dialogs, toasts and the import surfaces stay opaque deliberately, and the Settings copy for the mode now says so instead of claiming cards do. Measured with a known ground substituted behind the transparent window, since CDP never sees the acrylic: library toolbar chroma +2 as shipped, +16 with a white fill AND its backdrop-filter dropped, +30 for the bare pane beside it. RESIZE: Windows drops the legacy accent-policy acrylic for the whole of a live resize and the window stays transparent through it, so the desktop punches through sharp - measured by toggling window_glass alone, desktop icon labels readable through the titlebar. The blur cannot be kept (documented since Win10 2004, and the modern DWM backdrop was already rejected for the overlay-promotion blink), so armGlassResizeGuard grounds the canvas to --bg-sidebar for the length of the gesture. STALLS: "choppy but the FPS says 144" was 200-750ms MAIN-THREAD TASKS, not frame pacing - idle-scheduled work fires at scroll pauses by definition. Twin bake moved to display resolution and off the ready task (was a same-task 4K WebGL readback plus software blur, 100-170ms per rotation AND per artist entry); gl.finish() -> gl.flush() in buildMaster, which had been draining the whole 4K EASU/RCAS/grade chain on the main thread; copyCanvas realloc guarded; one app-wide scroll clock in scrollbar.js (markAppScroll/appScrollIdleFor) so all three periscopes stop cloning mid-gesture on ANY pane rather than only their own; bounded reclone with the querySelectorAll census deleted, the gate widened 250->1000ms, and the build/attach split across an idle callback and a rAF behind a generation guard; the hero rotation tick gated on scroll recency and its payload cache sized to the working set (2->4 entries, 24->32MiB, 32->64MiB); sessionTick no longer force-rewrites every 12s while merely PAUSED. Scroll storm went from 109-616ms long tasks to zero. Hover recalc and GC were measured innocent - do not re-chase them. 98/98 tests throughout.
This commit is contained in:
1 parent
7ab678fd22
commit
547cee6649
26 files changed
+1220
-144
No files matched your search
@@ -16,6 +16,7 @@
|
||||
playlistCoverFor,
|
||||
} from "../lib/state.svelte.js";
|
||||
import * as api from "../lib/api.js";
|
||||
import { markAppScroll, appScrollIdleFor } from "../lib/scrollbar.js";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
// ── the periscope (dark glass only) ─────────────────────────────────
|
||||
@@ -42,10 +43,10 @@
|
||||
const view = viewEl;
|
||||
const peri = periEl;
|
||||
let pending = 0;
|
||||
let lastScroll = 0;
|
||||
let lastNodes = 500;
|
||||
let recloneGen = 0;
|
||||
let lastRecloneMs = 4;
|
||||
const sync = () => {
|
||||
lastScroll = performance.now();
|
||||
markAppScroll();
|
||||
const c = peri.firstElementChild?.firstElementChild;
|
||||
if (c) c.style.transform = `translateY(${-view.scrollTop}px)`;
|
||||
};
|
||||
@@ -59,6 +60,11 @@
|
||||
// and is pinned inside by margins at its measured width.
|
||||
// height 142 = overscan + band + overscan: the filter rasters
|
||||
// this box, never the full cloned list.
|
||||
// BUILD, detached, measured — the duration feeds the next
|
||||
// debounce delay (see GlassBar; the old node census existed
|
||||
// only to guess it).
|
||||
const t0 = performance.now();
|
||||
const gen = ++recloneGen;
|
||||
const wrap = document.createElement("div");
|
||||
wrap.style.cssText =
|
||||
"position:absolute;top:-48px;left:-48px;right:-48px;height:142px;" +
|
||||
@@ -71,16 +77,23 @@
|
||||
c.style.height = "auto";
|
||||
c.style.overflow = "visible";
|
||||
c.querySelectorAll("canvas").forEach((n) => n.remove());
|
||||
lastNodes = c.querySelectorAll("*").length;
|
||||
wrap.appendChild(c);
|
||||
const fresh = !peri.firstElementChild;
|
||||
if (fresh) {
|
||||
wrap.style.opacity = "0";
|
||||
wrap.style.transition = "opacity 160ms ease";
|
||||
}
|
||||
peri.replaceChildren(wrap);
|
||||
if (fresh) requestAnimationFrame(() => (wrap.style.opacity = "1"));
|
||||
sync();
|
||||
lastRecloneMs = performance.now() - t0;
|
||||
// ATTACH in its own frame (see GlassBar): the style+layout
|
||||
// flush of the whole clone never shares a task with its
|
||||
// construction; stale builds and resumed scrolling requeue.
|
||||
requestAnimationFrame(() => {
|
||||
if (gen !== recloneGen) return;
|
||||
if (!appScrollIdleFor(1000)) return queueReclone();
|
||||
const fresh = !peri.firstElementChild;
|
||||
if (fresh) {
|
||||
wrap.style.opacity = "0";
|
||||
wrap.style.transition = "opacity 160ms ease";
|
||||
}
|
||||
peri.replaceChildren(wrap);
|
||||
if (fresh) requestAnimationFrame(() => (wrap.style.opacity = "1"));
|
||||
sync();
|
||||
});
|
||||
};
|
||||
// Trailing debounce; no `class` in the filter — hover flips were
|
||||
// re-cloning per mouse move, and the fog doesn't read hover states.
|
||||
@@ -88,13 +101,22 @@
|
||||
// GlassBar note: big pages were janking frames with reclone tasks).
|
||||
const queueReclone = () => {
|
||||
clearTimeout(pending);
|
||||
const delay = Math.min(600, 120 + lastNodes / 40);
|
||||
const delay = Math.min(600, 120 + lastRecloneMs * 3);
|
||||
pending = setTimeout(() => {
|
||||
// Never clone mid-scroll, never while hidden (see GlassBar).
|
||||
// APP-WIDE scroll recency (scrollbar.js note), a full second
|
||||
// of it, re-checked inside the idle callback with no forcing
|
||||
// timeout, and the callback yields on a thin idle slice:
|
||||
// this pane's own churn must not clone while the CONTENT
|
||||
// pane scrolls.
|
||||
if (document.hidden) return;
|
||||
if (performance.now() - lastScroll < 250) return queueReclone();
|
||||
if (!appScrollIdleFor(1000)) return queueReclone();
|
||||
if ("requestIdleCallback" in window) {
|
||||
requestIdleCallback(() => reclone(), { timeout: 800 });
|
||||
requestIdleCallback((deadline) => {
|
||||
if (!appScrollIdleFor(1000)) return queueReclone();
|
||||
if (deadline && deadline.timeRemaining() < 15) return queueReclone();
|
||||
reclone();
|
||||
});
|
||||
} else {
|
||||
reclone();
|
||||
}
|
||||
@@ -103,6 +125,8 @@
|
||||
const mo = new MutationObserver((records) => {
|
||||
for (const r of records) {
|
||||
if (r.target === view && (r.addedNodes.length || r.removedNodes.length)) {
|
||||
// Invalidate any built-but-unattached clone (GlassBar note).
|
||||
recloneGen++;
|
||||
peri.replaceChildren();
|
||||
break;
|
||||
}
|
||||
@@ -124,6 +148,7 @@
|
||||
view.addEventListener("scroll", sync, { passive: true });
|
||||
reclone();
|
||||
return () => {
|
||||
recloneGen++;
|
||||
mo.disconnect();
|
||||
ro.disconnect();
|
||||
document.removeEventListener("visibilitychange", onVis);
|
||||
@@ -442,6 +467,38 @@
|
||||
:global(:root.glass) .sidebar > .view {
|
||||
clip-path: inset(46px 0 0 0);
|
||||
}
|
||||
/* The rows ON the frost. --bg-highlight is opaque #242424, so under glass
|
||||
every nav row and every pinned/recent row painted a slab on the pane —
|
||||
the one Kyle photographed first. The white veil makes the same +18 step
|
||||
against the frost that #242424 makes against #121212 (see the ladder in
|
||||
global.css). Hover and active deliberately share one value: this sidebar
|
||||
has always distinguished active by TEXT COLOUR (the note above the base
|
||||
rule), and that is left exactly as it is. */
|
||||
:global(:root.glass) .nav-item:hover,
|
||||
:global(:root.glass) .rec:hover,
|
||||
:global(:root.glass) .nav-item.active,
|
||||
:global(:root.glass) .rec.on {
|
||||
background: var(--glass-hover);
|
||||
}
|
||||
:global(:root.glass) .gicon:hover {
|
||||
background: var(--glass-hover);
|
||||
}
|
||||
/* The artwork well behind a recent with no cover yet. */
|
||||
:global(:root.glass) .art-placeholder {
|
||||
background: var(--glass-surface);
|
||||
}
|
||||
/* --border is opaque #2a2a2a: a 1px divider on the frost read as a solid
|
||||
scratch. The hairline token is the same weight against glass. */
|
||||
:global(:root.glass) .rule {
|
||||
background: var(--glass-hairline);
|
||||
}
|
||||
/* The count badge's backdrop is the frosted pane, so its blur flattens the
|
||||
sample to black exactly like the bar's did — the fill already carries
|
||||
the look on its own. */
|
||||
:global(:root.glass) .bdg {
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
.peri {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
Reference in new issue
Block a user