Files
musicmouse/web/src/lib/theme.ts
Martin Bauer 2514ecbc35 Measure the Pi profile on the Pi, and keep only what helped
The first cut of ?pi=1 switched off the two things that look most expensive - the
backdrop-filter glass blur and the decorative CSS animation loops - and made the device
slower, not faster: 53.5% of a core idle became 118%. Measured on musicdolphin, sum of
the Firefox process tree, idle on the browse screen, 15s average:

  full app                                       53.5%
  + ambient canvas at half resolution            25.0%   <- the whole win
  + 30fps cap on top of that                     25.0%   (no idle change)
  + decorative CSS animation loops off           24.6%   (noise; left on)
  + backdrop-filter glass blur off              118.1%   <- 5x worse

The blur is what promotes each glass panel to its own compositing layer. Without it the
animated canvas and the whole album grid above it collapse into one layer and every
canvas frame repaints all of it. The most expensive-looking CSS in the app is what was
keeping the rest of it cheap. SHOW_GLASS_BLUR and SHOW_DECORATIVE_ANIMATIONS therefore
go back to unconditionally on, in both the music app and the typing game, with the
numbers written down next to them so the next person does not repeat this.

What is left is one real change - paint a quarter of the pixels - plus three caps that
only bite while something is playing and so are not in the table above: 30fps on the
canvas, 60 bubbles alive at once, and ten progress-bar re-renders a second. Those three
are unmeasured; measuring them means playing audio.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 13:44:05 +02:00

131 lines
5.9 KiB
TypeScript

/** Per-group colors and identity, and the feature toggles that have gone back and
* forth while this design was under review - flip one here rather than hunting
* through the components that read it. Most toggles are about the browse screen's
* shelf/row look, but some (`SHOW_GLASS_BLUR`, `SHOW_AMBIENCE`,
* `SHOW_DECORATIVE_ANIMATIONS`) reach further: the play view, the player bar, the
* room page and `App.tsx` all read this module too, wherever they share the same
* "cut this on weak hardware" knob.
*
* `SHOW_ROW_TITLES` and `SHOW_ROW_ICONS` both `true` reproduces the original shelf
* header - an icon badge plus label button above each row's tiles. */
import type { CSSProperties } from "react";
import type { Group } from "./search";
import type { UiState } from "./keyboard";
// ---------------------------------------------------------------- toggles --
/** Tint each shelf panel and its rows with the group's hue below. */
export const SHOW_ROW_TINT = false;
/** Show each shelf's title text ("Musik", "Hörbücher", "Podcasts") above its tiles. */
export const SHOW_ROW_TITLES = true;
/** Show the small icon badge (🎵/📖/🎙️) beside the title. Stands alone fine with
* `SHOW_ROW_TITLES` off (an icon-only header), but the original look needs both. */
export const SHOW_ROW_ICONS = true;
/** Vertical gap between shelves on the root browse screen, in px. */
export const ROW_SPACING = 25;
/** Fade the album/category grid in (with a slight upward slide) when a group or
* category is entered, and the album modal in when it opens. One animation per
* container, not per card, so cost stays flat regardless of grid size. */
export const ANIMATE_VIEW_TRANSITIONS = true;
/** Frost the glass panels/cards/rows with a real backdrop blur.
*
* The obvious thing to cut on weak hardware, and on the Pi that is exactly wrong: it
* measured five times *worse* with the blur off (118% of a core against 25%). The blur
* is what promotes each panel to its own compositing layer; without it the animated
* canvas and the whole album grid above it share one layer and every canvas frame
* repaints all of it. See the table in lib/lowPower.ts before touching this. */
export const SHOW_GLASS_BLUR = true;
/** Render the play view's animated canvas background (gradient + bubbles, both
* redrawn every frame at 60fps). Off falls back to `.stage`'s own static CSS
* gradient, which is still underneath the canvas either way.
*
* Deliberately still on under `?pi=1`: it is the app's whole look, and
* `AMBIENCE_QUALITY` in lib/lowPower.ts makes it affordable (quarter of the pixels,
* half the frames) rather than making it go away. */
export const SHOW_AMBIENCE = true;
/** Run the purely decorative CSS animation loops: the room page's bubble field and
* the dolphin mascot's bob/swim. Left on even under `?pi=1`: switching them off on the
* Pi moved the idle figure by 0.4 percentage points, which is noise, and they are the
* kind of detail this app is for. */
export const SHOW_DECORATIVE_ANIMATIONS = true;
// ------------------------------------------------------------------ colors --
/** Per-group hue (oklch degrees). The play view's ambient water uses the same
* family (`ambienceBaseFor` in `lib/ambience.ts`), so a tinted shelf already hints
* at the mood its albums play into - and so does the book/music card tint in
* `lib/covers.ts`, which reads `GROUP_HUE.audiobooks`/`GROUP_HUE.music` too. */
export const GROUP_HUE: Record<Group, number> = {
music: 210, // today's sea - Dolphin Beats' baseline hue
audiobooks: 55, // warm amber - richer than the old shared hue (80)'s dull brown
podcasts: 300, // the one otherwise-unused family
};
export const GROUP_ICON: Record<Group, string> = {
music: "🎵",
audiobooks: "📖",
podcasts: "🎙️",
};
export const GROUP_LABEL: Record<Group, string> = {
music: "Musik",
audiobooks: "Hörbücher",
podcasts: "Podcasts",
};
/** How much horizontal space the tab rail's own glass bar claims on the right edge
* (its `right` inset plus its rendered width) - scrollable content reserves this much
* padding on that side so a full row/grid never renders underneath it. Keep in sync
* with the geometry in `TabRail.tsx`. */
export const TAB_RAIL_CLEARANCE = 88;
/** Icon/label for the vertical tab rail - one entry per `UiState["page"]`. The
* smarthome tab only renders when Home Assistant is configured (see `App.tsx`); the
* other two always show. */
export const PAGE_ICON: Record<UiState["page"], string> = {
// A plain Unicode symbol, not the 🎵 emoji: that glyph's own built-in colour is a
// muted grey-blue on at least one real platform, which is illegible against the tab
// rail's own glass background and does not respond to `color` the way text does.
music: "♪",
room: "💡",
typing: "⌨️",
};
export const PAGE_LABEL: Record<UiState["page"], string> = {
music: "Musik",
room: "Mein Zimmer",
typing: "Tippen",
};
/** A shelf/row's frosted background and border, tinted with its group's hue - or,
* with `SHOW_ROW_TINT` off, `{}` so `.glass-panel`'s own neutral CSS shows through. */
export function glassTint(hue: number): CSSProperties {
if (!SHOW_ROW_TINT) return {};
return {
background: `linear-gradient(160deg, oklch(55% 0.1 ${hue} / .32), oklch(30% 0.06 ${hue} / .14))`,
borderColor: `oklch(75% 0.09 ${hue} / .4)`,
};
}
/** One list row's background, tinted with its group's hue and brighter/more opaque
* while it's the currently-playing row - or, with `SHOW_ROW_TINT` off, the same
* neutral highlight the row used before tinting existed. */
export function rowTint(hue: number, highlighted: boolean): CSSProperties {
if (!SHOW_ROW_TINT) {
return { background: `oklch(97% 0.01 210 / ${highlighted ? ".22" : ".10"})` };
}
return {
background: `oklch(${highlighted ? "62% 0.12" : "42% 0.08"} ${hue} / ${highlighted ? ".34" : ".2"})`,
borderColor: `oklch(78% 0.09 ${hue} / ${highlighted ? ".5" : ".28"})`,
};
}