Skip to content

Add motion/: one folder per animation asset, discovered like the boards - #13

Open
Jing-yilin wants to merge 24 commits into
mainfrom
worktree-remotion-skill
Open

Add motion/: one folder per animation asset, discovered like the boards#13
Jing-yilin wants to merge 24 commits into
mainfrom
worktree-remotion-skill

Conversation

@Jing-yilin

@Jing-yilin Jing-yilin commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Adds motion/, the video half of what canvas/ already does for the artboards: drop a folder into motion/src/<bucket>/<slug>/ and it becomes a Remotion composition named after the folder, shows up in the studio, and — once rendered — plays on the canvas. No registry, no build step, nothing to keep in step with the filesystem.

Ships with sixteen templates — spatial-gallery, plus fifteen taken off a single brand film, one motion effect each — plus Remotion's official agent skills vendored under .agents/.

Why motion/ sits where it does

remotion.config.ts points the public dir at ../mockups, so a composition reaches a board with staticFile("canvases/luma-ios/01-guest-top.html") and a photo with staticFile("canvases/apple-photos/assets/photos/01-minerva-1.jpg"). Motion consumes mockups, which is why it is a sibling of mockups/ rather than a folder inside it — the same relationship canvas/ has.

The bit worth reviewing: meta.json

Two bundlers have to agree on one composition's box. rspack builds the compositions; Vite builds the canvas and needs that box to size the preview. JSON is the one format both parse, so meta.json is a sidecar rather than a field in the TSX:

export { default as meta } from "./meta.json";   // rspack, via Root.tsx
import.meta.glob("../../motion/src/*/*/meta.json", { eager: true })   // Vite, via motionLibrary.ts

Discovery in Root.tsx uses require.context — that is rspack's API. import.meta.glob is Vite-only and does not exist in that bundler.

On the canvas

Rendered mp4s land on a Motion page (?canvas=motion), each looping at 478pt wide — the artboards' own column pitch, so a video lines up with the boards it was made from. server.fs.allow opens the repo root because ?url assets outside canvas/ are served, unlike the boards' ?raw HTML which is inlined at transform time.

The mp4 in out/ is committed (2.1 MB): it is the only way to see an asset without running the project, it is what the canvas plays, and it is what makes a diff reviewable.

tools/motionkit.py

The motion half of refkit.py, applying the repo's measurement rule to time. probe, flow, sheet, compare, selftest — ffmpeg piped straight into numpy, no temp files.

flow is the one that earns its keep. spatial-gallery looks like a slow camera move and is in fact two momentum flicks with a dead hold between them, which is a thing you measure, not a thing you notice.

Verification

  • npx remotion compositions lists spatial-gallery 30 1080x864 120 from folder structure alone
  • motionkit probe prints meta.json byte-identical to the committed file
  • motionkit flow on our own render: peak 84.8 px/frame at f9, total 1706×570, axis 18.5° — against the reference's ~80 at f8–12, 1630×582, 19.7°
  • motionkit selftest passes (phase correlation recovers known shifts exactly)
  • canvas lint / test / build clean; motion tsc clean

The measurements taken off the third-party reference clip are committed; the clip itself is not — the same split .gitignore already makes for ref-*.html boards.

The fifteen

One 68-second film taken apart into the fifteen things it actually does:

ground mesh-gradient
type count-up, word-cascade, text-marker, word-swap, word-grid
spheres orb-bloom, bokeh-orbit, particle-form, depth-flythrough
camera card-stack, focus-pull, lens-reveal
chrome pill-expand, logo-outro

They exist to be cut together, and that is the whole reason for the interface: every one takes durationInFrames as a prop rather than reading it from useVideoConfig. Inside a <Sequence>, useVideoConfig().durationInFrames still reports the composition's length, so a template that trusts it stretches wrong the moment it is placed in a cut. useDuration(override?) takes the prop and falls back to the composition, which keeps each one scrubbable on its own in the studio. Every one is also fully settled well before its own last frame, so two can butt against each other with nothing to hide the seam.

src/lib/ holds what more than one of them needs — the palette with the frame each colour came off, the two faces, the timing helpers, the mesh ground, the orb. It sits outside Root.tsx's require.context on purpose, so a shared module never becomes an empty entry in the studio sidebar.

Three things that cost real time

  • radial-gradient(circle 55% at ...) is invalid CSS — a circle's radius may not be a percentage — and one invalid layer silently drops the whole background shorthand. The mesh painted no blobs at all, with no error anywhere, until this became ellipse R% R%.
  • A bare width/height does not survive being a flex item next to something wide: it shrinks on the cross axis and the sphere comes out an egg. The flex: none guard lives in Orb.tsx, not at the one call site that showed the bug.
  • Unpinned, loadFont() fires 126 requests for Inter at the head of every render. fonts.ts pins weights and subset.

All three are now comments where they bit.

What is not reproduced

The motion — timings, easings, geometry, palette, type scale. Not the film's wordmark and not its ad copy: templates carry this repo's own strings and logo-outro sets the mark to "Motion", which is a rights line and also the point, since a template with someone else's copy baked in is a screenshot. The clip stays out of the repo, as .gitignore already requires.

src/lib/README.md carries the shared provenance and the commands to reproduce any number in it. Each template's own README carries its measurements, its props, and its deviations.

More motionkit

  • swatch <clip> <frame> --grid WxH area-averages a frame into cells — the shape of a gradient, as hex
  • swatch <clip> <frame> --crop W:H:X:Y censuses one region at full resolution — the exact hex of a chip
  • sheet --from/--to cuts a contact sheet down to one shot, which is how "the gradient moves" became forty frames you can fit a curve to

Both are how every number above was read.

Then measured again, and two of them were the wrong effect

Everything above was reviewed once against contact sheets. A second pass —
render all fifteen, tile them, and go back to the clip for anything that
looks off rather than tuning by eye — found that looking once is not enough.

word-swap was replicating an effect the film does not contain. It
crossfaded two words past each other, blurred and lifting, over a line held
still: a plausible thing to build from memory. Sampled at one frame per tile,
f213–f228 does something else entirely — a gap opens between two words over
six frames, the word changes on one frame with no blur and no dissolve,
and a sphere fades up into the space that was just made for it. The hard cut
is the effect, and it is rewritten around that. The template that blurs
words past each other is word-cascade; this one is deliberately not that.

depth-flythrough scattered its orbs. Each got a random x and y inside a
wander box — the obvious way to build "a crowd of spheres", and it piled
them into a blob with no depth in it. In f1480–f1545 the orbs sit on one
straight line
passing to the left of the camera, converging on x 0.56 /
y 0.50, each 0.63 the size of the one in front. wander and its seed are
gone; vanish and offset replace them, and screen x now runs through the
same perspective divide as the size, which is what turns a handful of circles
into a corridor.

Four more numbers that were guesses rather than measurements:

was is why
orb-bloom to 1.7 2.05 the 16:9 diagonal is 2.04 frame heights, so anything smaller leaves the corners showing
bokeh-orbit count 22 48 the sparseness was the count, not the radius — the reference ring is a dense overlapping chain running off three edges
particle-form rim 0.62 0.35 0.62 rendered a hollow ring
depth-flythrough bubble padding px * k em it was px * k inside a box whose type already scaled by k, so it scaled twice and the near bubble came out all padding

orb-bloom's chips now also leave over the last ten frames, which is where
the film cuts, so the template has an end state and not just a hold — the
whole point of the set being composable.

Eleven README reference ranges were wrong by 50–400 frames. They are corrected
against the clip, and each README now carries the motionkit sheet command
that reproduces its own sheet, so the next reader can check the claim instead
of taking it.

Each of the two rewrites is written up in its own README's Deviations,
mistake first. A template whose numbers came from memory looks exactly like
one whose numbers came from a measurement, right up until someone re-derives
them; saying which is which is the only thing that makes the difference legible.

