Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .agents/skills/cut-the-curve/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ Mechanics:

- **Mirrored eases:** exit `power4.in` + entry `power4.out`, same distance and duration —
the two halves of one `power4.inOut`, so velocity matches exactly at the cut.
`seam-gate` measures something slightly different: the average velocity over ~0.1s of
whole frames on each side of the cut. Mirrored halves read ≈1.0 there only when the cut
sits on the frame grid (a multiple of 1/fps).
- **The fade trick:** exit opacity completes at ~25–30% of its travel (fade ≈ 0.18–0.3s
vs motion 0.3–0.34s); entry ignites at ~0.35 opacity mid-path. Time the last fading
element to die right at the cut — a gap where nothing moves reads as dead air.
Expand Down
13 changes: 12 additions & 1 deletion .agents/skills/motion-doctrine/references/seam-gate.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ Lives at the project root. One row per seam; this is the vector ledger as data.
}
```

- `cut` — seconds on the master clock, the frame the incoming side ignites.
- `cut` — seconds on the master clock, the frame the incoming side ignites. Keep it on
the frame grid (a multiple of 1/fps, with the ledger `fps` matching the composition):
the player floors every seek to a frame, so an off-grid cut is measured around the
nearest frame and reported as `cut-grid`.
- `type` — `"cut"` (default; full vector checks), `"match-cut"` / `"morph"`
(carrier-continuity + overlap only; motion may start AT the boundary).
- `axis` — `"x"`, `"y"`, or `"z"` (z = scale). `dir` — sign of motion:
Expand All @@ -88,10 +91,18 @@ Lives at the project root. One row per seam; this is the vector ledger as data.
| `zero-overlap` | rule 6 — one side visible per frame, never both |
| `z-sign-scan` | rule 7 — incoming scene's own entrances don't fight the Z sign |
| `carrier-*` | rules 3/4 — carrier rect continuity, ancestor scale included |
| `cut-grid` (WARN) | `cut` sits between frames; checks ran around the nearest frame |

Velocities are measured on `getBoundingClientRect` (center for x/y, width-ratio for z),
so ancestor wrapper transforms are automatically included.

`speed-match` compares **average** velocities over whole frames, not the instantaneous
velocity at the cut: exit from cut−0.1s to cut−1f, entry from cut+1f to cut+0.1s. The two
windows mirror each other, so a mirrored ease pair (same ease family, distance and
duration) reads ≈1.0. The stamped defaults (x/y `power3.in` 12% · 0.34s →
`power4.out` 10% · 0.42s; z 0.21s → `expo.out` 0.5s) read ≈0.8 on x/y and ≈1.1 on z.
Lengthening an entry lowers its ratio.

`__player.seek()` resolves asynchronously, so the gate awaits each seek before it reads. If every
seam fails the same way (`exit-visible op 0.00`, `exit-moving 0 px/s`) and `probe` finds no movers,
the reads landed before the seek did and the gate measured t=0, not the film.
24 changes: 18 additions & 6 deletions .agents/skills/motion-doctrine/scripts/seam-gate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,7 @@ const fmtV = (v, axis) =>
async function verify() {
const ledgerPath = flag("ledger", "ledger.json");
const ledger = JSON.parse(readFileSync(ledgerPath, "utf8"));
const fps = ledger.fps || FPS,
dt = 1 / fps;
const fps = ledger.fps || FPS;
const compUrl = await ensureServer();
const { evalJs } = await openPage(compUrl);
const results = [];
Expand All @@ -377,10 +376,17 @@ async function verify() {
const add = (check, status, detail) => rows.push({ check, status, detail });
const cut = seam.cut;
const type = seam.type || "cut";
const tA1 = Math.max(0, cut - 0.1),
tA2 = Math.max(0, cut - dt);
const tB1 = cut + dt,
tB2 = cut + 0.1;
// The player floors every seek to the frame grid, so sampling at cut ± k/fps for an
// off-grid cut shifts both exit samples up to a frame early and the first entry sample
// onto the cut itself, under-reading exit and over-reading entry. Sample whole frames
// placed symmetrically around the nearest frame instead.
const cutFrame = Math.round(cut * fps);
const offGrid = Math.abs(cut * fps - cutFrame) > 1e-3;
const win = Math.max(1, Math.round(0.1 * fps));
const tA1 = Math.max(0, cutFrame - win) / fps,
tA2 = Math.max(0, cutFrame - 1) / fps;
const tB1 = (cutFrame + 1) / fps,
tB2 = (cutFrame + win) / fps;
const sels = [
seam.exit?.selector,
seam.entry?.selector,
Expand All @@ -390,6 +396,12 @@ async function verify() {
const S = {};
for (const t of [tA1, tA2, tB1, tB2])
S[t] = await evalJs(`__seamGate.sample(${t}, ${JSON.stringify([...new Set(sels)])})`, true);
if (offGrid)
add(
"cut-grid",
"WARN",
`cut ${cut}s is between frames at ${fps} fps; measured around ${(cutFrame / fps).toFixed(3)}s — put cuts on the frame grid`,
);

if (type === "cut") {
const ex = seam.exit,
Expand Down
3 changes: 3 additions & 0 deletions .claude/skills/cut-the-curve/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ Mechanics:

- **Mirrored eases:** exit `power4.in` + entry `power4.out`, same distance and duration —
the two halves of one `power4.inOut`, so velocity matches exactly at the cut.
`seam-gate` measures something slightly different: the average velocity over ~0.1s of
whole frames on each side of the cut. Mirrored halves read ≈1.0 there only when the cut
sits on the frame grid (a multiple of 1/fps).
- **The fade trick:** exit opacity completes at ~25–30% of its travel (fade ≈ 0.18–0.3s
vs motion 0.3–0.34s); entry ignites at ~0.35 opacity mid-path. Time the last fading
element to die right at the cut — a gap where nothing moves reads as dead air.
Expand Down
13 changes: 12 additions & 1 deletion .claude/skills/motion-doctrine/references/seam-gate.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ Lives at the project root. One row per seam; this is the vector ledger as data.
}
```

