Downscale cached cover art, and stop every animation loop under ?pi=1
Covers. Art out of an ID3 APIC frame is sized for a record sleeve: this library averaged 3000x3000 and 580 kB per cover, 140 MB across 284 albums. The browser was decoding nine megapixels - around 36 MB of bitmap - for every cover it painted, to show it in a 185 px card, on a Pi with 2 GB of RAM. The largest any screen in this app asks for is 340 px (the play view), so cache.store_cover now downscales to a 640 px long edge, which leaves room for a tablet at devicePixelRatio 2 and cuts the decode about twentyfold. Pillow was already a hard dependency, for colour extraction. scan_library reuses an album whose fingerprint is unchanged without re-reading its tags, so covers already on disk would never be rewritten - hence shrink_stored_covers(), a pass at the top of a scan. Reading a JPEG's dimensions only parses its header, so after the first run it costs one small read per album. Art already small enough is returned byte-identical rather than re-encoded, so repeated scans cannot slowly grind it down, and anything Pillow cannot read is passed through untouched: a cover that is too big is a performance problem, a cover that is missing is a visible one. Animation. Halving the ambient canvas to a quarter of the pixels at 30fps took it from 53.5% of a core to 25%, and 25% was still not good enough to use. A requestAnimationFrame loop repainting the viewport is a floor you cannot get under while it runs at all, so ?pi=1 now switches it off outright rather than thinning it, along with the decorative CSS loops, the view transitions, the typing game's bubbles and its next-key pulse. The pets stay on screen but hold still, through the same path prefers-reduced-motion already took - taking the animation away is the point, taking away what she earned is not. .stage keeps its own static gradient, so there is still a sea behind everything. Both blurs on the panels stay on. Dropping those measured five times worse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,9 +24,18 @@
|
||||
* grid included. The most expensive-looking CSS in the app is the thing keeping the
|
||||
* rest of it cheap. Do not "optimize" it away without re-running the numbers above.
|
||||
*
|
||||
* So the profile is one real change - paint a quarter of the pixels - plus three caps
|
||||
* that only bite while something is playing (bubbles spawn, curves are sampled per
|
||||
* frame, the progress bar re-renders) and that are therefore not in the table above.
|
||||
* That got it from 53.5% to 25%, and 25% was still not enough to use. The profile now
|
||||
* goes further and stops the animation instead of thinning it: under `?pi=1` the
|
||||
* ambient canvas is not rendered at all (`SHOW_AMBIENCE`), no bubbles rise anywhere,
|
||||
* the decorative CSS loops are off, the view transitions are off, and the typing game's
|
||||
* pets are placed but held still. A requestAnimationFrame loop that repaints the whole
|
||||
* viewport is a floor you cannot get under while it runs at all, and on a Pi 4 that
|
||||
* floor is too high. With it gone the browser has nothing to do between one keypress
|
||||
* and the next. `.stage` keeps its own static CSS gradient, so the screen still has a
|
||||
* sea behind it - it just no longer moves or follows the track.
|
||||
*
|
||||
* What stays: both blurs on the panels (see the table - dropping those was five times
|
||||
* worse), and the card blur off, which is a count problem rather than a blur problem.
|
||||
*
|
||||
* Read once at module load, from the URL and nowhere else: no persistence, no
|
||||
* auto-detection. The kiosk points Firefox at `http://localhost:8080/?pi=1` (see the
|
||||
@@ -67,6 +76,10 @@ export interface AmbienceQuality {
|
||||
maxBubbles: number;
|
||||
}
|
||||
|
||||
/** Unreachable under `?pi=1` as things stand, because `SHOW_AMBIENCE` switches the
|
||||
* canvas off there entirely. Kept, and kept accurate, because it is the setting that
|
||||
* matters the moment anyone turns the canvas back on for a weak device - the half-scale
|
||||
* backing store is what took it from 53.5% to 25%. */
|
||||
export const AMBIENCE_QUALITY: AmbienceQuality = LOW_POWER
|
||||
? { resolutionScale: 0.5, maxBackingStorePx: 1280, maxFps: 30, maxBubbles: 60 }
|
||||
: { resolutionScale: 1, maxBackingStorePx: 4096, maxFps: 0, maxBubbles: 0 };
|
||||
|
||||
@@ -33,7 +33,7 @@ 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;
|
||||
export const ANIMATE_VIEW_TRANSITIONS = !LOW_POWER;
|
||||
|
||||
/** Frost the glass panels/cards/rows with a real backdrop blur.
|
||||
*
|
||||
@@ -45,13 +45,16 @@ export const ANIMATE_VIEW_TRANSITIONS = true;
|
||||
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.
|
||||
* redrawn every frame). Off falls back to `.stage`'s own static CSS gradient, which is
|
||||
* still underneath the canvas either way - so the screen keeps a background, it just
|
||||
* stops being a per-track one and stops moving.
|
||||
*
|
||||
* 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;
|
||||
* Off under `?pi=1`. Halving its resolution and its frame rate first (see
|
||||
* `AMBIENCE_QUALITY`) was not enough on the device this exists for: a requestAnimationFrame
|
||||
* loop that repaints the full viewport is a floor you cannot get under while it runs at
|
||||
* all, and on a Pi 4 that floor is too high. Nothing else in the app needs a frame loop,
|
||||
* so with this off the browser has nothing to do between one keypress and the next. */
|
||||
export const SHOW_AMBIENCE = !LOW_POWER;
|
||||
|
||||
/** Frost the *repeated* glass surfaces - every album card in the grid, every row in a
|
||||
* track list - as opposed to the handful of panels wrapped around them.
|
||||
@@ -65,10 +68,11 @@ export const SHOW_AMBIENCE = true;
|
||||
export const SHOW_CARD_BLUR = !LOW_POWER;
|
||||
|
||||
/** 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;
|
||||
* the dolphin mascot's bob/swim. Off under `?pi=1`. On their own they measured as
|
||||
* noise, but "nothing on this screen moves by itself" is a property worth having
|
||||
* outright rather than a sum of small wins - a compositor with no animation to service
|
||||
* has nothing to wake up for. */
|
||||
export const SHOW_DECORATIVE_ANIMATIONS = !LOW_POWER;
|
||||
|
||||
// ------------------------------------------------------------------ colors --
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* through components. The hue lives in styles/app.css because CSS is where it is used;
|
||||
* it is repeated here only for the canvas, which cannot read a custom property. */
|
||||
|
||||
import { LOW_POWER } from "../lowPower";
|
||||
|
||||
/** The turquoise lagoon. Music is 210, Hörbücher 55, "Mein Zimmer" 300. */
|
||||
export const HUE = 175;
|
||||
|
||||
@@ -14,15 +16,20 @@ export const HUE = 175;
|
||||
export const SHOW_GLASS_BLUR = true;
|
||||
|
||||
/** The decorative rising bubbles behind everything. A dozen elements on a CSS transform
|
||||
* loop; measured as noise on the Pi, so they stay. */
|
||||
export const SHOW_BUBBLES = true;
|
||||
* loop - off under `?pi=1`, like every other loop in the app. */
|
||||
export const SHOW_BUBBLES = !LOW_POWER;
|
||||
|
||||
/** The earned pets swimming behind every screen. The reward that is always in view - off
|
||||
* only to rule it out when chasing a performance problem. */
|
||||
* only to rule it out when chasing a performance problem.
|
||||
*
|
||||
* Stays on under `?pi=1`, but the pets hold still there: `AquariumCreatures` already has
|
||||
* that exact mode for `prefers-reduced-motion`, where every pet is placed and shown and
|
||||
* simply does not swim. Taking the animation away is the point; taking away what she
|
||||
* earned is not. */
|
||||
export const SHOW_AQUARIUM_CREATURES = true;
|
||||
|
||||
/** Fade screens in on entry. */
|
||||
export const ANIMATE_VIEW_TRANSITIONS = true;
|
||||
export const ANIMATE_VIEW_TRANSITIONS = !LOW_POWER;
|
||||
|
||||
/** Show the on-screen keyboard with the finger colours. "auto" fades it out key by key
|
||||
* as each one is mastered - the scaffold that removes itself, which is the whole point
|
||||
|
||||
Reference in New Issue
Block a user