Fix jitter black border by rebuilding on Padding instead of Translate
Re-enables the base jitter (disabled two commits ago while isolating the
scanline work) with the actual fix, not another scale tweak -- two
earlier attempts to fix the black border by bumping _glitch_rest_art's
scale (breathing 1.15, then constant 1.15) both made it *worse* (680 and
532 black-corner hits across 200/133 frames, vs. 372 at a literal 1.0),
which was the tell that scale was never the real problem.
Root cause, confirmed via isolated pixel-sampled test renders: pixlet's
animation.Transformation + Translate can't reveal content beyond what's
visible at rest -- it only moves already-visible pixels, it doesn't pull
in new ones -- so no scale on the wrapped content could have fixed a
Translate-based shift. render.Padding, used elsewhere in this file
specifically because it reliably *grows* a widget's declared canvas
(scanlines, the zoom's own ART_SIZE vs ART_SLOT margin), doesn't have
that limitation. _glitch_shift already existed using exactly that
Padding approach and had already been validated safe ("no black border
at +-2px, at both rest and peak zoom") -- it just wasn't what the
current jitter was built on.
Rebuilt _glitch_jitter as a render.Sequence of discrete states built
from _glitch_shift, instead of one animation.Transformation with
Translate keyframes. Sequence was deliberately avoided earlier in this
app's history (it rebases frame index per state, confirmed in pixlet's
own source, which previously desynced a *live*, frame-dependent zoom
nested inside it) -- but that concern doesn't apply here: _glitch_rest_art
(reverted to a plain locked-1.0 scale, since scale was never the issue)
is static and frame-independent, so there's nothing left to desync.
Verified: 0 black-corner hits across 199 frames (was ~370-680 under
every earlier attempt), and payload is now far smaller too (54KB
worst-case at strong zoom + random position, vs. 451KB before) --
Sequence's frame-level deduplication compresses much better than the
continuously-interpolating Transformation did.
This commit is contained in:
1 parent
927a23e557
commit
7d748361ab
1 file changed
+41
-62
+41
-62
@@ -438,38 +438,17 @@ def _glitch_rest_art(art_bytes, zoom_position, subsonic_id):
|
|||||||
# origin/zoom_position as the real zoom, so it doesn't visually jump
|
# 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.
|
# to a different part of the image when the glitch starts or ends.
|
||||||
#
|
#
|
||||||
# Deliberately does NOT call _zoom_transformation(art_bytes, 1.15,
|
# Scale is locked at 1.0 (via _zoom_transformation's own top_scale =
|
||||||
# ...): that function always *breathes* between 1.0 and whatever
|
# max(zoom_scale, 1.0), so passing 1.0 forces a constant Scale(1.0,
|
||||||
# scale is passed in (it's the same "Ken Burns" animation the live
|
# 1.0) with no breathing at all) -- this is exactly the "rest" state
|
||||||
# zoom uses, over its own ART_ZOOM_DURATION cycle), so passing 1.15
|
# _glitch_shift's own margin was already validated against (see its
|
||||||
# still spends part of every cycle back down near the unsafe 1.0
|
# comment below). A black border was seen here earlier, but it traced
|
||||||
# floor -- confirmed empirically, it made the black border *worse*
|
# to the *shift* mechanism (animation.Transformation + Translate,
|
||||||
# (680 black-corner hits across 200 frames, vs. 372 at a literal
|
# since replaced -- see _glitch_jitter), not to this scale; two
|
||||||
# 1.0). What's needed here is a scale that's constantly 1.15, never
|
# different attempts to fix it by bumping the scale (1.15 breathing,
|
||||||
# breathing back down to 1.0, which this builds directly rather than
|
# then a constant 1.15) each made the border worse, not better, which
|
||||||
# reusing that function.
|
# was the tell that scale was never the real problem.
|
||||||
#
|
return _zoom_transformation(art_bytes, 1.0, zoom_position, subsonic_id)
|
||||||
# At exactly scale=1.0, the outer Box auto-fits the oversized art to
|
|
||||||
# exactly ART_SLOT with no overflow left over (see _zoom_transformation's
|
|
||||||
# own comment for that behavior), leaving zero spare margin for the
|
|
||||||
# jitter's shift to reveal and producing a real, on-device black
|
|
||||||
# border (confirmed: every corner checked across 133 frames of a real
|
|
||||||
# render, not an isolated edge case). A constant 1.15 gives enough
|
|
||||||
# overflow to safely cover the jitter's +-1px shift while staying
|
|
||||||
# close enough to 1.0 to still read as "the full picture," not a
|
|
||||||
# noticeable zoom.
|
|
||||||
art = render.Image(src = art_bytes, width = ART_SIZE, height = ART_SIZE)
|
|
||||||
origin_x, origin_y = _zoom_spot(zoom_position, subsonic_id)
|
|
||||||
static_scale = animation.Transformation(
|
|
||||||
child = art,
|
|
||||||
duration = 1,
|
|
||||||
origin = animation.Origin(origin_x, origin_y),
|
|
||||||
keyframes = [
|
|
||||||
animation.Keyframe(percentage = 0.0, transforms = [animation.Scale(1.15, 1.15)]),
|
|
||||||
animation.Keyframe(percentage = 1.0, transforms = [animation.Scale(1.15, 1.15)]),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return static_scale
|
|
||||||
|
|
||||||
def _glitch_states_for_cycle(cycle_index):
|
def _glitch_states_for_cycle(cycle_index):
|
||||||
# Rotate through GLITCH_STATE_POOL instead of slicing (which doesn't
|
# Rotate through GLITCH_STATE_POOL instead of slicing (which doesn't
|
||||||
@@ -512,23 +491,28 @@ def _glitch_base_jitter(zoom, shift_x):
|
|||||||
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_jitter(zoom):
|
def _glitch_jitter(zoom):
|
||||||
# Built as ONE animation.Transformation (Translate-only keyframes)
|
# Built as a render.Sequence of discrete shifted states, each using
|
||||||
# wrapping the real `zoom`, not a render.Sequence of separately-held
|
# _glitch_shift (Padding-based), not animation.Transformation +
|
||||||
# states (the previous approach). That turned out to be a real bug,
|
# Translate (the previous approach). That seemed reasonable but
|
||||||
# not just a tuning issue: Sequence.Paint rebases the frame index to
|
# produced a real, confirmed-on-device black border that no amount of
|
||||||
# 0 for whichever child is currently playing (confirmed in pixlet's
|
# scale adjustment on `zoom` could fix -- root-caused via isolated,
|
||||||
# own source -- `c.Paint(dc, bounds, frameIdx-fc)`), so every time the
|
# pixel-sampled test renders to a more fundamental pixlet limitation:
|
||||||
# jitter switched to a new held state, the *embedded* zoom saw a
|
# Transformation + Translate can't reveal content beyond what's
|
||||||
# near-zero frame index and reset toward the start of its own
|
# visible at rest, it only moves already-visible pixels around.
|
||||||
# ART_ZOOM_DURATION cycle before snapping back on the next state --
|
# Padding genuinely grows the declared canvas instead, which is
|
||||||
# exactly the "big size jumps, not at the same zoom level as the art"
|
# exactly the mechanism _glitch_shift already uses and had already
|
||||||
# that were reported. animation.Transformation does not rebase frame
|
# validated as safe ("no black border at +-2px, at both rest and peak
|
||||||
# index for its child (`t.Child.Paint(dc, bounds, frameIdx)` -- same
|
# zoom") before this ever used Translate.
|
||||||
# frameIdx in and out), so nesting it around `zoom` instead keeps the
|
#
|
||||||
# zoom perfectly in sync with the real art alongside it.
|
# A much earlier version of this file *did* use render.Sequence for
|
||||||
total_ticks = GLITCH_REPEATS * _GLITCH_CYCLE_TICKS
|
# the jitter and was abandoned for a real bug -- but that bug was
|
||||||
keyframes = []
|
# Sequence.Paint rebasing the frame index to 0 for whichever child is
|
||||||
tick = 0
|
# currently playing (confirmed in pixlet's own source --
|
||||||
|
# `c.Paint(dc, bounds, frameIdx-fc)`), which desynced the *live*,
|
||||||
|
# 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.
|
||||||
|
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):
|
||||||
# Capped to +-1px -- a full-frame shift reads very differently
|
# Capped to +-1px -- a full-frame shift reads very differently
|
||||||
@@ -539,19 +523,14 @@ 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).
|
||||||
shift = random.number(0, 2) - 1 # random.number requires min >= 0
|
shift = random.number(0, 2) - 1 # random.number requires min >= 0
|
||||||
keyframes.append(animation.Keyframe(
|
shifted = render.Box(width = ART_SLOT, height = ART_SLOT, child = _glitch_shift(zoom, shift))
|
||||||
percentage = float(tick) / total_ticks,
|
for _ in range(GLITCH_HOLD_TICKS):
|
||||||
transforms = [animation.Translate(shift, 0)],
|
children.append(shifted)
|
||||||
))
|
rest = render.Box(width = ART_SLOT, height = ART_SLOT, child = zoom)
|
||||||
tick += GLITCH_HOLD_TICKS
|
for _ in range(GLITCH_PAUSE_TICKS):
|
||||||
keyframes.append(animation.Keyframe(
|
children.append(rest)
|
||||||
percentage = float(tick) / total_ticks,
|
|
||||||
transforms = [animation.Translate(0, 0)],
|
|
||||||
))
|
|
||||||
tick += GLITCH_PAUSE_TICKS
|
|
||||||
|
|
||||||
jittered = animation.Transformation(child = zoom, duration = total_ticks, keyframes = keyframes)
|
return render.Sequence(children = children)
|
||||||
return render.Box(width = ART_SLOT, height = ART_SLOT, child = jittered)
|
|
||||||
|
|
||||||
# Line count and spacing are derived from each other, not picked
|
# Line count and spacing are derived from each other, not picked
|
||||||
# independently: the small-sawtooth sweep below only loops seamlessly
|
# independently: the small-sawtooth sweep below only loops seamlessly
|
||||||
@@ -682,7 +661,7 @@ def _glitch_effect(zoom):
|
|||||||
width = ART_SLOT,
|
width = ART_SLOT,
|
||||||
height = ART_SLOT,
|
height = ART_SLOT,
|
||||||
child = render.Stack(children = [
|
child = render.Stack(children = [
|
||||||
# _glitch_jitter(zoom), # disabled -- isolating scanlines for now
|
_glitch_jitter(zoom),
|
||||||
_glitch_scanlines(),
|
_glitch_scanlines(),
|
||||||
_glitch_corner_ticks("#00000080"),
|
_glitch_corner_ticks("#00000080"),
|
||||||
]),
|
]),
|
||||||
|
|||||||