formats: the resting pose is the hold, not the longest dwell
Element::rest() picked the keyframe with the largest gap to the next keyframe's
time. That reads a keyframe as a value held until the next one; it is the start
of a ramp toward it. A long gap after keyframe k means the screen spends that
time arriving at k+1, so the settled pose is at the far end of the gap.
The title wordmark zooms in over five frames and holds at (184,193) at 100% from
t=251 to t=264. The old rule picked the frame before the long gap: (179,186) at
101%, still mid-zoom.
Measured against the framebuffer capture of the running title screen, which is a
1:1 crop so frame coordinates map directly (confirmed: the copyright line lands
on row 669 in the capture and in both composites). Edge-correlated over the
wordmark box:
plateau (landed) best 0.4597 at shift (0,0)
longest dwell (old) best 0.1511 at shift (+3,+8), 0.1268 at (0,0)
The old composite scores 3x lower and only peaks after being moved, by about the
(-5,-7) that picking kf4 instead of kf5 predicts.
It also fixes six title elements the old rule rested at alpha 0x00 where the
capture plainly shows them, and pteff00.prm - the full-screen fade quad painted
last - which rested at opaque black. That was the blocker on .prm compositing.
Adds tools/re-capture/align_to_capture.py, which is how this was scored, and
turns the .prm test that deliberately asserted the old defect into a guard on
the fix.
Not settled and now the next item: compose ignores the keyframe fade alpha
entirely (blit modulates by tint only), which is why choosing the wrong keyframe
was invisible until now.
This commit is contained in:
103
docs/re/structures/ui-resting-pose.md
Normal file
103
docs/re/structures/ui-resting-pose.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# A keyframe is the start of a ramp, not a pose that is held
|
||||
|
||||
**Status:** ✅ `CONFIRMED` against the framebuffer capture of the running title
|
||||
screen — the new rule aligns at **zero shift**, the old one had to be moved.
|
||||
🟡 the fallback for groups that never hold is unverified. ❔ interpolation
|
||||
between keyframes is still not implemented, only the resting pose.
|
||||
|
||||
## The rule that was wrong
|
||||
|
||||
`Element::rest()` answers "where is this element when the screen is just sitting
|
||||
there", and every composite the port draws depends on it. It used to pick the
|
||||
keyframe with the **largest gap to the next keyframe's time** — the frame that
|
||||
"dwells longest".
|
||||
|
||||
That reads a keyframe as a value held until the next one. It is not: a keyframe
|
||||
is the **start of a ramp toward the next one**. So a long gap after keyframe *k*
|
||||
means the screen spends that whole time *arriving at* `k+1` — the settled pose is
|
||||
at the **far** end of the gap, not the near one.
|
||||
|
||||
The title wordmark makes it concrete. `ptlogo1.t32` zooms in from off-screen:
|
||||
|
||||
```
|
||||
kf0 150% (-116, -7) a=0x00 t=26 off-screen, invisible
|
||||
kf1 150% (-116, -7) a=0x00 t=34
|
||||
kf2 112% ( 109, 123) a=0x80 t=38
|
||||
kf3 103% ( 165, 171) a=0xc0 t=40
|
||||
kf4 101% ( 179, 186) a=0xe0 t=42 ← old rule picked this
|
||||
kf5 100% ( 184, 193) a=0xff t=251 ← the settled pose
|
||||
kf6 100% ( 184, 193) a=0xff t=264 ← held here
|
||||
kf7 100% ( 184, 193) a=0x00 t=None fades out
|
||||
```
|
||||
|
||||
The gap 42 → 251 is by far the largest, so the old rule picked **kf4** — 1 % too
|
||||
large and 5 px up-left, a frame from mid-zoom. The pose the screen actually holds
|
||||
is kf5–kf6.
|
||||
|
||||
## The rule that is right
|
||||
|
||||
**The resting pose is the hold: the longest run of consecutive keyframes with an
|
||||
identical pose.** Ties go to the later run, matching the in → hold → out shape.
|
||||
Groups that ramp through every frame and never hold fall back to longest-dwell.
|
||||
|
||||
## The measurement
|
||||
|
||||
`docs/re/captures/title-screen-oracle.png` is a framebuffer capture of the
|
||||
running title screen. It is a **1:1 crop** of the 1280×720 frame (1279×675) —
|
||||
verified by the copyright line landing on row 669 in the capture and in both
|
||||
composites — so frame coordinates map directly and a shift is meaningful.
|
||||
|
||||
Edge-correlated over the wordmark box (x 150–1150, y 200–400) with
|
||||
`tools/re-capture/align_to_capture.py`. Gradient magnitude, not colour: the
|
||||
capture's planet is mid-explosion and orange while ours is blue, and the wordmark
|
||||
materials are undecoded, so a pixel diff would measure everything except the
|
||||
question being asked.
|
||||
|
||||
| resting rule | best correlation | at shift | at (0,0) |
|
||||
|---|---|---|---|
|
||||
| **plateau** (landed) | **0.4597** | **(0, 0)** | 0.4597 |
|
||||
| longest dwell (old) | 0.1511 | (+3, +8) | 0.1268 |
|
||||
|
||||
The old composite peaks 3× lower **and only after being moved** — displaced by
|
||||
about the (−5,−7) that kf4-instead-of-kf5 predicts. The new one is already where
|
||||
the game puts it.
|
||||
|
||||
* [composited with the plateau rule](../captures/ui-layout/title-composited-plateau-rest.png)
|
||||
* [composited with the old longest-dwell rule](../captures/ui-layout/title-composited-longest-dwell-rest.png)
|
||||
|
||||
## What else it fixed, on the same screen
|
||||
|
||||
The old rule systematically picked the **invisible** end of a fade-in. On the
|
||||
title screen it rested these at alpha `0x00`, where the capture plainly shows
|
||||
them:
|
||||
|
||||
`ptlogo_tm` (the ™), `ptcopyright`, `ptlogo_back2`, `ptlogo_back2eff`,
|
||||
`ptloop01`/`ptloop02`.
|
||||
|
||||
And `pteff00.prm` — the full-screen fade quad that paints **last** — rested at
|
||||
**opaque black**. That is the whole screen, and it is why `.prm` compositing was
|
||||
blocked ([`ui-prm-primitives.md`](ui-prm-primitives.md)).
|
||||
|
||||
None of that was visible before, because `compose` never applied the `fade`
|
||||
alpha at all: `blit` modulates by `tint` only, and `tint` is `0xffffffff` on
|
||||
essentially every keyframe. The wrong keyframes were being chosen and then their
|
||||
one distinguishing field was ignored. That is worth stating as its own finding —
|
||||
see below.
|
||||
|
||||
## What is not settled
|
||||
|
||||
* ❔ **`compose` ignores the keyframe `fade` alpha entirely.** `blit` uses
|
||||
`tint`. Applying `fade` is the obvious next step and is what makes the resting
|
||||
pose visible rather than academic — but it changes every screen and needs its
|
||||
own A/B against the capture. It is also the prerequisite for drawing `.prm`
|
||||
quads, whose entire content is that alpha.
|
||||
* 🟡 **The fallback.** Groups with no two adjacent keyframes alike still use
|
||||
longest-dwell. How many there are, and whether the correct answer for them is
|
||||
the *last* keyframe instead, is unmeasured.
|
||||
* ❔ **Interpolation.** Only the resting pose is decoded; nothing tweens. A
|
||||
viewer that animates these screens needs the ramp, and whether it is linear is
|
||||
unknown.
|
||||
* 🟡 One-shot flashes (`ptlogoall_eff`, `ptlogoall_eff2`) ramp 0 → 0x80 → 0x4b →
|
||||
0 and never hold at a visible value, so the plateau rule rests them at alpha 0
|
||||
— invisible. That is *probably* right for a settled screen, but the capture
|
||||
cannot confirm it while `fade` is unapplied.
|
||||
Reference in New Issue
Block a user