re(ui): Q1 -- the interpolation law holds, the group timeline does not

Q1's gate asks whether the ramp is linear. It is, and that result stands:
it rests on the splash's _eff glows, which reproduce exactly. This adds
the part that does not.

The test is a calibration, not a fit. Fix the clock on
palogo_gamearts_eff -- declared 15-unit fade-in 0@15 -> 255@30 against
captured alphas 34,68,102,136,170,204,238, a constant step of 34, giving
t = 2f - 171 -- then check that against the glow's own next landmark: its
declared hold ends t=45, predicted frame 108.0, observed last full-alpha
frame 107. Then apply it to palogo_gamearts in the same bundle and the
same frames, with no free parameter left:

  declared a=232 at t=206 -> frame 188.5, observed alpha 255
  declared a= 32 at t=210 -> frame 190.5, observed alpha 255

The logo is still at full alpha nine frames after it should read 32; its
fade-out runs ~17 frames late; its declared 80-frame fade-in is never
drawn. Not culling -- the same element is submitted down to a=7 on the
way out. Calibration-free version: the declared fade-out spends 12 of 16
units dropping 23/255 of the alpha, and the capture has no such plateau.

Candidate, offered and NOT adopted: if +36 held the NEXT keyframe's time,
the fade-out shape fits (RMS 4.05 vs 12.13, two elements) and the
decoder's "last block's time is unreadable" special case disappears --
the last block would simply have no successor. Rejected for now because
it explains neither the missing fade-in nor the lateness, and because the
_eff elements cannot discriminate between the readings at all (with four
blocks the shift only relabels the phases). Decoder unchanged.

Also withdrawn, mine, within the iteration: "the _eff glows hold a
constant alpha 33". They ramp 34 -> 255 in steps of 34. I printed the
series minimum and read it as its range, with a "14 distinct colours"
column sitting next to it saying otherwise.
This commit is contained in:
Sylpheed RE agent
2026-08-28 22:56:17 +00:00
parent f5097779e4
commit 8c1a5669b8
6 changed files with 208 additions and 1 deletions

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Do the splash's DECLARED keyframe ramps reproduce the captured ones?
Calibrate the animation clock on one element, then apply that calibration to a
DIFFERENT element in the same bundle and the same frames. No free parameter is
left for the second element, so it is a real test rather than a fit.
The `_eff` glows calibrate it (they are the basis of Q1's "linear, 2 units per
frame" result). The `palogo_*` logos are then checked against it.
splash_ramp_check.py docs/re/captures/ui-timing/splash-build-draws.log
"""
import csv, subprocess, sys, collections, tempfile, os
LOG = sys.argv[1] if len(sys.argv) > 1 else \
"docs/re/captures/ui-timing/splash-build-draws.log"
here = os.path.dirname(os.path.abspath(__file__))
csvf = tempfile.NamedTemporaryFile(suffix=".csv", delete=False).name
subprocess.run([sys.executable, os.path.join(here, "kf_time_probe.py"), LOG,
"--csv", csvf], check=True, capture_output=True)
series = collections.defaultdict(dict)
for r in csv.DictReader(open(csvf)):
if r["col"]:
series[(r["w"], r["h"])][int(r["frame"])] = int(r["col"][:2], 16)
EFF, LOGO = ("525", "90"), ("499", "72") # gamearts_eff, gamearts
# --- calibration, from the eff element's declared 15-unit fade-in 0@15 -> 255@30
eff = series[EFF]
ramp = [(f, a) for f, a in sorted(eff.items()) if a < 255][:7]
steps = [ramp[i + 1][1] - ramp[i][1] for i in range(len(ramp) - 1)]
print("CONTROL — the eff glow's fade-in must be linear at a constant step")
print(f" alphas {[a for _, a in ramp]} steps {steps}")
assert len(set(steps)) == 1, "eff fade-in is not a constant step; calibration void"
k_per_frame = steps[0] / (255 / 15) # alpha step -> units per frame
f0 = ramp[0][0]
t_at = lambda f: 15 + k_per_frame * (f - f0) + k_per_frame
print(f" => {k_per_frame:.3f} units/frame; t(f) = {k_per_frame:.0f}*f - "
f"{k_per_frame * f0 - 15 - k_per_frame:.0f}")
# the calibration's own check: the eff's declared hold ends at t=45
end_hold = max(f for f, a in eff.items() if a == 255)
print(f" check: declared hold ends t=45 -> predicted frame "
f"{f0 + (45 - 15) / k_per_frame - 1:.1f}; observed last full-alpha frame {end_hold}")
# --- now the logo, with no freedom left
decl = [(15, 0), (30, 0), (190, 255), (194, 255), (206, 232), (210, 32)]
logo = series[LOGO]
first, last = min(logo), max(logo)
print("\nLOGO — palogo_gamearts, checked against that calibration")
print(f" observed: first drawn frame {first} at alpha {logo[first]}; "
f"full alpha through {max(f for f,a in logo.items() if a==255)}; "
f"fade-out {first if logo[first]<255 else min(f for f,a in sorted(logo.items()) if a<255)}..{last}")
for t, a in decl:
fr = f0 + (t - 15) / k_per_frame - 1
got = logo.get(round(fr))
print(f" declared a={a:3d} at t={t:3d} -> frame {fr:6.1f} observed alpha "
f"{got if got is not None else 'NOT DRAWN'}")
print("\nSHAPE, independent of any calibration:")
span = decl[-1][0] - decl[3][0]
print(f" declared fade-out spans t={decl[3][0]}..{decl[-1][0]} ({span} units); of that,")
print(f" {decl[4][0]-decl[3][0]}/{span} units drop only {decl[3][1]-decl[4][1]}/255 of the alpha (a near-flat leg),")
print(f" {decl[5][0]-decl[4][0]}/{span} units drop {decl[4][1]-decl[5][1]}/255 (a cliff).")
fo = [(f, a) for f, a in sorted(logo.items()) if f >= 198]
print(f" captured fade-out: {[a for _, a in fo]}")
d = [fo[i][1] - fo[i+1][1] for i in range(len(fo)-1)]
print(f" per-frame drops: {d} -> no near-flat leg")
os.unlink(csvf)