Pi profile: no glass blur, keep the drift; consolidate the perf knobs

With the ambient canvas off under ?pi=1, the backdrop blur no longer buys anything:
it paid for itself by giving each panel a compositing layer above the animating
canvas, and with nothing animating underneath it was just cost - enough to make the
browse rows visibly sluggish on the device. So the Pi profile now drops every backdrop
blur (`glassBlur`, which the music app and the typing game share), and turns the
drift and the moving mascot back on: plain CSS transform loops the compositor runs by
itself, which measured as noise.

The knob set is consolidated to match: one glass toggle instead of panel and card
blur, one shadows toggle for cards and tiles, the typing game's drift and view
transitions folded into the shared ones, and the backing-store cap fixed by profile
rather than a setting. The parent dialog groups them into Darstellung / Abspielen /
System and hides the canvas tuning while the canvas is off. Stored settings move to
musicmouse.perf.v2, since v1 drafts held the old preset in full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 07:19:06 +02:00
parent 2a629faf50
commit 1da2274076
10 changed files with 172 additions and 174 deletions

View File

@@ -766,7 +766,7 @@ export function CategoryTile({
: `oklch(95% 0.015 ${GROUP_HUE.music} / .66)`,
borderRadius: allBooks ? "6px 18px 18px 6px" : "16px",
// One blurred shadow per tile: dropped under `?pi=1`, like `cardShadow`.
boxShadow: PERF.tileShadows ? "0 6px 18px var(--shadow)" : "none",
boxShadow: PERF.shadows ? "0 6px 18px var(--shadow)" : "none",
}}
>
<div

View File

