From 385e20785cd898902cdd62ffc679866757431fb4 Mon Sep 17 00:00:00 2001 From: jim kolpack Date: Fri, 9 Oct 2026 16:57:53 -0400 Subject: [PATCH] Fix jitter invisible in pixlet serve by restoring the _glitch_hold pattern Found by comparing against an earlier commit (4983e81) where jitter was last confirmed visible, before this file's jitter architecture was rewritten twice since (once for a live-zoom desync bug, once for a Translate black-border bug). The shift mechanism (_glitch_shift, Padding-based) was identical in both versions -- the actual difference was how a state gets "held" for multiple ticks in the render.Sequence. The rewritten version held a state by appending the same widget reference to the Sequence's children list GLITCH_HOLD_TICKS times in a row. That produced a correctly-encoded, pixel-diffable-as-different file via plain `pixlet render`, but was reported as showing no jitter at all through pixlet serve's live preview -- repeated-identical-children apparently doesn't advance playback the same way every Sequence consumer expects. Restored _glitch_hold(child, duration): a no-op animation.Transformation (empty keyframe transforms) wrapping each state once, pinning its own FrameCount to `duration` so Sequence holds on a single child instead of relying on duplicate entries. This is the original, proven-working pattern from before the rewrites -- unchanged other than operating on the now-static _glitch_rest_art instead of the live zoom. Verified 0 black-corner hits across 194 frames, 185KB worst-case payload (strong zoom + random position), both comfortably within budget. --- subwave_nowplaying.star | 80 ++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/subwave_nowplaying.star b/subwave_nowplaying.star index a4796a3..3fb4ce7 100644 --- a/subwave_nowplaying.star +++ b/subwave_nowplaying.star @@ -495,19 +495,46 @@ def _glitch_base_jitter(zoom, shift_x): # 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 + # to `duration` ticks, so render.Sequence holds on it for that long + # before advancing to the next child -- Sequence has no other way to + # control how long a given child plays. + # + # This matters more than it looks: an earlier version of this + # function "held" a state by appending the same widget reference to + # the children list GLITCH_HOLD_TICKS times in a row instead of + # wrapping it once in a duration-N _glitch_hold. That rendered fine + # via plain `pixlet render` to a file (confirmed, pixel-diffed, a + # clean alternating shift) but was reported as showing no jitter at + # all in pixlet serve's live preview -- repeated-identical-children + # apparently doesn't advance the same way every consumer of a + # Sequence expects. Wrapping each state in its own _glitch_hold is + # the original, proven-working pattern this app used before that + # rewrite. + return animation.Transformation( + child = child, + duration = duration, + width = ART_SLOT, + height = ART_SLOT, + keyframes = [ + animation.Keyframe(percentage = 0.0, transforms = []), + animation.Keyframe(percentage = 1.0, transforms = []), + ], + ) + def _glitch_jitter(zoom): # Built as a render.Sequence of discrete shifted states, each using # _glitch_shift (Padding-based), not animation.Transformation + - # Translate (the previous approach). That seemed reasonable but - # produced a real, confirmed-on-device black border that no amount of - # scale adjustment on `zoom` could fix -- root-caused via isolated, - # pixel-sampled test renders to a more fundamental pixlet limitation: - # Transformation + Translate can't reveal content beyond what's - # visible at rest, it only moves already-visible pixels around. - # Padding genuinely grows the declared canvas instead, which is - # exactly the mechanism _glitch_shift already uses and had already - # validated as safe ("no black border at +-2px, at both rest and peak - # zoom") before this ever used Translate. + # Translate (an approach tried in between). Translate seemed + # reasonable but produced a real, confirmed-on-device black border + # that no amount of scale adjustment on `zoom` could fix -- + # root-caused via isolated, pixel-sampled test renders to a more + # fundamental pixlet limitation: Transformation + Translate can't + # reveal content beyond what's visible at rest, it only moves + # already-visible pixels around. Padding genuinely grows the declared + # canvas instead, which is exactly the mechanism _glitch_shift + # already uses. # # A much earlier version of this file *did* use render.Sequence for # the jitter and was abandoned for a real bug -- but that bug was @@ -517,6 +544,8 @@ def _glitch_jitter(zoom): # frame-dependent zoom that version wrapped. That no longer applies: # `zoom` here is _glitch_rest_art's static, non-animated image, so # there's nothing frame-dependent left for Sequence to desync. + pause = _glitch_hold(render.Box(width = ART_SLOT, height = ART_SLOT, child = zoom), GLITCH_PAUSE_TICKS) + children = [] for cycle_index in range(GLITCH_REPEATS): for _ in range(GLITCH_STATES_PER_CYCLE): @@ -528,30 +557,17 @@ def _glitch_jitter(zoom): # cancel out, e.g. -2 and 2, which silently zeroed out the # jitter in about half the states when this was tried before). # - # Two attempts to make this more visible were each tried and - # reverted: +-2px reopened the black border (10 corner hits - # across 130 frames, vs 0 at +-1px -- _glitch_shift's margin - # genuinely isn't big enough for +-2px in every - # zoom_position/origin combination). A filter.Hue tint on - # each state was tried next, in both orderings relative to - # _glitch_shift's Padding -- tinting the already-padded result - # tinted the shift's own blank margin too (a visible teal - # strip on whichever edge got padded, reported as "a - # gradient" on the art), and tinting first, before padding - # (to keep the margin neutral), instead exposed that the - # margin's true underlying fill is dark enough to read as a - # real black border (34 hits across 200 frames). Both - # reverted; the shift stays plain, capped at +-1px, with no - # color tint -- confirmed 0 black-corner hits at this - # configuration, which matters more here than making the - # burst louder. + # A filter.Hue tint was also tried, to make this more visible + # without a bigger (riskier) shift -- reverted, it tinted + # _glitch_shift's own blank margin along with the real art in + # both orderings tried, either as a visible color strip on + # whichever edge got padded, or (once the margin was kept + # genuinely untinted) as a real black border. Plain shift, + # no color tint -- confirmed 0 black-corner hits. shift = random.number(0, 2) - 1 # random.number requires min >= 0 shifted = render.Box(width = ART_SLOT, height = ART_SLOT, child = _glitch_shift(zoom, shift)) - for _ in range(GLITCH_HOLD_TICKS): - children.append(shifted) - rest = render.Box(width = ART_SLOT, height = ART_SLOT, child = zoom) - for _ in range(GLITCH_PAUSE_TICKS): - children.append(rest) + children.append(_glitch_hold(shifted, GLITCH_HOLD_TICKS)) + children.append(pause) return render.Sequence(children = children)