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:
1 parent
9f63adbfbf
commit
4a2b3caac8
3 files changed
+73
-47
No files matched your search
@@ -46,20 +46,30 @@ 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 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.
|
||||
`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
|
||||
(`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).
|
||||
|
||||
**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
|
||||
|
||||
+21
-15
@@ -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)
|
||||
|
||||
+28
-18
@@ -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",
|
||||
),
|
||||
],
|
||||
|
||||