Add motion/: one folder per animation asset, discovered like the boards - #13
Open
Jing-yilin wants to merge 24 commits into
Open
Jing-yilin wants to merge 24 commits into
Jing-yilin wants to merge 24 commits into
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
motion/, the video half of whatcanvas/already does for the artboards: drop a folder intomotion/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 doesremotion.config.tspoints the public dir at../mockups, so a composition reaches a board withstaticFile("canvases/luma-ios/01-guest-top.html")and a photo withstaticFile("canvases/apple-photos/assets/photos/01-minerva-1.jpg"). Motion consumes mockups, which is why it is a sibling ofmockups/rather than a folder inside it — the same relationshipcanvas/has.The bit worth reviewing:
meta.jsonTwo 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.jsonis a sidecar rather than a field in the TSX:Discovery in
Root.tsxusesrequire.context— that is rspack's API.import.meta.globis 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.allowopens the repo root because?urlassets outsidecanvas/are served, unlike the boards'?rawHTML 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.pyThe 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.flowis the one that earns its keep.spatial-gallerylooks 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 compositionslistsspatial-gallery 30 1080x864 120from folder structure alonemotionkit probeprintsmeta.jsonbyte-identical to the committed filemotionkit flowon 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 selftestpasses (phase correlation recovers known shifts exactly)lint/test/buildclean;motiontsccleanThe measurements taken off the third-party reference clip are committed; the clip itself is not — the same split
.gitignorealready makes forref-*.htmlboards.The fifteen
One 68-second film taken apart into the fifteen things it actually does:
mesh-gradientcount-up,word-cascade,text-marker,word-swap,word-gridorb-bloom,bokeh-orbit,particle-form,depth-flythroughcard-stack,focus-pull,lens-revealpill-expand,logo-outroThey exist to be cut together, and that is the whole reason for the interface: every one takes
durationInFramesas a prop rather than reading it fromuseVideoConfig. Inside a<Sequence>,useVideoConfig().durationInFramesstill 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 outsideRoot.tsx'srequire.contexton 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 wholebackgroundshorthand. The mesh painted no blobs at all, with no error anywhere, until this becameellipse R% R%.width/heightdoes not survive being a flex item next to something wide: it shrinks on the cross axis and the sphere comes out an egg. Theflex: noneguard lives inOrb.tsx, not at the one call site that showed the bug.loadFont()fires 126 requests for Inter at the head of every render.fonts.tspins 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-outrosets 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.gitignorealready requires.src/lib/README.mdcarries 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 WxHarea-averages a frame into cells — the shape of a gradient, as hexswatch <clip> <frame> --crop W:H:X:Ycensuses one region at full resolution — the exact hex of a chipsheet --from/--tocuts a contact sheet down to one shot, which is how "the gradient moves" became forty frames you can fit a curve toBoth 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-swapwas replicating an effect the film does not contain. Itcrossfaded 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-flythroughscattered its orbs. Each got a random x and y inside awanderbox — the obvious way to build "a crowd of spheres", and it piledthem 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.
wanderand itsseedaregone;
vanishandoffsetreplace them, and screen x now runs through thesame 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:
orb-bloomtobokeh-orbitcountparticle-formrimdepth-flythroughbubble paddingpx * kempx * kinside a box whose type already scaled byk, so it scaled twice and the near bubble came out all paddingorb-bloom's chips now also leave over the last ten frames, which is wherethe 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 sheetcommandthat 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 --noEmitclean; all sixteen compositions render via./render.sh(5.6 MB of CRF-28 previews total)spatial-galleryproduced a byte-identical mp4, which is the determinism rule holdingThe
filmsbucket, and what assembling the set foundsrc/films/brand-film/is the fifteen templates cut back into one 43.2 spiece, 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 wasbuilt to make, exercised for the first time: inside a
<Sequence>,useVideoConfig().durationInFramesstill reports the film's 1296 frames, so atemplate 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-cascade0.19 → 0.247,bokeh-orbit0.13 → 0.3,orb-bloom0.045 →0.09,
focus-pull0.13 → 0.29,logo-outro0.062 → 0.107,card-stackandpill-expandwhole 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 (theitalic belongs to
word-grid), its bullets are flat pale discs rather than litorbs, and
card-stack's cards are one key light washing across the row insteadof eight differently coloured cards.
motionkit extentThe 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 givedirectly comparable fractions of frame.
comparecannot answer this question — it wants a frame-aligned pair, and43.2 s against 68.4 s in a different shot order is not one.
selftestcoversextentwith a bar of ink on a full-frame gradient: it must find the bar andnot the ramp, within the halo that subtracting a blur necessarily leaves.
src/films/brand-film/README.mdcarries the cut table, the per-shot ratiotable 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