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.
This commit is contained in:
poprhythm committed 2026-10-09 16:57:53 -04:00
1 parent 60feeaec2c
commit 385e20785c
1 file changed
+48 -32
+48 -32
View File
@@ -495,19 +495,46 @@ def _glitch_base_jitter(zoom, shift_x):
# crop, so it reads as the full image nudging rather than a band. # 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)) 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): def _glitch_jitter(zoom):
# Built as a render.Sequence of discrete shifted states, each using # Built as a render.Sequence of discrete shifted states, each using
# _glitch_shift (Padding-based), not animation.Transformation + # _glitch_shift (Padding-based), not animation.Transformation +
# Translate (the previous approach). That seemed reasonable but # Translate (an approach tried in between). Translate seemed
# produced a real, confirmed-on-device black border that no amount of # reasonable but produced a real, confirmed-on-device black border
# scale adjustment on `zoom` could fix -- root-caused via isolated, # that no amount of scale adjustment on `zoom` could fix --
# pixel-sampled test renders to a more fundamental pixlet limitation: # root-caused via isolated, pixel-sampled test renders to a more
# Transformation + Translate can't reveal content beyond what's # fundamental pixlet limitation: Transformation + Translate can't
# visible at rest, it only moves already-visible pixels around. # reveal content beyond what's visible at rest, it only moves
# Padding genuinely grows the declared canvas instead, which is # already-visible pixels around. Padding genuinely grows the declared
# exactly the mechanism _glitch_shift already uses and had already # canvas instead, which is exactly the mechanism _glitch_shift
# validated as safe ("no black border at +-2px, at both rest and peak # already uses.
# zoom") before this ever used Translate.
# #
# A much earlier version of this file *did* use render.Sequence for # 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 # 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: # frame-dependent zoom that version wrapped. That no longer applies:
# `zoom` here is _glitch_rest_art's static, non-animated image, so # `zoom` here is _glitch_rest_art's static, non-animated image, so
# there's nothing frame-dependent left for Sequence to desync. # 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 = [] children = []
for cycle_index in range(GLITCH_REPEATS): for cycle_index in range(GLITCH_REPEATS):
for _ in range(GLITCH_STATES_PER_CYCLE): 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 # cancel out, e.g. -2 and 2, which silently zeroed out the
# jitter in about half the states when this was tried before). # jitter in about half the states when this was tried before).
# #
# Two attempts to make this more visible were each tried and # A filter.Hue tint was also tried, to make this more visible
# reverted: +-2px reopened the black border (10 corner hits # without a bigger (riskier) shift -- reverted, it tinted
# across 130 frames, vs 0 at +-1px -- _glitch_shift's margin # _glitch_shift's own blank margin along with the real art in
# genuinely isn't big enough for +-2px in every # both orderings tried, either as a visible color strip on
# zoom_position/origin combination). A filter.Hue tint on # whichever edge got padded, or (once the margin was kept
# each state was tried next, in both orderings relative to # genuinely untinted) as a real black border. Plain shift,
# _glitch_shift's Padding -- tinting the already-padded result # no color tint -- confirmed 0 black-corner hits.
# 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.
shift = random.number(0, 2) - 1 # random.number requires min >= 0 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)) shifted = render.Box(width = ART_SLOT, height = ART_SLOT, child = _glitch_shift(zoom, shift))
for _ in range(GLITCH_HOLD_TICKS): children.append(_glitch_hold(shifted, GLITCH_HOLD_TICKS))
children.append(shifted) children.append(pause)
rest = render.Box(width = ART_SLOT, height = ART_SLOT, child = zoom)
for _ in range(GLITCH_PAUSE_TICKS):
children.append(rest)
return render.Sequence(children = children) return render.Sequence(children = children)
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.