From 1074ece0c638abee35384cef513017af3651c8e0 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 1 Nov 2026 21:13:55 +0100 Subject: [PATCH] added debug --- web/src/blob/blob.ts | 20 +++++++++++++++++++- web/src/debug.ts | 17 ++++++++++++++++- web/src/style.css | 2 ++ web/src/visual.ts | 12 +++++++++--- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/web/src/blob/blob.ts b/web/src/blob/blob.ts index 83d0aa9..bc7a8fc 100644 --- a/web/src/blob/blob.ts +++ b/web/src/blob/blob.ts @@ -65,6 +65,8 @@ export interface DropState { } export class LiquidBlob { + /// Why the last `create` gave up, for `?debug=drop`. + static failure = ""; readonly canvas = document.createElement("canvas"); /// Called if the GPU drops the context; the page shows plain art until `onRestored`. onLost: (() => void) | null = null; @@ -113,7 +115,8 @@ export class LiquidBlob { try { return new LiquidBlob(touch); } catch (err) { - console.warn("liquid visuals off:", (err as Error).message); + LiquidBlob.failure = (err as Error).message; + console.warn("liquid visuals off:", LiquidBlob.failure); return null; } } @@ -391,6 +394,21 @@ export class LiquidBlob { ); } + /// What `?debug=drop` shows: the GPU, and how far the drop got. + describe(): string { + const gl = this.gl; + const info = gl.getExtension("WEBGL_debug_renderer_info"); + const gpu = info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER); + const hp = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT); + return [ + `gpu ${gpu}`, + `vertex highp ${hp ? hp.precision : "?"} bits`, + `ready ${this.ready} · lost ${gl.isContextLost()} · error ${gl.getError()}`, + `morph ${this.morph.toFixed(2)} · quality ${this.quality} · canvas ${this.canvas.width}x${this.canvas.height}`, + `in page ${this.canvas.isConnected} · css ${this.canvas.clientWidth}x${this.canvas.clientHeight}`, + ].join("\n"); + } + /// Stepped down to the lowest quality: the GPU had trouble keeping up. get struggling(): boolean { return this.quality === QUALITY.length - 1; diff --git a/web/src/debug.ts b/web/src/debug.ts index 3894e91..607fe6b 100644 --- a/web/src/debug.ts +++ b/web/src/debug.ts @@ -1,5 +1,6 @@ // `?debug=audio`: live bars of what features.ts hears, for tuning its constants by -// ear. Loaded only when asked for. +// ear. `?debug=drop`: why the liquid drop does or does not show, readable on a +// phone without remote dev tools. Loaded only when asked for. import type { Features } from "./features"; @@ -36,3 +37,17 @@ export function audioDebug(): (f: Features) => void { }); }; } + +/// A box of text, rewritten at most a few times a second. +export function dropDebug(): (text: string) => void { + const el = document.createElement("pre"); + el.className = "audio-debug drop-debug"; + document.body.append(el); + let at = 0; + return (text) => { + const now = performance.now(); + if (now - at < 250) return; + at = now; + el.textContent = text; + }; +} diff --git a/web/src/style.css b/web/src/style.css index 100d21e..dfabb50 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -482,6 +482,8 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff; pointer-events: none; } +.drop-debug { margin: 0; padding: 0.4rem 0.5rem; max-width: calc(100vw - 1.4rem); font: 10px/1.35 ui-monospace, monospace; color: #fffc; white-space: pre-wrap; } + .card.paused .art { transform: scale(0.96); filter: brightness(0.7); } .paused-badge { position: absolute; diff --git a/web/src/visual.ts b/web/src/visual.ts index 520ee4e..1b210e3 100644 --- a/web/src/visual.ts +++ b/web/src/visual.ts @@ -68,6 +68,7 @@ export class Visualizer { private reduced = matchMedia("(prefers-reduced-motion: reduce)"); private setting: VisualsSetting = localStorage.getItem(VISUALS_KEY) === "simple" ? "simple" : "liquid"; private debug: ((f: Features) => void) | null = null; + private dropDebug: ((text: string) => void) | null = null; /// A press on the drop; `on` once it moved enough to turn it. private grab: { id: number; x: number; y: number; on: boolean } | null = null; private spun = false; @@ -103,9 +104,9 @@ export class Visualizer { if (this.spun) e.stopPropagation(); this.spun = false; }, true); - if (new URLSearchParams(location.search).get("debug") === "audio") { - import("./debug").then((m) => { this.debug = m.audioDebug(); }); - } + const debug = new URLSearchParams(location.search).get("debug"); + if (debug === "audio") import("./debug").then((m) => { this.debug = m.audioDebug(); }); + if (debug === "drop") import("./debug").then((m) => { this.dropDebug = m.dropDebug(); this.start(); }); } get visuals(): VisualsSetting { @@ -529,6 +530,11 @@ export class Visualizer { ? this.features.fromAnalyser(an, now / 1000, dt) : this.features.fromLoudness(card.loudnessAt(progress), now / 1000, dt); this.debug?.(f); + this.dropDebug?.([ + `setting ${this.setting} · reduced motion ${this.reduced.matches} · touch ${this.touch} · mixing ${mixed}`, + `blob ${this.blob === undefined ? "not asked" : this.blob === null ? `failed: ${LiquidBlob.failure}` : "made"} · card liquid ${!!card.el?.classList.contains("liquid")}`, + this.blob?.describe() ?? "", + ].join("\n")); let busy = this.shine(now, dt, f.kick) || playing; const liquid = !!this.blob && !!card.el?.classList.contains("liquid"); if (!this.reduced.matches) {