- `cut` — seconds on the master clock, the frame the incoming side ignites.
- `cut` — seconds on the master clock, the frame the incoming side ignites. Keep it on
the frame grid (a multiple of 1/fps, with the ledger `fps` matching the composition):
the player floors every seek to a frame, so an off-grid cut is measured around the
nearest frame and reported as `cut-grid`.
- `type` — `"cut"` (default; full vector checks), `"match-cut"` / `"morph"`
(carrier-continuity + overlap only; motion may start AT the boundary).
- `axis` — `"x"`, `"y"`, or `"z"` (z = scale). `dir` — sign of motion:
Expand All @@ -88,10 +91,18 @@ Lives at the project root. One row per seam; this is the vector ledger as data.
| `zero-overlap` | rule 6 — one side visible per frame, never both |
| `z-sign-scan` | rule 7 — incoming scene's own entrances don't fight the Z sign |
| `carrier-*` | rules 3/4 — carrier rect continuity, ancestor scale included |
| `cut-grid` (WARN) | `cut` sits between frames; checks ran around the nearest frame |

Velocities are measured on `getBoundingClientRect` (center for x/y, width-ratio for z),
so ancestor wrapper transforms are automatically included.

`speed-match` compares **average** velocities over whole frames, not the instantaneous
velocity at the cut: exit from cut−0.1s to cut−1f, entry from cut+1f to cut+0.1s. The two
windows mirror each other, so a mirrored ease pair (same ease family, distance and
duration) reads ≈1.0. The stamped defaults (x/y `power3.in` 12% · 0.34s →
`power4.out` 10% · 0.42s; z 0.21s → `expo.out` 0.5s) read ≈0.8 on x/y and ≈1.1 on z.
Lengthening an entry lowers its ratio.

`__player.seek()` resolves asynchronously, so the gate awaits each seek before it reads. If every
seam fails the same way (`exit-visible op 0.00`, `exit-moving 0 px/s`) and `probe` finds no movers,
the reads landed before the seek did and the gate measured t=0, not the film.
24 changes: 18 additions & 6 deletions .claude/skills/motion-doctrine/scripts/seam-gate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,7 @@ const fmtV = (v, axis) =>
async function verify() {
const ledgerPath = flag("ledger", "ledger.json");
const ledger = JSON.parse(readFileSync(ledgerPath, "utf8"));
const fps = ledger.fps || FPS,
dt = 1 / fps;
const fps = ledger.fps || FPS;
const compUrl = await ensureServer();
const { evalJs } = await openPage(compUrl);
const results = [];
Expand All @@ -377,10 +376,17 @@ async function verify() {
const add = (check, status, detail) => rows.push({ check, status, detail });
const cut = seam.cut;
const type = seam.type || "cut";
const tA1 = Math.max(0, cut - 0.1),
tA2 = Math.max(0, cut - dt);
const tB1 = cut + dt,
tB2 = cut + 0.1;
// The player floors every seek to the frame grid, so sampling at cut ± k/fps for an
// off-grid cut shifts both exit samples up to a frame early and the first entry sample
// onto the cut itself, under-reading exit and over-reading entry. Sample whole frames
// placed symmetrically around the nearest frame instead.
const cutFrame = Math.round(cut * fps);
const offGrid = Math.abs(cut * fps - cutFrame) > 1e-3;
const win = Math.max(1, Math.round(0.1 * fps));
const tA1 = Math.max(0, cutFrame - win) / fps,
tA2 = Math.max(0, cutFrame - 1) / fps;
const tB1 = (cutFrame + 1) / fps,
tB2 = (cutFrame + win) / fps;
const sels = [
seam.exit?.selector,
seam.entry?.selector,
Expand All @@ -390,6 +396,12 @@ async function verify() {
const S = {};
for (const t of [tA1, tA2, tB1, tB2])
S[t] = await evalJs(`__seamGate.sample(${t}, ${JSON.stringify([...new Set(sels)])})`, true);
if (offGrid)
add(
"cut-grid",
"WARN",
`cut ${cut}s is between frames at ${fps} fps; measured around ${(cutFrame / fps).toFixed(3)}s — put cuts on the frame grid`,
);

if (type === "cut") {
const ex = seam.exit,
Expand Down
Loading