@@ -19,30 +19,46 @@ type NumKey = { [K in keyof PerfSettings]: PerfSettings[K] extends number ? K :
/** What drifts and how the mascot moves, in the skin's own words. */
const DRIFT = SKIN.drift === "leaves" ? "Blätter" : "Blasen";
const DRIFT_MOVING = SKIN.drift === "leaves" ? "Fallende Blätter" : "Aufsteigende Blasen";
const MASCOT_MOVING = SKIN.mascotMotion === "walk" ? "wanderndes" : "schwimmendes";
const TOGGLES: [BoolKey, string, string][] = [
["ambience", "Animierter Hintergrund", "Canvas im Abspielbildschirm (größter CPU-Verbraucher)"],
["cardBlur", "Weichzeichner auf Karten", "Backdrop-Blur je Album-Karte und Titelzeile"],
["viewTransitions", "Übergänge", "Ein-/Ausblenden beim Öffnen von Gruppen und Alben"],
["decorativeAnimations", "Deko-Animationen", `${DRIFT} im Zimmer, ${MASCOT_MOVING} Maskottchen`],
["cardShadows", "Schatten auf Karten", "Schlagschatten unter den Album-Karten"],
["tileShadows", "Schatten auf Kacheln", "Schlagschatten unter den Kacheln im Raster"],
["petAnimations", "Tippen: Tiere bewegen sich", "Aus: Tiere stehen still"],
["tippenBubbles", `Tippen: ${DRIFT}`, `${DRIFT_MOVING} im Hintergrund`],
["tippenViewTransitions", "Tippen: Übergänge", "Ein-/Ausblenden der Bildschirme"],
["serviceWorker", "Service Worker", "Aus: vorhandener Worker wird entfernt"],
["coverWarmup", "Cover vorladen", "Cover im Hintergrund dekodieren"],
type Item =
| { kind: "toggle"; key: BoolKey; label: string; hint: string }
| { kind: "number"; key: NumKey; label: string; hint: string; min: number; max: number; step: number };
/** Each section's items, in order. An item with `when` only shows while that toggle is on
* in the draft - the canvas tuning means nothing with the canvas off. */
const SECTIONS: { title: string; items: (Item & { when?: BoolKey })[] }[] = [
{
title: "Darstellung",
items: [
{ kind: "toggle", key: "glassBlur", label: "Glaseffekt", hint: "Weichzeichner hinter Reihen, Karten und Leisten" },
{ kind: "toggle", key: "shadows", label: "Schatten", hint: "Schlagschatten unter Karten und Kacheln" },
{ kind: "toggle", key: "viewTransitions", label: "Übergänge", hint: "Ein-/Ausblenden beim Öffnen und Wechseln" },
{ kind: "toggle", key: "decorativeAnimations", label: "Deko-Animationen", hint: `${DRIFT} und ${MASCOT_MOVING} Maskottchen` },
{ kind: "toggle", key: "petAnimations", label: "Tippen: Tiere bewegen sich", hint: "Aus: Tiere stehen still" },
],
},
{
title: "Abspielen",
items: [
{ kind: "toggle", key: "ambience", label: "Animierter Hintergrund", hint: "Canvas im Abspielbildschirm - nur zusammen mit Glaseffekt" },
{ kind: "number", key: "ambienceScale", when: "ambience", label: "Hintergrund: Auflösung", hint: "Faktor der Canvas-Größe", min: 0.1, max: 1, step: 0.05 },
{ kind: "number", key: "ambienceMaxFps", when: "ambience", label: "Hintergrund: max. FPS", hint: "0 = unbegrenzt", min: 0, max: 60, step: 1 },
{ kind: "number", key: "ambienceMaxBubbles", when: "ambience", label: `Hintergrund: max. ${DRIFT}`, hint: "0 = unbegrenzt", min: 0, max: 500, step: 5 },
{ kind: "number", key: "playbackClockFps", label: "Fortschrittsbalken: Updates/s", hint: "0 = jeder Frame", min: 0, max: 60, step: 1 },
],
},
{
title: "System",
items: [
{ kind: "toggle", key: "coverWarmup", label: "Cover vorladen", hint: "Cover im Hintergrund dekodieren" },
{ kind: "toggle", key: "serviceWorker", label: "Service Worker", hint: "Aus: vorhandener Worker wird entfernt" },
],
},
];
const NUMBERS: [NumKey, string, string, number, number, number][] = [
["playbackClockFps", "Fortschrittsbalken: Updates/s", "0 = jeder Frame", 0, 60, 1],
["ambienceScale", "Hintergrund: Auflösung", "Faktor der Canvas-Größe", 0.1, 1, 0.05],
["ambienceMaxPx", "Hintergrund: max. Pixel", "Obergrenze je Achse", 256, 4096, 64],
["ambienceMaxFps", "Hintergrund: max. FPS", "0 = unbegrenzt", 0, 60, 1],
["ambienceMaxBubbles", `Hintergrund: max. ${DRIFT}`, "0 = unbegrenzt", 0, 500, 5],
];
const HINT = { fontSize: 12, fontWeight: 600, color: "oklch(50% 0.03 var(--hue))" } as const;
const LABEL = { fontSize: 14, fontWeight: 800, color: "var(--ink)" } as const;
const pill = (background: string, color: string) =>
({
@@ -100,49 +116,62 @@ export function PerfPanel({ onClose }: { onClose: () => void }) {
</button>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 12, marginTop: 18 }}>
{TOGGLES.map(([key, label, hint]) => (
<label key={key} style={{ display: "flex", alignItems: "center", gap: 12 }}>
<input
type="checkbox"
checked={draft[key]}
onChange={(event) => setDraft({ ...draft, [key]: event.target.checked })}
style={{ width: 22, height: 22 }}
/>
<span style={{ display: "flex", flexDirection: "column", gap: 2 }}>
<span style={{ fontSize: 14, fontWeight: 800, color: "var(--ink)" }}>{label}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: "oklch(50% 0.03 var(--hue))" }}>
{hint}
</span>
</span>
</label>
))}
{NUMBERS.map(([key, label, hint, min, max, step]) => (
<label key={key} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={{ fontSize: 14, fontWeight: 800, color: "var(--ink)" }}>{label}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: "oklch(50% 0.03 var(--hue))" }}>
{hint}
</span>
<input
type="number"
min={min}
max={max}
step={step}
value={draft[key]}
onChange={(event) => {
const value = event.target.valueAsNumber;
if (Number.isFinite(value)) setDraft({ ...draft, [key]: value });
}}
<div style={{ display: "flex", flexDirection: "column", gap: 12, marginTop: 6 }}>
{SECTIONS.map(({ title, items }) => (
<div key={title} style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div
style={{
padding: "8px 12px",
borderRadius: 10,
border: "2px solid oklch(85% 0.02 var(--hue))",
fontSize: 16,
fontWeight: 700,
...HINT,
fontWeight: 900,
textTransform: "uppercase",
letterSpacing: 0.6,
marginTop: 14,
}}
/>
</label>
>
{title}
</div>
{items
.filter((item) => !item.when || draft[item.when])
.map((item) =>
item.kind === "toggle" ? (
<label key={item.key} style={{ display: "flex", alignItems: "center", gap: 12 }}>
<input
type="checkbox"
checked={draft[item.key]}
onChange={(event) => setDraft({ ...draft, [item.key]: event.target.checked })}
style={{ width: 22, height: 22 }}
/>
<span style={{ display: "flex", flexDirection: "column", gap: 2 }}>
<span style={LABEL}>{item.label}</span>
<span style={HINT}>{item.hint}</span>
</span>
</label>
) : (
<label key={item.key} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={LABEL}>{item.label}</span>
<span style={HINT}>{item.hint}</span>
<input
type="number"
min={item.min}
max={item.max}
step={item.step}
value={draft[item.key]}
onChange={(event) => {
const value = event.target.valueAsNumber;
if (Number.isFinite(value)) setDraft({ ...draft, [item.key]: value });
}}
style={{
padding: "8px 12px",
borderRadius: 10,
border: "2px solid oklch(85% 0.02 var(--hue))",
fontSize: 16,
fontWeight: 700,
}}
/>
</label>
),
)}
</div>
))}
<div style={{ display: "flex", gap: 10, marginTop: 6 }}>
@@ -154,7 +183,7 @@ export function PerfPanel({ onClose }: { onClose: () => void }) {
</button>
</div>
<div style={{ fontSize: 12, fontWeight: 600, color: "oklch(50% 0.03 var(--hue))", lineHeight: 1.5 }}>
<div style={{ ...HINT, lineHeight: 1.5 }}>
Wird nur in diesem Browser gespeichert und gilt nur mit <code>?pi=1</code>.
</div>
</div>