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.
Root-caused the "pop" from the Scale+Translate centered zoom (reverted a
few commits back) to pixlet's own source: render/animation/translate.go
rounds a Translate's x/y to the nearest whole pixel every frame by
default (rounding = "round"), while animation/scale.go never rounds
Scale. A continuously-interpolating Scale combined with a rounded
Translate holds still for several frames, then jumps a whole pixel at
once -- a real, confirmed pixlet behavior, not a flaw in this app's
formula.
Transformation accepts a `rounding` parameter; passing "none" disables
the snapping. Confirmed via frame-by-frame pixel-delta analysis: the
per-frame change is now even throughout the animation (consistently
~900 AE out of 1024 possible, every frame) instead of long near-zero
stretches punctuated by sudden full-frame jumps.
Re-enables the centered zoom (Scale + Translate) that was reverted for
jerkiness, now smooth. ZOOM_LEVELS["strong"] stays capped at 1.45 --
that black-border ceiling is unrelated to rounding and still applies.
Verified against the live station: no pop, no black border at any zoom
level, payload ~370KB (up from ~220-320KB, since rounding="none" means
WebP can dedupe fewer near-identical frames) but still well under the
460KB device limit.
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.
Dividing the centering shift by top_scale (previous commit) avoided the
strong-zoom black border, but made the shift a nonlinear function of the
scale value being interpolated -- mid-animation, the pan no longer
tracked the zoom at a constant rate, which read as jerky/skippy motion.
Reverted shift to the original linear formula, (1-s)*(ART_SIZE/2), which
moves smoothly in lockstep with Scale's own eased interpolation. Instead,
capped ZOOM_LEVELS["strong"] at 1.45 (down from 1.7) -- the highest value
where this plain linear formula stays safely inside the art's bounds,
verified with the calibration tool (no black border across the full
animation cycle, at both 1.45x and against a live station).
The centering Translate formula, (1-s)*(ART_SIZE/2), was derived assuming
it's applied in final output pixels. That held up at a moderate zoom
(1.4x) but pushed past the image's actual edge at a strong one (1.7x) --
a real, on-device black border in the bottom-right corner, confirmed with
the calibration tool (true black RGB(0,0,0) pixels, not just dark
content).
Dividing the shift by the scale factor keeps the crop inside the image's
bounds at every zoom level the app offers, verified via the calibration
tool at 1.0/1.15/1.4/1.7x and against a live station -- at the cost of
the most extreme (1.7x) setting being very slightly off-center rather
than perfectly centered, which is a trade worth making since going
outside the art's bounds is worse. Moderate and subtle remain visibly
centered.
dev/calibration.png is a synthetic grid (concentric color rings, corner
markers, a crosshair) and dev/calibration_test.star runs it through the
same zoom logic as the real _art_widget, so cropping can be checked
visually without depending on what a live station happens to be playing.
This is what was used to find and verify the centered-zoom fix.