#!/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)