Verification, this pass

  • npx tsc --noEmit clean; all sixteen compositions render via ./render.sh (5.6 MB of CRF-28 previews total)
  • re-rendering spatial-gallery produced a byte-identical mp4, which is the determinism rule holding
  • stills for all fifteen reviewed against reference contact sheets; seven needed correction and were corrected

The films bucket, and what assembling the set found

src/films/brand-film/ is the fifteen templates cut back into one 43.2 s
piece, in the source film's own shot order. It is a shot list and nothing else
— no animation, easing, colour or copy — and it overrides exactly one prop
per shot, durationInFrames. That is the composability claim the whole set was
built to make, exercised for the first time: inside a <Sequence>,
useVideoConfig().durationInFrames still reports the film's 1296 frames, so a
template that read its length from there would time itself against the film
while occupying 84 frames of it. Nothing dissolves anywhere in the cut; every
template opens settled and settles again before its own last frame.

Then the reason to assemble it. Comparing the cut against the source shot by
shot showed twelve of the fifteen templates had been authored 1.3x to 2.4x
too small
, and every one of them looked right on its own. A template scrubbed
alone has no scale — it fills its own frame whatever size its contents are.
word-cascade 0.19 → 0.247, bokeh-orbit 0.13 → 0.3, orb-bloom 0.045 →
0.09, focus-pull 0.13 → 0.29, logo-outro 0.062 → 0.107, card-stack and
pill-expand whole boxes at ×1.35, and so on; each traced to a named frame.

Three of the corrections were not sizes: orb-bloom's chips are upright (the
italic belongs to word-grid), its bullets are flat pale discs rather than lit
orbs, and card-stack's cards are one key light washing across the row instead
of eight differently coloured cards.

motionkit extent

The measurement is committed rather than left in a scratchpad, because twelve
prop tables now cite it. It subtracts a wide Gaussian from a frame and boxes
what survives, so type and hard chrome register while gradients and bokeh do
not; at the same --width, a 2880-wide reference and a 1920-wide render give
directly comparable fractions of frame.

python3 tools/motionkit.py extent 7481_0.mp4 32
python3 tools/motionkit.py extent src/films/brand-film/out/brand-film.mp4 135
python3 tools/motionkit.py extent 7481_0.mp4 295 --band 0.15,0.35,0.85,0.65

compare cannot answer this question — it wants a frame-aligned pair, and
43.2 s against 68.4 s in a different shot order is not one. selftest covers
extent with a bar of ink on a full-frame gradient: it must find the bar and
not the ramp, within the halo that subtracting a blur necessarily leaves.

src/films/brand-film/README.md carries the cut table, the per-shot ratio
table above, and the honest list of what is deliberately not matched — the
reference's live footage and product UI (most of the missing 25 s), the
wordmark and ad copy, Instrument Serif's missing weight, and our lens-free
render.

🤖 Generated with Claude Code

Jing-yilin and others added 4 commits September 2, 2026 02:32
`npx skills add remotion-dev/skills` installs all twelve into
`.claude/skills/`, pinned by `skills-lock.json`. Upstream's files, kept as
upstream ships them: `npx skills update` refreshes them, hand-edits do not
survive. The repo's own three skills stay in `.agents/skills/`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same layout as clone-prototype, new-ui-mock and prototype-canvas: the files
sit in .agents/skills/, .claude/skills/ holds only symlinks. `npx skills
update` follows them and leaves the links alone, verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drop a folder into motion/src/<bucket>/<slug>/ and it becomes a Remotion
composition named after the folder, appears in the studio, and — once
rendered — plays on the canvas. Same contract the artboards already have:
discovery by convention, no registry to keep in step with the filesystem.

