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

+28 -18
View File
@@ -91,12 +91,16 @@ COLOR_SCHEMES = {
DEFAULT_COLOR_SCHEME = "cyberpunk" # matches the station's own site theme
# How far in the album art zoom goes, relative to its fully-zoomed-out rest
# state (see _art_widget below for why scale never goes below 1.0).
# state (see _art_widget below for why scale never goes below 1.0). "strong"
# is capped at 1.45, not pushed further: with the centering Translate in
# _art_widget, empirically anything above ~1.46 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.
ZOOM_LEVELS = {
"off": 1.0,
"subtle": 1.15,
"moderate": 1.4,
"strong": 1.7,
"strong": 1.45,
}
DEFAULT_ZOOM_LEVEL = "moderate"
@@ -224,36 +228,42 @@ def _art_widget(art_bytes, zoom_scale):
# 1.0". Using the same wrapper unconditionally guarantees the "off"
# state and the rest state of every zoom level line up exactly.
#
# animation.Scale's pivot is fixed at the top-left corner (confirmed
# empirically; an explicit `origin` param has no effect at all). That
# means zooming in crops progressively more off the bottom and right
# than the top and left -- a real cosmetic asymmetry. Pairing Scale
# with a Translate to re-center it was tried (and visually worked),
# but introduced a worse problem: combining Scale + Translate causes
# pixlet to periodically re-rasterize the whole image with a visible
# "pop" (confirmed with both WebP and GIF output, and independent of
# zoom amplitude -- so it's pixlet's animation engine, not this app's
# math). Scale *alone*, with no Translate at all, has none of that
# popping. Given the choice between a subtle, constant crop asymmetry
# and a periodic visible pop, the asymmetry is the lesser issue, so
# this uses Scale only.
# animation.Scale's pivot is the center of the widget by default (per
# pixlet's own docs) -- so zooming in via Scale alone is already close
# to centered, just slightly skewed toward the top-left as zoom
# increases. Pairing Scale with a Translate removes that residual skew.
#
# Root-caused the earlier "periodic pop" bug (found when this was first
# tried) to pixlet's own animation.Transformation source: a Translate's
# x/y are rounded to the nearest whole pixel every frame by default
# (`rounding = "round"`), while Scale is never rounded. Combined with a
# continuously-interpolating Scale, that rounding makes the Translate
# hold still for several frames and then jump a whole pixel at once --
# a real, confirmed pixlet behavior (see render/animation/translate.go),
# not a flaw in this app's math. `rounding = "none"` disables that
# snapping, letting Translate interpolate continuously alongside Scale
# -- confirmed via frame-by-frame pixel-delta analysis: the per-frame
# change is now even throughout the animation, instead of long still
# stretches punctuated by sudden full-frame jumps.
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)],
transforms = [animation.Scale(1.0, 1.0), animation.Translate(0, 0)],
),
animation.Keyframe(
percentage = 0.5,
transforms = [animation.Scale(top_scale, top_scale)],
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)],
transforms = [animation.Scale(1.0, 1.0), animation.Translate(0, 0)],
curve = "ease_in_out",
),
],
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.