Rotate the random zoom spot over time, not just per-track

Random zoom position was previously picked once per track (stable via
subsonic_id) and held for that track's entire runtime. Changed it to
rotate on a ~10s cadence based on wall-clock time, still offset by a
per-track hash so different tracks don't all land on the same spot in
the same window. This lets a single long track show off different
sections of its own artwork over time, rather than being stuck on one
spot until the next track starts.
This commit is contained in:
poprhythm committed 2026-10-02 22:19:59 -04:00
1 parent d8add24c5e
commit f6893bdcdf
2 files changed
+43 -21

No files matched your search

+19 -9
View File
@@ -24,8 +24,12 @@ app's config when adding it to a device.
- **Album art zoom** — how far the album art's breathing zoom effect goes - **Album art zoom** — how far the album art's breathing zoom effect goes
in: Off, Subtle, Moderate (default), or Strong. in: Off, Subtle, Moderate (default), or Strong.
- **Zoom position** — where the zoom centers on: Center (default), or - **Zoom position** — where the zoom centers on: Center (default), or
Random (a handful of preset off-center spots, picked per-track so a Random (a handful of preset off-center spots). Rotates on a ~10s cadence
still-playing track doesn't jump between spots on every refresh). based on wall-clock time, offset per-track so different tracks don't all
land on the same spot at the same moment — meaning it can change on
*every* refresh, not just when the track changes, so a long track gets
to show off different sections of its art over time instead of being
stuck on one spot for its whole runtime.
## How it works ## How it works
@@ -55,13 +59,19 @@ app's config when adding it to a device.
came from testing it set to its own default value and comparing the came from testing it set to its own default value and comparing the
result against itself, which is of course identical either way). The result against itself, which is of course identical either way). The
"Random" zoom position picks from a handful of preset off-center spots "Random" zoom position picks from a handful of preset off-center spots
(`ZOOM_RANDOM_SPOTS`), chosen per-track from `subsonic_id` the same way (`ZOOM_RANDOM_SPOTS`), kept modest (0.3-0.7 of the image's own bounds,
the old corner-selection logic worked, so a still-playing track doesn't not the full 0.0-1.0 range) and verified safe (no black border, even at
jump between spots on every `NOWPLAYING_TTL` refresh. Those spots are the strongest 1.7x zoom) via the calibration tool; more extreme corners
kept modest (0.3-0.7 of the image's own bounds, not the full 0.0-1.0 weren't tested and may not be safe. Which spot is picked rotates on a
range) and verified safe (no black border, even at the strongest 1.7x `ZOOM_SPOT_ROTATE_SECONDS` (10s) cadence based on wall-clock time
zoom) via the calibration tool; more extreme corners weren't tested and (`time.now().unix`), offset by a hash of `subsonic_id` so different
may not be safe. tracks don't all land on the same spot in the same 10s window. This was
deliberately *not* made purely per-track-stable (i.e. the same spot for
a track's entire runtime, the way the old corner-selection logic for
the since-removed Translate approach worked): the ask was to see
different sections of a single piece of art over time, which requires
the spot to be able to change mid-playback, not just when the track
changes.
An earlier version tried to fix a *different* problem — `animation.Scale` An earlier version tried to fix a *different* problem — `animation.Scale`
zooming in asymmetrically toward one corner — by pairing Scale with a zooming in asymmetrically toward one corner — by pairing Scale with a
+24 -12
View File
@@ -104,13 +104,22 @@ DEFAULT_ZOOM_LEVEL = "moderate"
# own 36x36 bounds. Kept modest (0.3-0.7, not full 0.0-1.0) and verified # own 36x36 bounds. Kept modest (0.3-0.7, not full 0.0-1.0) and verified
# safe (no black border, even at the strongest 1.7x zoom) via the # safe (no black border, even at the strongest 1.7x zoom) via the
# calibration tool -- more extreme corners weren't tested and may not be. # calibration tool -- more extreme corners weren't tested and may not be.
# Picked per-track (stable via subsonic_id, like the old corner-selection # Only used when the "Zoom position" schema field is set to "random" --
# logic) rather than per-render, so a still-playing track doesn't jump # "center" always zooms on the image's own center, exposed as a config
# between spots on every NOWPLAYING_TTL refresh. Only used when the # choice since some covers (e.g. ones with a centered logo or face) read
# "Zoom position" schema field is set to "random" -- "center" always zooms # better with a predictable center zoom than a random one.
# on the image's own center, exposed as a config choice since some covers #
# (e.g. ones with a centered logo or face) read better with a predictable # Rotates on a ~10s cadence (matching ART_ZOOM_DURATION below) based on
# center zoom than a per-track random one. # wall-clock time, not just per-track: a time bucket picks the spot, offset
# by a per-track hash so different tracks don't all land on the same spot
# at the same moment. This means the spot can change on *every* refresh,
# including mid-playback of a single still-running track -- not just when
# the track itself changes -- so a long track gets to show off different
# sections of its art over time, which is the whole point of "random"
# rather than "center". One real limitation: a one-shot `pixlet render`
# CLI call reads the system clock same as the live server would, so this
# is still deterministic for a given moment -- it just varies *which*
# moment you rendered at, same as the live progress bar in _track_progress.
ZOOM_CENTER_SPOT = (0.5, 0.5) ZOOM_CENTER_SPOT = (0.5, 0.5)
ZOOM_RANDOM_SPOTS = [ ZOOM_RANDOM_SPOTS = [
(0.5, 0.5), (0.5, 0.5),
@@ -119,16 +128,19 @@ ZOOM_RANDOM_SPOTS = [
(0.3, 0.7), (0.3, 0.7),
(0.7, 0.3), (0.7, 0.3),
] ]
ZOOM_SPOT_ROTATE_SECONDS = 10
DEFAULT_ZOOM_POSITION = "center" DEFAULT_ZOOM_POSITION = "center"
def _zoom_spot_for_track(zoom_position, subsonic_id): def _zoom_spot(zoom_position, subsonic_id):
if zoom_position != "random": if zoom_position != "random":
return ZOOM_CENTER_SPOT return ZOOM_CENTER_SPOT
total = 0 track_offset = 0
for ch in subsonic_id.elems(): for ch in subsonic_id.elems():
total += ord(ch) track_offset += ord(ch)
return ZOOM_RANDOM_SPOTS[total % len(ZOOM_RANDOM_SPOTS)]
time_bucket = int(time.now().unix / ZOOM_SPOT_ROTATE_SECONDS)
return ZOOM_RANDOM_SPOTS[(time_bucket + track_offset) % len(ZOOM_RANDOM_SPOTS)]
def main(config): def main(config):
station_url = config.get("station_url", DEFAULT_STATION_URL) station_url = config.get("station_url", DEFAULT_STATION_URL)
@@ -266,7 +278,7 @@ def _art_widget(art_bytes, zoom_scale, zoom_position, subsonic_id):
# toward the bottom-right -- the opposite of the bug it was meant to # toward the bottom-right -- the opposite of the bug it was meant to
# fix. Since `origin` already does directly and correctly what that # fix. Since `origin` already does directly and correctly what that
# Translate was clumsily approximating, this uses `origin` alone. # Translate was clumsily approximating, this uses `origin` alone.
origin_x, origin_y = _zoom_spot_for_track(zoom_position, subsonic_id) origin_x, origin_y = _zoom_spot(zoom_position, subsonic_id)
top_scale = max(zoom_scale, 1.0) top_scale = max(zoom_scale, 1.0)
zoom = animation.Transformation( zoom = animation.Transformation(
child = art, child = art,
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.