@impractical-ai/motion 0.1.7 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/engine.d.ts +154 -23
- package/assets/impractical/SKILL.md +35 -263
- package/dist/{api-DR3NJ7TI.js → api-4WE4LYPB.js} +2 -2
- package/dist/{chunk-Z74SLKEU.js → chunk-6FVZ6IWR.js} +6 -3
- package/dist/{chunk-MQKW7NFI.js → chunk-CZANULCU.js} +1 -1
- package/dist/{chunk-M7SHFM25.js → chunk-WB7FRXAG.js} +57 -4
- package/dist/{commands-R3LJU2BW.js → commands-YJFMKK2K.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/{stdio-XUVQKDCB.js → stdio-S7VGBKNS.js} +2944 -1466
- package/package.json +3 -3
- package/assets/kits/actions.ts +0 -803
- package/assets/kits/ensemble.ts +0 -483
- package/assets/kits/highlight.ts +0 -374
- package/assets/kits/impact.ts +0 -103
- package/assets/kits/kits.ts +0 -343
- package/assets/kits/looks.ts +0 -935
- package/assets/kits/morphkit.ts +0 -438
- package/assets/kits/reveal.ts +0 -1364
- package/assets/kits/sentence.ts +0 -218
- package/assets/kits/streamkit.ts +0 -195
package/assets/engine.d.ts
CHANGED
|
@@ -41,6 +41,75 @@ export type TransitionResult = {
|
|
|
41
41
|
root: HTMLElement
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** What the camera dives through. Every mode computes its own geometry —
|
|
45
|
+
* never hand-tune margins or wrap glyphs in spans to fake these. */
|
|
46
|
+
export type ThroughVia =
|
|
47
|
+
| 'surface' // the element's face: its rect covers the frame (default)
|
|
48
|
+
| 'glyph' // one character's COUNTER (the enclosed hole) covers the frame
|
|
49
|
+
| 'word' // one word's rect covers the frame
|
|
50
|
+
| 'anchor'
|
|
51
|
+
|
|
52
|
+
export type ThroughOpts = {
|
|
53
|
+
/** Dive mode. Default 'surface'. 'glyph' needs `char` (or `charIndex`),
|
|
54
|
+
* 'word' needs `word`; both address plain text — no wrapper spans.
|
|
55
|
+
* 'anchor' dives toward the element and, at the cut, match-frames the
|
|
56
|
+
* element matching the SAME selector in the post-swap world (its twin),
|
|
57
|
+
* so the subject persists while everything around it changes. */
|
|
58
|
+
via?: ThroughVia
|
|
59
|
+
/** glyph mode: the character to dive through — first occurrence in the
|
|
60
|
+
* target's text. The dive passes through its enclosed counter (o, e, a,
|
|
61
|
+
* 0…); characters without a hole throw with a diagnostic. */
|
|
62
|
+
char?: string
|
|
63
|
+
/** glyph mode: 0-based index into the target's text, instead of `char`. */
|
|
64
|
+
charIndex?: number
|
|
65
|
+
/** word mode: the word to dive through — first occurrence. */
|
|
66
|
+
word?: string
|
|
67
|
+
/** Takeover color. Defaults: surface/anchor — the target's effective
|
|
68
|
+
* background (first non-transparent backgroundColor walking up from the
|
|
69
|
+
* target); glyph/word — the surface BEHIND the text (the walk starts at
|
|
70
|
+
* the text element's parent), i.e. what actually shows through the
|
|
71
|
+
* counters. */
|
|
72
|
+
color?: string
|
|
73
|
+
/** Dive duration in seconds. Default 0.5. */
|
|
74
|
+
duration?: number
|
|
75
|
+
/** Dive ease. Default 'power3.in' (accelerates into the surface). */
|
|
76
|
+
ease?: string
|
|
77
|
+
/** Cover overshoot: 1.0 = the dive geometry (element rect, glyph counter,
|
|
78
|
+
* or word rect per `via`) exactly covers the frame at the cut. Default
|
|
79
|
+
* 1.15, so a few px of drift can never expose an edge. Override only for
|
|
80
|
+
* taste — every mode measures its own geometry, so no mode NEEDS a
|
|
81
|
+
* hand-tuned margin. */
|
|
82
|
+
margin?: number
|
|
83
|
+
/** The world swap, applied as seek-safe `tl.set`s at the exact cut moment
|
|
84
|
+
* while the frame is fully covered: `hide` gets autoAlpha 0, `show` gets
|
|
85
|
+
* autoAlpha 1. For anything richer, place your own `tl.set`s at the
|
|
86
|
+
* returned `cutAt` time. */
|
|
87
|
+
swap?: { hide?: gsap.TweenTarget; show?: gsap.TweenTarget }
|
|
88
|
+
/** Element in the post-swap world to ARRIVE from: at the cut the camera
|
|
89
|
+
* snaps to this element's own cover framing (measured after the swap), so
|
|
90
|
+
* releasing the overlay reveals it filling the frame, and the pull-out
|
|
91
|
+
* starts inside the new world — the match-frame landing. Omit to pull out
|
|
92
|
+
* from the dive's own framing. */
|
|
93
|
+
land?: string | Element
|
|
94
|
+
/** Framing the pull-out settles on. Default the full frame
|
|
95
|
+
* `{ scale: 1, x: 0, y: 0 }`. */
|
|
96
|
+
settle?: { scale: number; x: number; y: number }
|
|
97
|
+
/** Pull-out duration. Default 1.0. */
|
|
98
|
+
outDuration?: number
|
|
99
|
+
/** Pull-out ease. Default 'expo.out'. */
|
|
100
|
+
outEase?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type ThroughResult = {
|
|
104
|
+
/** Full span: dive + pull-out. */
|
|
105
|
+
duration: number
|
|
106
|
+
/** Absolute timeline time of the cut — the frame is guaranteed fully
|
|
107
|
+
* covered here; schedule extra world-swap `tl.set`s at this time. */
|
|
108
|
+
cutAt: number
|
|
109
|
+
/** The takeover overlay's root element. */
|
|
110
|
+
root: HTMLElement
|
|
111
|
+
}
|
|
112
|
+
|
|
44
113
|
export type TransitionKit = {
|
|
45
114
|
/** Cover the frame — put this at the end of a clip. Appends to the
|
|
46
115
|
* timeline's end when no position is given. */
|
|
@@ -56,6 +125,29 @@ export type TransitionKit = {
|
|
|
56
125
|
opts?: TransitionOpts,
|
|
57
126
|
position?: gsap.Position
|
|
58
127
|
) => TransitionResult
|
|
128
|
+
/** The go-through: dive the camera INTO something until its color takes
|
|
129
|
+
* over the whole frame, cut worlds under the cover, then pull out into the
|
|
130
|
+
* new world. NEVER hand-roll this (a magic-scale zoom + a hand-timed
|
|
131
|
+
* `tl.set` swap + a re-aim) — through() computes the exact cover scale for
|
|
132
|
+
* its dive geometry, measures it in its state AT THE DIVE'S START TIME
|
|
133
|
+
* (entrances, moves, and tilts since build are all accounted for), and
|
|
134
|
+
* snaps a full-frame overlay in the right color at the cut so the
|
|
135
|
+
* takeover is guaranteed to the pixel. Four dive modes via `opts.via`:
|
|
136
|
+
* 'surface' (default — the element's face), 'glyph' (through one
|
|
137
|
+
* character's enclosed counter: `{ via: 'glyph', char: 'o' }` on plain
|
|
138
|
+
* text, no wrapper span — the hole is measured from the actual glyph
|
|
139
|
+
* outline, so the margin is computed, never tuned), 'word' (`{ via:
|
|
140
|
+
* 'word', word: 'world' }` — the word's rect), and 'anchor' (match-cut:
|
|
141
|
+
* dive toward the element, swap the world at the cut, and land on the
|
|
142
|
+
* element matching the same selector in the new world, so the subject
|
|
143
|
+
* persists while everything around it changes). Deterministic: every
|
|
144
|
+
* value is resolved once at build; the swap is `tl.set`s, never
|
|
145
|
+
* callbacks. Appends at the timeline's end when no position is given. */
|
|
146
|
+
through: (
|
|
147
|
+
target: string | Element,
|
|
148
|
+
opts?: ThroughOpts,
|
|
149
|
+
position?: gsap.Position
|
|
150
|
+
) => ThroughResult
|
|
59
151
|
}
|
|
60
152
|
|
|
61
153
|
// ─── Text — named, deterministic text animations ─────────────────────────
|
|
@@ -1050,11 +1142,16 @@ export type SentenceHandle = {
|
|
|
1050
1142
|
// ─── Morph — any element becomes any other element ───────────────────────
|
|
1051
1143
|
|
|
1052
1144
|
export type MorphOpts = {
|
|
1053
|
-
/** 'apple' =
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
* the
|
|
1057
|
-
* eased envelope). One motion, no
|
|
1145
|
+
/** 'apple' = a firm in-out flight that keeps the deformation READABLE —
|
|
1146
|
+
* the shape change is the subject, so it may not be over in three frames.
|
|
1147
|
+
* 'spring' = the same flight with the reference bounce: a LIGHT damped
|
|
1148
|
+
* sine riding the same path, calibrated from the dnyxstudios AE expression
|
|
1149
|
+
* (amp .04·v, freq 1.8Hz, decay e^-3t, eased envelope). One motion, no
|
|
1150
|
+
* delayed settle, ~one visible overshoot.
|
|
1151
|
+
*
|
|
1152
|
+
* Both are `ease-in-out-*` by design. A morph is travel WITHIN the frame,
|
|
1153
|
+
* and the house table reserves `ease-out-*` for entrances; a front-loaded
|
|
1154
|
+
* curve finishes the transformation before the viewer can look at it. */
|
|
1058
1155
|
feel?: 'apple' | 'spring'
|
|
1059
1156
|
/** Tune the 'spring' bounce. `amp` = first overshoot as a fraction of the
|
|
1060
1157
|
* travel (default 0.025 — light), `freq` Hz (default 1.8), `decay` per
|
|
@@ -1065,17 +1162,26 @@ export type MorphOpts = {
|
|
|
1065
1162
|
/** Flight curve — ANY GSAP ease works: house names, 'elastic.out(1, 0.4)',
|
|
1066
1163
|
* 'back.out(1.8)'… Overshoots extrapolate past the target (kept renderable). */
|
|
1067
1164
|
ease?: string
|
|
1068
|
-
/** 0..1 —
|
|
1165
|
+
/** 0..1 in EASED space — where along the SHAPE CHANGE the surface swaps
|
|
1166
|
+
* from A to B. Not a fade midpoint: at this instant both sides hold the
|
|
1167
|
+
* same box and the same chrome, so the swap is a match-frame cut. 0.5
|
|
1168
|
+
* means "when the shape is halfway between the two," which is what the
|
|
1169
|
+
* viewer actually perceives. */
|
|
1069
1170
|
handoff?: number
|
|
1070
|
-
/**
|
|
1071
|
-
*
|
|
1072
|
-
|
|
1171
|
+
/** Fraction of the flight over which unmatched content departs and
|
|
1172
|
+
* arrives (default 0.42). Content displaces along the travel vector and
|
|
1173
|
+
* must be clear of the seam — that emptiness is what makes the cut
|
|
1174
|
+
* invisible, so widening this past ~0.5 will start to show the swap. */
|
|
1175
|
+
content?: number
|
|
1073
1176
|
/** Arc lift in px (0 disables). Default: auto from travel distance. */
|
|
1074
1177
|
arc?: number
|
|
1075
|
-
/**
|
|
1076
|
-
* arrive
|
|
1077
|
-
*
|
|
1078
|
-
|
|
1178
|
+
/** Content choreography (default 'displace'): unmatched children leave and
|
|
1179
|
+
* arrive by moving ALONG the travel vector — replacement reads as motion,
|
|
1180
|
+
* and both sides are clear of the seam before the surfaces swap.
|
|
1181
|
+
* `false` = containers only; children hold still and stay counter-scaled,
|
|
1182
|
+
* which is right when the two sides share a background image or the
|
|
1183
|
+
* content is meant to look continuous. */
|
|
1184
|
+
parts?: 'displace' | false
|
|
1079
1185
|
/** Sub-component matching (default 'auto'): elements tagged
|
|
1080
1186
|
* `data-morph="name"` on BOTH sides (any depth) morph INTO each other —
|
|
1081
1187
|
* each pair rides its own interpolated box inside the flight, so the
|
|
@@ -1368,9 +1474,12 @@ export type DirectionContext = {
|
|
|
1368
1474
|
* a long dwell at both ends and a confident glide through the middle.
|
|
1369
1475
|
* These defaults ARE the recommended camera physics: do NOT pass ease
|
|
1370
1476
|
* or duration here unless the user explicitly asks for a different
|
|
1371
|
-
* camera feel. The
|
|
1372
|
-
* (`duration:
|
|
1373
|
-
*
|
|
1477
|
+
* camera feel. The one sanctioned exception: an instant reframe behind
|
|
1478
|
+
* a covering element (`duration: 0.01`). A slow linear drift
|
|
1479
|
+
* (`duration: 2–5, ease: 'none'`) is NOT one — sub-pixel motion on the
|
|
1480
|
+
* whole stage re-rasterizes every glyph each frame and renders as
|
|
1481
|
+
* micro-flicker; the motion lab flags it as DRIFT and export blocks.
|
|
1482
|
+
* Hold the camera still instead. Anything between 0.05–0.4s is
|
|
1374
1483
|
* neither a move nor a cut — avoid it. `ease-in-out-cubic` is for
|
|
1375
1484
|
* ELEMENT travel, never the camera. */
|
|
1376
1485
|
move: (
|
|
@@ -1427,7 +1536,21 @@ export type DirectionContext = {
|
|
|
1427
1536
|
* 'pixels', 'iris', opts `{ color?, direction?: 'left'|'right'|'up'|
|
|
1428
1537
|
* 'down', duration?, cell?, at? }`. `out` with no position appends at the
|
|
1429
1538
|
* clip's end — the standard way to end a clip for a cut; `in` at position
|
|
1430
|
-
* 0 starts the clip covered and reveals it.
|
|
1539
|
+
* 0 starts the clip covered and reveals it.
|
|
1540
|
+
* `transition.through(target, { via?, char?, word?, swap?, land?, color?,
|
|
1541
|
+
* duration?, outDuration? }, position?)` is the GO-THROUGH: dive INTO
|
|
1542
|
+
* something until its color takes over the frame, world-swap under the
|
|
1543
|
+
* cover, pull out into the new world. `via` picks the dive geometry:
|
|
1544
|
+
* 'surface' (default, the element's face), 'glyph' (`{ via: 'glyph',
|
|
1545
|
+
* char: 'o' }` — through the character's enclosed counter, measured from
|
|
1546
|
+
* the real glyph outline on plain text: no wrapper spans, no hand-tuned
|
|
1547
|
+
* margins), 'word' (`{ via: 'word', word: 'world' }` — the word's rect),
|
|
1548
|
+
* 'anchor' (match-cut: the subject persists across the cut — the same
|
|
1549
|
+
* selector's twin in the new world is the landing — while everything
|
|
1550
|
+
* around it changes). Never hand-roll a dive (a magic-scale camera.move +
|
|
1551
|
+
* a hand-timed swap): through() computes the exact cover scale, measures
|
|
1552
|
+
* at the dive's own start time, and guarantees full coverage at the cut.
|
|
1553
|
+
* All return `{ duration }` (full span including
|
|
1431
1554
|
* stagger) for sequencing, and accept `(target, effect, opts?, position?)`
|
|
1432
1555
|
* with opts `{ by?: 'chars'|'words'|'lines', duration?, stagger?, ease?,
|
|
1433
1556
|
* from?: 'start'|'center'|'end'|'edges' }`. */
|
|
@@ -1596,10 +1719,17 @@ export type DirectionContext = {
|
|
|
1596
1719
|
* eases the container instead of live reflow. */
|
|
1597
1720
|
sentence: SentenceKit['sentence']
|
|
1598
1721
|
/** Universal shape morph: any element becomes any other element, a rect, or
|
|
1599
|
-
* `'frame'`.
|
|
1600
|
-
*
|
|
1601
|
-
*
|
|
1602
|
-
*
|
|
1722
|
+
* `'frame'`. Both real elements ride ONE interpolated box (position, size,
|
|
1723
|
+
* corner radius, arc lift) while the source's chrome is driven onto the
|
|
1724
|
+
* target's — at `handoff` the two are the same picture, so the surfaces
|
|
1725
|
+
* HARD-CUT and never crossfade. Matched sub-components (`data-morph="name"`
|
|
1726
|
+
* on both sides) travel; unmatched content displaces along the travel
|
|
1727
|
+
* vector; children are counter-scaled so nothing stretches.
|
|
1728
|
+
*
|
|
1729
|
+
* Author the TARGET hidden (`autoAlpha: 0`); the morph reveals it at
|
|
1730
|
+
* arrival. Every visual state is a pure function of eased progress, so it
|
|
1731
|
+
* is export-worker safe. For MANY-to-many structure — recursing into
|
|
1732
|
+
* sub-components, N→1 gathers, 1→N splits — install `kit/morphtree`. */
|
|
1603
1733
|
morph: MorphKit['morph']
|
|
1604
1734
|
/** The chip⇄full-bleed round trip: grows the element to cover the frame,
|
|
1605
1735
|
* holds, returns home (`back: false` to stay). Scale IS the transition —
|
|
@@ -1700,8 +1830,9 @@ export type BuiltScene = {
|
|
|
1700
1830
|
// promo-in ease-in-quart (.895,.03,.685,.22) — exits, dives
|
|
1701
1831
|
//
|
|
1702
1832
|
// Rule of thumb from the reference promos: entrances ~0.4–0.6s on promo-out
|
|
1703
|
-
// (visually settled inside 0.15s), line staggers ~0.2s, holds
|
|
1704
|
-
// linear scale drift
|
|
1833
|
+
// (visually settled inside 0.15s), line staggers ~0.2s, holds are held STILL
|
|
1834
|
+
// (a slow linear scale drift re-rasterizes glyphs every frame and renders as
|
|
1835
|
+
// micro-flicker — never add one), pops that need bounce use
|
|
1705
1836
|
// back.out(1.8–2), and background/color cuts are HARD (no crossfade).
|
|
1706
1837
|
|
|
1707
1838
|
/** Registered ease names usable anywhere GSAP accepts an ease. */
|
|
@@ -10,7 +10,7 @@ build, the render, and the studio the user tweaks in — you provide the
|
|
|
10
10
|
direction. Everything runs through the **Impractical MCP server**; the video's
|
|
11
11
|
source code lives in the cloud, never in the user's project directory.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Setup check (once per session)
|
|
14
14
|
|
|
15
15
|
Call the `account_status` tool (`whoami` is its legacy alias). If the
|
|
16
16
|
Impractical tools are not available, the current coding agent is not connected.
|
|
@@ -19,269 +19,41 @@ Tell the user to run:
|
|
|
19
19
|
npx --yes @impractical-ai/motion setup --agent
|
|
20
20
|
|
|
21
21
|
Then ask them to restart or reload their coding agent and type `continue`.
|
|
22
|
-
Claude Code users who specifically want the full plugin can run the same
|
|
23
|
-
with `--claude` instead. If the weekly quota is exhausted, call
|
|
24
|
-
the user the authenticated billing page.
|
|
25
|
-
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
recommendations that produced it.
|
|
48
|
-
|
|
49
|
-
Gather from the user (ask only for what's missing):
|
|
50
|
-
|
|
51
|
-
- **Value prop** (required): one or two sentences on what ships and why it matters.
|
|
52
|
-
- Optional: their product's front-end code, a brand URL, screen recordings.
|
|
53
|
-
|
|
54
|
-
Before `create_video`, write a compact creative contract. This is a planning
|
|
55
|
-
record, not a new scene format:
|
|
56
|
-
|
|
57
|
-
- audience
|
|
58
|
-
- the single idea the viewer should remember
|
|
59
|
-
- the visual moment that proves it without explanatory copy
|
|
60
|
-
- how the reference's hierarchy/pacing will be translated rather than copied
|
|
61
|
-
- the real product source (and whether anything must be reconstructed)
|
|
62
|
-
- opening, proof, and payoff
|
|
63
|
-
|
|
64
|
-
When the local quality-loop experiment is enabled, pass that contract as
|
|
65
|
-
`creativeContract` to `create_video`. The tool will reject an unplanned start.
|
|
66
|
-
|
|
67
|
-
If they gave a brand URL and no kit exists yet:
|
|
68
|
-
|
|
69
|
-
brand_list
|
|
70
|
-
brand_extract { url: "https://example.com" }
|
|
71
|
-
brand_extract { jobId: "<returned run id>", wait: true }
|
|
72
|
-
|
|
73
|
-
Then scaffold the project:
|
|
74
|
-
|
|
75
|
-
create_video { valueProp: "...", brandSlug?: "example-com", appCode?: "...", templateId?: "...", templateParams?: {...} }
|
|
76
|
-
|
|
77
|
-
This creates the project and a draft with starter files. It deliberately does
|
|
78
|
-
**not** run a server-side director — you are the director.
|
|
79
|
-
|
|
80
|
-
## 3. Direct it
|
|
81
|
-
|
|
82
|
-
A scene is two files, and both must exist for the preview to build:
|
|
83
|
-
|
|
84
|
-
- **`App.tsx`** — the stage markup. Plain React and CSS; this is the product UI
|
|
85
|
-
on screen.
|
|
86
|
-
- **`direction.ts`** — the choreography. `export default (ctx) => { ... }`
|
|
87
|
-
against one paused GSAP timeline.
|
|
88
|
-
|
|
89
|
-
Write them with `write_draft_files` (whole files) or `edit_draft_file` (exact
|
|
90
|
-
string replacement — prefer this for revisions). Every save rebuilds the
|
|
91
|
-
preview and returns the committed revision plus its build job id immediately.
|
|
92
|
-
Follow that id with `job_status { jobIds: ["..."], wait: true }`; that result is
|
|
93
|
-
your compile loop, so read and fix any build error rather than guessing.
|
|
94
|
-
|
|
95
|
-
Rules that matter:
|
|
96
|
-
|
|
97
|
-
- `brand.json` is law for colors and fonts. Read it; do not invent a palette.
|
|
98
|
-
- Never edit `project.json`.
|
|
99
|
-
- The timeline is rendered by seeking frame by frame. Anything that is not a
|
|
100
|
-
pure function of timeline position — `Date.now()`, `Math.random()`,
|
|
101
|
-
`setTimeout`, CSS animations, autoplaying `<video>` — desynchronises the
|
|
102
|
-
export. Use `ctx.video()` for footage.
|
|
103
|
-
- Only `.ts/.tsx/.js/.jsx/.css/.json` files sync, max 100 per draft.
|
|
104
|
-
|
|
105
|
-
Craft, briefly — the full language is `guidance.motionDirection` in the
|
|
106
|
-
`motion_context` result. That copy is live and versioned; this summary is a
|
|
107
|
-
snapshot, so when the two disagree, the tool result wins.
|
|
108
|
-
|
|
109
|
-
- **Beat it out before writing code.** Every beat needs a time, a visible
|
|
110
|
-
subject, and a narrative purpose. An intentional reading hold is valid.
|
|
111
|
-
- **Text enters on `rise`** — `text.reveal(el, 'rise')` on `promo-out`, lines
|
|
112
|
-
~0.2s apart. Do *not* default type to `'blur'`: word-by-word de-blur reads as
|
|
113
|
-
out-of-focus, and across a stagger it turns to mush. Blur is for whole-frame
|
|
114
|
-
moves — `fx.spike(el, 'blur')` alongside a whip or dive, where it hides the
|
|
115
|
-
cut. Never animate blur above 20px.
|
|
116
|
-
- **Every ease is a named house curve.** `ease-out-*` for entrances,
|
|
117
|
-
`ease-in-out-*` for travel and every camera move, `ease-in-*` for exits and
|
|
118
|
-
dives only. Never leave an ease unspecified; never put `ease-in-*` on an
|
|
119
|
-
entrance. The promo subset: `promo-out`, `promo-soft`, `promo-inout`,
|
|
120
|
-
`promo-in`, `launch-out`.
|
|
121
|
-
- **Decisive**: entrances 0.4–0.6s, exits shorter, staggers ~0.2s apart.
|
|
122
|
-
Longer movement needs a narrative reason; important states may hold still.
|
|
123
|
-
- Budget one primary and at most one supporting motion. Keep backgrounds stable
|
|
124
|
-
unless their movement is explicitly authored.
|
|
125
|
-
- Hold long enough to read. A beat that looks legible scrubbing the timeline is
|
|
126
|
-
usually still too fast on screen.
|
|
127
|
-
- Cut hard between sections; crossfading backgrounds reads as cheap.
|
|
128
|
-
- One idea per beat. If two things move for different reasons at once, split them.
|
|
129
|
-
|
|
130
|
-
### The five moves that separate a real video from a generated one
|
|
131
|
-
|
|
132
|
-
These are the engine's `ctx.fx` and `ctx.mask` kits. Reaching for them is most
|
|
133
|
-
of the gap between output that looks authored and output that looks scripted.
|
|
134
|
-
Full signatures are in the engine types; the rules of thumb are:
|
|
135
|
-
|
|
136
|
-
1. **Hide every whip cut with one spike, never two tweens.**
|
|
137
|
-
```ts
|
|
138
|
-
camera.move(camera.framing('[data-next]'), { duration: 0.28, ease: 'promo-inout' }, 'whip')
|
|
139
|
-
fx.spike('[data-panel]', 'blur', { amount: 14 }, 'whip') // ONE call
|
|
140
|
-
```
|
|
141
|
-
Hand-writing `tl.to(stage, { filter: 'blur(12px)' })` up and back is the
|
|
142
|
-
single most common way this looks amateur: the two tweens drift out of sync
|
|
143
|
-
and the user gets two unlinked controls. `fx.spike` is one tween, in and
|
|
144
|
-
out, locked to the move.
|
|
145
|
-
|
|
146
|
-
2. **Feather full-frame reveals.** `mask.reveal(el, 'feather', { direction, softness: 0.3 })`
|
|
147
|
-
— a hard edge across a whole frame reads cheap, and `clip-path` cannot make
|
|
148
|
-
a soft one at all. Use `wipe`/`iris`/`slit`/`barn` when you want the hard
|
|
149
|
-
edge deliberately; `radial` and `blinds` for softer variety.
|
|
150
|
-
|
|
151
|
-
3. **Land on light, not on opacity.** One `fx.spike(el, 'bloom')` as a beat
|
|
152
|
-
settles reads as a light source. Fading opacity reads as a slideshow.
|
|
153
|
-
|
|
154
|
-
4. **Put colour inside the letters.** `mask.fill(el, { gradient, drift })` uses
|
|
155
|
-
`background-clip: text`, so the gradient lives in the glyphs and moves
|
|
156
|
-
through them. Far stronger than a coloured headline, and one call.
|
|
157
|
-
|
|
158
|
-
5. **Composite light instead of shipping a PNG.**
|
|
159
|
-
`mask.blend(disc, 'screen')` behind type gives glow with no asset.
|
|
160
|
-
`multiply` for ink on cream, `luminosity` for duotone.
|
|
161
|
-
|
|
162
|
-
Two things to get right or the effect misfires:
|
|
163
|
-
|
|
164
|
-
- **Grain must be continuous.** Apply it once to a full-frame plate that sits
|
|
165
|
-
above every beat and is never cut (`fx.apply(plate, 'grain', { amount: 0.2 })`),
|
|
166
|
-
or it visibly blinks at each hard cut.
|
|
167
|
-
- **Effects are seasoning.** One or two per beat. `displace`, `chroma`, and
|
|
168
|
-
`threshold` are accents for a single hit, not a look to hold.
|
|
169
|
-
|
|
170
|
-
Ask `motion_context` for the **`optical`** style to see all of this composed in
|
|
171
|
-
one reference, or the **`whip-cut`**, **`soft-reveal`**, and **`gradient-type`**
|
|
172
|
-
templates for a working scene you can adapt.
|
|
173
|
-
|
|
174
|
-
## 4. Look at your work
|
|
175
|
-
|
|
176
|
-
get_frames { draftId: "...", times: [0, 1, 2, 3] }
|
|
177
|
-
|
|
178
|
-
This renders real frames on the user's machine and returns them as images. Ask
|
|
179
|
-
for several times in one call — once the page is open the extra frames are
|
|
180
|
-
nearly free.
|
|
181
|
-
|
|
182
|
-
**Always look at frames before telling the user a change is done.** If a frame
|
|
183
|
-
shows text clipped, an element off-frame, or a blank region, fix it and look
|
|
184
|
-
again. This is the difference between shipping a video and shipping a guess.
|
|
185
|
-
|
|
186
|
-
When the `get_motion` tool is available (the local motion lab experiment),
|
|
187
|
-
prefer it as the first look after every preview build:
|
|
188
|
-
|
|
189
|
-
get_motion { draftId: "..." } // whole scene
|
|
190
|
-
get_motion { draftId: "...", from: 4.8, to: 6.4 } // one weak window
|
|
191
|
-
|
|
192
|
-
Static frames measure composition; `get_motion` measures motion — dense
|
|
193
|
-
contact sheets where the difference between adjacent tiles IS the motion,
|
|
194
|
-
plus measured pixel change per interval, dead-air spans, and the timeline's
|
|
195
|
-
actual tweens with any that run without producing visible change. Fix what it
|
|
196
|
-
flags, then use `get_frames` for composition close-ups.
|
|
197
|
-
|
|
198
|
-
With the quality loop also enabled, the final review is two-part and export
|
|
199
|
-
requires both on the same revision: `get_frames { reviewMode: "final" }` for
|
|
200
|
-
composition and legibility at native resolution, and
|
|
201
|
-
`get_motion { draftId, reviewMode: "final" }` for full-duration motion
|
|
202
|
-
coverage at one frame in five.
|
|
203
|
-
|
|
204
|
-
For a new launch video, build and review the proof beat before completing the
|
|
205
|
-
rest. When the local quality loop is enabled:
|
|
206
|
-
|
|
207
|
-
get_frames { draftId: "...", times: [/* proof beat */], reviewMode: "proof" }
|
|
208
|
-
|
|
209
|
-
For the final pass, review at least eight representative timestamps including
|
|
210
|
-
`t=0` and the settled closing frame:
|
|
211
|
-
|
|
212
|
-
get_frames { draftId: "...", times: [/* whole timeline */], reviewMode: "final" }
|
|
213
|
-
|
|
214
|
-
The final critic scores the visible result—not the code or effort—on narrative
|
|
215
|
-
clarity, visual proof, product authenticity, composition, legibility, pacing,
|
|
216
|
-
reference fidelity, and technical integrity. Use timestamped evidence. A weak
|
|
217
|
-
score is a revision brief, not permission to describe the cut as “clean.”
|
|
218
|
-
|
|
219
|
-
## 5. Export and hand off
|
|
220
|
-
|
|
221
|
-
export_video { draftId: "...", qualityReview?: {/* required by local quality loop */} }
|
|
222
|
-
job_status { jobIds: ["<returned job id>"], wait: true }
|
|
223
|
-
studio_link { projectId: "...", slug: "..." }
|
|
224
|
-
|
|
225
|
-
Always give the user the studio link — that is where they watch the cuts and
|
|
226
|
-
tweak visually. The export URL expires, so only export once they are happy.
|
|
22
|
+
Claude Code users who specifically want the full plugin can run the same
|
|
23
|
+
command with `--claude` instead. If the weekly quota is exhausted, call
|
|
24
|
+
`upgrade` to give the user the authenticated billing page.
|
|
25
|
+
|
|
26
|
+
## Get the playbook, then follow it
|
|
27
|
+
|
|
28
|
+
The full, current director playbook is SERVED, not bundled: call **`motion_context`**
|
|
29
|
+
with the user's brief (plus `brandSlug` and `contentType`
|
|
30
|
+
when known). Its `guidance.director` field carries the complete house playbook
|
|
31
|
+
for this library version — workflow, review discipline, export rules. Read it
|
|
32
|
+
and follow it before writing any scene code. `engine_types` returns the
|
|
33
|
+
TypeScript contract a scene is written against; read it before writing a
|
|
34
|
+
direction.
|
|
35
|
+
|
|
36
|
+
## The tool loop (the shape; the served playbook has the craft)
|
|
37
|
+
|
|
38
|
+
Create with `create_video`, then write `App.tsx` and `direction.ts` with
|
|
39
|
+
`write_draft_files` (whole files) or `edit_draft_file` (exact string
|
|
40
|
+
replacement — prefer it for revisions). Every save rebuilds the preview and
|
|
41
|
+
returns the committed revision plus its build job id immediately; follow that
|
|
42
|
+
id with `job_status` and fix any build error it returns. Call `install_kits`
|
|
43
|
+
once right after `create_video` — the kit sources are served into the draft.
|
|
44
|
+
`brand_extract` builds a brand kit from a URL. Call `get_frames` to SEE the
|
|
45
|
+
result before telling the user it is done, then `export_video`, then hand the
|
|
46
|
+
user the `studio_link`.
|
|
227
47
|
|
|
228
48
|
## Timeouts and lost acknowledgements
|
|
229
49
|
|
|
230
50
|
Build, export, and brand-extraction calls return their job or run id before
|
|
231
|
-
waiting.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
to check whether it already started or completed.
|
|
241
|
-
- After `brand_extract`, continue its returned id with `brand_extract { jobId, wait: true }`. If the
|
|
242
|
-
start acknowledgement was lost, check `brand_list` / `brand_get` before
|
|
243
|
-
starting another extraction, especially when `reextract: true`.
|
|
244
|
-
|
|
245
|
-
Retry only when those reads prove the mutation did not commit. Otherwise
|
|
246
|
-
continue from the state you observed; a duplicate edit or export wastes quota.
|
|
247
|
-
|
|
248
|
-
## The personal motion profile
|
|
249
|
-
|
|
250
|
-
The `motion_profile` tool manages what Impractical remembers about this user's
|
|
251
|
-
taste. The rules are strict because trust in the profile depends on them:
|
|
252
|
-
|
|
253
|
-
- **Read before directing.** The personalized context from `motion_context`
|
|
254
|
-
already includes the resolved profile; call `motion_profile { action: 'get' }`
|
|
255
|
-
only when the user asks about their profile or you need rule keys.
|
|
256
|
-
- **`remember` is for explicit requests only** — "remember that I like…",
|
|
257
|
-
"always keep my videos under 15 seconds". Never call `remember` for a
|
|
258
|
-
preference you merely inferred.
|
|
259
|
-
- **Inferred preferences go through `propose`.** If the user's feedback during
|
|
260
|
-
this session implies a durable preference, submit it as a structured rule
|
|
261
|
-
with `propose`. It becomes a pending proposal that has zero effect until the
|
|
262
|
-
user confirms it.
|
|
263
|
-
- **Present at most one pending proposal, and only after a completed video.**
|
|
264
|
-
If `motion_context` returned `pendingProposal`, mention it once after the
|
|
265
|
-
export or when the user is clearly done — one sentence, e.g. "Want me to
|
|
266
|
-
remember that you prefer faster pacing? (yes/no)". Call `confirm` or
|
|
267
|
-
`reject` with their answer. Never present more than one, never mid-work, and
|
|
268
|
-
**never silently confirm**.
|
|
269
|
-
- The user can `pause`, `forget` a single rule, or `reset` everything; do these
|
|
270
|
-
only when asked.
|
|
271
|
-
|
|
272
|
-
## Conflicts
|
|
273
|
-
|
|
274
|
-
The web tweak chat edits the same draft. `edit_draft_file` reads, edits and
|
|
275
|
-
writes in one call against the current revision, so ordinary races resolve
|
|
276
|
-
themselves. If a tool reports the draft changed underneath your edit, re-read
|
|
277
|
-
the file and re-apply your change on top of the new content — never paste stale
|
|
278
|
-
content back.
|
|
279
|
-
|
|
280
|
-
If a tool reports the draft is locked by a job, wait for that job with
|
|
281
|
-
`job_status { wait: true }` and retry.
|
|
282
|
-
|
|
283
|
-
## Costs
|
|
284
|
-
|
|
285
|
-
- Preview builds and exports run on Impractical's infrastructure; the direction
|
|
286
|
-
thinking is yours, so iterate on the code freely.
|
|
287
|
-
- Preview rebuilds take tens of seconds. Batch your edits, then look at frames.
|
|
51
|
+
waiting. If the MCP client kills a call and shows only a timeout, the mutation
|
|
52
|
+
may still have committed. **Never blindly retry a mutating or metered call.**
|
|
53
|
+
Verify state first: after a write, use `list_draft_files` and
|
|
54
|
+
`read_draft_file` to confirm the intended revision; after an export, check
|
|
55
|
+
`get_project`; after `brand_extract`, continue its returned id.
|
|
56
|
+
Retry only when those reads prove the mutation did not commit.
|
|
57
|
+
|
|
58
|
+
Before directing an existing draft, read `notes.json` — the user's queued
|
|
59
|
+
edits come first; mark each handled item `done`.
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
waitForJobs,
|
|
14
14
|
withAdmissionReceipt,
|
|
15
15
|
withMeteredCall
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
16
|
+
} from "./chunk-CZANULCU.js";
|
|
17
|
+
import "./chunk-6FVZ6IWR.js";
|
|
18
18
|
import "./chunk-77HVPD4G.js";
|
|
19
19
|
export {
|
|
20
20
|
ApiContractError,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@impractical-ai/motion",
|
|
6
|
-
version: "0.1.
|
|
6
|
+
version: "0.1.10",
|
|
7
7
|
private: false,
|
|
8
8
|
description: "Create and refine Impractical Motion videos from coding agents and the terminal.",
|
|
9
9
|
homepage: "https://github.com/Openpod/impractical-motion#readme",
|
|
@@ -28,9 +28,9 @@ var package_default = {
|
|
|
28
28
|
node: ">=22"
|
|
29
29
|
},
|
|
30
30
|
scripts: {
|
|
31
|
-
build: 'node ../scripts/verify-locked-cli-install.mjs && node ../scripts/build-engine-dts.mjs --cli-only && rm -rf dist && ./node_modules/esbuild/bin/esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --splitting --outdir=dist --external:playwright-core --banner:js="#!/usr/bin/env node" && chmod +x dist/index.js',
|
|
31
|
+
build: 'node ../scripts/verify-locked-cli-install.mjs && node ../scripts/build-engine-dts.mjs --cli-only && rm -rf dist && ./node_modules/esbuild/bin/esbuild src/index.ts --define:__BUILD_COMMIT__=\\"$(git rev-parse --short HEAD 2>/dev/null || echo dev)\\" --bundle --platform=node --format=esm --target=node22 --splitting --outdir=dist --external:playwright-core --banner:js="#!/usr/bin/env node" && chmod +x dist/index.js',
|
|
32
32
|
typecheck: "tsc --noEmit",
|
|
33
|
-
test: "npm run typecheck && npm run build && esbuild tests/onboarding.test.ts tests/mcp-tools.test.ts tests/mcp-frames.test.ts tests/mcp-motion.test.ts tests/mcp-motion-score.test.ts tests/mcp-install-font.test.ts tests/mcp-metric-catalog.test.ts tests/mcp-text-census.test.ts tests/mcp-phased-flow.test.ts tests/mcp-preflight.test.ts tests/engine-dts.test.ts tests/mcp-connect.test.ts tests/mcp-longpoll.test.ts tests/mcp-agent-timeout.test.ts tests/api-hardening.test.ts --bundle --platform=node --format=esm --packages=external --outdir=dist --out-extension:.js=.mjs && node --test dist/onboarding.test.mjs dist/mcp-tools.test.mjs dist/mcp-frames.test.mjs dist/mcp-motion.test.mjs dist/mcp-motion-score.test.mjs dist/mcp-install-font.test.mjs dist/mcp-metric-catalog.test.mjs dist/mcp-text-census.test.mjs dist/mcp-phased-flow.test.mjs dist/mcp-preflight.test.mjs dist/engine-dts.test.mjs dist/mcp-connect.test.mjs dist/mcp-longpoll.test.mjs dist/mcp-agent-timeout.test.mjs dist/api-hardening.test.mjs",
|
|
33
|
+
test: "npm run typecheck && npm run build && esbuild tests/onboarding.test.ts tests/mcp-tools.test.ts tests/mcp-frames.test.ts tests/mcp-browser-pool.test.ts tests/mcp-motion.test.ts tests/mcp-craft.test.ts tests/mcp-depthkit.test.ts tests/mcp-motion-score.test.ts tests/mcp-install-font.test.ts tests/mcp-metric-catalog.test.ts tests/mcp-text-census.test.ts tests/mcp-phased-flow.test.ts tests/mcp-preflight.test.ts tests/engine-dts.test.ts tests/mcp-connect.test.ts tests/mcp-longpoll.test.ts tests/mcp-agent-timeout.test.ts tests/api-hardening.test.ts --bundle --platform=node --format=esm --packages=external --outdir=dist --out-extension:.js=.mjs && node --test dist/onboarding.test.mjs dist/mcp-tools.test.mjs dist/mcp-frames.test.mjs dist/mcp-browser-pool.test.mjs dist/mcp-motion.test.mjs dist/mcp-craft.test.mjs dist/mcp-depthkit.test.mjs dist/mcp-motion-score.test.mjs dist/mcp-install-font.test.mjs dist/mcp-metric-catalog.test.mjs dist/mcp-text-census.test.mjs dist/mcp-phased-flow.test.mjs dist/mcp-preflight.test.mjs dist/engine-dts.test.mjs dist/mcp-connect.test.mjs dist/mcp-longpoll.test.mjs dist/mcp-agent-timeout.test.mjs dist/api-hardening.test.mjs",
|
|
34
34
|
prepack: "npm run build"
|
|
35
35
|
},
|
|
36
36
|
publishConfig: {
|
|
@@ -56,6 +56,8 @@ var PRODUCTION_APP_URL = "https://motion.impractical.ai";
|
|
|
56
56
|
var PRODUCTION_SUPABASE_URL = "https://uaxjweednprtbhbceify.supabase.co";
|
|
57
57
|
var PACKAGE_NAME = "@impractical-ai/motion";
|
|
58
58
|
var PACKAGE_VERSION = package_default.version;
|
|
59
|
+
var BUILD_COMMIT = "779568a" ? "779568a" : "dev";
|
|
60
|
+
var BUILD_ID = `${PACKAGE_VERSION}+${BUILD_COMMIT}`;
|
|
59
61
|
var PACKAGE_COMMAND = `npx --yes ${PACKAGE_NAME}`;
|
|
60
62
|
|
|
61
63
|
export {
|
|
@@ -63,5 +65,6 @@ export {
|
|
|
63
65
|
PRODUCTION_SUPABASE_URL,
|
|
64
66
|
PACKAGE_NAME,
|
|
65
67
|
PACKAGE_VERSION,
|
|
68
|
+
BUILD_COMMIT,
|
|
66
69
|
PACKAGE_COMMAND
|
|
67
70
|
};
|