Revert centered zoom; Scale+Translate causes a periodic rendering pop

Combining animation.Scale with animation.Translate to center the zoom
(two commits back) visually fixed the top-left crop bias, but introduced
a worse problem: pixlet periodically re-rasterizes the whole image with a
visible "pop" roughly every 0.7-1s, confirmed with both WebP and GIF
output and independent of zoom amplitude -- a property of pixlet's
animation engine when Scale and Translate are combined, not something
fixable from this app's math. Scale alone, with no Translate, has none of
this popping (confirmed via isolated testing).

Reverted _art_widget to Scale-only (as it was before centering), and
restored ZOOM_LEVELS["strong"] to 1.7 (the strong-zoom black border only
applied to the Translate-based centering, not plain Scale). This brings
back the original top-left crop bias as a known, accepted cosmetic
trade-off -- better than a periodic visible pop. Updated the README and
dev/calibration_test.star to match and documented the full history for
anyone who wants to revisit centering later.
This commit is contained in:
poprhythm committed 2026-10-01 22:48:15 -04:00
1 parent 5f42319e26
commit 9f63adbfbf
3 files changed
+79 -77

No files matched your search

+45 -38
View File
@@ -34,45 +34,52 @@ app's config when adding it to a device.
instead of stale track info.
- If a track has no `subsonic_id`, or the cover fetch fails, falls back to a
full-width layout with no art rather than a broken image.
- Album art gets a slow "Ken Burns" breathing effect: eases from the full
frame in to a zoomed-in crop of the top-left corner, then back out, on a
continuous ~60s round-trip loop (not a one-shot reveal — see below).
Rendered oversized (45x45 vs. the 32x32 display slot) since
`animation.Scale` has no visible effect on a widget whose declared size
already matches its container, and scaled between 1.0 and 1.4 — only ever
scaling *up*, never down. That's a fix for a real on-device bug: an
earlier version zoomed *out* from a corner (scaling down from 1.0 toward
~0.71, reached via `animation.Translate`), which showed up on a real
device as a black border cutting into the art. Empirical testing (against
the actual Row layout, at native canvas size) showed the outer `Box`
auto-fits an oversized child to its own size at `Scale(1.0, 1.0)` — so
scaling *down* from there shrinks the already-fitted image further,
exposing background outside its bounds. Scaling *up* only ever crops
further into the image, which is always safe. `animation.Scale`'s pivot
defaults to the top-left corner (confirmed empirically — an explicit
`origin` parameter was tried and had no effect at all), so there's no
reliable way to zoom into any corner but that one; the effect always
zooms into the top-left. Verified this doesn't blow the payload budget:
measured ~280KB for a full render against the live station, comfortably
under the 460KB device limit.
- Album art gets a slow "Ken Burns" breathing effect: starts and ends on
the full frame, easing in to a zoomed-in crop at the midpoint, on a
continuous ~10s round-trip loop (not a one-shot reveal — see below). How
far it zooms in is configurable (Off/Subtle/Moderate/Strong — see
Configuration). Rendered oversized (36x36 vs. the 32x32 display slot,
just enough headroom for `animation.Scale` to have a visible effect)
and scaled *up only*, from 1.0 to the configured zoom amount, never
below 1.0 — scaling down was tried (to make the rest state reveal the
literal whole image, or to re-center an off-center crop) and both
attempts produced a real, on-device black border; see the fuller
history below.
**Why a loop, not a one-shot reveal:** a one-way version looked jumpy in
practice — since the app re-renders on `NOWPLAYING_TTL`, each fresh
render restarts its own animation from tick 0, so whatever the *previous*
pushed image happened to be showing gets abruptly replaced by a fresh
zoomed-in frame. Looping back-and-forth doesn't eliminate that (a
server-side re-render always restarts at tick 0 regardless of app logic),
but ending exactly where it started means the animation also loops
seamlessly *on-device* between refreshes, which reads as far less jarring
than a one-way reveal that snaps back. Considered using genuinely random
per-frame content (like `ormakers-tronbyt`'s LoRa segment, which pre-crops
60 frames directly from a high-resolution *static* source photo via
offline Pillow processing) for a sharper zoom, but that technique
fundamentally doesn't transfer here: album art is fetched live per-track
at render time, and pixlet's Starlark sandbox has no image-cropping/
pixel-analysis capability at all (confirmed by checking its full module
list) — so there's no way to pre-bake or crop-at-render-time from a
source we don't have until the request hits the live station.
`animation.Scale`'s pivot defaults to the top-left corner (confirmed
empirically — an explicit `origin` parameter has no effect at all), so
zooming in crops a bit more off the bottom/right than the top/left. A
centered version (pairing Scale with a computed `animation.Translate`)
was built and visually worked, but combining Scale with Translate turned
out to make pixlet periodically re-rasterize the whole image with a
visible "pop" every ~0.7-1s — confirmed with both WebP and GIF output,
independent of zoom amplitude, so it's a property of pixlet's animation
engine, not this app's math. Scale *alone* has none of that popping.
Given the choice between a subtle, constant crop asymmetry and a
periodic visible pop, this app accepts the asymmetry. (`dev/calibration_test.star`
has a working centered version if anyone wants to revisit this trade-off.)
Verified payload stays well under the 460KB device limit at every zoom
level, measured against the live station.
**Why a loop, not a one-shot reveal, and why it's short:** a one-way
version looked jumpy in practice — since the app re-renders on
`NOWPLAYING_TTL` (and the server appears to refresh pushed apps well
under a minute regardless), a fresh render restarts its own animation
from tick 0, so whatever the *previous* pushed image happened to be
showing gets abruptly replaced. A loop that starts and ends in the same
state doesn't eliminate that restart, but makes it invisible — and
keeping the round-trip short (~10s) makes it far more likely a full
cycle plays before the next refresh cuts it off; a 60s version tried
earlier only ever showed its outbound half. Considered using genuinely
random per-frame content (like `ormakers-tronbyt`'s LoRa segment, which
pre-crops 60 frames directly from a high-resolution *static* source
photo via offline Pillow processing) for a sharper zoom, but that
technique fundamentally doesn't transfer here: album art is fetched live
per-track at render time, and pixlet's Starlark sandbox has no
image-cropping/pixel-analysis capability at all (confirmed by checking
its full module list) — so there's no way to pre-bake or crop-at-render-
time from a source we don't have until the request hits the live
station.
- Shows the current show name (`context.activeShow.name`) and host name
(`dj.name`) below the artist, separated by a thin divider line, and the
local time/temperature (`context.clock`/`context.weather`) below that —
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.