From ef38fabebbca9b361b9f0aa1749f0c646c4f0a09 Mon Sep 17 00:00:00 2001 From: jim kolpack Date: Fri, 9 Oct 2026 15:48:50 -0400 Subject: [PATCH] Fix scanlines going blank instead of regenerating; reduce count; animate sweep Reduced visible scanlines from 8 to 6 per window (dropping the last two of the 8 evenly-spaced SCANLINE_PERIOD positions, rather than changing the period itself, so it keeps dividing evenly into ART_SLOT). Also animated the scanlines to sweep downward (previously static), matching SUB/WAVE's own continuously-animated scanlines. A first attempt built a buffer twice the art slot's height, pre-shifted with a negative render.Padding so the middle 32 rows lined up with the window at rest, then swept the full ART_SLOT distance in one pass. That's what produced "scanlines disappearing at the bottom without regenerating at the top": empirically, negative Padding didn't behave like the (well-proven elsewhere in this file) positive case, which reliably grows a widget's size -- confirmed via an isolated test that it went blank partway through a static sweep instead of revealing new rows. Replaced with a repeating sawtooth instead of one big sweep: the buffer is only one extra SCANLINE_PERIOD taller than the art slot (the same small, already-proven-safe oversized margin used throughout this file, e.g. the zoom's own ART_SIZE vs. ART_SLOT), and the animation ramps down by exactly one period, snaps back to 0, and repeats -- since the pattern repeats every period anyway, the snap is invisible (a standard infinite- scrolling-texture trick), and the translate distance never leaves the confirmed-safe small range. Verified via an isolated test (translating a plain Column child by small amounts, mirroring the technique already used for the zoom/jitter) before building this in, and via the real render: no black border across all 125 frames of a real render, payload well under budget (193KB). --- subwave_nowplaying.star | 62 +++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/subwave_nowplaying.star b/subwave_nowplaying.star index ce954f3..078efff 100644 --- a/subwave_nowplaying.star +++ b/subwave_nowplaying.star @@ -522,20 +522,64 @@ def _glitch_jitter(zoom): jittered = animation.Transformation(child = zoom, duration = total_ticks, keyframes = keyframes) return render.Box(width = ART_SLOT, height = ART_SLOT, child = jittered) +SCANLINE_PERIOD = 4 # 1px line, 3px gap +SCANLINE_SWEEP_DURATION = 160 # 8s total at the default 50ms/tick +SCANLINE_CYCLES = 8 # how many times the sawtooth below repeats over that +_SCANLINE_CYCLE_TICKS = SCANLINE_SWEEP_DURATION // SCANLINE_CYCLES + def _glitch_scanlines(): - # render.Sequence doesn't loop (it just stops painting once its total - # frame count is exhausted -- confirmed in pixlet's own source), so - # the RGB-split jitter above plays once per show_static window and - # then goes quiet; these persist for the whole window as a constant - # texture rather than a one-shot burst, closer to how SUB/WAVE's - # scanlines sweep continuously while its live class is applied. + # Modeled on SUB/WAVE's own scanlines, which sweep continuously via + # an animated `background-position-y` rather than sitting still. + # + # An earlier version tried a single large sweep across a buffer built + # twice as tall as the art slot, pre-shifted with a *negative* + # render.Padding so the pattern's middle 32 rows lined up with the + # window at rest. That didn't work how a positive Padding (which + # reliably *grows* a widget's declared size -- used elsewhere in this + # file, e.g. _glitch_band) does; empirically it went blank partway + # through instead of revealing new rows at the top, which is what + # "scanlines disappearing at the bottom without regenerating at the + # top" looked like. + # + # This avoids all of that: the buffer is only ONE extra + # SCANLINE_PERIOD taller than the art slot (the same small, proven- + # safe oversized margin used everywhere else in this file -- e.g. the + # zoom's own ART_SIZE vs. ART_SLOT), and instead of one big sweep it's + # a repeating sawtooth -- ramp down by exactly one SCANLINE_PERIOD, + # then snap back to 0 and repeat. Since the pattern repeats every + # SCANLINE_PERIOD anyway, a one-period displacement looks identical to + # the starting position, so the snap is invisible -- a standard + # infinite-scrolling-texture trick -- while the translate distance + # never has to leave the small range already confirmed safe. + tall = ART_SLOT + SCANLINE_PERIOD lines = [] - for y in range(ART_SLOT): - if y % 4 == 0: + for y in range(tall): + # SCANLINE_PERIOD=4 over ART_SLOT=32 gives 8 evenly-spaced lines + # per window; skipping 2 of those 8 positions (dropping the last + # two in each 32-row repeat, rather than changing the period + # itself) keeps the period dividing evenly into things cleanly. + if y % SCANLINE_PERIOD == 0 and (y % ART_SLOT) < ART_SLOT - 2 * SCANLINE_PERIOD: lines.append(render.Box(width = ART_SLOT, height = 1, color = "#00000080")) else: lines.append(render.Box(width = ART_SLOT, height = 1, color = "#00000000")) - return render.Column(children = lines) + pattern = render.Column(children = lines) + + keyframes = [] + for cycle in range(SCANLINE_CYCLES): + cycle_start = cycle * _SCANLINE_CYCLE_TICKS + ramp_end = cycle_start + _SCANLINE_CYCLE_TICKS - 1 # last tick of the cycle is the snap back to 0 + keyframes.append(animation.Keyframe( + percentage = float(cycle_start) / SCANLINE_SWEEP_DURATION, + transforms = [animation.Translate(0, 0)], + )) + keyframes.append(animation.Keyframe( + percentage = float(ramp_end) / SCANLINE_SWEEP_DURATION, + transforms = [animation.Translate(0, SCANLINE_PERIOD)], + curve = "linear", + )) + + sweeping = animation.Transformation(child = pattern, duration = SCANLINE_SWEEP_DURATION, keyframes = keyframes) + return render.Box(width = ART_SLOT, height = ART_SLOT, child = sweeping) def _glitch_corner_ticks(color): tick_size = 5