- motion/src/Root.tsx auto-discovers via require.context (rspack's; the
  canvas's import.meta.glob is Vite-only and does not exist there).
- meta.json is a JSON sidecar rather than a field in the TSX so both
  bundlers can read it: rspack builds the compositions, Vite builds the
  canvas and needs the box to size the preview.
- remotion.config.ts points the public dir at ../mockups, which is why
  motion/ is a sibling of mockups/ and not inside it: motion consumes
  mockups. A composition reaches a board with staticFile("canvases/...").
- canvas/: a Motion page lays the rendered mp4s out at 478pt, the
  artboards' own column pitch, so a video lines up with the boards it was
  made from. server.fs.allow opens the repo root because ?url assets
  outside canvas/ are served, not inlined the way the boards' ?raw is.
- tools/motionkit.py is the motion half of refkit.py: probe, flow, sheet,
  compare, selftest. It applies the repo's measurement rule to time —
  spatial-gallery looks like a slow camera move and is in fact two
  momentum flicks with a dead hold between them.

The measurements taken off a third-party clip are committed; the clip is
not, same split .gitignore already makes for ref-*.html boards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in claude-ios, the clone-prototype lessons, and the canvas's move to
Bun. Only README.md overlapped, and only in prose: main's canvas now runs on
Bun, so the Motion section no longer calls the two projects npm projects.
motion/ stays on npm — Remotion bundles with rspack and ships its own Chrome
Headless Shell, so Bun buys it nothing.

canvas: bun install --frozen-lockfile, lint, test (1 passed), build all clean
against the merged tree. motion: tsc clean, compositions still lists
spatial-gallery from folder structure alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d8b7a528c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread motion/src/Root.tsx Outdated
(key: string): MotionAsset;
};
}
).context("./", true, /\/index\.tsx$/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict discovery to asset-root entry points

When an asset organizes supporting code under a nested barrel such as src/templates/foo/components/index.tsx, this recursive pattern also treats that file as a MotionAsset. The subsequent render then reads missing meta/Component exports and can prevent the entire Remotion Studio or compositions command from loading. Match only ./templates/<slug>/index.tsx and ./films/<slug>/index.tsx so supporting modules remain safe.

Useful? React with 👍 / 👎.

Comment on lines +90 to +91
const last = Math.ceil(TWO_PI / alpha);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound card indices for reverse motion

When a reusable gallery receives negative drift or negative flick peaks, thetaEnd is below the initial angle, but these bounds assume travel only increases. That omits a large section of the helix at the start or end of the animation, producing empty edges and popping cards; mixed-direction schedules can similarly exceed both endpoint bounds. Compute the minimum and maximum travel across the composition and derive both index limits from those extrema.

Useful? React with 👍 / 👎.

The Motion page is a page you have to know to open. The welcome board is the
one thing anyone opens first, so the renders go there too — under the two rows
of example boards, at the same column pitch so the rows line up. Clicking a
card opens the Motion page, where the same render plays at full width.

A card's cover is the mp4 itself rather than a still: a still of a video is not
what the card is offering. `path` on canvas-link now means cover art — a
board's path or a rendered mp4's URL — and the card is only as tall as its
composition's aspect, because cropping a landscape render into a phone-shaped
card throws most of the frame away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf03d593d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread motion/src/Root.tsx Outdated
return (
<Composition
key={key}
id={slugOf(key)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Namespace composition IDs by bucket

When a template and a film use the same folder slug—a valid arrangement under the documented templates/<slug> and films/<slug> layout—both registrations receive the same composition ID. Remotion requires composition IDs to be unique, so this prevents Studio and render commands from loading the library; the canvas also keys metadata by slug alone in readMotionLibrary(), which can assign either asset the other's dimensions. Include the bucket in both the composition ID and the canvas metadata key.

Useful? React with 👍 / 👎.

Comment thread canvas/src/App.tsx
@@ -373,6 +392,216 @@ function layoutWelcomeExtras(
LIBRARY_LABEL_HEIGHT +
LIBRARY_GAP;
}

layoutWelcomeMotionRow(editor, page, top);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Render the motion row when no example boards remain

When the welcome board is the only remaining board but rendered motion assets exist, the earlier if (!targets.length) return exits before this call. This is a supported setup—the root README tells users to delete the supplied board folders—but it silently omits the promised motion cards from the welcome page even though the separate Motion page is created. Move this call ahead of that return or avoid returning when motion assets are available.

Useful? React with 👍 / 👎.

At half a board wide the render was the smallest thing on a board full of
phone screenshots, which is backwards: it is the one card with motion in it.
478 is the artboards' own column pitch and what the Motion page already uses,
so the card is now the same width in both places.

The heading was also wrapping to two lines and setting over the card. The row
above it already says to click a card, so this one only has to name itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21083736eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const grazing = Math.abs(Math.sin(yaw));
const band = 50 - 30 * Math.sin(yaw); // reflection slides with the yaw

const card = cards[((i % cards.length) + cards.length) % cards.length];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require at least one card before indexing

When the reusable gallery receives cards={[]}—for example while a caller has no media—the modulo expression produces NaN, so card is undefined and the subsequent card.background access crashes the composition instead of rendering an empty gallery. Guard the empty state or represent cards as a non-empty tuple.

Useful? React with 👍 / 👎.

Comment thread canvas/src/motionLibrary.ts Outdated
/** Every preview is this wide, so the column pitch matches the boards' 478pt artboards. */
export const MOTION_PREVIEW_WIDTH = 478;

const VIDEO_PATTERN = /motion\/src\/([^/]+)\/([^/]+)\/out\/[^/]+\.mp4$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Discover only the canonical render per asset

When an asset's out/ directory contains more than one MP4, such as an old render alongside a new candidate, this wildcard creates multiple library entries for the same bucket and slug. layoutMotionPage() then assigns those entries the same motion-file:<bucket>/<slug> shape ID, while the welcome row also reuses one ID, so the batch can overwrite or omit renders instead of producing one deterministic card. Match only the canonical out/<slug>.mp4 file or explicitly select one output.

Useful? React with 👍 / 👎.

A review of the tree found the conventions worked only because there is
exactly one asset in it. Every fix here is a rule that fails silently on
the second or the fifth.

Discovery (motion/src/Root.tsx)
- The pattern pinned the filename but not the depth, so an asset's own
  components/index.tsx would have become a composition with no meta and
  failed deep inside Remotion without naming the folder.
- A missing meta.json now throws with the path and the one-line fix.
- Two folders of the same name in different buckets now throw: a
  composition id is its folder name and Remotion's ids are global.
- Buckets group the studio sidebar via <Folder>.

The join (canvas/src/motionLibrary.ts)
- Renders were keyed by slug alone, so films/x and templates/x would have
  shared whichever meta.json the glob enumerated last and one would draw
  at the other's aspect. Keyed by bucket and slug now.
- out/*.mp4 matched any filename, so a scratch compare.mp4 became a second
  entry colliding on the shape id. Only out/<slug>.mp4 counts.
- Buckets are named in the glob rather than *, so a folder under a third
  bucket is loudly absent instead of bundled and silently dropped.
- meta.json takes an optional name, because humanizing a slug cannot
  express casing: "luma-ios-launch" is not "Luma iOS launch".
- The join is a pure function over paths, with a test for all four rules.

Layout (canvas/src/App.tsx, MotionFileShapeUtil.tsx)
- Both rows wrap at four across. The welcome strip stops at four and its
  heading says how many more are on the Motion page; the Motion page runs
  each bucket down in lines of four, each as tall as its own tallest video.
- A culled shape no longer mounts its <video>: tldraw culls with
  display:none rather than unmounting, and a hidden video keeps decoding.

Rendering (motion/render.sh)
- render.sh <slug> derives out/<slug>.mp4 from the slug, because a render
  written anywhere else is a render the canvas does not show. No argument
  renders every asset.
- CRF 28, not Remotion's default 18: out/ holds a preview that loops
  four-across on a board, and the master is the composition. The committed
  spatial-gallery render goes 2.2 MB -> 603 kB.

Evidence
- reference/motion.txt regenerated with tools/motionkit.py, in source
  pixels — the old series was half-res with a "multiply by 2" footgun in
  the header. Every number BRIEF.md quotes was re-checked against it:
  peak 83.5 px/frame at f9, total 1630x582, axis 19.6 degrees.
- BRIEF.md pointed at ~/Downloads for the source and listed frames that
  were never committed. It now names the clip in reference/ and gives the
  motionkit commands that regenerate the rest.
- .gitignore's reference/ rule is an allowlist rather than a list of video
  extensions, so the next clip dropped in as .webm is ignored too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 178aceaca3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread canvas/src/App.tsx
Comment on lines +523 to +524
const bySlug = () =>
editor.getPages().find((c) => c.meta.canvasSlug === MOTION_PAGE_SLUG);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve the generated Motion page slug

When a board is added at mockups/canvases/motion/—a slug currently accepted by board discovery and not documented as reserved—this lookup reuses that board's page instead of creating a separate Motion page. The later rename and video layout then mix generated motion previews into the board and replace its configured page name; either reject/reserve this board slug or use a namespace that cannot collide with board folders.

Useful? React with 👍 / 👎.

Brings in duolingo-ios, tools/artgen.py, refkit's key and sample --only,
and main's rewritten README opening.

One conflict, in README.md: main replaced the repo tree block with a "how
you use it" paragraph, and this branch had edited that block to mention
Remotion's vendored skills. Took main's paragraph whole — the Remotion
facts already live in the Motion section below it, which merged clean.
Adjusted "Three skills" to say three of ours plus Remotion's twelve, since
that count is only wrong once this branch lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14d7f98875

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread motion/render.sh
if [ $# -gt 0 ]; then
render "$1"
else
for dir in src/*/*/; do render "$(basename "$dir")"; done

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict bulk rendering to composition buckets

When shared code uses a nested directory under the documented src/lib/ location, such as src/lib/components/, this wildcard treats that directory as an asset even though Root.tsx registers compositions only from films and templates. The loop consequently invokes remotion render components ...; because no such composition exists and the script uses set -e, the bulk render stops before rendering all real assets. Restrict this loop, and the lookup in render(), to the two registered buckets.

Useful? React with 👍 / 👎.

Comment thread motion/README.md
Comment on lines +79 to +80
cd motion
npx remotion studio # scrub everything

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add dependency installation to the motion setup

In a fresh clone following the repository setup instructions, only canvas/ dependencies are installed, yet this new motion guide proceeds directly to npx remotion without ever installing the dependencies in motion/package-lock.json. That leaves the local CLI, React, Remotion, and TypeScript unavailable, so the documented studio, render, and lint commands do not work reliably from a clean checkout. Add an npm ci step before these commands.

Useful? React with 👍 / 👎.

A 68-second brand film taken apart into the fifteen things it actually does,
each its own composition: mesh-gradient; count-up, word-cascade, text-marker,
word-swap, word-grid; orb-bloom, bokeh-orbit, particle-form, depth-flythrough;
card-stack, focus-pull, lens-reveal; pill-expand, logo-outro.

They are built to be cut together, which is what shapes the interface: every
one takes `durationInFrames` as a prop rather than reading it from
`useVideoConfig` — inside a `<Sequence>` that still reports the composition's
length, so a template that trusts it stretches wrong the moment it is placed in
a cut — and every one is fully settled well before its own last frame, so two
can butt against each other with nothing to hide the seam.

src/lib/ carries what more than one of them needs: the palette with the frame
each colour came off, the two faces, the timing helpers, the mesh ground, the
orb. It sits outside Root.tsx's require.context on purpose, so a shared module
never becomes an empty entry in the studio sidebar.

Three things worth writing down, all now in comments where they bit:

- `radial-gradient(circle 55% at ...)` is invalid — a circle's radius may not be
  a percentage — and one invalid layer silently drops the WHOLE background
  shorthand. The mesh painted no blobs at all until this was `ellipse R% R%`.
- A bare width/height does not survive being a flex item next to something wide.
  The orb came out an egg; `flex: none` lives in Orb.tsx, not at the call site.
- Unpinned, `loadFont()` fires 126 requests for Inter at the head of every
  render. fonts.ts pins weights and subset.

The film's motion is reproduced; its wordmark and its ad copy are not, and the
clip stays out of the repo. Templates carry this repo's own strings — which is
also what makes them templates. src/lib/README.md has the whole provenance and
the commands to reproduce any number in it; each template's README has its own
measurements and its deviations.

motionkit grows `swatch` (colours off one frame, as a grid or a crop census)
and `sheet --from/--to` (a contact sheet of one shot rather than the whole
clip). Both are how the numbers above were read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef72f0d38a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/motionkit.py
if lo >= hi:
sys.exit(f"motionkit: --from {a.start} is not before --to {hi}")
indices = [round(lo + i * (hi - lo) / max(1, a.count - 1)) for i in range(a.count)]
frames, _ = read_gray_rgb(a.video, a.width)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decode only the frames selected for the contact sheet

When sheet is run on a long reference clip—or even with a narrow --from/--to range—read_gray_rgb() still decodes every frame into memory before the selected indices are used. At the default 320px width, the documented 68-second, 30fps example requires roughly 336 MiB just for RGB pixels and scales linearly with clip length, so this can become extremely slow or be killed for memory despite requesting only ten tiles. Seek or select the requested frames during ffmpeg decoding instead.

Useful? React with 👍 / 👎.

rather than inside `ellipse()`, which takes no angle. The radii are
allowed past 100% — that is how the aperture runs off the edges at the
end of the shot without anything having to be scaled up to let it. */}
<AbsoluteFill style={{ transform: `rotate(${tilt}deg)` }}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the rotated reveal covering the frame corners

With any nonzero tilt—including the default -18—rotating a viewport-sized AbsoluteFill makes its rectangular corners move inside the parent, leaving triangular areas where the original background remains visible. Enlarging the ellipse cannot fill those areas because the revealed background is itself confined to that rotated rectangle, so the default animation never actually reaches its promised full-frame reveal. Oversize the rotating layer or rotate only the aperture coordinate system while keeping the revealed content full-frame.

Useful? React with 👍 / 👎.

style={{
fontSize: size * s.k,
lineHeight: 1.35,
padding: `${0.5 * s.k}em ${0.9 * s.k}em`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid applying the perspective scale twice to padding

Whenever an orb is away from the focal distance, fontSize: size * s.k already scales every em measurement by s.k; multiplying the padding's numeric em value by s.k again therefore makes bubble padding grow as , while the orb, text, gap, and radius grow linearly. Near the camera the default parameters can produce several em of padding and an oversized bubble that overwhelms the frame, breaking the intended perspective projection. Keep the padding values constant in em, or express linearly scaled padding in pixels.

Useful? React with 👍 / 👎.

Rendered all fifteen, tiled them against the source, and went back to the
clip for everything that looked off instead of tuning by eye. Two of them
turned out not to be replicating anything the film does.

word-swap was a crossfade. Sampled f213-f228 one frame per tile and the
film does something else entirely: a gap opens between two words over six
frames, the word changes on ONE frame - no blur, no rise, no dissolve -
and a sphere fades up into the space that was just made for it. The hard
cut is the whole effect. Rewritten around it; the template that blurs
words past each other is word-cascade, and this one is deliberately not
that.

depth-flythrough scattered its orbs. Each got a random x and y inside a
wander box, which is the obvious way to build "a crowd of spheres" and
piled them into a blob with no depth in it. In f1480-f1545 the orbs sit
on ONE straight line passing left of the camera, converging on x 0.56 /
y 0.50, each 0.63 the size of the one in front. wander and its seed are
gone; vanish and offset replace them and screen x now runs through the
same perspective divide as the size. Bubbles alternate speaker off the
label index, not the orb index - every other orb is bare, so i % 2 put
every bubble on the same side and the alternation never fired - and are
culled below 20px of type, because past that a third bubble is litter
rather than distance.

Four more numbers that were guesses:

- orb-bloom to: 1.7 -> 2.05. The 16:9 diagonal is 2.04 frame heights, so
  anything smaller leaves the corners showing. Chips now leave over the
  last ten frames, which is where the film cuts, so the template has an
  end state and not just a hold.
- bokeh-orbit count: 22 -> 48. The sparseness was the count, not the
  radius; the reference ring is a dense overlapping chain running off
  three edges.
- particle-form rim: 0.62 -> 0.35. 0.62 rendered a hollow ring.
- depth-flythrough bubble padding was px * k inside an em box, so it
  scaled twice and the near bubble came out all padding.

Eleven README reference ranges were wrong by 50-400 frames. Corrected
against the clip, and each README now carries the motionkit command that
reproduces its sheet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

if (!library.length) return;

P2 Badge Initialize the Motion page without board files

When a checkout has rendered motion assets but no HTML boards, this return prevents the later layoutMotionPage() call from running, so the canvas exposes none of the renders despite motion discovery being independent of the board library. Continue with an empty library so a motion-only project still gets its generated Motion page.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread canvas/src/App.tsx
Comment on lines +485 to +486
id: `canvas-row-heading:${page.id}:${title}`,
text: title,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the welcome motion heading ID stable

When the rendered asset count crosses four or otherwise changes its overflow count in a persisted canvas, title changes and therefore produces a new shape ID; the old heading is never updated or removed, so reloads accumulate overlapping headings at the same coordinates. Use a stable row ID and let createAnnotation() refresh its text.

Useful? React with 👍 / 👎.

Comment thread tools/motionkit.py
Comment on lines +129 to +130
speed = [np.hypot(dx, dy) for _, dx, dy, _, _ in rows]
peak = int(np.argmax(speed))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle flow inputs with fewer than two frames

When flow receives a valid one-frame clip, measure() returns no displacement rows and np.argmax(speed) raises on the empty sequence (with rows[-1] failing next), so the command exits with an internal traceback rather than reporting zero motion or a useful input error. Guard the empty-row case before computing the peak.

Useful? React with 👍 / 👎.

`src/films/brand-film/` is the Delphi set assembled back into one 43.2s
piece, in the source film's own shot order. It is a shot list and nothing
else: no animation, no easing, no colour, no copy, and exactly one prop
overridden per shot — `durationInFrames`. That single override is the
composability claim the whole set was built to make, and this is the first
place it is actually exercised, because inside a <Sequence> the config's
own duration is the film's 1296 frames and not the shot's 84.

Then the point of assembling it. Comparing the cut against the source shot
by shot showed twelve of the fifteen templates had been authored between
1.3x and 2.4x too small, and every one of them looked right on its own: a
template scrubbed alone has no scale, since it fills its own frame whatever
size its contents are. Corrected against a measured frame in each case, and
the prop tables follow.

Beyond the numbers, three things the measurement decided rather than taste:
`orb-bloom`'s chips are upright (the italic belongs to `word-grid`) and its
bullets are flat pale discs, not lit orbs; `card-stack`'s cards are one key
light washing across the row, not eight differently coloured cards.

`motionkit extent` is that measurement, committed so the numbers reproduce.
It subtracts a wide Gaussian from a frame and boxes what survives, so type
and hard chrome register while gradients and bokeh do not. Measured at the
same --width, a 2880-wide reference and a 1920-wide render give directly
comparable fractions. `compare` cannot answer this — it wants a frame
aligned pair, and 43.2s against 68.4s in a different order is not one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ba8556caf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/motionkit.py
Comment on lines +77 to +78
frames = np.frombuffer(raw, np.uint8).reshape(-1, dh, dw)
return frames.astype(np.float32), 1.0 / scale

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stream frames while measuring optical flow

When flow is run on the documented 68-second, 30-fps reference at the default 540px width, this retains the complete ffmpeg byte buffer and then allocates a float32 copy of every frame—approximately 1.6 GiB before FFT working memory. This can make the primary measurement command swap heavily or be killed on modest machines; process consecutive frames incrementally instead of materializing the entire clip.

Useful? React with 👍 / 👎.

Comment thread tools/motionkit.py
Comment on lines +121 to +123
print(json.dumps(
{"fps": round(fps), "width": w, "height": h, "durationInFrames": n},
indent=2,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve fractional source frame rates

For common sources such as 30000/1001 or 24000/1001, this rounds 29.97/23.976 to 30/24 in the generated meta.json snippet even though the preceding probe output uses the exact rate. Copying the advertised snippet into a composition therefore changes its playback rate and duration, with drift increasing for longer clips; emit the measured fractional value instead of rounding it.

Useful? React with 👍 / 👎.

Comment thread tools/motionkit.py
Comment on lines +143 to +146
sx = sum(dx for dx, _ in moving)
sy = sum(dy for _, dy in moving)
if sx:
print(f"axis {np.degrees(np.arctan2(sy, sx)):.1f} deg from horizontal")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report the axis for vertical motion

For a purely vertical pan, every measured dx and therefore sx is zero, so this condition suppresses the axis line even though atan2(sy, 0) correctly yields ±90 degrees. Since identifying the pan axis is an advertised output of flow, print the angle whenever moving samples exist rather than requiring horizontal displacement.

Useful? React with 👍 / 👎.

… wrong

Comparing the assembled film to the reference again turned up three faults in
the film and three in the comparison. Both kinds are written down, because
half the corrections in the last pass were to things that were never broken.

In the film:

- `pill-expand`'s card grew in six frames instead of twenty-five, and its
  title and body crossfaded together. Measuring the card's own pixel width
  gives 529 -> 821 over +23 to +48, ease-in-out cubic to within a percent at
  every sample; measuring ink under luma 160 in each half of the card gives
  the title leading the body by two frames, 35% up against 5% at +42. The
  card is briefly empty in the reference too, at +38 to +40 -- but it is
  still growing there, which is why it never reads as a blank slab.
- `DIM` spreads `MESH`, so when the band stopped being a triangle its
  `bandLeft` and `bandRight` changed meaning underneath it -- half-height
  widths became ground-crossings -- and its band silently doubled. That put
  150 luma across the left half of `particle-form` and `focus-pull` where the
  reference has 105. Re-fitted by binning every pixel along the band's own
  axis and masking the ink, which is the only way to read a shot that never
  has an empty frame: rmse 1.2 luma, against the 30-50 it was out by.
- "f1400 is the bare gradient" is not true and two files said so; it carries
  the block's bloom and a glyph of the next shot.

In the comparison:

- `depth-flythrough` is the one shot that runs the reference's own length on
  the reference's own clock, and it was paired as if it did not. It read
  0.70, then 1.25, and it is 1.01.
- `word-swap`'s extent band was reading type in one clip and orb in the
  other. Measured directly, the reference's orb settles at 0.142 W and ours
  at 0.1375.
- `bokeh-orbit`'s 0.89 and `pill-expand`'s 0.92 are `extent` being blunt, not
  the renders. The film README now says where it is blunt and why five of the
  fifteen shots cannot be measured with it at all.

Every other row of the table is inside 1.08.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f18c1f05a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread canvas/src/App.tsx
Comment on lines +594 to +599
if (props.src !== shape.props.src || props.h !== shape.props.h) {
editor.updateShape({
id: shape.id,
type: shape.type,
props: { src: shape.props.src, h: shape.props.h },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reflow rows when preview height changes

When an asset's meta.json aspect changes on an already persisted canvas, this block resizes only the preview. Its existing label is not repositioned because createAnnotation() only updates text, and previews in subsequent wrapped lines or buckets retain coordinates calculated from the old height. A taller render therefore overlaps those labels or later previews after an ordinary reload; update the generated positions together or trigger a full relayout when the height changes.

Useful? React with 👍 / 👎.

…to time a fade

Two passes have now split `pill-expand`'s title and body, in opposite
directions, and both were reading the same broken instrument: a count of
pixels under luma 160 inside the card. A count crosses a threshold, it does
not ramp. Text at 30% opacity on white sits at luma 187 and registers as
nothing at all, so the count reports a fade as a step and the step lands
wherever the two halves happen to cross 160 -- which for a heavier title and a
lighter body is not the same frame.

Measuring the mean luma of each half instead, normalised between the empty
card at +40 and the settled card at +60, is linear in opacity because the card
is white and the ink darkens it in proportion:

        +42   +44   +46   +48   +50   +52
  title 0.18  0.46  0.73  0.93  0.97  0.99
  body  0.11  0.37  0.68  0.90  0.95  0.97

They land together -- the body trails by about half a frame, finer than a
30 fps template can express -- and the shape is an ease-out quad over 11
frames, not the helper's default cubic, which is a frame ahead the whole way.
Both halves now run off one `enter`; worst gap against the reference 0.09 of
the fade, against 0.41 before.

`extent`'s pill-expand row goes the way `word-swap`'s did. It read 0.92, and
0.89 once its pair was corrected -- this template's offsets run from `at`, and
the cut opens it on eight frames of bare ground, so film frame 481 is f1080
and not f1088. Neither number is about the card: both are the paragraph behind
it, plus 0.02 W a side of blur residual from subtracting a 24 px Gaussian off
an already-blurred backdrop. The card itself is 409 px of 960 in the reference
against ours at 410, tracking within 3 px at every sample of its open, so the
twenty-five-frame ease-in-out cubic restored last commit is landing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc5cbfc19e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread canvas/src/App.tsx
Comment on lines +452 to +453
const missing = cards.filter((card) => !editor.getShape(card.id));
if (missing.length) editor.createShapes(missing);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile changes to the welcome-card subset

When a newly rendered film or alphabetically earlier asset enters the first four returned by readMotionLibrary(), assets changes but this only creates the new card: existing cards are never moved to their new indices, and the card displaced from the four-item slice is never removed. On a persisted canvas, the new card therefore overlaps the previous first card while the evicted card remains as a fifth preview, defeating the bounded row until the user manually forces a full relayout. Reconcile generated card membership and positions whenever the selected subset changes.

Useful? React with 👍 / 👎.

…rd-stack

The film opened on 72 frames of bare `mesh-gradient`. Two things wrong with
that. The reference opens on black and rises into a ground with type already
on it -- it never holds on a bare ground anywhere in 68.4 s -- so ours put its
brightest frame where the source has its darkest. And it was the wrong ground:
`mesh-gradient` is fitted on f1172-f1280 by its own README, the crimson second
half, while the opening's ground is `word-cascade`'s cocoa radial. That
template's first line claimed it was "the warm ground the reference film puts
under its opening shots", which is how it got there; the line is now corrected,
and says what it once cost.

The shot is dropped and a 24-frame fade from black replaces it, in the film
rather than in a template, because a template that faded up on its own could
only ever be the first shot of a cut. Mean luma over the settled ground at f24
runs 0.00 / 0.10 / 0.41 / 0.85 / 1.00 at f0/6/12/18/24; an ease-in-out cubic
gives 0.00 / 0.06 / 0.50 / 0.94 / 1.00. The cut is 1224 frames, 40.8 s, and
every frame number in the film README and the ratio table moves with it.

`card-stack` was the worst shot on the contact sheet, and two of its props
were out:

- `blur` 7 -> 34. On a 38-row band at 0.72-0.79 of H, counting pixels that
  step more than 4 luma finds edges: the reference has 11, which is the sharp
  card's two and little else. 7 rendered 35 -- every card in the row hard
  edged. 20, 26, 30 and 34 render 35, 22, 15, 10.
- `riseFrames` 24 -> 20 and `step` 1.5 -> 1.0. Timed on the median of the
  row's whole top edge, normalised into a fraction travelled so the two clips'
  sizes drop out: the reference is 0.68 / 0.85 / 0.94 / 0.98 / 1.00 at +6 to
  +19, ours was 0.59 / 0.74 / 0.88 / 0.91 / 0.96 and five frames late. It now
  renders 0.67 / 0.85 / 0.93 / 0.99 / 1.00.

The keystone did not fit, and the README now says so at length rather than
carrying a knob that pretends otherwise. Six props were swept at it. Every one
traded coverage for lean, and the reason turned out to be that all six were
measuring the same artefact: the first four grids compared the reference's
settled f57 against our frame 19, which is still mid-slide, and the two
`perspectiveOrigin` grids were reading a row that the camera move had sunk
0.14 of H down the frame. Putting the row back with `top` looked like a
landing -- area 0.566 against 0.522, top edge 0.480 against 0.469, slope 0.374
against 0.367 -- and rendered a sharp card towering over its neighbours,
because `top` moves every card in the layout plane while the projection only
drags the ones that have receded, and the sharp card is at z=0 where
`perspectiveOrigin` cannot reach it.

Two things were wrong underneath all of it. The slope metric counted the soft
transitions between neighbouring cards as lean, which is why the reference read
0.367 and nothing could reach it. And the reference's card tops at f61 are
0.444 to 0.556 of H scattered about its sharp card's 0.451 -- there is no
systematic fan in that row for a camera height to match. What is left is per
card and wants the whole row pushed off z=0, which is a different change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d373793bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread canvas/src/App.tsx
Comment on lines +585 to +586
const missing = shapes.filter((shape) => !editor.getShape(shape.id));
if (missing.length) editor.createShapes(missing);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reposition previews when discovery order changes

When a newly rendered asset sorts before an existing asset in its bucket, placed assigns the existing preview a new position, but this block creates only the missing shape and the update loop below never updates x or y. On a persisted Motion page, the new preview is therefore created on top of the old first preview until the user forces a full relayout, so ordinary asset discovery produces an overlapping layout.

Useful? React with 👍 / 👎.

Comment thread tools/motionkit.py
s = json.loads(out)["streams"][0]
num, den = s["r_frame_rate"].split("/")
fps = float(num) / float(den)
frames = int(s.get("nb_frames") or 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat nonnumeric frame counts as unavailable

When ffprobe reports an unavailable frame count as the literal "N/A", as occurs for common WebM/Matroska streams, int("N/A") raises before the packet-count fallback can run. Since every motionkit command calls probe(), such a reference cannot be probed, measured, or sampled; parse only numeric values and otherwise fall back to -count_packets.

Useful? React with 👍 / 👎.

…teleporting

The film butted every shot against the next, on the claim in its own header
that a template which settles before its last frame needs nothing at the
seam. Measured, that was the film's biggest fault. ffmpeg's scene detector at
0.2 finds one hard cut in the source (f1172, 0.87) and two soft events; it
found ten in ours, four at 1.00. Mean |dL| from one frame to the next across
the seams, ranked: bokeh-orbit -> text-marker 209, depth-flythrough ->
lens-reveal 165, focus-pull -> depth-flythrough 138, pill-expand -> count-up
106, lens-reveal -> word-grid 81, count-up -> orb-bloom 69, orb-bloom ->
particle-form 38, card-stack -> word-swap 30. The other five were under 9.

The source's joins are cross-dissolves. Mean luma across a cross-fade of two
stills is linear in the fade's opacity, so the normalised luma across a join
IS its curve, and every (length, easing, start) was scored against it. Four
short ones -- f667, f822, f1467, f1586 -- fit ease-in-out cubic best, at 8,
11, 10 and 9 frames, rmse 0.014-0.036, which is the curve the fade from black
already used. So a shot in the cut now carries a third number, how many of
its first frames overlap the previous shot's last, and fades in over them on
that curve; nothing else in the film changed and no template knows. Seven
joins dissolve, at the source's own lengths where the source joins the same
two shots (depth-flythrough in at f1467 and out at f1586, orb-bloom out at
f1336, count-up out at f1274, card-stack out at f140, lens-reveal out at
f1776) and at the nearest join of the same kind where it cuts to footage
between them. pill-expand -> count-up stays a cut: it is the source's one
hard cut, 238 -> 136 there against 240 -> 135 here. After: 52, 49, 34, 106,
15, 24, 11, 14. The source's own dissolves peak at 44 and 49 per frame. The
scene detector finds five events, and two of the four soft ones are
lens-reveal's own reveal, which steps 66 per frame where the source's steps
15 -- the template's, not the cut's, and written down rather than changed.
The cut is 1147 frames, 38.2 s; the frame numbers in the README move with it.

Two of the fits did not take the one curve and the README says which: the
card-stack fade prefers ease-in-out quad over 14 (0.021 against the cubic's
0.048), the join into the crimson before word-grid an ease-out over 22 (0.035
against 0.110). And count-up -> orb-bloom fades where the source defocuses
the numeral; the faithful fix is a leave on count-up, which was not made.

word-cascade, separately, moved its type in one-frame jumps: 163 px sideways
at f10 as "got" joined the row and 150 px up at f14 as the second line
appeared. The template laid out only the units that had arrived, so each
arrival re-centred the flex row, and it did that on the strength of a README
line saying the reference re-centres "You've" alone at f20 and the pair at
f24. Measured (ink over luma 140 at 960 wide), the reference does the
opposite: "You've" holds its left edge at 0.263 of W from f17 to f31 while
"got" arrives beside it, and what moves is the whole block, once, upward --
top 0.400 -> 0.294 of H over f18-f31, 0.26 / 0.41 / 0.49 / 0.58 / 0.68 / 0.75
/ 0.82 / 0.88 / 0.91 / 0.92 / 0.96 / 0.98 / 1.00 of the way, ease-out cubic
over 16 to rmse 0.033. Every unit is now laid out from frame 0 and the block
starts centred on its first line and lifts (`liftAt` 10, `liftFrames` 16).
Rendered, the left edge holds 0.263 from f11 and the block top moves in steps
of 0.026, 0.023, 0.018 ... 0.002 of H; the reference's largest step is 0.028.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1c140a6ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

rise,
),
display: "inline-block",
color: accent && piece.includes(accent) ? accentColor : color,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply accents across animation-unit boundaries

When accent spans multiple animation units—such as accent="digital mind" with unit="word", or any multi-letter accent with unit="letter"—no piece individually contains the full substring, so the requested accent is never rendered. Conversely, a substring smaller than a piece recolors the entire piece. Track each piece's range within the full text and color the actual matching range rather than testing each piece independently.

Useful? React with 👍 / 👎.

The reference carries "Your / digital mind / is born" over the particle
figure: f1380 has 3.77% ink left of 0.45 W and 5.78% right of it, at once.
The cut ran them as sequential shots, so they never shared a frame -- the
right of the frame was empty for 100 frames and then the left was for 72.

`focus-pull` now overlaps its whole length as a layer rather than a
dissolve: it composites at full opacity, and the cut passes it
`gradient: null` so the shot on top does not repaint the `DIM` ground both
templates draw. `GradientProps | null` is the only behavioural change to
the template; alone it keeps its ground.

Placing it needed the arrival measured, and a plain box mean cannot do it:
the type covers well under half its box and the ground under it drifts by
more than the type contributes. Mean excess over the box's own per-frame
median can -- the median is the ground, and excess light is linear in
opacity where an ink count over a threshold crosses a step. It says the
figure climbs from f1342 and the type from f1345: four frames apart, not
the twenty the first placement assumed. That twenty came from this
template's own header, which recorded f1372 -- the frame the *push* was
measured from -- as the start of the shot.

So `particle-form` is 80 frames with `focus-pull` overlapping 72, which
puts the type at frame 8 of the figure's run, four frames after its dots
start at frame 4. Both run out together, and the shot's last six frames
are the figure alone, as in the source.

Laid over a ground that no longer moves, the block's full-strength opening
read as a pop -- 0.00 to 1.00 of final in one frame, the same fault the
opening shot was just fixed for. The source ramps 0.01 / 0.06 / 0.15 /
0.25 / 0.34 / 0.43 / 0.53 / 0.62 / 0.76 / 0.87 / 1.00 at f1345 / 47 / 49 /
51 / 53 / 55 / 57 / 60 / 65 / 68 / 72, a straight line at 0.045 a frame:
`fadeIn`, 22 frames, linear. The block's own area is flat over f1358-f1367
while the light still climbs, so it is opacity and not growth.

The film is 1055 frames. The scene detector at 0.3 finds one event in it,
the hard cut at 15.6 s, which is the one the source has.

Still off, and written down rather than papered over: our figure holds
flat once its fade lands where the source's keeps growing, 3.51% to 4.93%
of the left half over f1376-f1389. That is `particle-form`'s documented
hold and this pass did not touch it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 3, 2026

Copy link
Copy Markdown

Deploying super-prototyping with  Cloudflare Pages  Cloudflare Pages

Latest commit: d300f34
Status: ✅  Deploy successful!
Preview URL: https://a932e028.super-prototyping.pages.dev
Branch Preview URL: https://worktree-remotion-skill.super-prototyping.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcb077fd50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread canvas/src/App.tsx
Comment on lines +594 to +599
if (props.src !== shape.props.src || props.h !== shape.props.h) {
editor.updateShape({
id: shape.id,
type: shape.type,
props: { src: shape.props.src, h: shape.props.h },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh persisted motion shape names

When an asset's optional meta.name changes without changing the MP4 or its aspect ratio, this update is skipped and the persisted shape retains its old props.name. Although the separate caption refreshes, MotionFile still uses the stale value for the video's title and getText(), leaving canvas search and accessibility metadata inconsistent after reload; compare and update name alongside src and h.

Useful? React with 👍 / 👎.

Jing-yilin and others added 2 commits September 4, 2026 05:46
A second film, and the first one that is about the repo rather than about a
reference: seven shots at 300 frames, walking `mockups/canvases/duolingo-ios`
through the phases the clone actually ran — grid the capture, sample it region
by region, try to name the face and refuse, generate eight boards off one
token block, re-render and diff, park the reference under the replica.

The phone is not a screenshot. Every board on screen is the artboard's own
HTML in an <IFrame> out of `mockups/`, so the reel cannot claim a fidelity the
boards have since lost: edit a token and the next render shows the edit. And
every number it says out loud lives in `data.ts`, copied off that folder's two
evidence boards and its README — the three probes with the technique that took
each, the 0.353 that made `refkit font` refuse, the eight deltas and their
mean. Nothing rounded to fit a line.

The shots dip rather than dissolve. `brand-film` cross-fades because its
source does; two shots of small type overlapping would put two sets of numbers
on one frame at the moment the reel is asking to be read. Same reason every
shot's last element settles at least six frames before its own fade begins.

`index.tsx` throws at import if the cut does not sum to
`meta.durationInFrames`, so the shot table in the README cannot drift from the
film without the render failing first. `src/env.d.ts` is what lets the
comparison figure come in by `import` — `setPublicDir` points staticFile at
the artboards, which leaves `assets/` unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq
The reel is about reading a repository — a token block, a diff, a folder of
evidence — so it now sits on the ground that material is normally read on.
GitHub's light theme by its own token names: `canvas.default` for the ground,
`fg.default` and `fg.muted` for ink, one `accent.fg` blue for everything the
film points with, one `danger.fg` red for the single refusal.

The dark theme was the logo's, and three things in it only worked on black.
The boards were white-edged panels lit from behind; they are Primer cards now,
one hairline border and a soft resting shadow, because on white there is
nothing to light and a glow only fogs the board's own edge. The end card's
wordmark had a lilac bloom behind it, which is gone. And the comparison figure
has a white ground of its own, which on a white film left it with no edge at
all — Primer's border is that edge.

Two colours deliberately did not move, and the README says why. `MINOR` and
`MAJOR` stay cyan and red because `refkit grid` draws cyan every 10pt and red
every 50; they are the tool's colours, not the theme's, and they are only ever
drawn over a board. `DUO_GREEN` is `--d-u-green`, measured off the capture.
The film's accent blue took over every job `MINOR` was doing as chrome, which
it should not have been doing in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87f3acc66a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread canvas/src/App.tsx
Comment on lines +585 to +586
const missing = shapes.filter((shape) => !editor.getShape(shape.id));
if (missing.length) editor.createShapes(missing);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove previews whose renders are no longer discovered

When a canonical MP4 is deleted or an asset is renamed, an existing persisted Motion page keeps its old motion-file shape and caption because this path only creates missing current shapes and never removes generated IDs absent from assets. On the next load the stale preview remains, usually pointing at a Vite URL that now returns 404; reconcile the generated shape set against the discovered assets during ordinary initialization rather than requiring a manual force-relayout.

Useful? React with 👍 / 👎.

The reel dipped: every shot faded to nothing inside its own slot and the next
arrived on empty ground. The argument for that was that overlapping two shots
of small type would put two sets of numbers on one frame where the reel is
asking to be read. The argument was right about the type and wrong about the
join — what it actually produced was eight frames of blank white page between
every pair of shots, which is more abrupt than a dissolve and not less.

The slots in the cut are unchanged. Each shot's <Sequence> now runs OVERLAP
frames past its own and spends them getting out of the way, and the type
problem is handled where it lives, in `useJoin`. The two are kept apart in
time rather than dissolved evenly: the fall eases out — deliberately not
`leave()`, which eases in and so holds near full opacity until the last moment
— and is under a tenth six frames past the cut, and the rise waits those six
frames out. They cross at roughly 8% against 40%. Every shot writes its
heading at the same 96, 92, so that separation is the whole trick.

The rise is also what stopped three shots punching a hole through the one they
replace. Sample's board, Face's headline and Generate's token block never had
an entrance of their own, which under a dip cost nothing because there was
nothing behind them; under an overlap they came up at full opacity on top of a
shot still at 60%. They now arrive with the shot.

Two smaller things follow. The phase stamp, the only thing on screen that
changes its text without moving, now fades across the join on its own beat
instead of flicking over on the cut frame. And the end card takes six frames
of head like every other shot, so the wordmark is not rising through the tail
of the figure before it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df68f7c76c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread canvas/src/App.tsx
@@ -472,6 +724,7 @@ function initializeCanvasLibrary(editor: Editor) {
}
}

layoutMotionPage(editor, libraryPages);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Initialize motion pages without canvas boards

When rendered motion assets exist but readCanvasLibrary() returns no HTML boards, the earlier if (!library.length) return exits before this newly added call. The Motion page is therefore never created and ?canvas=motion cannot display otherwise valid renders, so initialize the motion library before the board-only early return.

Useful? React with 👍 / 👎.

The reel was written for someone who already knows what a corner fit is. It
opened on `1A · GRID`, put `--d-card-x 24.1pt / row scan at y 150` beside two
more like it, printed a refusal as `0.353 · NO CALL · the top two sit inside
0.05`, and scrolled twenty-five lines of raw `:root` past eight boards. All of
it true, none of it readable in a second and a half by anyone meeting the repo
for the first time — and a film that looks like it is proving something without
being legible is the failure mode this project exists to avoid.

So the numbers stay in `data.ts` and mostly stop at the screen. The phase
stamps become `step 1 of 6`; the eyebrows become measure / sample / typeface /
build / check. Each shot keeps one or two measurements with a plain gloss:
three sizes off the grid without the scans that took them, three probes named
by what was read rather than the token they land in, a confidence bar that is
never printed as a figure, the `:root` block under the film's own names for its
values, one mean delta with the sentence that makes 2.04 a size you can
picture rather than nine numbers with no scale.

Everything dropped is still one file away, with a comment saying where it went
and which evidence board it came off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20b3fc1853

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/motionkit.py
capture_output=True, text=True, check=True,
).stdout
s = json.loads(out)["streams"][0]
num, den = s["r_frame_rate"].split("/")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the average rate for variable-rate inputs

When probe receives a variable-frame-rate capture, this reads FFmpeg's base rate rather than its average playback rate; FFmpeg defines r_frame_rate as the rate able to represent every timestamp—“it is the least common multiple of all framerates in the stream”—whereas avg_frame_rate is the average (FFmpeg AVStream documentation). Consequently, both the displayed duration and the generated meta.json FPS can be badly wrong even after preserving fractional values, so use avg_frame_rate or derive the rate from frame count and duration.

Useful? React with 👍 / 👎.

Jing-yilin and others added 2 commits September 5, 2026 05:00
The shots overlapped but nothing crossed a join, so every cut was a
dissolve between two unrelated frames — which is what made seven shots
read as seven slides.

Three of these joins were never two frames. Shots 1 and 2 both hold
screen 01. Shot 3's headline is `.card .u` on that screen — "Order food
and drink" at 96px instead of 19.4. Shot 4's eight boards are shot 5's
eight rows. A fourth is a sentence rather than a picture: shot 3 ends on
the stand-in the tool declared, and RECIPE now opens on `typeface
ui-rounded`, which is where the board's own `:root` opens too.

So each of those moves instead of cross-fading. `useJoin` gains a carry
mode: a carried layer skips the fade on the side it is handed across,
and skips the shrink in both directions, because the shrink is there to
put a *dissolving* shot behind its replacement and a 1.5% wobble is
exactly what gives a match-move away. Five of the seven shots now return
two AbsoluteFills — the carried object, and everything that is theirs
alone. Both sides run the same ramp on their own clocks (`frames - 2`
against `frame + 2`), so they agree to the pixel on the frame they
share: measured at 0.38 MSE across the strip handover, which is the
h.264 noise floor and not a seam.

Verify→TwoRows and TwoRows→End stay dissolves. The comparison figure is
a bitmap whose top row needs 0.80 horizontally against 1.04 vertically
to line up with the strip, and a match-move that has to stretch to land
is a worse lie than a cut.

Also, since the thumbnails took 23px out of Verify's row: gap 20 -> 16,
label 250 -> 240, so `08 league promotion` still clears its bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq
Shot 1 started with the board already parked at 250, 202 beside a
heading and a readout, which buries the one thing the film is about
under the film's commentary on it. It now arrives centre frame at 0.95,
holds six frames with nothing else on screen, and walks left to 250,
202 — the heading rises as it goes, and the grid starts drawing before
it has finished settling, so the ruler comes down onto the board rather
than after it.

That makes three legs on one object with no cut in any of them: centre,
`HELD_A`, then `HELD_B` in shot 2. `between()` nests, so a board can be
mid-leg on two legs at once.

The heading waits until frame 30 because its line runs to about x 1016
at top 92 and the board crosses under it until the walk is half done.

Measure 46 -> 66, so the film is 320 frames (10.7 s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48c4a969c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/motionkit.py
return s["width"], s["height"], fps, frames


def read_gray(path, width=None):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor display rotation when probing dimensions

When a phone recording stores portrait orientation in its display matrix instead of the coded dimensions, width and height here describe the unrotated stream even though FFmpeg applies autorotation by default during decoding (FFmpeg advanced video options). probe therefore emits a landscape meta.json for a portrait clip, while read_gray derives its scale and source-pixel conversion from the wrong axes, distorting frames and reported motion. Apply the display rotation to the probed dimensions, or disable autorotation consistently when decoding.

Useful? React with 👍 / 👎.

The reel jumped from "we could not name the typeface" to eight finished
boards, which put the two phases that do the actual work off screen. The
design system was a scrolling list in the corner of the build shot, and
asset extraction was not in the film at all — so a viewer came away
thinking the boards were drawn rather than measured and cut.

Two new shots, and `Generate` gives up the list it was carrying.

`System` (step 4) is the block on its own: the face on a rule, twelve
swatches, nine measurements, and the line the whole repo turns on —
nothing here was chosen, every value was read off the screenshot. Split
by what the value is rather than by index, so adding a colour to RECIPE
puts it in the swatches without touching the shot.

`Art` (step 5) draws six boxes out of `crops.json` on screen 01, in the
screen's own pt, then shows the six PNGs those boxes cut, at 1.55x on a
common baseline because their heights are the measurement. 128 pieces,
and one sentence for the comparison: cut, they are the original's own
pixels; redrawn, the best attempt still moves things. The four scores
behind that sentence (0 · 3.96 · 18.41 · 38.53) are in `data.ts` above
CROPS, with the size effect that keeps icons in CSS.

`Generate` (step 6) is now only what it claims: one script, two inputs,
eight screens. The strip re-centres to x 359 now that nothing sits to
its left.

Screen 01 is on screen continuously from frame 0 to frame 282 — five
placements, six shots, no cut in any of them. It comes back at 0.48
beside the block that describes it, grows to 0.78 to be cut up, and
flies into the first slot of the strip. Six of the eight joins are now
carried rather than dissolved.

The crop boxes leave on their own ramp (40, over 10) rather than the
shot's, because that layer does not fade — `Generate` flies the same
board on — so a box still 90% up on the last frame popped off on the
cut: 86.05 MSE across the handover, against 2.52 once it is gone. The
2.52 that is left is the iframe rasterising a second time — taking the
spent overlay out of the tree entirely does not move it — and at 44 dB
it is not a seam.

`crops.json`'s six boxes and the 128 count reach `data.ts`, and
`assets/art/` turns out to be committed, so the PNGs load through
`staticFile` the same way an artboard does. Nothing new is committed
for the film.

420 frames (14.0 s), eight numbered steps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfa0a1f50c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread motion/package.json
"name": "motion",
"version": "1.0.0",
"private": true,
"license": "MIT",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the package license to the repository

This package advertises the newly added motion code as MIT, but the root LICENSE and README.md state that the repository's own code is Apache-2.0. Even though the package is private, SBOM and license-scanning tools consume this field and will misclassify the code's licensing; use the repository's Apache-2.0 identifier unless this subpackage is intentionally relicensed and accompanied by the appropriate license terms.

Useful? React with 👍 / 👎.

The shot was spending its second half on a progress bar under "how sure
the match is", drawing FONT_SCORE as a length. Two problems with it. A
viewer who has not been told what a glyph-shape score is reads a short
bar as a loading indicator, not as a refusal — and a score is not what
this step produces. The step produces a size.

So the bar and the TOO CLOSE TO CALL row go, and what replaces them is
how a size is actually read: the height of a capital divided by how tall
capitals are in the face you set it in. Then the two ratios that number
turns on — 0.714 assumed off another face, 0.762 measured on this one —
and what the gap cost: every size 6% too large until the face itself was
measured on a render rather than the capture.

Which is also what finally makes the failed match matter on screen
rather than being a footnote. You cannot read a size off a screenshot
without knowing the face, so the shot that admits it could not name one
now says what that admission cost. 0.353 stays in `data.ts`, where a
reader can see it in context; the film never prints it.

CAP joins data.ts, cited to the canvas README's "What the file said, and
what the renders corrected". Face grows 46 -> 54 frames so the four
beats settle six frames before the fade; every start after it shifts by
eight and the film is 428 frames (14.3 s). Starts are computed from CUT,
so only meta.json and the README table needed the new numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzapdP1QwGhzCF5AA4Dwqq
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant