Fix the periodic zoom pop at its root: disable Translate's pixel rounding

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.
This commit is contained in:
poprhythm committed 2026-10-02 22:03:05 -04:00
1 parent 9f63adbfbf
commit 4a2b3caac8
3 files changed
+73 -47

No files matched your search

+21 -15
View File
@@ -1,7 +1,8 @@
"""
Dev tool, not part of the deployed app: renders dev/calibration.png through
the exact same zoom logic as _art_widget in subwave_nowplaying.star, so you
can visually check how much margin gets cropped at rest and at full zoom.
can visually check how much margin gets cropped at rest and at full zoom,
and whether the crop is even on all sides.
calibration.png is a 360x360 grid: concentric 10%-wide color rings (magenta
outermost, then yellow, then cyan) plus four corner markers (red TL, green
@@ -14,17 +15,20 @@ Usage:
Keep ART_SIZE/ART_SLOT/the zoom math below in sync with the real
_art_widget if that ever changes.
Note: a centered (Scale + Translate) version of this zoom was tried and
visually fixed the top-left crop bias below, but combining Scale with
Translate causes pixlet to periodically re-rasterize the whole image with
a visible "pop" -- confirmed with both WebP and GIF output, independent of
zoom amplitude, so it's pixlet's animation engine, not app-level math. This
tool (and the real app) stick to Scale alone -- a real but minor crop-bias
cosmetic issue, instead of the pop a centered zoom introduces. If you want
to re-investigate centering, add animation.Translate back in here first
and compare frame-to-frame pixel deltas (see git history around the
"Center the album art zoom" / "Revert jerky zoom fix" commits) before
touching the real app.
History, if this ever needs revisiting: a Scale+Translate centered zoom
was tried, then reverted after it produced a periodic rendering "pop"
(confirmed with both WebP and GIF output, independent of zoom amplitude).
Root cause, found in pixlet's own source
(render/animation/translate.go): a Translate's x/y are rounded to the
nearest whole pixel every frame by default (`rounding = "round"`), while
Scale is never rounded -- so a continuously-interpolating Scale combined
with a rounded Translate holds still for several frames, then jumps a
whole pixel at once. Passing `rounding = "none"` on the Transformation
(see below) disables that snapping and fixed it -- confirmed via
frame-by-frame pixel-delta analysis (even per-frame change throughout,
instead of long still stretches punctuated by sudden jumps). If a future
change reintroduces jerkiness, check whether `rounding = "none"` is still
present before suspecting the shift formula.
"""
load("render.star", "render")
@@ -38,13 +42,15 @@ ART_ZOOM_DURATION = 200
def _art_widget(art_bytes, zoom_scale):
art = render.Image(src = art_bytes, width = ART_SIZE, height = ART_SIZE)
top_scale = max(zoom_scale, 1.0)
shift = (1.0 - top_scale) * (ART_SIZE / 2.0)
zoom = animation.Transformation(
child = art,
duration = ART_ZOOM_DURATION,
rounding = "none",
keyframes = [
animation.Keyframe(percentage = 0.0, transforms = [animation.Scale(1.0, 1.0)]),
animation.Keyframe(percentage = 0.5, transforms = [animation.Scale(top_scale, top_scale)], curve = "ease_in_out"),
animation.Keyframe(percentage = 1.0, transforms = [animation.Scale(1.0, 1.0)], curve = "ease_in_out"),
animation.Keyframe(percentage = 0.0, transforms = [animation.Scale(1.0, 1.0), animation.Translate(0, 0)]),
animation.Keyframe(percentage = 0.5, transforms = [animation.Scale(top_scale, top_scale), animation.Translate(shift, shift)], curve = "ease_in_out"),
animation.Keyframe(percentage = 1.0, transforms = [animation.Scale(1.0, 1.0), animation.Translate(0, 0)], curve = "ease_in_out"),
],
)
return render.Box(width = ART_SLOT, height = ART_SLOT, child = zoom)
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.