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

+24 -20
View File
@@ -11,24 +11,27 @@ TR, blue BL, white BR) and a center crosshair.
Usage:
pixlet render dev/calibration_test.star -o calib.gif
pixlet render dev/calibration_test.star zoom_scale=1.0 -o calib_rest.gif # rest only
pixlet render dev/calibration_test.star ox=0.3 oy=0.3 -o calib_offcenter.gif
Keep ART_SIZE/ART_SLOT/the zoom math below in sync with the real
_art_widget if that ever changes.
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.
was tried first and produced a periodic rendering "pop", root-caused to
pixlet's own source (render/animation/translate.go): Translate's x/y are
rounded to the nearest whole pixel every frame by default, while Scale
never rounds, so a continuously-interpolating Scale combined with a
rounded Translate holds still for several frames then jumps a whole pixel
at once. `rounding = "none"` fixed that. But the Translate's *magnitude*
was also wrong: it assumed Scale pivots at the top-left corner by
default, when pixlet's docs say the default `origin` is already the
center (0.5, 0.5) -- so the "centering" Translate was overcorrecting,
visibly skewing the zoom toward the bottom-right instead. Replaced with
`origin` directly (which does, correctly, what that Translate was
clumsily approximating), confirmed via this calibration tool with
genuinely different (non-default) origin values -- they do change the
output, despite an earlier, flawed test (origin set to its own default
and compared against itself) suggesting `origin` had no effect at all.
"""
load("render.star", "render")
@@ -39,25 +42,26 @@ ART_SIZE = 36
ART_SLOT = 32
ART_ZOOM_DURATION = 200
def _art_widget(art_bytes, zoom_scale):
def _art_widget(art_bytes, zoom_scale, origin_x, origin_y):
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",
origin = animation.Origin(origin_x, origin_y),
keyframes = [
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"),
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"),
],
)
return render.Box(width = ART_SLOT, height = ART_SLOT, child = zoom)
def main(config):
zoom_scale = float(config.get("zoom_scale", "1.4"))
origin_x = float(config.get("ox", "0.5"))
origin_y = float(config.get("oy", "0.5"))
return render.Root(child = render.Row(children = [
_art_widget(CALIB.readall(), zoom_scale),
_art_widget(CALIB.readall(), zoom_scale, origin_x, origin_y),
render.Box(width = 32, height = 32, color = "#111111"),
]))
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.