---
title: The two transition libraries
summary: 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.
lede: Pick a transition that does not cost you an afternoon
eyebrow: Best practice
accent: wave
order: 2
---

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.
