Give the glitch segment its own non-animated, full-res art
Root-caused the "big size jumps, not at the same zoom level as the art" report: the jitter embedded the live `zoom` Transformation inside a render.Sequence of separately-held states, and Sequence.Paint rebases the frame index to 0 for whichever child is currently playing (confirmed in pixlet's own source -- c.Paint(dc, bounds, frameIdx-fc)). Every time the jitter switched to a new held state (every ~100-150ms), the embedded zoom saw a near-zero frame index and reset toward the start of its own 10s cycle before snapping back on the next state. Rather than keep the jitter perfectly synced to the live zoom's current position (tried via a single Translate-keyframed Transformation instead of Sequence, which doesn't rebase frame index for its child, and does work), went with the simpler fix requested instead: the glitch segment now jitters its own separate, non-animated rendering of the art (scale locked at 1.0 throughout, via _zoom_transformation's own zoom_scale argument) rather than the live mid-cycle zoom. Same origin/zoom_position, so it doesn't jump to a different part of the image when the glitch starts or ends; the live zoom on the base layer underneath keeps running throughout regardless, and is what's left showing once the glitch window ends. Verified: renders cleanly, no black border across all 109 frames of a worst-case render (strong zoom, random position, all four corners), and payload dropped way down (121.6KB worst case, vs. the 460KB device limit) since the rest-art layer no longer depends on live zoom state, giving WebP much more to dedupe.
This commit is contained in:
1 parent
6f9b6ce838
commit
4cf6c7d506
1 file changed
+59
-43
+59
-43
@@ -296,11 +296,15 @@ def _art_widget(art_bytes, zoom_scale, zoom_position, subsonic_id, show_static):
|
||||
if not show_static:
|
||||
return 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)])
|
||||
# The glitch segment gets its own non-animated, full-resolution
|
||||
# (scale locked at 1.0) rendering of the art, rather than the live
|
||||
# `zoom` above -- simpler than keeping the jitter in sync with
|
||||
# wherever the real zoom's 10s cycle currently is (see
|
||||
# _glitch_rest_art for why that was worth avoiding). The live zoom on
|
||||
# the base `cropped` layer underneath keeps running throughout; once
|
||||
# the glitch window ends, that's what's left showing.
|
||||
rest_art = _glitch_rest_art(art_bytes, zoom_position, subsonic_id)
|
||||
return render.Stack(children = [cropped, _glitch_effect(rest_art)])
|
||||
|
||||
def _zoom_transformation(art_bytes, zoom_scale, zoom_position, subsonic_id):
|
||||
art = render.Image(src = art_bytes, width = ART_SIZE, height = ART_SIZE)
|
||||
@@ -424,6 +428,18 @@ _GLITCH_CYCLE_TICKS = GLITCH_STATES_PER_CYCLE * GLITCH_HOLD_TICKS + GLITCH_PAUSE
|
||||
_GLITCH_TICKS_PER_SECOND = 20 # 1000ms / 50ms-per-tick
|
||||
GLITCH_REPEATS = int(GLITCH_ACTIVE_SECONDS * _GLITCH_TICKS_PER_SECOND / _GLITCH_CYCLE_TICKS) + 1
|
||||
|
||||
def _glitch_rest_art(art_bytes, zoom_position, subsonic_id):
|
||||
# A separate, non-animated rendering of the art (scale locked at 1.0
|
||||
# throughout, via _zoom_transformation's own zoom_scale argument) for
|
||||
# the glitch segment to jitter, instead of reusing the live `zoom`
|
||||
# that's actually mid-cycle on the base layer underneath. Simpler than
|
||||
# keeping the jitter synced to wherever that 10s cycle currently is --
|
||||
# there's nothing here that depends on frame position, so nothing to
|
||||
# desync regardless of how the jitter's own timing is built. Same
|
||||
# origin/zoom_position as the real zoom, so it doesn't visually jump
|
||||
# to a different part of the image when the glitch starts or ends.
|
||||
return _zoom_transformation(art_bytes, 1.0, zoom_position, subsonic_id)
|
||||
|
||||
def _glitch_states_for_cycle(cycle_index):
|
||||
# Rotate through GLITCH_STATE_POOL instead of slicing (which doesn't
|
||||
# wrap around in Starlark), so each cycle's window of states can
|
||||
@@ -464,47 +480,47 @@ 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.
|
||||
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):
|
||||
pause = _glitch_hold(render.Box(width = ART_SLOT, height = ART_SLOT), GLITCH_PAUSE_TICKS)
|
||||
|
||||
sequence = []
|
||||
# Built as ONE animation.Transformation (Translate-only keyframes)
|
||||
# wrapping the real `zoom`, not a render.Sequence of separately-held
|
||||
# states (the previous approach). That turned out to be a real bug,
|
||||
# not just a tuning issue: Sequence.Paint rebases the frame index to
|
||||
# 0 for whichever child is currently playing (confirmed in pixlet's
|
||||
# own source -- `c.Paint(dc, bounds, frameIdx-fc)`), so every time the
|
||||
# jitter switched to a new held state, the *embedded* zoom saw a
|
||||
# near-zero frame index and reset toward the start of its own
|
||||
# ART_ZOOM_DURATION cycle before snapping back on the next state --
|
||||
# exactly the "big size jumps, not at the same zoom level as the art"
|
||||
# that were reported. animation.Transformation does not rebase frame
|
||||
# index for its child (`t.Child.Paint(dc, bounds, frameIdx)` -- same
|
||||
# frameIdx in and out), so nesting it around `zoom` instead keeps the
|
||||
# zoom perfectly in sync with the real art alongside it.
|
||||
total_ticks = GLITCH_REPEATS * _GLITCH_CYCLE_TICKS
|
||||
keyframes = []
|
||||
tick = 0
|
||||
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. Capped to +-1px -- a
|
||||
# full-frame shift reads very differently than a thin band
|
||||
# shifting, so it stays subtle. Randomized rather than a fixed
|
||||
# alternating +1/-1 (which read as too mechanically regular,
|
||||
# a predictable back-and-forth) or derived from shift1/shift2
|
||||
# (several pool entries have those nearly or exactly cancel
|
||||
# out, e.g. -2 and 2, which silently zeroed out the jitter in
|
||||
# about half the states).
|
||||
base_shift = random.number(0, 2) - 1 # random.number requires min >= 0
|
||||
state = render.Stack(children = [
|
||||
_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)
|
||||
for _ in range(GLITCH_STATES_PER_CYCLE):
|
||||
# Capped to +-1px -- a full-frame shift reads very differently
|
||||
# than a thin band shifting, so it stays subtle. Randomized
|
||||
# rather than a fixed alternating +1/-1 (too mechanically
|
||||
# regular) or derived from shift1/shift2 (several
|
||||
# GLITCH_STATE_POOL entries have those nearly or exactly
|
||||
# cancel out, e.g. -2 and 2, which silently zeroed out the
|
||||
# jitter in about half the states when this was tried before).
|
||||
shift = random.number(0, 2) - 1 # random.number requires min >= 0
|
||||
keyframes.append(animation.Keyframe(
|
||||
percentage = float(tick) / total_ticks,
|
||||
transforms = [animation.Translate(shift, shift)],
|
||||
))
|
||||
tick += GLITCH_HOLD_TICKS
|
||||
keyframes.append(animation.Keyframe(
|
||||
percentage = float(tick) / total_ticks,
|
||||
transforms = [animation.Translate(0, 0)],
|
||||
))
|
||||
tick += GLITCH_PAUSE_TICKS
|
||||
|
||||
return render.Sequence(children = sequence)
|
||||
jittered = animation.Transformation(child = zoom, duration = total_ticks, keyframes = keyframes)
|
||||
return render.Box(width = ART_SLOT, height = ART_SLOT, child = jittered)
|
||||
|
||||
def _glitch_scanlines():
|
||||
# render.Sequence doesn't loop (it just stops painting once its total
|
||||
|
||||