Replace Translate-based centering with animation.Origin; add zoom position config

The previous "fix" for Scale's crop bias paired Scale with a computed
Translate, on the mistaken assumption that Scale's default pivot is the
top-left corner. It isn't: pixlet's docs say the default origin is
already the center (0.5, 0.5). That Translate was sized assuming Scale
had no self-centering at all, so it overcorrected, visibly skewing the
zoom toward the bottom-right -- the opposite of the bug it meant to fix.

animation.Origin does directly and correctly what that Translate was
clumsily approximating, so the Translate (and its rounding="none"
workaround) is removed entirely in favor of Origin. This also means
"strong" no longer needs the 1.45 safety cap -- restored to 1.7, since
the black-border risk was specific to the Translate formula, not Scale +
Origin (verified via the calibration tool at several off-center origins).

Added a "Zoom position" schema option: Center (default) or Random. Random
picks from a handful of preset off-center spots, chosen per-track from
subsonic_id the same way the old corner-selection logic worked, so a
still-playing track doesn't jump between spots on every refresh.

Verified: no black border, no popping (confirmed via frame-delta
analysis -- smooth throughout, no Translate means rounding is moot), and
payload well under the 460KB limit at every zoom level/position
combination, against the live station.
This commit is contained in:
poprhythm committed 2026-10-02 22:12:25 -04:00
1 parent 4a2b3caac8
commit d8add24c5e
3 files changed
+121 -73

No files matched your search

+35 -23
View File
@@ -23,6 +23,9 @@ app's config when adding it to a device.
muted color for the time/temp line.
- **Album art zoom** — how far the album art's breathing zoom effect goes
in: Off, Subtle, Moderate (default), or Strong.
- **Zoom position** — where the zoom centers on: Center (default), or
Random (a handful of preset off-center spots, picked per-track so a
still-playing track doesn't jump between spots on every refresh).
## How it works
@@ -46,30 +49,39 @@ app's config when adding it to a device.
attempts produced a real, on-device black border; see the fuller
history below.
`animation.Scale`'s pivot defaults to the *center* of the widget per
pixlet's own docs, so Scale alone is already close to centered — just
slightly skewed toward the top-left as the zoom amount increases.
Paired with a computed `animation.Translate` to cancel out that residual
skew. Combining Scale with Translate at all was tried once before and
reverted after it produced a periodic rendering "pop" roughly every
0.7-1s — confirmed with both WebP and GIF output, independent of zoom
amplitude. Root-caused to pixlet's own source
Where the zoom centers on is controlled via `animation.Origin`, which
works correctly and does real work here — contrary to an earlier,
mistaken belief in this app's own history that it "had no effect" (that
came from testing it set to its own default value and comparing the
result against itself, which is of course identical either way). The
"Random" zoom position picks from a handful of preset off-center spots
(`ZOOM_RANDOM_SPOTS`), chosen per-track from `subsonic_id` the same way
the old corner-selection logic worked, so a still-playing track doesn't
jump between spots on every `NOWPLAYING_TTL` refresh. Those spots are
kept modest (0.3-0.7 of the image's own bounds, not the full 0.0-1.0
range) and verified safe (no black border, even at the strongest 1.7x
zoom) via the calibration tool; more extreme corners weren't tested and
may not be safe.
An earlier version tried to fix a *different* problem — `animation.Scale`
zooming in asymmetrically toward one corner — by pairing Scale with a
*computed* `animation.Translate`, reasoning (wrongly) that Scale's
default pivot is the top-left corner. It isn't: pixlet's own docs say
the default origin is already the center. That Translate, sized
assuming Scale had no self-centering at all, overcorrected and visibly
skewed the zoom toward the *opposite* corner instead. It also introduced
a periodic rendering "pop" (confirmed with both WebP and GIF output,
independent of zoom amplitude) root-caused to pixlet's own source
(`render/animation/translate.go`): a Translate's x/y are rounded to the
nearest whole pixel every frame by default, while Scale is never
rounded, so a continuously-changing Scale combined with a rounded
Translate holds still for several frames and then jumps a whole pixel
at once. Passing `rounding = "none"` on the `Transformation` disables
that snapping and fixes it — confirmed via frame-by-frame pixel-delta
analysis (the amount of change is now even from frame to frame, instead
of long still stretches punctuated by sudden jumps). "strong" is capped
at 1.45 (not pushed further): above ~1.46 the centered crop starts
revealing a real black border in the bottom-right corner, verified with
the calibration tool — this is the safe ceiling, not an arbitrary
choice. Verified payload stays well under the 460KB device limit at
every zoom level, measured against the live station (though with
`rounding = "none"`, WebP can no longer dedupe as many near-identical
frames, so payloads run larger than the "pop" version did — ~370KB
vs. ~220-320KB — still comfortably within budget).
nearest whole pixel every frame by default, while Scale never rounds,
so a continuously-changing Scale combined with a rounded Translate
holds still for several frames and then jumps a whole pixel at once.
Since `animation.Origin` already does directly and correctly what that
Translate was clumsily approximating, the Translate was removed
entirely rather than patched further (e.g. with `rounding = "none"`,
which does fix the popping but wasn't needed once Origin replaced it).
Verified payload stays well under the 460KB device limit at every zoom
level and position, 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
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.