diff --git a/dev/calibration_test.star b/dev/calibration_test.star index 8b08a22..768d53c 100644 --- a/dev/calibration_test.star +++ b/dev/calibration_test.star @@ -32,6 +32,19 @@ 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") @@ -41,6 +54,7 @@ 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) @@ -50,9 +64,9 @@ def _art_widget(art_bytes, zoom_scale, origin_x, origin_y): 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.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(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) diff --git a/subwave_nowplaying.star b/subwave_nowplaying.star index a5150dc..4ebd486 100644 --- a/subwave_nowplaying.star +++ b/subwave_nowplaying.star @@ -343,16 +343,47 @@ def _zoom_transformation(art_bytes, zoom_scale, zoom_position, subsonic_id, scal # `origin` picks where the zoom centers on, which also gives a free # "zoom to a different spot each time" effect (ZOOM_SPOTS above) by # just varying it per track. An earlier version tried to recenter the - # zoom by pairing Scale with a *computed* Translate, on the mistaken - # assumption that Scale's default pivot is the top-left corner -- it - # isn't; pixlet's own 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 was meant to - # fix. Since `origin` already does directly and correctly what that - # Translate was clumsily approximating, this uses `origin` alone. + # zoom by pairing Scale with a *computed, animated* Translate, on the + # mistaken assumption that Scale's default pivot is the top-left + # corner -- it isn't; pixlet's own 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 was meant to fix. Since `origin` already does directly and + # correctly what that Translate was clumsily approximating, this uses + # `origin` for the zoom-position pivot. + # + # Separately, though: `origin` only has any effect while Scale is + # actually changing frame to frame -- confirmed empirically (varying + # origin_x/origin_y produced byte-identical output for a Scale that's + # pinned to the same constant value across every keyframe, at *any* + # scale, not just 1.0). At the two instants where this animation's own + # Scale is momentarily constant (percentage 0.0 and 1.0, i.e. "rest"), + # there's nothing for origin to pivot, so the crop that's left falls + # back to render.Box's own default handling of an oversized + # Transformation child -- which isn't a centered crop at all: checked + # with the calibration tool (concentric rings plus a center crosshair), + # the crosshair sat several pixels down-and-right of where a centered + # crop would put it, i.e. the box was effectively showing more of the + # image's top-left and cropping away a disproportionate amount from + # the bottom-right -- matching exactly what was reported ("losing + # ~10% on bottom and right, not zoomed"). + # + # Fixed with a *constant* Translate -- the same (tx, ty) at every + # keyframe, found empirically via the calibration tool's crosshair + # (the value that put it dead-center at rest also held up at peak + # zoom). This is not the same mistake as the earlier abandoned + # attempt: that Translate *varied* over the animation (fighting + # Origin's own pivot, and per pixlet's own source, interacting badly + # with Scale's continuous interpolation vs. Translate's per-frame + # rounding -- the "periodic pop" bug). A constant value changes + # nothing frame to frame, so there's no interpolation for it to + # mismatch against -- confirmed smooth across a full cycle, no pop. + # It's purely compensating for render.Box's own fixed crop bias, which + # doesn't depend on zoom_scale/zoom_position at all. origin_x, origin_y = _zoom_spot(zoom_position, subsonic_id) top_scale = max(zoom_scale, 1.0) + box_crop_bias = -2 * scale zoom = animation.Transformation( child = art, duration = ART_ZOOM_DURATION, @@ -360,16 +391,16 @@ def _zoom_transformation(art_bytes, zoom_scale, zoom_position, subsonic_id, scal keyframes = [ animation.Keyframe( percentage = 0.0, - transforms = [animation.Scale(1.0, 1.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)], + 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)], + transforms = [animation.Scale(1.0, 1.0), animation.Translate(box_crop_bias, box_crop_bias)], curve = "ease_in_out", ), ],