Answers the port's ask: ptbtneff01 is ANIMATED while a button is focused, not drawn once and held. The existing page said 'the ring SPINS' from one frame at a large angle, which is equally consistent with a static draw at a fixed angle. No angle is quoted anywhere. The 360-bin angular estimator written for this FAILED its own control -- a synthetic 30 deg came back as 0 deg (peak 0.596) while 90/180/270 came back exactly -- so it was not used. What settles it needs no angle: total annulus brightness is conserved to 0.4 % while individual angular bins swing by 24, i.e. brightness moving AROUND the ring, which excludes a pulse. The temporal-std map is a clean annulus, falling to ~1 both inside and outside the stroke, which excludes positional jitter. Period from the profile's autocorrelation: eight evenly spaced peaks, mean 2.177 s over nine revolutions. Even spacing is the internal check a drifting instrument cannot pass. That is 120 units = 60 frames = 2.00 s at a true 30 Hz. Also measured, same run: the ring is the ONLY moving thing on the settled main menu -- temporal std is exactly 0.000 on every unfocused button, the labels and the footer. And the ring's centre, located from the std map at game (520.7, 339.7), matches the declared leaf offset's prediction of (521, 340). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNR5Y79D1T4bBr6gJQaWFP
120 lines
5.8 KiB
Markdown
120 lines
5.8 KiB
Markdown
# ✅ The main menu's focus ring spins continuously — period **2.18 s**, measured
|
||
|
||
**Status:** ✅ **measured** (not on the disc as a period; the disc declares the
|
||
ramp, the running game supplies the rate). Taken 2026-08-29 against Xenia Canary
|
||
with the disc mounted at `/disc`.
|
||
|
||
**Question this closes:** the port asked whether `ptbtneff01` — the 42×46 ring on
|
||
the focused button — is *animated* while a button sits focused, or drawn once and
|
||
held. It had shipped the ring at 0° and marked that known-wrong.
|
||
[`structures/ui-button-focus-record.md`](structures/ui-button-focus-record.md)
|
||
already said "the ring SPINS" from **one** frame showing it at a large angle;
|
||
that is consistent with a continuous spin *and* with a static draw at a fixed
|
||
angle, so it did not answer the question asked.
|
||
|
||
## What the ring actually does
|
||
|
||
Five single frames from one run, 4 s apart, focus held on `TUTORIAL` throughout:
|
||
|
||

|
||
|
||
The ring carries a bright head, and the head is at a different angular position
|
||
in every frame. It is still moving 16 s in, so it does **not** ramp once and
|
||
stop.
|
||
|
||
⚠️ **The 20 s mean of the same run is a uniform circle**
|
||
([`ring-20s-mean-uniform.png`](captures/focus-ring/ring-20s-mean-uniform.png)) —
|
||
that is the spin smearing itself out, and it is why an averaged frame must never
|
||
be read as a single frame. A human looking at the live game sees the head; the
|
||
average does not have one.
|
||
|
||
## The measurement, and why it is not an angle
|
||
|
||
🔴 **No angle is estimated anywhere.** The corpus's centroid estimator fails its
|
||
own control by up to 19.8°, and a 360-bin angular cross-correlation written for
|
||
this measurement **also failed its control** — a synthetic 30° rotation of a live
|
||
frame came back as 0° (peak 0.596), while 90/180/270° came back exactly (peak
|
||
1.000), i.e. the estimator only resolves the exact pixel permutations. It was
|
||
therefore not used.
|
||
|
||
What was used needs no angle. Two observables separate *rotation* from a
|
||
*brightness pulse*, and both were taken in the same run:
|
||
|
||
| observable | rotation predicts | pulse predicts | **measured** |
|
||
|---|---|---|---|
|
||
| total annulus brightness | conserved | varies | **0.4 % spread over 16 s** (5 frames); **0.53 %** over 359 frames |
|
||
| per-angular-bin brightness | varies (a travelling feature) | varies together | **per-bin sd 24.2**, max 80.3, against a per-frame angular sd of 42.2 |
|
||
|
||
Brightness moves *around* the annulus while the total holds. A pulse is excluded.
|
||
|
||
The temporal standard deviation over 103 frames is **an annulus** and nothing
|
||
else — dark inside, dark outside, peaking exactly on the ring's stroke
|
||
(radial std: r 0–4 → 1.08, r 10–13 → **36.75**, r 20–26 → 1.18):
|
||
|
||

