The two transition libraries
GroundSlate renders transitions two ways, in ffmpeg between clips and in the browser inside a page. A move's cost on one side tells you nothing about its cost on the other. How to read both catalogs and pick a transition that does not cost an afternoon.
A transition between two shots is either rendered by ffmpeg from two finished clips, or drawn by the browser inside the page being captured. GroundSlate ships both. They are not one library and a copy of it. They are two renderers, good at different things.
Writing your film as HTML does not decide which one you use. A composition can be rendered to clips and joined with the ffmpeg catalog, or handed to the join as one file with a time window per scene. One continuous page makes sense when kinetic type or word-level sync has to run across a transition, because that cannot be split into separate files. Cutting into shots is just as valid. Neither is the default.
The ffmpeg catalog
list_transitions()
Camera moves and 3D turns between two clips, each with the length it was designed for and a cost tier. render_transition puts one move between two clips; render_sequence assembles N clips with N-1 moves in one call.
Thirteen are cost: light and eighteen are cost: heavy. Light moves are scaling and overlay work: under a couple of seconds per join. Heavy moves are per-pixel work (a perspective warp, a displacement field or a radial deformation) run by filters built into the app's ffmpeg. They take five to thirty seconds per join at 1080 by 1920, and 21 to 124 seconds at 4K, measured on an M-series Mac. The whole dynamic family is heavy, as are all four 3D turns, pull_back, dolly_zoom and lens_warp. ground_shake is heavy too, though it does no per-pixel work.
On a film with a transition on every beat this adds up, because every draft render pays for each transition again. Read cost from the catalog, not from the name.
list_transitions(detail="motion") adds kinds to each row, which describes what the move does to pixels rather than how it feels on a cut. It also adds carried_by, the cross-reference between the two libraries.
The page catalog
list_transitions(engine="html")
A hundred moves in eight families, listed by the same tool, so you can see what is there without fetching anything. engine="all" lists both libraries at once.
page-editorial and page-optical are the restrained ones: pushes, focus pulls and optical handovers. page-reveal and page-mask are wipes and shaped reveals. page-fluid is for a cut that should feel liquid: its blur is computed from the speed of the move's own easing, so the smear follows the motion. page-glass is one frosted effect in twenty shapes. page-3d and page-rig turn groups of elements under the stage's own pinhole camera, and the last few rigs are elastic: they overshoot their stop and settle back with a fading shake.
Three names, card_flip, cube_rotate and door_swing, carry a shared flag. They are generated from the ffmpeg catalog and match it in move, easing, duration and direction. They are not promised to match pixel for pixel; that has not been measured.
To use them, copy the files into your project:
get_kit("transition kit", include_notes=True)
You get gs-transitions.js and moves.js back as text to save beside your page. include_notes=True adds the kit's NOTES.md, which holds the measurements and the false positives described below. The files are copied rather than injected by the renderer, so a composition stays self-contained and runs in a plain browser.
The two rules the page side does not bend
Every move is a pure function of t. GroundSlate renders a page by seeking it: window.__seek(t), screenshot, next frame. It never plays the page. So no transition:, no @keyframes, no animation:, and no state carried between seeks. A CSS transition starts on the change and is mid-flight when the screenshot is taken, so the same page renders differently on a faster Mac.
A transition overlaps its two scenes; it does not add time. The film runs the total of the scene lengths minus the total of the transition lengths. The ffmpeg side uses the same arithmetic, so a film comes out the same length either way.
Expect preview_html to report seek_not_pure at a 3D transition. The kit's notes show this is a false positive: the pixel difference is only on the edges of strokes inside a layer the compositor promotes, which does not re-rasterise bit for bit.
The costs invert, which is why there are two libraries
cost is what ffmpeg spends, and it does not carry over. The four 3D turns are heavy in ffmpeg because it computes a perspective projection per pixel, and a browser does that in hardware, correctly and for free. The moves the page library does best are among the moves ffmpeg does worst. Never judge an in-page move by its cost tier; it will be wrong in both directions.
So a page row has no cost. It has a measured number instead.
sec_per_frame, and reading the right column
Every page row reports sec_per_frame as {still, video}: the measured seconds to capture one frame while that move is on screen, over <img> scenes and over <video> scenes. The two differ by roughly ten times, so read the right one.
Over stills, a frame with no transition costs about 25 milliseconds. cross_wipe adds almost nothing, at around 31, and the other wipes sit between 37 and 49. The turning rigs are the expensive end at roughly 200. That is a real difference, but still affordable across a few short transitions.
Over video scenes the baseline is already about 177 milliseconds, and the same rigs run 600 to 750. A video frame waits for a decode and a paint per element, and a 3D transition copies each scene into a blurred background that has its own <video>, so two scenes become four decodes. A ten second 1920 by 1080 film with two video scenes joined by cube_rotate took 141 seconds to capture.
If your scenes are stills, read still and spend freely. Once a scene holds footage, a 3D transition on it is no longer cheap. A move with no sec_per_frame has not been measured; that does not mean it is free.
The seven that must be rendered, never imitated
Seven ffmpeg moves have no browser equivalent: pull_back, dolly_zoom, lens_warp, warp_morph, swirl_morph, ripple_bloom and liquid_swipe. They are per-pixel displacement, and the page library leaves them out on purpose.
Do not fake one in CSS. A blurred crossfade under the same name is worse than not having the move. If one of them really is the right transition, render it. All seven are heavy, so render it once, not on every draft.
Picking, in one line each
- Two finished clips, and you want the move at full quality: ffmpeg, from the light thirteen unless you have a reason.
- A transition that type or word sync has to run across: the page, inside one composition.
- A card flip, a cube, a door, or anything that turns: the page, where the perspective is free.
- A liquid join, a frosted handover, a wipe: the page. There is no ffmpeg counterpart.
- A warp, a swirl, a ripple, a liquid swipe, a dolly zoom, a lens warp, a pull back: ffmpeg, once.
On a short with dialogue, cut between the shots of a conversation instead of reaching for either library. The snap moves inside the shots are the motion and the cuts are the rhythm. Offer a transition when a cut changes place or time, and let the creator ask for it.