Files
subwave-nowplaying/dev/calibration_test.star
poprhythm a4a8e37242 Fix asymmetric album art crop -- was showing more top-left, losing bottom/right
Root-caused with the calibration tool's crosshair marker: at rest and
near-rest (zoom breathing cycle's endpoints, where Scale is momentarily
pinned to a constant value), animation.Origin has no effect at all --
confirmed empirically, varying origin_x/origin_y produced byte-identical
output whenever Scale doesn't actually change frame to frame, regardless
of the scale value itself. With nothing for Origin to pivot, the crop
fell back to render.Box's own default handling of an oversized
Transformation child, which isn't centered: the crosshair sat several
pixels down-and-right of true center, meaning the art was effectively
showing more of its own top-left and cropping away a disproportionate
chunk of the bottom-right -- exactly "losing ~10% on bottom and right,
when it's not zoomed".

Fixed with a *constant* Translate (same value at every keyframe, not
animated) -- found empirically via the calibration tool (the (tx, ty)
that centered the crosshair at rest also held up at peak zoom, checked
against both). This isn't the mistake an earlier, abandoned attempt
made: that version's Translate *varied* over the animation fighting
Origin's own pivot, and interacted badly with Scale's continuous
interpolation (the historical "periodic pop" bug, per pixlet's own
source on Translate's per-frame rounding). A constant value never
changes frame to frame, so there's nothing for it to round-mismatch
against -- confirmed smooth across a full cycle.

Scaled by `scale` (box_crop_bias = -2 * scale) to stay correct at 2x.
dev/calibration_test.star updated to match, per its own sync requirement
with the real _zoom_transformation.

Verified: crosshair now dead-center at rest (calibration tool), 0
black-corner hits for jitter+bands in isolation at worst-case settings
(strong zoom, random position) at both 1x and 2x, and 0 black/void
corners across a full zoom cycle at all four ZOOM_RANDOM_SPOTS values
with strong zoom.
2026-10-10 14:23:33 -04:00

82 lines
4.2 KiB
Python

"""
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,
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
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 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.
Later: `origin` turned out to only have any effect while Scale is
actually changing frame to frame (confirmed with this tool: varying
origin_x/origin_y produced identical output for a Scale pinned to a
constant value at every keyframe). At the two instants this animation's
own Scale is momentarily constant (percentage 0.0 and 1.0, "rest"), the
crop falls back to render.Box's own default handling of an oversized
Transformation child, which isn't centered -- the crosshair below sat
several pixels toward the bottom-right of center. Fixed with BOX_CROP_BIAS,
a *constant* Translate (same value at every keyframe, found via this
tool's crosshair) -- not the same mistake as the first attempt above,
since a constant value never changes frame to frame, so there's nothing
for it to round-mismatch against Scale's interpolation.
"""
load("render.star", "render")
load("animation.star", "animation")
load("calibration.png", CALIB = "file")
ART_SIZE = 36
ART_SLOT = 32
ART_ZOOM_DURATION = 200
BOX_CROP_BIAS = -2 # see the docstring above; keep in sync with box_crop_bias in the real _zoom_transformation
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)
zoom = animation.Transformation(
child = art,
duration = ART_ZOOM_DURATION,
origin = animation.Origin(origin_x, origin_y),
keyframes = [
animation.Keyframe(percentage = 0.0, transforms = [animation.Scale(1.0, 1.0), animation.Translate(BOX_CROP_BIAS, BOX_CROP_BIAS)]),
animation.Keyframe(percentage = 0.5, transforms = [animation.Scale(top_scale, top_scale), animation.Translate(BOX_CROP_BIAS, BOX_CROP_BIAS)], curve = "ease_in_out"),
animation.Keyframe(percentage = 1.0, transforms = [animation.Scale(1.0, 1.0), animation.Translate(BOX_CROP_BIAS, BOX_CROP_BIAS)], 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, origin_x, origin_y),
render.Box(width = 32, height = 32, color = "#111111"),
]))