custom-transitions
Skill6 starsChanged 19 days agoMITView on GitHub
How to write a new scene transition as an inline JSON definition — the slot contract, the token vocabulary a definition may read, the four mechanisms a transition can be built from, and the frictions that make a plausible-looking definition render wrong. Every rule here was rendered, and the numbers are what was drawn. Use when authoring or reviewing a transition definition, or when a transition renders at the wrong speed, cuts, or is refused by the validator.
In this repository67 other files it gives its agents.
- ai-presenterSkill
- animate-designSkill
- app-ui-animationSkill
- asset-auditSkill
- assetsSkill
- background-musicSkill
- caption-decorationsSkill
- character-riggingSkill
- design-layer-importSkill
- drama-series-writingSkill
- edit-footageSkill
- edit-on-your-computerSkill
- edit-pipelineSkill
- face-hidingSkill
- figma-storyboard-animationSkill
- film-animaticSkill
- film-cast-designSkill
- film-cuttingSkill
- film-finishingSkill
- film-shot-generationSkill
- film-story-writingSkill
- footage-analysisSkill
- footage-reframingSkill
- generate-mediaSkill
- get-startedSkill
- large-scene-assemblySkill
- linked-element-motionSkill
- make-characterSkill
- make-comicsSkill
- make-screen-videosSkill
- make-short-dramaSkill
- motion-design-craftSkill
- motion-intentSkill
- motion-pattern-librarySkill
- motion-reviewSkill
- motion-scene-continuitySkill
- motion-tracking-from-videoSkill
- movie-producerSkill
- open-picsart-appSkill
- photo-slideshowSkill
- picsart-add-mediaSkill
- picsart-video-rulesSkill
- quality-reviewSkill
- quick-merge-and-exportSkill
- realistic-color-gradeSkill
- recastSkill
- recreate-videoSkill
- reel-structureSkill
- reference-fragment-matchingSkill
- resting-frame-rulesSkill
- retention-hooksSkill
- scene-3d-authoringSkill
- scene-animation-referenceSkill
- scene-building-basicsSkill
- scene-template-remixSkill
- screen-recording-analysisSkill
- screen-recording-layoutsSkill
- screen-recording-zoomSkill
- screen-ui-redesignSkill
- sd25-peSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 24
Before writing a definition, require four inputs:
- Very specific: line 31
If any of these is still undecided, return to motion-scene-continuity (fetch with picsart_scene_recipe, name motion-scene-continuity).
- Very specific: line 283
If the definition claims duration independence, render the SAME definition at two materially different duration values.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Writing a transition
A transition is DATA: a JSON definition, declared inline as a type: "transition"
asset. This is what the definition has to say, and what goes wrong.
Check what already exists first. picsart_scene_capabilities({sections:
["transitions"]}) returns every built-in transition with its id, description
and parameters; if one of them does what you need, apply it by id and stop
here. Authoring is for the case where none of them does.
Incoming continuity contract
This skill defines an already-decided seam; it does not decide what two beats should mean together. Before writing a definition, require four inputs:
- the semantic relationship — continuation, replacement, reveal, arrival, interruption, or reset;
- the intended travel vector, or an explicit decision that this boundary resets it;
- the visual carrier, when the viewer should be able to follow one;
- the concrete reason no shipped catalog transition expresses that relationship.
If any of these is still undecided, return to
motion-scene-continuity (fetch with picsart_scene_recipe, name motion-scene-continuity). Do not invent a custom transition merely
because the adjacent scenes feel disconnected; the representation may instead be one persistent
layer, a hard cut, or an existing crossfade or directional transition.
The shape of a definition
Two parts: meta identifies it, scene IS a scene — the same document model as
anywhere else, with two extra powers.
{ "meta": { "id": "crossfade", "displayName": "Crossfade",
"description": "Linear dissolve from source to destination: the two weights always sum to one, so the background never shows through an opaque pair." },
"scene": {
"version": "1.0",
"composition": { "width": {"$eval":"width"}, "height": {"$eval":"height"},
"fps": 30, "duration": {"$eval":"duration"} },
"layers": [
{ "id": "source", "start": 0, "duration": {"$eval":"duration"},
"content": { "kind": "slot", "role": "source" },
"animations": { "opacity": { "keyframes": [
{ "time": 0, "value": 1 }, { "time": {"$eval":"duration"}, "value": 0 } ] } } },
{ "id": "destination", "start": 0, "duration": {"$eval":"duration"}, "blendMode": "add",
"content": { "kind": "slot", "role": "destination" },
"animations": { "opacity": { "keyframes": [
{ "time": 0, "value": 0 }, { "time": {"$eval":"duration"}, "value": 1 } ] } } } ] } }That is a complete, shipping transition. The destination ADDS onto the fading
source inside the firing's own canvas, so the weights are t and 1 − t and sum to
one. Two normal fades would each let the composition background through: at
the midpoint a red→red crossfade over green read G = 132, a 25% background
share.
The two extra powers:
content: { "kind": "slot", "role": "source" | "destination" } — a hole
filled at render time with the outgoing clip's last frame or incoming clip's first frame. A
definition never names a clip; it arranges two boundary-frame slots. Exactly one of each role. The
definition animates those supplied frames on its own clock; neighboring clip animation does not
continue live through the transition window. If the seam needs a genuinely live shared object,
keep that object alive outside this slot contract.
Firing-supplied tokens. width, height and duration are handed in by the
call site and are NOT declared in parameters. Read them as {"$eval":"width"}
or inside an expression as {"$eval": "width / 2"}.
What a definition may read
| Source | Declared where | Read as |
width, height, duration | nowhere — the firing supplies them | {"$eval":"duration"} |
| Your own knobs | scene.parameters | {"$eval":"softness"} |
| The selected variant's row | scene.variants.table.<row> | {"$eval":"sweep"} |
{"$eval": "..."} computes over all three. The grammar is small on purpose — it
admits min, max, abs, floor, ceil, round, sqrt, sin, cos,
atan2(y, x) (angles in radians) and nothing else. There is no hypot and no ternary; and/or/not exist for $when gates.
The frame diagonal is sqrt(width * width + height * height), so a circle that
must cover the frame from its centre needs a diameter of exactly that.
Names that are refused rather than shadowed: the grammar's own
(min max abs floor ceil round sqrt sin cos atan2 and or not true false), the firing-supplied
three, and a variant row's own selector. The check covers EVERY row, not the
selected one — a clean row does not excuse a broken sibling.
Variants
One definition, many looks. by names the selector param; each row's fields join
the scope for that firing.
"parameters": { "variant": { "type": "text", "default": "circle",
"enum": ["circle", "star", "bar"] } },
"variants": { "by": "variant", "table": {
"circle": { "isCircle": true, "isStar": false, "isBar": false },
"star": { "isCircle": false, "isStar": true, "isBar": false } } }Gate whole layers or shape items on a row field with "$when": "isStar". A row
field beats a firing param of the same name.
The four mechanisms, and what each costs
| Built from | Example | Needs an asset | Fits any duration |
| Opacity | crossfade | no | yes, by construction |
| Transform | slide, diagonal_slide | no | yes, by construction |
| Effect | blur_dissolve | no | yes, by construction |
| Mask — shape layer | iris, shape_reveal | no | yes, by construction |
| Mask — video/lottie matte | none shipped (push, splatter were) | yes, remote | no — see below |
The first four animate on the DEFINITION's own clock. Their keyframes are written
at {"$eval":"duration"}, so whatever window the author asks for is the window
they fill. Nothing else is needed.
Prefer a shape mask over a video matte. It needs no asset, no network,
no external renderer, and it cannot drift out of time with its window. Never
ship a .webm matte: iOS (AVFoundation) cannot decode WebM, so the incoming
clip just cut in there — which is why push, bubble, cubes, free_fall,
kaleida and splatter were removed from the catalogue. The matte notes below
are kept for a matte in a format supported by the intended playback environment.
Keep a definition cheap enough to play in real time on a phone. A blur's cost
grows with its radius times the area it covers, and a firing pays it on every frame
of the window. movement_camera peaked at a full-frame whip blur, 260 px at 720p,
which cost up to ~180 ms a frame at the cut and stalled playback, so it was removed
from the catalogue too. Blur a small layer, or keep the radius low.
iris does a circle, a six-point star and a widening bar from one definition:
"mask": { "invert": false, "clip": {
"id": "iris_mask",
"transform": { "position": [{"$eval":"width / 2"}, {"$eval":"height / 2"}] },
"content": { "kind": "shape", "items": [
{ "$when": "isCircle", "type": "ellipse",
"size": [{"$eval":"width * 1.6"}, {"$eval":"width * 1.6"}], "position": [0,0] },
{ "$when": "isStar", "type": "polystar", "starType": "star", "points": 6,
"position": [0,0], "outerRadius": {"$eval":"max(width, height) * 1.45"},
"innerRadius": {"$eval":"max(width, height) * 0.80"} },
{ "type": "fill", "color": "#ffffffff" } ] },
"animations": { "scale": { "keyframes": [
{ "time": 0, "value": [0.001, 0.001], "easing": "ease_in" },
{ "time": {"$eval":"duration"}, "value": [1, 1] } ] } } } }Measured on a 640×480 frame, iris/circle at duration 0.4, 1.0 and 2.0 gave
the SAME progression every time — 0% / ~5% / 66% / 100% across the window. Circle,
star and bar read 1.1 / 26.9 / 97.0, 1.8 / 45.5 / 100, 1.0 / 26.5 / 100 at
t = 2.7 / 3.0 / 3.3 through a 1s window.
shape_reveal is the larger worked example: twenty-one choreographies, with the
geometry written as fractions of width/height so a circle stays a circle at
every aspect. Its five wipe_* rows are the exception: an edge at any angle
is drawn in units of the frame diagonal inside a group that rotates by angle
and scales by diagonal / 100, so it covers every aspect at every angle. Those
rows also supply lineShape and panelShape for the accent layers (a divider
stroke drawn over the blur band, a colour panel under the destination); every
other row supplies them empty, because a body may only read a row key every row
carries. Each variant's row declares its shape tree ONCE, and three layers
draw it with "items": {"$eval": "shape"} (the sharp mask, the blur band, and
the band's lagged cut). The lagged copy is that same tree on a later clock:
"start": {"$eval": "0.22 * duration"}, "timeMode": "layer". A later start
alone does NOT delay it, because keyframe times are composition seconds by
default. The lagged copy then moved in lockstep and the blur band disappeared.
One more friction it measured:
- Land the motion before the window ends. A track keyed to end exactly at
durationon an ease-out is still short of its end pose on the last frame at short windows (0.4s). Its blurred band then cleared in one frame. Keying both copies to land by0.9 * durationleft the last frames clean at 0.4s, 1s and 2s, and at 24 fps.
Repeat a mask piece with $each over a number. In a definition, $each
may name a whole number N (a parameter or a row field) and makes N copies whose
item is the copy's index. tile_mosaic nests {"$each": "rows", "as": "r"}
and {"$each": "columns", "as": "c"} inside the mask's shape items, one
group per tile, each with its own opacity keyframes timed from r and c.
The variant picks the order through numeric row weights (byRow,
byDiagonal, byRandom): a row field is filled at the definition's top scope,
so it cannot read r or c itself. A fraction or a negative count is refused with invalid_each.
The shape vocabulary is ellipse, rect, polystar, path, fill, stroke,
gradientFill, gradientStroke, group, trim, repeater, roundCorners,
offsetPaths, zigZag, merge.
Frictions
A shape item discriminates on type, not kind. Content is kind, shape
items are type — they mirror Lottie's own item vocabulary. Getting it wrong fails
with missing_field on the item's type, listing every tag it could have been. Their
names are also camelCase (gradientFill, roundCorners, zigZag) while the
rest of the format is snake_case.
The window is CENTRED on the cut and eats duration/2 out of EACH neighbouring
clip. A 7.25s transition needs 3.625s of both. Too long for the clips and the
validator refuses the whole document with transition_window_exceeds_clip. Size a
transition against the shortest clip it sits between, not against the composition.
A time-based matte does not fit the window by itself. A .webm or a lottie
plays at 1× from its own zero. Too long a window and the wipe finishes early and
then the matte runs out before the window ends. Too short and it is a hard cut: splatter/blood_drop_1 in a 1.0s
window reached 9.8% and then gained 90 points in a single frame. The fix, which
every matte family carries, is
to map the window onto the matte's own travel time. lead is a second row field:
the last source second at which the matte still equals its first frame, so the
window does not spend its opening standing still (cubes/style10 holds for
1.08s of a 1.63s matte, and without lead moved only in the last third):
"trim": { "in": { "$eval": "lead" }, "out": { "$eval": "sweep" } },
"speed": { "$eval": "(sweep - lead) / duration" }sweep is a variant-row field holding the matte's active source extent in source
seconds — measure it as the first frame where no more than ~1.16% of the
matte's pixels remain above 0.5 alpha, the rule every shipped matte row
follows. speed is refused
without an explicit trim: time remapping needs a known source extent, and the
renderer cannot probe for one. A matte definition with no sweep is refused the same way.
A matte whose edge travels the full frame in one direction (push) must fill;
a shaped matte keeps cover; verify at 9:16 as well as 16:9. cover crops a
16:9 matte to its middle 32% on a portrait frame, so push's left/right edge
crossed the visible strip in one matte frame and rendered as a hard cut; fill
maps the whole travel onto any frame. A shaped matte (a splatter, a bubble)
would distort under fill.
A shaped matte that keeps cover pays for it in time instead. A portrait frame
crops the 16:9 matte to its centre, which clears first, so a single sweep
finished splatter/blood_splatter_1 at 45% of a 9:16 window and held. Cover-fit
rows therefore carry sweep_tall (9:16) and sweep_square (1:1) next to
sweep (16:9), and the trim end interpolates between them on the frame's aspect:
"out": { "$eval": "sweep_tall + (sweep_square - sweep_tall) * min(1, max(0, (width / height - 0.5625) * 16 / 7)) + (sweep - sweep_square) * min(1, max(0, (width / height - 1) * 9 / 7))" }speed is that same expression, minus lead, over duration. Measure each
value with the same residue rule as sweep: the first frame where no more than
the row's 16:9 residue remains, rendering the matte under its own cover fit on
a 9:16 frame for sweep_tall and on a 1:1 frame for sweep_square.
A splatter matte leaves an edge residue on 16:9. Stopping at the ~1.16%
threshold leaves ~1–1.6% of the outgoing clip at the frame edges, which drops
out on the window's last frame. That is accepted: holding the matte until it is
fully clear would spend most of the window on an almost-empty tail, and cover
crops the residue away on 9:16.
A mask is an owned clip. mask.clip spans the host layer's window, and its
keyframes count from the host's start. It carries its own transform,
transforms[] and animations, so an animated shape mask animates. A mask that
starts later than its host (a lag matte) puts a composition in the clip's
content, with the delayed layer inside it — see shape_reveal's lag mask.
The non-active part of a clip is its first or last frame. A transition shows the outgoing clip's last frame and the incoming clip's first, not live playback of both. Design the beat around that.
Verifying one
The definition is data, so the only question worth asking is what the renderer
drew. Validate it with picsart_scene_validate, then render full-resolution stills at the
sample times below in ONE call per duration:
picsart_scene_contact_sheet with times: [t1, t2, …] and resolution set to the scene's composition.width and composition.height. Without resolution the sheet returns downscaled thumbnails, which are not for judging position. The sheet image scales every frame to fit one picture; to judge full-resolution detail, open a frame's url.
Sample ACROSS the window, not at its midpoint: the failures above all look fine
at 50%. For a window [c - d/2, c + d/2] take at least 0.0, 0.25, 0.5, 0.75,
1.0 of it, and measure coverage rather than eyeballing — the incoming clip's
share of the frame, frame by frame, is what tells you whether it swept or cut.
If the definition claims duration independence, render the SAME definition at two materially
different duration values. If it claims aspect independence, render at 16:9 and 9:16. A
transition tested at one duration or one frame shape may still be useful, but scope the claim to
that tested case. A matte authored for one frame shape is fit onto every other, and a crop can hide
the travel that makes it a transition.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

