Jitter the underlying art in sync with the RGB-split bands

SUB/WAVE's original effect shifts the whole cover frame along with the
color-split bands, not just the bands moving over a static image
underneath. Added that: each burst state now includes a shifted (but
untinted, uncropped) full copy of the art beneath the two RGB-split
bands, nudged by the average of the two bands' own shift values.

Applying the shift to the already-cropped ART_SLOT x ART_SLOT image (the
way the bands used to work) would have exposed a blank edge -- the same
black-border problem from much earlier in this project. Instead, the
shift is applied to the *raw* zoom Transformation before its final crop,
reusing the same oversized margin the zoom animation itself relies on.
Confirmed safe both in an isolated test (+-2px at rest and peak zoom,
no border) and by scanning all 200 frames of a real worst-case render
(strong zoom, random position) for black/transparent corner pixels --
none found.

This required exposing the zoom Transformation itself to the glitch
code instead of just the final cropped image, so _zoomed_art became
_zoom_transformation (returns the raw widget; callers wrap it in the
ART_SLOT Box themselves).

Payload note: worst case (strong zoom + random position) measured 428KB,
up from ~220KB before this change but still under the 460KB device
limit -- margin is noticeably thinner now (~7%) than it's been
recently, worth keeping an eye on if more effects get added later.
This commit is contained in:
poprhythm committed 2026-10-08 19:02:37 -04:00
1 parent c82503c73f
commit e51c2bd422
1 file changed
+41 -13
+41 -13
View File
@@ -284,14 +284,19 @@ ART_SLOT = 32
ART_ZOOM_DURATION = 200 # 10s round-trip (5s in, 5s out) at the default 50ms/tick
def _art_widget(art_bytes, zoom_scale, zoom_position, subsonic_id, show_static):
cropped = _zoomed_art(art_bytes, zoom_scale, zoom_position, subsonic_id)
zoom = _zoom_transformation(art_bytes, zoom_scale, zoom_position, subsonic_id)
cropped = render.Box(width = ART_SLOT, height = ART_SLOT, child = zoom)
if not show_static:
return cropped
return render.Stack(children = [cropped, _glitch_effect(cropped)])
# Passing the raw (pre-crop) `zoom` down to the glitch effect, not the
# already-flattened `cropped`, so it can jitter the art's position
# using the same oversized margin the zoom animation itself relies on
# -- see _glitch_band and _glitch_base_jitter for why that matters.
return render.Stack(children = [cropped, _glitch_effect(zoom)])
def _zoomed_art(art_bytes, zoom_scale, zoom_position, subsonic_id):
def _zoom_transformation(art_bytes, zoom_scale, zoom_position, subsonic_id):
art = render.Image(src = art_bytes, width = ART_SIZE, height = ART_SIZE)
# Always route through animation.Transformation, even when zoom is off
@@ -338,7 +343,7 @@ def _zoomed_art(art_bytes, zoom_scale, zoom_position, subsonic_id):
),
],
)
return render.Box(width = ART_SLOT, height = ART_SLOT, child = zoom)
return zoom
# Modeled on SUB/WAVE's own web UI, which applies a track-transition
# effect to the cover art (found via its compiled CSS/JS, not written down
@@ -421,20 +426,37 @@ def _glitch_states_for_cycle(cycle_index):
start = (cycle_index * GLITCH_STATES_PER_CYCLE) % pool_size
return [GLITCH_STATE_POOL[(start + i) % pool_size] for i in range(GLITCH_STATES_PER_CYCLE)]
def _glitch_band(zoomed_art, top, height, shift_x, hue_change):
tinted = filter.Hue(child = zoomed_art, change = hue_change)
def _glitch_shift(zoom, shift_x):
# Applied to the *raw* zoom Transformation (not an already-cropped
# ART_SLOT x ART_SLOT image) and only clipped to ART_SLOT afterward,
# so a horizontal nudge reveals more of the same oversized margin the
# zoom animation itself relies on, rather than exposing a blank edge
# with nothing behind it -- confirmed empirically (no black border at
# +-2px, at both rest and peak zoom) before building this in.
return render.Padding(pad = (max(0, shift_x), 0, max(0, -shift_x), 0), child = zoom)
def _glitch_band(zoom, top, height, shift_x, hue_change):
tinted = filter.Hue(child = _glitch_shift(zoom, shift_x), change = hue_change)
band = render.Padding(
pad = (0, top, 0, max(0, ART_SLOT - top - height)),
child = render.Box(width = ART_SLOT, height = height, child = tinted),
)
shifted = render.Padding(pad = (max(0, shift_x), 0, max(0, -shift_x), 0), child = band)
# render.Padding *grows* its child's declared size by the pad amount
# rather than nudging content within a fixed frame -- without this
# outer Box to clip it back down to exactly ART_SLOT, the horizontal
# shift above made this (and everything stacked with it) wider than
# the 32px art slot, bleeding into the text column next to it.
return render.Box(width = ART_SLOT, height = ART_SLOT, child = shifted)
return render.Box(width = ART_SLOT, height = ART_SLOT, child = band)
def _glitch_base_jitter(zoom, shift_x):
# The art itself jittering in sync with the RGB-split bands (not just
# the tinted bands moving over a static image underneath) -- modeled
# on the original SUB/WAVE effect, which shifts the whole cover frame
# along with the color split. Uses the same safe raw-zoom shift
# technique as the bands above, just without the hue tint or height
# crop, so it reads as the full image nudging rather than a band.
return render.Box(width = ART_SLOT, height = ART_SLOT, child = _glitch_shift(zoom, shift_x))
def _glitch_hold(child, duration):
# A no-op Transformation purely to pin a Sequence child's FrameCount
@@ -452,15 +474,21 @@ def _glitch_hold(child, duration):
],
)
def _glitch_jitter(zoomed_art):
def _glitch_jitter(zoom):
pause = _glitch_hold(render.Box(width = ART_SLOT, height = ART_SLOT), GLITCH_PAUSE_TICKS)
sequence = []
for cycle_index in range(GLITCH_REPEATS):
for top1, h1, shift1, hue1, top2, h2, shift2, hue2 in _glitch_states_for_cycle(cycle_index):
# The art itself nudges along with the bands, not just the
# bands moving over a static image -- splitting the difference
# between the two bands' own shifts rather than adding a
# separate value to tune per state.
base_shift = int((shift1 + shift2) / 2.0)
state = render.Stack(children = [
_glitch_band(zoomed_art, top1, h1, shift1, hue1),
_glitch_band(zoomed_art, top2, h2, shift2, hue2),
_glitch_base_jitter(zoom, base_shift),
_glitch_band(zoom, top1, h1, shift1, hue1),
_glitch_band(zoom, top2, h2, shift2, hue2),
])
sequence.append(_glitch_hold(state, GLITCH_HOLD_TICKS))
sequence.append(pause)
@@ -525,12 +553,12 @@ def _glitch_corner_ticks(color):
),
)
def _glitch_effect(zoomed_art):
def _glitch_effect(zoom):
return render.Box(
width = ART_SLOT,
height = ART_SLOT,
child = render.Stack(children = [
_glitch_jitter(zoomed_art),
_glitch_jitter(zoom),
_glitch_scanlines(),
_glitch_corner_ticks("#33ff9980"),
]),
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.