|
||
|
||
⚠️ A positional *jitter* would smear variation outside the stroke. It does not:
|
||
variation falls to ~1 both inside and outside, so the ring is not moving, it is
|
||
turning.
|
||
|
||
### The period
|
||
|
||
A dense 359-frame filmstrip (24 s at **15.03 fps against a requested 15 fps** —
|
||
the consumer kept up exactly, so these timestamps are not backlogged) gives the
|
||
annulus's 360-bin profile per frame, correlated against frame 0. A rotating ring
|
||
returns to itself once per revolution, so the trace's period **is** the spin
|
||
period — again with no angle estimated.
|
||
|
||
Autocorrelation local maxima, in seconds:
|
||
|
||
```
|
||
2.18 4.36 6.52 8.70 10.86 13.02 15.22 17.42
|
||
spacings: 2.18 2.16 2.18 2.16 2.16 2.20 2.20 mean 2.177 s
|
||
```
|
||
|
||
**Eight consecutive evenly-spaced peaks over nine revolutions.** A drifting
|
||
instrument cannot produce even spacing, which is the internal check on the
|
||
number.
|
||
|
||
Raw trace committed at [`data/focus-ring-period-corr.npy`](data/focus-ring-period-corr.npy)
|
||
(rows: t, correlation-with-frame-0, annulus mean).
|
||
|
||
### What the period is in the game's own units
|
||
|
||
⚠️ **2.18 s is wall-clock under this emulator, and the emulator is not running
|
||
the game at 30 Hz.** The corpus measures 27.6–28.8 fps here. `ptbtneff01`
|
||
declares its first keyframe at **t = 120**, and under the settled reading
|
||
(1 unit = 1/60 s, 2 units per rendered frame) 120 units is **60 rendered
|
||
frames** — which at 27.6–28.8 fps spans **2.08–2.17 s**. The measurement sits at
|
||
the top of that band.
|
||
|
||
**So the spin is one revolution per 120 units = 60 frames = 2.00 s at a true
|
||
30 Hz**, and no new constant is needed to account for it. 🟡 The 2.18 s is
|
||
consistent with the declared 120 rather than a re-derivation of it: the guest
|
||
frame rate was not measured in this same run, so the agreement is
|
||
consistency, not closure.
|
||
|
||
## Two other things the same run measured
|
||
|
||
* ✅ **The focus ring is the ONLY moving thing on the settled main menu.** Over
|
||
103 frames / 20 s untouched, temporal std is **exactly 0.000** on every
|
||
unfocused button box, on the `NEW GAME` label, and on the `ptmsg` footer. Only
|
||
the focused button's box moves (std 4.46 against a background noise floor of
|
||
0.906). A port that draws the main menu statically plus a spinning ring is
|
||
drawing everything that moves.
|
||
* ✅ **The ring is `ptbtneff01`, positionally confirmed.** Its centre was located
|
||
from the temporal-std map at game **(520.7, 339.7)**. The declared leaf offset
|
||
applied to button 3's rest position (542, 322) predicts **(521, 340)**. That
|
||
is a sub-pixel agreement between a decoded declaration and a live measurement,
|
||
and it is what ties the annulus to the record rather than to "a circle near the
|
||
cursor".
|
||
|
||
## Reach
|
||
|
||
* One run, one emulator, English locale, `GP_TITLE` build 5.
|
||
* The period is measured on **one** focused button (`OPTIONS`, button 4) and the
|
||
spin is shown on a second (`TUTORIAL`, button 3). Not checked on all five, and
|
||
not checked on `EXTRAS`.
|
||
* Says nothing about the direction of rotation — the estimator that would give a
|
||
signed angle failed its control and was not used.
|