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:
1 parent
c82503c73f
commit
e51c2bd422
1 file changed
+41
-13
+41
-13
@@ -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"),
|
||||
]),
|
||||
|
||||