Fix black border in album art zoom by only ever scaling up

The corner-reveal zoom scaled down from 1.0 toward ~0.71 (via Translate
to shift toward a corner), which showed up on a real device as a black
border cutting into the art. Empirical testing against the actual Row
layout at native canvas size showed the outer Box auto-fits an oversized
child at Scale(1.0, 1.0), so scaling down from there shrinks the already-
fitted image further and exposes background outside its bounds. Scaling
up only ever crops further into the image, which is always safe.

This drops Translate and the per-track corner selection (animation.Scale
pivots at the top-left corner by default with no way to change it -- an
explicit origin parameter was tried and had no effect), so the effect now
always zooms into the top-left corner instead of a track-dependent one.
This commit is contained in:
poprhythm committed 2026-09-29 15:42:15 -04:00
1 parent 83b817c3b2
commit cda256d1c2
2 files changed
+75 -79

No files matched your search

+38 -49
View File
@@ -72,7 +72,7 @@ def main(config):
)
if art:
content = render.Row(children = [_art_widget(art, now["subsonic_id"]), text_column])
content = render.Row(children = [_art_widget(art), text_column])
else:
content = render.Column(
main_align = "center",
@@ -86,72 +86,61 @@ def main(config):
return render.Root(child = content)
# Album art gets a slow "Ken Burns" breathing effect: eases from one corner
# out to the full frame, then back into that same corner, on a continuous
# loop -- not a one-shot reveal. A one-shot version (start in a corner, end
# on the full frame, then hold) looked jumpy: since the app re-renders on
# NOWPLAYING_TTL and each fresh render restarts its own animation from tick
# 0, whatever the *previous* pushed image happened to be showing at that
# moment gets abruptly replaced by a fresh "zoomed into a corner" frame. The
# back-and-forth loop doesn't eliminate that (a server-side re-render always
# restarts at tick 0 regardless), but ending exactly where it started means
# the animation also loops seamlessly on-device between refreshes, which in
# practice reads as far less jarring than a one-way reveal that snaps back.
# Album art gets a slow "Ken Burns" breathing effect: eases from the full
# frame in to a zoomed-in crop of the top-left corner, then back out, on a
# continuous loop -- not a one-shot reveal. A one-shot version (start
# zoomed in, end on the full frame, then hold) looked jumpy: since the app
# re-renders on NOWPLAYING_TTL and each fresh render restarts its own
# animation from tick 0, whatever the *previous* pushed image happened to
# be showing at that moment gets abruptly replaced by a fresh zoomed-in
# frame. The back-and-forth loop doesn't eliminate that (a server-side
# re-render always restarts at tick 0 regardless), but ending exactly where
# it started means the animation also loops seamlessly on-device between
# refreshes, which in practice reads as far less jarring than a one-way
# reveal that snaps back.
#
# Art is rendered oversized (45x45, vs. the 32x32 display slot) since
# animation.Scale above 1.0 has no visible effect on a widget whose declared
# size already matches its container (confirmed elsewhere this session) --
# starting at native 1.0 scale of the *oversized* image and easing down to
# ~32/45 achieves "zoom out to reveal everything" while only ever using
# Scale <= 1.0. Combining Scale + Translate in one Transformation, and
# combining that with the app's 4 already-scrolling Marquee lines, was
# verified not to trigger any *additional* glitching beyond the already-
# accepted minor Marquee quirk (see README). Duration 1200 ticks x the
# default 50ms/tick = 60s round-trip (30s out, 30s back); verified
# empirically that a slower zoom compresses *smaller*, not larger, since more
# adjacent ticks become pixel-identical at this resolution -- ~97-264KB
# depending on the art's own complexity, well under the 460KB device limit.
# animation.Scale has no visible effect on a widget whose declared size
# already matches its container. Only ever scaling *up* from 1.0 (never
# below) is deliberate, not cosmetic: this app originally scaled down to
# ~32/45 to "zoom out" from a corner reached via animation.Translate, which
# produced a real, on-device black border. Empirical testing (rendering the
# actual Row layout at native canvas size, no forced output dimensions)
# showed the outer Box auto-fits an oversized child to its own size at
# animation.Scale(1.0, 1.0) -- so scaling *down* from there shrinks the
# already-fitted image further, exposing background outside its bounds
# (the black border). Scaling *up* only ever crops further into the image,
# which is always safe. animation.Scale's pivot defaults to the top-left
# corner (confirmed empirically; an explicit `origin` param had no effect
# at all, so isn't used here) -- there's no reliable way to zoom into any
# other corner, so this always zooms into the top-left. Duration 1200
# ticks x the default 50ms/tick = 60s round-trip (30s in, 30s out);
# verified empirically that a slower zoom compresses *smaller*, not
# larger, since more adjacent ticks become pixel-identical at this
# resolution -- well under the 460KB device limit.
ART_SIZE = 45
ART_SLOT = 32
ART_MAX_OFFSET = (ART_SIZE - ART_SLOT) / 2.0 # 6.5px: centered -> true corner
ART_END_SCALE = float(ART_SLOT) / float(ART_SIZE) # ~0.71: full art fits in the slot
ART_ZOOM_SCALE = 1.4 # how far in to zoom on the top-left corner
ART_ZOOM_DURATION = 1200 # 60s at the default 50ms/tick
CORNER_OFFSETS = [
(-ART_MAX_OFFSET, -ART_MAX_OFFSET),
(ART_MAX_OFFSET, -ART_MAX_OFFSET),
(-ART_MAX_OFFSET, ART_MAX_OFFSET),
(ART_MAX_OFFSET, ART_MAX_OFFSET),
]
def _corner_for_track(subsonic_id):
# Pseudo-random but stable per track, so re-rendering the same
# still-playing track (every NOWPLAYING_TTL seconds) doesn't jarringly
# jump to a different corner -- only a genuinely new track does.
total = 0
for ch in subsonic_id.elems():
total += ord(ch)
return CORNER_OFFSETS[total % len(CORNER_OFFSETS)]
def _art_widget(art_bytes, subsonic_id):
corner_x, corner_y = _corner_for_track(subsonic_id)
big_art = render.Image(src = art_bytes, width = ART_SIZE, height = ART_SIZE)
def _art_widget(art_bytes):
art = render.Image(src = art_bytes, width = ART_SIZE, height = ART_SIZE)
zoom = animation.Transformation(
child = big_art,
child = art,
duration = ART_ZOOM_DURATION,
keyframes = [
animation.Keyframe(
percentage = 0.0,
transforms = [animation.Scale(1.0, 1.0), animation.Translate(corner_x, corner_y)],
transforms = [animation.Scale(ART_ZOOM_SCALE, ART_ZOOM_SCALE)],
),
animation.Keyframe(
percentage = 0.5,
transforms = [animation.Scale(ART_END_SCALE, ART_END_SCALE), animation.Translate(0, 0)],
transforms = [animation.Scale(1.0, 1.0)],
curve = "ease_in_out",
),
animation.Keyframe(
percentage = 1.0,
transforms = [animation.Scale(1.0, 1.0), animation.Translate(corner_x, corner_y)],
transforms = [animation.Scale(ART_ZOOM_SCALE, ART_ZOOM_SCALE)],
curve = "ease_in_out",
),
],
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.