diff --git a/dev/calibration_test.star b/dev/calibration_test.star index ca5232c..e4c657d 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) / top_scale + shift = (1.0 - top_scale) * (ART_SIZE / 2.0) zoom = animation.Transformation( child = art, duration = ART_ZOOM_DURATION, diff --git a/subwave_nowplaying.star b/subwave_nowplaying.star index 2ddf611..03c91cd 100644 --- a/subwave_nowplaying.star +++ b/subwave_nowplaying.star @@ -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" @@ -233,21 +237,19 @@ def _art_widget(art_bytes, zoom_scale): # Scale with a Translate re-centers the zoom so it crops evenly from # all sides instead -- verified against a calibration grid image. # - # 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. + # shift = (1-s)*(ART_SIZE/2) is linear in the scale value, which keeps + # it moving smoothly in lockstep with Scale's own eased interpolation + # throughout the animation. A tempting "fix" for a black-border bug at + # strong zoom was dividing this by `top_scale` to pull the shift in at + # high scale values -- that avoided the border, but made the shift a + # *nonlinear* function of the scale being interpolated, so partway + # through the animation the pan no longer tracked the zoom at a + # constant rate -- visibly jerky. Reverted: instead, ZOOM_LEVELS caps + # "strong" at a value where this plain linear formula is already safe + # (verified via the calibration tool), rather than changing the + # formula's shape. top_scale = max(zoom_scale, 1.0) - shift = (1.0 - top_scale) * (ART_SIZE / 2.0) / top_scale + shift = (1.0 - top_scale) * (ART_SIZE / 2.0) zoom = animation.Transformation( child = art, duration = ART_ZOOM_DURATION,