From 9f2216c692cea44b739e769f3b56760888d30391 Mon Sep 17 00:00:00 2001 From: jim kolpack Date: Thu, 1 Oct 2026 22:23:52 -0400 Subject: [PATCH] Fix black border at strong album art zoom by scaling the recenter shift The centering Translate formula, (1-s)*(ART_SIZE/2), was derived assuming it's applied in final output pixels. That held up at a moderate zoom (1.4x) but pushed past the image's actual edge at a strong one (1.7x) -- a real, on-device black border in the bottom-right corner, confirmed with the calibration tool (true black RGB(0,0,0) pixels, not just dark content). Dividing the shift by the scale factor keeps the crop inside the image's bounds at every zoom level the app offers, verified via the calibration tool at 1.0/1.15/1.4/1.7x and against a live station -- at the cost of the most extreme (1.7x) setting being very slightly off-center rather than perfectly centered, which is a trade worth making since going outside the art's bounds is worse. Moderate and subtle remain visibly centered. --- dev/calibration_test.star | 2 +- subwave_nowplaying.star | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/dev/calibration_test.star b/dev/calibration_test.star index e4c657d..ca5232c 100644 --- a/dev/calibration_test.star +++ b/dev/calibration_test.star @@ -30,7 +30,7 @@ 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) + shift = (1.0 - top_scale) * (ART_SIZE / 2.0) / top_scale zoom = animation.Transformation( child = art, duration = ART_ZOOM_DURATION, diff --git a/subwave_nowplaying.star b/subwave_nowplaying.star index e2ccab3..2ddf611 100644 --- a/subwave_nowplaying.star +++ b/subwave_nowplaying.star @@ -230,14 +230,24 @@ def _art_widget(art_bytes, zoom_scale): # bottom and right than the top and left -- most of the animation's # duration is spent between keyframes, not sitting at the symmetric # rest state, so this read as a persistent bottom/right crop. Pairing - # Scale with a Translate that's a deterministic function of the same - # scale value (`shift`) re-centers the zoom so it crops evenly from + # Scale with a Translate re-centers the zoom so it crops evenly from # all sides instead -- verified against a calibration grid image. - # This is safe because scale never drops below 1.0 here: the - # black-border bug from an earlier version came from combining + # + # The naive "final pixel shift" formula, (1-s)*(ART_SIZE/2), looked + # right at a moderate zoom (1.4x) but pushed past the image's actual + # edge at a strong one (1.7x) -- a real, on-device black border in the + # bottom-right corner. That means Translate isn't applied in final + # output pixels here; empirically, dividing by `top_scale` keeps the + # crop inside the image's bounds at every zoom level this app offers + # (verified via the calibration tool at both 1.4x and 1.7x), at the + # cost of the *most* extreme (1.7x) zoom being very slightly + # off-center rather than perfectly centered -- a trade worth making + # since going outside the art's bounds is worse than a slight + # asymmetry. This is safe because scale never drops below 1.0 here: + # the black-border bug from an earlier version came from combining # Translate with scale *below* 1.0, not from Translate itself. top_scale = max(zoom_scale, 1.0) - shift = (1.0 - top_scale) * (ART_SIZE / 2.0) + shift = (1.0 - top_scale) * (ART_SIZE / 2.0) / top_scale zoom = animation.Transformation( child = art, duration = ART_ZOOM_DURATION,