@impractical-ai/motion 0.1.0
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 +914 -0
- package/assets/impractical/SKILL.md +215 -0
- package/dist/chunk-2RJVXP4V.js +325 -0
- package/dist/index.js +1585 -0
- package/dist/stdio-5F5UX4VN.js +31880 -0
- package/package.json +36 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
// Impractical Motion — public engine surface v1.0.
|
|
2
|
+
//
|
|
3
|
+
// A scene is two files:
|
|
4
|
+
// App.tsx the stage markup (plain React + CSS; this is your product UI)
|
|
5
|
+
// direction.ts the choreography — `export default (ctx: DirectionContext) => {...}`
|
|
6
|
+
//
|
|
7
|
+
// The timeline is created PAUSED and rendered by seeking frame by frame, so
|
|
8
|
+
// every animation must be a function of timeline position alone. Never use
|
|
9
|
+
// Date.now(), Math.random(), setTimeout, CSS animations, or autoplaying video —
|
|
10
|
+
// they desynchronise the export. Use ctx.video() for footage.
|
|
11
|
+
//
|
|
12
|
+
// Generated by scripts/build-engine-dts.mjs — do not edit by hand.
|
|
13
|
+
|
|
14
|
+
import type gsap from 'gsap'
|
|
15
|
+
import type { SplitText } from 'gsap/SplitText'
|
|
16
|
+
|
|
17
|
+
// ─── Transitions — cover and reveal the whole frame ──────────────────────
|
|
18
|
+
|
|
19
|
+
export type TransitionStyle =
|
|
20
|
+
| 'wipe' // a solid slab sweeps across the frame
|
|
21
|
+
| 'pixels' // a grid of squares snaps in, sweeping directionally
|
|
22
|
+
| 'iris'
|
|
23
|
+
|
|
24
|
+
export type TransitionDirection = 'left' | 'right' | 'up' | 'down'
|
|
25
|
+
|
|
26
|
+
export type TransitionOpts = {
|
|
27
|
+
color?: string
|
|
28
|
+
/** Travel direction of the sweep (wipe/pixels). Default 'left'. */
|
|
29
|
+
direction?: TransitionDirection
|
|
30
|
+
duration?: number
|
|
31
|
+
ease?: string
|
|
32
|
+
/** Pixel cell size in px (pixels style). Default: frame height / 9. */
|
|
33
|
+
cell?: number
|
|
34
|
+
/** Iris center as fractions of the frame, e.g. `{ x: 0.7, y: 0.4 }`. */
|
|
35
|
+
at?: { x?: number; y?: number }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type TransitionResult = {
|
|
39
|
+
duration: number
|
|
40
|
+
/** The overlay's root element — e.g. to hide it manually mid-timeline. */
|
|
41
|
+
root: HTMLElement
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type TransitionKit = {
|
|
45
|
+
/** Cover the frame — put this at the end of a clip. Appends to the
|
|
46
|
+
* timeline's end when no position is given. */
|
|
47
|
+
out: (
|
|
48
|
+
style: TransitionStyle,
|
|
49
|
+
opts?: TransitionOpts,
|
|
50
|
+
position?: gsap.Position
|
|
51
|
+
) => TransitionResult
|
|
52
|
+
/** Start covered and reveal the scene — put this at a clip's start
|
|
53
|
+
* (position 0). Continues the same travel direction as its `out`. */
|
|
54
|
+
in: (
|
|
55
|
+
style: TransitionStyle,
|
|
56
|
+
opts?: TransitionOpts,
|
|
57
|
+
position?: gsap.Position
|
|
58
|
+
) => TransitionResult
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Text — named, deterministic text animations ─────────────────────────
|
|
62
|
+
|
|
63
|
+
export type TextUnit = 'chars' | 'words' | 'lines'
|
|
64
|
+
|
|
65
|
+
export type RevealEffect =
|
|
66
|
+
| 'rise' // masked characters rise in from below (the house classic)
|
|
67
|
+
| 'fall' // masked characters drop in from above
|
|
68
|
+
| 'blur' // words de-blur and fade in
|
|
69
|
+
| 'wave' // characters bob up with an overshoot, heavily overlapped
|
|
70
|
+
| 'flip' // characters flip up from flat (3D rotationX)
|
|
71
|
+
| 'pop' // characters scale in with a springy overshoot
|
|
72
|
+
| 'slide' // words slide in from the left
|
|
73
|
+
| 'typewriter' // characters appear one by one, no easing
|
|
74
|
+
| 'scramble' // characters cycle through glyphs, then lock in
|
|
75
|
+
| 'wipe' // the whole element is revealed by a moving clip edge
|
|
76
|
+
| 'bounce' // characters drop onto the baseline and bounce (CustomBounce)
|
|
77
|
+
| 'elastic' // characters stretch up from the baseline (elastic ease)
|
|
78
|
+
| 'arc' // characters swing in along a curved path (MotionPath)
|
|
79
|
+
| 'streak'
|
|
80
|
+
|
|
81
|
+
export type ExitEffect =
|
|
82
|
+
| 'rise' // masked characters continue up and out
|
|
83
|
+
| 'fall' // masked characters drop down and out
|
|
84
|
+
| 'blur' // words blur away
|
|
85
|
+
| 'fade' // the whole element fades out
|
|
86
|
+
| 'pop' // characters shrink away
|
|
87
|
+
| 'scramble' // characters unlock into glyph noise, then vanish
|
|
88
|
+
| 'wipe' // a clip edge eats the element left to right
|
|
89
|
+
| 'scatter' // characters burst apart and fall under gravity (Physics2D)
|
|
90
|
+
| 'streak'
|
|
91
|
+
|
|
92
|
+
export type EmphasisEffect =
|
|
93
|
+
| 'pulse' // scale up and settle back (a breath)
|
|
94
|
+
| 'wiggle' // playful rotation wiggle that settles (CustomWiggle)
|
|
95
|
+
| 'shake'
|
|
96
|
+
|
|
97
|
+
export type StaggerFrom = 'start' | 'center' | 'end' | 'edges'
|
|
98
|
+
|
|
99
|
+
export type TextEffectOpts = {
|
|
100
|
+
by?: TextUnit // which unit to animate (effects pick a sensible default)
|
|
101
|
+
duration?: number // per-unit duration, seconds
|
|
102
|
+
stagger?: number // seconds between units
|
|
103
|
+
ease?: string
|
|
104
|
+
from?: StaggerFrom // where the stagger radiates from (default 'start')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type EmphasisOpts = {
|
|
108
|
+
duration?: number // full span of the gesture
|
|
109
|
+
strength?: number // pulse: target scale; wiggle: degrees; shake: px
|
|
110
|
+
ease?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type UnderlineOpts = {
|
|
114
|
+
color?: string
|
|
115
|
+
thickness?: number // stroke width, px
|
|
116
|
+
duration?: number
|
|
117
|
+
ease?: string
|
|
118
|
+
offsetY?: number // gap between text baseline box and the line, px
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type TextResult = {
|
|
122
|
+
duration: number // full span including stagger — for sequencing
|
|
123
|
+
targets: Element[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type CountOpts = {
|
|
127
|
+
to: number
|
|
128
|
+
from?: number // defaults to whatever number the element already shows
|
|
129
|
+
duration?: number
|
|
130
|
+
ease?: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type TextKit = {
|
|
134
|
+
/** Animate text in. Returns the total span so you can sequence after it. */
|
|
135
|
+
reveal: (
|
|
136
|
+
target: string | Element,
|
|
137
|
+
effect: RevealEffect,
|
|
138
|
+
opts?: TextEffectOpts,
|
|
139
|
+
position?: gsap.Position
|
|
140
|
+
) => TextResult
|
|
141
|
+
/** Animate text out (mirror of reveal). */
|
|
142
|
+
exit: (
|
|
143
|
+
target: string | Element,
|
|
144
|
+
effect: ExitEffect,
|
|
145
|
+
opts?: TextEffectOpts,
|
|
146
|
+
position?: gsap.Position
|
|
147
|
+
) => TextResult
|
|
148
|
+
/** Deterministic number count-up on an element's text (for stat pills and
|
|
149
|
+
* badges — pairs with a camera push-in). Snaps to integers on the
|
|
150
|
+
* timeline; width is locked so the layout doesn't breathe. */
|
|
151
|
+
count: (
|
|
152
|
+
target: string | Element,
|
|
153
|
+
opts: CountOpts,
|
|
154
|
+
position?: gsap.Position
|
|
155
|
+
) => TextResult
|
|
156
|
+
/** A settle-back gesture on a whole element (no split): pulse, wiggle,
|
|
157
|
+
* shake. Great on CTAs and accent words after they appear. */
|
|
158
|
+
emphasize: (
|
|
159
|
+
target: string | Element,
|
|
160
|
+
effect: EmphasisEffect,
|
|
161
|
+
opts?: EmphasisOpts,
|
|
162
|
+
position?: gsap.Position
|
|
163
|
+
) => TextResult
|
|
164
|
+
/** Draw a hand-sketched underline stroke beneath an element. The stroke
|
|
165
|
+
* spans the element's box — target a tight element (an inline-block span
|
|
166
|
+
* around the exact words), not a full-width heading. */
|
|
167
|
+
underline: (
|
|
168
|
+
target: string | Element,
|
|
169
|
+
opts?: UnderlineOpts,
|
|
170
|
+
position?: gsap.Position
|
|
171
|
+
) => TextResult
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Logo — named, deterministic brand-mark animations ───────────────────
|
|
175
|
+
|
|
176
|
+
export type LogoRevealEffect =
|
|
177
|
+
| 'pop' // scales in with a springy overshoot
|
|
178
|
+
| 'blur' // de-blurs and settles from slightly oversized
|
|
179
|
+
| 'rise' // rises in from below behind an overflow mask
|
|
180
|
+
| 'wipe' // a clip edge sweeps the logo in (direction opt)
|
|
181
|
+
| 'iris' // a circular clip opens from the center
|
|
182
|
+
| 'slit' // a letterbox slit opens from a horizontal line
|
|
183
|
+
| 'spin' // rotates in while scaling up
|
|
184
|
+
| 'flip' // 3D flip up from edge-on
|
|
185
|
+
| 'bounce' // drops in and bounces on the baseline (CustomBounce)
|
|
186
|
+
| 'glitch' // seeded slice-band glitch that settles clean
|
|
187
|
+
| 'draw' // SVG only: strokes trace the paths, fills crossfade in
|
|
188
|
+
| 'build'
|
|
189
|
+
|
|
190
|
+
export type LogoExitEffect =
|
|
191
|
+
| 'pop' // shrinks away
|
|
192
|
+
| 'blur' // blurs out
|
|
193
|
+
| 'fade' // plain fade
|
|
194
|
+
| 'wipe' // a clip edge eats the logo (direction opt)
|
|
195
|
+
| 'iris' // the circular clip closes
|
|
196
|
+
| 'drop'
|
|
197
|
+
|
|
198
|
+
export type LogoEmphasisEffect =
|
|
199
|
+
| 'pulse' // scale up and settle back (one breath)
|
|
200
|
+
| 'breathe' // slow repeated scale loop (finite — set repeats)
|
|
201
|
+
| 'wiggle'
|
|
202
|
+
|
|
203
|
+
export type LogoDirection = 'left' | 'right' | 'up' | 'down'
|
|
204
|
+
|
|
205
|
+
export type LogoEffectOpts = {
|
|
206
|
+
duration?: number
|
|
207
|
+
ease?: string
|
|
208
|
+
stagger?: number // draw/build: seconds between shapes
|
|
209
|
+
direction?: LogoDirection // wipe: which way the edge travels
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export type LogoEmphasisOpts = {
|
|
213
|
+
duration?: number
|
|
214
|
+
strength?: number // pulse/breathe: target scale; wiggle: degrees
|
|
215
|
+
ease?: string
|
|
216
|
+
repeats?: number // breathe: number of breaths (default 3)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export type ShineOpts = {
|
|
220
|
+
color?: string // band color (default white)
|
|
221
|
+
width?: number // band width as a fraction of the logo box (default 0.18)
|
|
222
|
+
angle?: number // gradient angle, degrees (default 105)
|
|
223
|
+
duration?: number
|
|
224
|
+
ease?: string
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export type LogoResult = {
|
|
228
|
+
duration: number // full span including stagger — for sequencing
|
|
229
|
+
targets: Element[]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type LogoKit = {
|
|
233
|
+
/** Animate a logo in. Returns the total span so you can sequence after it. */
|
|
234
|
+
reveal: (
|
|
235
|
+
target: string | Element,
|
|
236
|
+
effect: LogoRevealEffect,
|
|
237
|
+
opts?: LogoEffectOpts,
|
|
238
|
+
position?: gsap.Position
|
|
239
|
+
) => LogoResult
|
|
240
|
+
/** Animate a logo out (mirror of reveal). */
|
|
241
|
+
exit: (
|
|
242
|
+
target: string | Element,
|
|
243
|
+
effect: LogoExitEffect,
|
|
244
|
+
opts?: LogoEffectOpts,
|
|
245
|
+
position?: gsap.Position
|
|
246
|
+
) => LogoResult
|
|
247
|
+
/** A settle-back gesture on the whole mark: pulse, breathe, wiggle. */
|
|
248
|
+
emphasize: (
|
|
249
|
+
target: string | Element,
|
|
250
|
+
effect: LogoEmphasisEffect,
|
|
251
|
+
opts?: LogoEmphasisOpts,
|
|
252
|
+
position?: gsap.Position
|
|
253
|
+
) => LogoResult
|
|
254
|
+
/** A glint sweep across the mark. Follows the logo's silhouette (the
|
|
255
|
+
* sheen is masked by the SVG shapes or the image alpha). */
|
|
256
|
+
shine: (
|
|
257
|
+
target: string | Element,
|
|
258
|
+
opts?: ShineOpts,
|
|
259
|
+
position?: gsap.Position
|
|
260
|
+
) => LogoResult
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─── Effects — CSS filters and SVG filter primitives ─────────────────────
|
|
264
|
+
|
|
265
|
+
export type FilterTier = 'css' | 'svg'
|
|
266
|
+
|
|
267
|
+
export type FilterEffect =
|
|
268
|
+
// ── css tier ────────────────────────────────────────────────────────────
|
|
269
|
+
/** Gaussian blur. Replaces the hand-rolled whip-pan "stage-blur spike". */
|
|
270
|
+
| 'blur'
|
|
271
|
+
/** A halo in the element's own colour, from stacked drop-shadows. */
|
|
272
|
+
| 'glow'
|
|
273
|
+
/** Colour lifts (>1) or drains (<1). */
|
|
274
|
+
| 'saturate'
|
|
275
|
+
/** Brightness pull-down — for backgrounding a layer under an overlay. */
|
|
276
|
+
| 'dim'
|
|
277
|
+
/** Hue rotation drift. */
|
|
278
|
+
| 'hue'
|
|
279
|
+
// ── svg tier ────────────────────────────────────────────────────────────
|
|
280
|
+
/** DIRECTIONAL blur along `angle`. CSS filter cannot do this at all. */
|
|
281
|
+
| 'smear'
|
|
282
|
+
/** Chromatic aberration: red and blue channels offset in opposite
|
|
283
|
+
* directions. The glitch/impact look, without an asset. */
|
|
284
|
+
| 'chroma'
|
|
285
|
+
/** Turbulence-driven displacement: heat haze, liquid warp, dissolve. */
|
|
286
|
+
| 'displace'
|
|
287
|
+
/** Fractal-noise film grain composited over the source. */
|
|
288
|
+
| 'grain'
|
|
289
|
+
/** Bright-pass blur screened back over the source — bloom. */
|
|
290
|
+
| 'bloom'
|
|
291
|
+
/** Posterize to N discrete levels per channel. */
|
|
292
|
+
| 'threshold'
|
|
293
|
+
|
|
294
|
+
export type FilterOpts = {
|
|
295
|
+
/** Peak value of the effect's primary parameter. */
|
|
296
|
+
amount?: number
|
|
297
|
+
/** Secondary constant: turbulence frequency, bloom radius, grain scale. */
|
|
298
|
+
detail?: number
|
|
299
|
+
duration?: number
|
|
300
|
+
ease?: string
|
|
301
|
+
/** Direction in degrees for `smear` and `chroma`. 0 = horizontal. */
|
|
302
|
+
angle?: number
|
|
303
|
+
/** Tint for `glow` and `bloom`. Defaults to the element's computed colour. */
|
|
304
|
+
color?: string
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export type FilterResult = {
|
|
308
|
+
/** Full span in seconds, including the return leg of a `spike`. */
|
|
309
|
+
duration: number
|
|
310
|
+
targets: Element[]
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export type FilterKit = {
|
|
314
|
+
/** Ramp an effect ON and leave it there — a state change. */
|
|
315
|
+
apply: (
|
|
316
|
+
target: string | Element | Element[],
|
|
317
|
+
effect: FilterEffect,
|
|
318
|
+
opts?: FilterOpts,
|
|
319
|
+
position?: gsap.Position
|
|
320
|
+
) => FilterResult
|
|
321
|
+
/** Ramp it back to nothing. */
|
|
322
|
+
clear: (
|
|
323
|
+
target: string | Element | Element[],
|
|
324
|
+
effect: FilterEffect,
|
|
325
|
+
opts?: FilterOpts,
|
|
326
|
+
position?: gsap.Position
|
|
327
|
+
) => FilterResult
|
|
328
|
+
/** In and out in ONE gesture: the whip-pan blur, the impact bloom, the
|
|
329
|
+
* glitch chroma pop. One tween, so the Tweak panel shows ONE row with one
|
|
330
|
+
* slider, one duration and one ease — instead of the two rows the
|
|
331
|
+
* hand-rolled pattern produces, which the user has to keep in sync. */
|
|
332
|
+
spike: (
|
|
333
|
+
target: string | Element | Element[],
|
|
334
|
+
effect: FilterEffect,
|
|
335
|
+
opts?: FilterOpts,
|
|
336
|
+
position?: gsap.Position
|
|
337
|
+
) => FilterResult
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ─── Masks — soft edges, mattes, fills, and blend modes ──────────────────
|
|
341
|
+
|
|
342
|
+
export type MaskShape =
|
|
343
|
+
/** Hard directional edge (clip-path inset) — same geometry as logo.wipe. */
|
|
344
|
+
| 'wipe'
|
|
345
|
+
/** SOFT directional edge. The capability clip-path structurally lacks. */
|
|
346
|
+
| 'feather'
|
|
347
|
+
/** Hard circle growing from a point. */
|
|
348
|
+
| 'iris'
|
|
349
|
+
/** Soft circle with a feathered falloff. */
|
|
350
|
+
| 'radial'
|
|
351
|
+
/** Letterbox opening from a horizontal centre line. */
|
|
352
|
+
| 'slit'
|
|
353
|
+
/** Two panels parting from the centre outward. */
|
|
354
|
+
| 'barn'
|
|
355
|
+
/** N bands opening together. */
|
|
356
|
+
| 'blinds'
|
|
357
|
+
/** Caller-supplied clip-path or mask-image value — the escape hatch. */
|
|
358
|
+
| 'shape'
|
|
359
|
+
|
|
360
|
+
export type BlendMode =
|
|
361
|
+
| 'screen'
|
|
362
|
+
| 'multiply'
|
|
363
|
+
| 'overlay'
|
|
364
|
+
| 'difference'
|
|
365
|
+
| 'exclusion'
|
|
366
|
+
| 'color-dodge'
|
|
367
|
+
| 'soft-light'
|
|
368
|
+
| 'luminosity'
|
|
369
|
+
| 'plus-lighter'
|
|
370
|
+
| 'normal'
|
|
371
|
+
|
|
372
|
+
export type MaskDirection = 'left' | 'right' | 'up' | 'down'
|
|
373
|
+
|
|
374
|
+
export type MatteKind = 'luma' | 'alpha'
|
|
375
|
+
|
|
376
|
+
export type MaskOpts = {
|
|
377
|
+
direction?: MaskDirection
|
|
378
|
+
/** Feather width as a fraction of the element's box. 0 = hard edge. */
|
|
379
|
+
softness?: number
|
|
380
|
+
/** Band count for `blinds`. */
|
|
381
|
+
bands?: number
|
|
382
|
+
/** Centre for `iris` / `radial`, as fractions of the box. */
|
|
383
|
+
at?: { x?: number; y?: number }
|
|
384
|
+
/** For `shape`: any clip-path value. */
|
|
385
|
+
shape?: string
|
|
386
|
+
duration?: number
|
|
387
|
+
ease?: string
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export type MatteOpts = {
|
|
391
|
+
/** 'luma' uses the source's brightness, 'alpha' its transparency. */
|
|
392
|
+
kind?: MatteKind
|
|
393
|
+
/** Sweep the matte across the target rather than holding it still. */
|
|
394
|
+
sweep?: boolean
|
|
395
|
+
duration?: number
|
|
396
|
+
ease?: string
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export type FillOpts = {
|
|
400
|
+
/** A CSS gradient, e.g. 'linear-gradient(90deg,#ED1D24,#00B8FF)'. */
|
|
401
|
+
gradient?: string
|
|
402
|
+
/** Or an image URL. */
|
|
403
|
+
image?: string
|
|
404
|
+
/** How far the fill drifts across the glyphs, in percent. */
|
|
405
|
+
drift?: number
|
|
406
|
+
duration?: number
|
|
407
|
+
ease?: string
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export type BlendOpts = {
|
|
411
|
+
/** Opacity the layer fades up from. Default 0. */
|
|
412
|
+
from?: number
|
|
413
|
+
duration?: number
|
|
414
|
+
ease?: string
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export type MaskResult = {
|
|
418
|
+
duration: number
|
|
419
|
+
targets: Element[]
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export type MaskKit = {
|
|
423
|
+
/** Reveal ANY element through a moving mask — the general verb that
|
|
424
|
+
* `text.reveal('wipe')` and `logo.reveal('iris')` are special cases of. */
|
|
425
|
+
reveal: (
|
|
426
|
+
target: string | Element | Element[],
|
|
427
|
+
shape: MaskShape,
|
|
428
|
+
opts?: MaskOpts,
|
|
429
|
+
position?: gsap.Position
|
|
430
|
+
) => MaskResult
|
|
431
|
+
/** The mirror: mask the element back out. */
|
|
432
|
+
hide: (
|
|
433
|
+
target: string | Element | Element[],
|
|
434
|
+
shape: MaskShape,
|
|
435
|
+
opts?: MaskOpts,
|
|
436
|
+
position?: gsap.Position
|
|
437
|
+
) => MaskResult
|
|
438
|
+
/** Track matte: `source`'s silhouette becomes `target`'s visibility. */
|
|
439
|
+
matte: (
|
|
440
|
+
target: string | Element,
|
|
441
|
+
source: string | Element,
|
|
442
|
+
opts?: MatteOpts,
|
|
443
|
+
position?: gsap.Position
|
|
444
|
+
) => MaskResult
|
|
445
|
+
/** Fill an element's GLYPHS with a live layer and drift it. */
|
|
446
|
+
fill: (
|
|
447
|
+
target: string | Element,
|
|
448
|
+
opts?: FillOpts,
|
|
449
|
+
position?: gsap.Position
|
|
450
|
+
) => MaskResult
|
|
451
|
+
/** Composite a layer against its beat with a blend mode. */
|
|
452
|
+
blend: (
|
|
453
|
+
target: string | Element | Element[],
|
|
454
|
+
mode: BlendMode,
|
|
455
|
+
opts?: BlendOpts,
|
|
456
|
+
position?: gsap.Position
|
|
457
|
+
) => MaskResult
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ─── Type axes — variable-font weight/width/slant animation ──────────────
|
|
461
|
+
|
|
462
|
+
export type AxisName = 'wght' | 'wdth' | 'slnt' | 'opsz' | (string & {})
|
|
463
|
+
|
|
464
|
+
export type AxisEffect =
|
|
465
|
+
/** Ramp the axis from its current value to `to` and hold. */
|
|
466
|
+
| 'ramp'
|
|
467
|
+
/** Ramp up and settle back — one breath. */
|
|
468
|
+
| 'pulse'
|
|
469
|
+
/** A per-character wave that travels through the line once. */
|
|
470
|
+
| 'wave'
|
|
471
|
+
|
|
472
|
+
export type AxisOpts = {
|
|
473
|
+
/** Target axis value. Clamped to the face's declared range. */
|
|
474
|
+
to?: number
|
|
475
|
+
from?: number
|
|
476
|
+
duration?: number
|
|
477
|
+
ease?: string
|
|
478
|
+
/** wave: seconds between adjacent characters. */
|
|
479
|
+
stagger?: number
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export type AxisResult = { duration: number; targets: Element[] }
|
|
483
|
+
|
|
484
|
+
export type AxisKit = {
|
|
485
|
+
animate: (
|
|
486
|
+
target: string | Element | Element[],
|
|
487
|
+
axis: AxisName,
|
|
488
|
+
effect: AxisEffect,
|
|
489
|
+
opts?: AxisOpts,
|
|
490
|
+
position?: gsap.Position
|
|
491
|
+
) => AxisResult
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ─── 3D — a Three.js layer under the deterministic timeline ──────────────
|
|
495
|
+
|
|
496
|
+
export type ThreeLayerOpts = {
|
|
497
|
+
/** Camera vertical FOV in degrees. */
|
|
498
|
+
fov?: number
|
|
499
|
+
/** Initial camera position. Default { x: 0, y: 0, z: 6 }. */
|
|
500
|
+
camera?: { x?: number; y?: number; z?: number }
|
|
501
|
+
/** Layer opacity 0..1 (the canvas is a normal stage element). */
|
|
502
|
+
opacity?: number
|
|
503
|
+
/** Stacking: 'front' (default) composites the transparent layer above
|
|
504
|
+
* the DOM; 'back' slots it behind — note an opaque App background will
|
|
505
|
+
* hide a 'back' layer entirely. */
|
|
506
|
+
depth?: 'back' | 'front'
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export type ThreeLayer = {
|
|
510
|
+
/** The scene graph — add meshes, lights, groups. */
|
|
511
|
+
scene: THREE.Scene
|
|
512
|
+
camera: THREE.PerspectiveCamera
|
|
513
|
+
/** The canvas element, for tweening opacity/filters like any element. */
|
|
514
|
+
canvas: HTMLCanvasElement
|
|
515
|
+
/** Drive the layer over [position, position+duration]. `update` runs every
|
|
516
|
+
* frame with eased progress 0..1 BEFORE the render — set uniforms, camera
|
|
517
|
+
* and material state from `t` alone (no clocks, no accumulation). Call
|
|
518
|
+
* once per active span; overlapping drives render redundantly. */
|
|
519
|
+
drive: (
|
|
520
|
+
duration: number,
|
|
521
|
+
update?: (t: number) => void,
|
|
522
|
+
opts?: { ease?: string },
|
|
523
|
+
position?: gsap.Position
|
|
524
|
+
) => { duration: number }
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export type ThreeKit = {
|
|
528
|
+
/** Create a 3D layer sized to the stage. Everything async about it (first
|
|
529
|
+
* compile, first paint) completes before window.__ready. */
|
|
530
|
+
layer: (opts?: ThreeLayerOpts) => ThreeLayer
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ─── Engine — the direction context you write against ────────────────────
|
|
534
|
+
|
|
535
|
+
/** Render directives a scene (or a kit, during the direction) hands to the
|
|
536
|
+
* exporter. The channel already exists end to end: `export const output` in
|
|
537
|
+
* direction.ts → loadScene → buildScene → window.__config → the exporter's
|
|
538
|
+
* probe — and because __config is assigned AFTER the direction runs, a kit
|
|
539
|
+
* can set these from inside a direction call. Everything optional; absent
|
|
540
|
+
* means today's behaviour exactly. */
|
|
541
|
+
export type RenderConfig = {
|
|
542
|
+
/** Temporal motion blur (Phase 6). `off` (default) renders one sample per
|
|
543
|
+
* frame. Tiers fix samples-per-frame; `runs` limits blur to intervals so a
|
|
544
|
+
* 30s clip with 6s of camera moves does not pay the multiplier throughout. */
|
|
545
|
+
blur?: {
|
|
546
|
+
tier?: 'off' | 'light' | 'full'
|
|
547
|
+
/** Shutter angle in degrees, 0–360. Default 180. Costs nothing — it only
|
|
548
|
+
* changes WHICH times get sampled, never how many. */
|
|
549
|
+
shutter?: number
|
|
550
|
+
/** Blur only these intervals, in seconds. Absent = the whole timeline. */
|
|
551
|
+
runs?: Array<{ from: number; to: number }>
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Committed output of scripts/analyze-music.mjs — one JSON per track in
|
|
556
|
+
* music/analysis/. A static file, identical in every export shard, which is
|
|
557
|
+
* what makes audio-reactive motion compatible with seek-anywhere rendering. */
|
|
558
|
+
export type TrackAnalysis = {
|
|
559
|
+
analyzerVersion: number
|
|
560
|
+
rate: number // envelope grid Hz
|
|
561
|
+
duration: number // exact decoded track length, seconds
|
|
562
|
+
bpm: number
|
|
563
|
+
downbeatPhase: number
|
|
564
|
+
beats: number[]
|
|
565
|
+
downbeats: number[]
|
|
566
|
+
onsets: number[]
|
|
567
|
+
sections: Array<{ start: number; end: number; energy: number }>
|
|
568
|
+
energy: number[]
|
|
569
|
+
bands: { low: number[]; mid: number[]; high: number[] }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export type AudioKit = {
|
|
573
|
+
/** True when a real analysis file was loaded. When false every envelope
|
|
574
|
+
* reads 0 and the beat grid is synthetic — directions never throw. */
|
|
575
|
+
live: boolean
|
|
576
|
+
bpm: number
|
|
577
|
+
beats: number[]
|
|
578
|
+
downbeats: number[]
|
|
579
|
+
onsets: number[]
|
|
580
|
+
sections: Array<{ start: number; end: number; energy: number }>
|
|
581
|
+
/** Overall loudness at timeline time t, 0..1. Pure lookup — the exporter
|
|
582
|
+
* loops the track under a longer video, so t wraps at the track length. */
|
|
583
|
+
energy: (t: number) => number
|
|
584
|
+
band: (t: number, band: 'low' | 'mid' | 'high') => number
|
|
585
|
+
/** Timeline time of beat n (downbeat-phase corrected). */
|
|
586
|
+
beat: (n: number) => number
|
|
587
|
+
/** Map the energy envelope onto element properties: one proxy tween whose
|
|
588
|
+
* onUpdate reads energy(t) — a pure function of playhead, so it renders
|
|
589
|
+
* identically from any seek. `map` receives 0..1 and returns gsap vars to
|
|
590
|
+
* set, e.g. (e) => ({ scale: 1 + e * 0.06 }). NO smoothing at read time:
|
|
591
|
+
* an IIR filter over successive frames is the shard-seam bug. */
|
|
592
|
+
drive: (
|
|
593
|
+
target: string | Element,
|
|
594
|
+
map: (energy: number) => Record<string, number | string>,
|
|
595
|
+
opts?: { band?: 'low' | 'mid' | 'high'; from?: number; duration?: number },
|
|
596
|
+
position?: gsap.Position
|
|
597
|
+
) => { duration: number }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export type Output = {
|
|
601
|
+
width: number
|
|
602
|
+
height: number
|
|
603
|
+
fps: number
|
|
604
|
+
render?: RenderConfig
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export type Rect = { left: number; top: number; width: number; height: number }
|
|
608
|
+
|
|
609
|
+
export type FramingTarget = string | Element | Rect | FramingTarget[]
|
|
610
|
+
|
|
611
|
+
export type Framing = { scale: number; x: number; y: number }
|
|
612
|
+
|
|
613
|
+
export type FramingOpts = {
|
|
614
|
+
scale?: number // explicit zoom level; skips fitting
|
|
615
|
+
fitWidth?: number // target occupies this fraction of the frame width
|
|
616
|
+
fitHeight?: number // target occupies this fraction of the frame height
|
|
617
|
+
maxScale?: number
|
|
618
|
+
offsetX?: number // nudge the framed center, in scene px
|
|
619
|
+
offsetY?: number
|
|
620
|
+
clamp?: boolean // keep the viewport inside the stage (no black past edges)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export type TypeOpts = {
|
|
624
|
+
cps?: number // characters per second
|
|
625
|
+
caret?: boolean
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export type TweenOverride = {
|
|
629
|
+
sig?: string
|
|
630
|
+
start?: number // timeline position, seconds
|
|
631
|
+
duration?: number
|
|
632
|
+
ease?: string
|
|
633
|
+
stagger?: number
|
|
634
|
+
vars?: Record<string, number | string> // end values (scale, x, autoAlpha, …)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export type TextStyleOverride = {
|
|
638
|
+
fontFamily?: string
|
|
639
|
+
fontSize?: number
|
|
640
|
+
fontWeight?: number
|
|
641
|
+
textAlign?: 'left' | 'center' | 'right'
|
|
642
|
+
color?: string
|
|
643
|
+
x?: number
|
|
644
|
+
y?: number
|
|
645
|
+
hidden?: boolean
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export type ObjectPositionOverride = {
|
|
649
|
+
x?: number
|
|
650
|
+
y?: number
|
|
651
|
+
scaleX?: number
|
|
652
|
+
scaleY?: number
|
|
653
|
+
rotation?: number
|
|
654
|
+
hidden?: boolean
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export type AddedTextLayer = {
|
|
658
|
+
text: string
|
|
659
|
+
x: number
|
|
660
|
+
y: number
|
|
661
|
+
width: number
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export type SceneOverrides = {
|
|
665
|
+
tweens?: Record<string, TweenOverride>
|
|
666
|
+
cameraSets?: Record<string, Partial<Framing>> // camera.set calls, by order
|
|
667
|
+
text?: Record<string, string> // stable text-fragment id -> replacement copy
|
|
668
|
+
textStyles?: Record<string, TextStyleOverride>
|
|
669
|
+
addedText?: Record<string, AddedTextLayer>
|
|
670
|
+
objects?: Record<string, ObjectPositionOverride> // stable UI wrapper id -> scene-pixel offset
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export type TweenKind =
|
|
674
|
+
| 'camera'
|
|
675
|
+
| 'element'
|
|
676
|
+
| 'group'
|
|
677
|
+
| 'typing'
|
|
678
|
+
| 'caret'
|
|
679
|
+
| 'hold'
|
|
680
|
+
// Kit proxy tweens (see src/stage/kits.ts KitProxy). Without their own
|
|
681
|
+
// kinds they fall through to 'hold', which labels them "Hold the frame",
|
|
682
|
+
// HIDES their ease selector (studio/main.tsx:2792) and files them under the
|
|
683
|
+
// "pause" filter chip — three real defects.
|
|
684
|
+
| 'filter'
|
|
685
|
+
| 'mask'
|
|
686
|
+
| 'axis'
|
|
687
|
+
| 'three'
|
|
688
|
+
|
|
689
|
+
export type TweenInfo = {
|
|
690
|
+
id: number
|
|
691
|
+
sig: string
|
|
692
|
+
kind: TweenKind
|
|
693
|
+
label: string
|
|
694
|
+
start: number
|
|
695
|
+
duration: number // per-target duration (excludes stagger span)
|
|
696
|
+
total: number // full span including stagger
|
|
697
|
+
ease: string // '' = gsap default
|
|
698
|
+
stagger?: number
|
|
699
|
+
vars: Record<string, number | string>
|
|
700
|
+
overridden: boolean
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export type CameraSetInfo = Framing & { id: number; overridden: boolean }
|
|
704
|
+
|
|
705
|
+
export type SceneInspection = {
|
|
706
|
+
config: Output
|
|
707
|
+
duration: number
|
|
708
|
+
tweens: TweenInfo[]
|
|
709
|
+
cameraSets: CameraSetInfo[]
|
|
710
|
+
overrides: SceneOverrides
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export type DirectionContext = {
|
|
714
|
+
gsap: typeof gsap
|
|
715
|
+
tl: gsap.core.Timeline // the master timeline (created paused)
|
|
716
|
+
stage: HTMLElement // scene wrapper the camera transforms
|
|
717
|
+
config: Output
|
|
718
|
+
q: <T extends HTMLElement>(selector: string) => T
|
|
719
|
+
qa: (selector: string) => HTMLElement[]
|
|
720
|
+
camera: {
|
|
721
|
+
framing: (target: FramingTarget, opts?: FramingOpts) => Framing
|
|
722
|
+
full: () => Framing
|
|
723
|
+
set: (framing: Framing) => void
|
|
724
|
+
move: (
|
|
725
|
+
framing: Framing,
|
|
726
|
+
vars?: gsap.TweenVars,
|
|
727
|
+
position?: gsap.Position
|
|
728
|
+
) => void
|
|
729
|
+
}
|
|
730
|
+
/** Deterministic, seek-driven typing with a fake caret. */
|
|
731
|
+
typeInto: (
|
|
732
|
+
target: string | HTMLTextAreaElement | HTMLInputElement,
|
|
733
|
+
text: string,
|
|
734
|
+
opts?: TypeOpts,
|
|
735
|
+
position?: gsap.Position
|
|
736
|
+
) => { duration: number; caret: HTMLElement | null }
|
|
737
|
+
/** Rect the finished text will occupy in `target` (for framing the close-up). */
|
|
738
|
+
measureTypedLine: (
|
|
739
|
+
target: string | HTMLTextAreaElement | HTMLInputElement,
|
|
740
|
+
text: string
|
|
741
|
+
) => Rect
|
|
742
|
+
/** Rect of an element's rendered text content (a block element's box
|
|
743
|
+
* spans its full column — this measures just the text). */
|
|
744
|
+
textRect: (target: string | Element) => Rect
|
|
745
|
+
/** gsap SplitText — animate .chars / .words / .lines. */
|
|
746
|
+
split: (
|
|
747
|
+
target: string | Element,
|
|
748
|
+
vars?: SplitText.Vars
|
|
749
|
+
) => SplitText
|
|
750
|
+
/** Empty tween that extends the timeline (a beat / settle). */
|
|
751
|
+
hold: (duration: number, position?: gsap.Position) => void
|
|
752
|
+
/** Timeline-driven MP4/WebM playback: scrubs the element's currentTime,
|
|
753
|
+
* so scrubbing and export frames are exact (a free-running <video> would
|
|
754
|
+
* shimmer — never autoplay one). `from` = source offset in seconds;
|
|
755
|
+
* `duration` defaults to the rest of the source. Returns `{ duration }`.
|
|
756
|
+
* The element is muted and paused; exports wait for each frame's decode. */
|
|
757
|
+
video: (
|
|
758
|
+
target: string | HTMLVideoElement,
|
|
759
|
+
opts?: { from?: number; duration?: number; playbackRate?: number },
|
|
760
|
+
position?: gsap.Position
|
|
761
|
+
) => { duration: number }
|
|
762
|
+
/** Named text animations (built on SplitText, all deterministic).
|
|
763
|
+
* `text.reveal(el, effect)` in: rise, fall, blur, wave, flip, pop, slide,
|
|
764
|
+
* typewriter, scramble, wipe, bounce, elastic, arc, streak. `text.exit(el,
|
|
765
|
+
* effect)` out: rise, fall, blur, fade, pop, scramble, wipe, scatter,
|
|
766
|
+
* streak. `text.count(el, { to, from?, duration? })` counts a number up on
|
|
767
|
+
* the timeline (stat pills). `text.emphasize(el, 'pulse'|'wiggle'|'shake')`
|
|
768
|
+
* is a settle-back gesture for CTAs/accent words. `text.underline(el)`
|
|
769
|
+
* draws a sketched stroke under a tight element.
|
|
770
|
+
*
|
|
771
|
+
* `transition.out(style)` / `transition.in(style)` cover/reveal the whole
|
|
772
|
+
* FRAME with a color (overlay outside the camera): styles 'wipe',
|
|
773
|
+
* 'pixels', 'iris', opts `{ color?, direction?: 'left'|'right'|'up'|
|
|
774
|
+
* 'down', duration?, cell?, at? }`. `out` with no position appends at the
|
|
775
|
+
* clip's end — the standard way to end a clip for a cut; `in` at position
|
|
776
|
+
* 0 starts the clip covered and reveals it. All return `{ duration }` (full span including
|
|
777
|
+
* stagger) for sequencing, and accept `(target, effect, opts?, position?)`
|
|
778
|
+
* with opts `{ by?: 'chars'|'words'|'lines', duration?, stagger?, ease?,
|
|
779
|
+
* from?: 'start'|'center'|'end'|'edges' }`. */
|
|
780
|
+
text: TextKit
|
|
781
|
+
/** Named logo/brand-mark animations (all deterministic). Target the
|
|
782
|
+
* element wrapping the mark — an inline `<svg>`, an `<img>`, or a div
|
|
783
|
+
* around one. `logo.reveal(el, effect)` in: pop, blur, rise, wipe, iris,
|
|
784
|
+
* slit, spin, flip, bounce, glitch, draw, build. `draw` (strokes trace the
|
|
785
|
+
* SVG paths, fills crossfade in — works even on fills-only logos) and
|
|
786
|
+
* `build` (shapes assemble one by one) need INLINE SVG with path/shape
|
|
787
|
+
* elements; on an `<img>` or path-less SVG they fall back to `wipe` with a
|
|
788
|
+
* console warning. `logo.exit(el, effect)` out: pop, blur, fade, wipe,
|
|
789
|
+
* iris, drop. `logo.emphasize(el, 'pulse'|'breathe'|'wiggle')` is a
|
|
790
|
+
* settle-back gesture (breathe takes `repeats`, finite). `logo.shine(el)`
|
|
791
|
+
* sweeps a glint across the mark, masked by the logo's own silhouette.
|
|
792
|
+
* All return `{ duration }` (full span including stagger) and accept
|
|
793
|
+
* `(target, effect, opts?, position?)` with opts `{ duration?, ease?,
|
|
794
|
+
* stagger?, direction?: 'left'|'right'|'up'|'down' }`. */
|
|
795
|
+
logo: LogoKit
|
|
796
|
+
transition: TransitionKit
|
|
797
|
+
/** Real 3D — a Three.js layer under the same paused timeline. Use for
|
|
798
|
+
* depth After Effects fakes and CSS perspective cannot reach: real
|
|
799
|
+
* cameras, lights, materials. `const L = three.layer({ camera: {z: 6} })`
|
|
800
|
+
* gives `{ scene, camera, canvas, drive }` — add meshes and lights to
|
|
801
|
+
* `L.scene` (import * as THREE from 'three'), animate positions/rotations
|
|
802
|
+
* with ordinary `tl.to(mesh.position, {...})` tweens, then
|
|
803
|
+
* `L.drive(duration, (t) => {...}, opts?, position?)` renders the layer
|
|
804
|
+
* over that span (set uniforms/camera from `t` ALONE — no clocks, no
|
|
805
|
+
* setAnimationLoop, no accumulation; the frame must be a pure function of
|
|
806
|
+
* timeline position). The canvas is a normal stage element: tween its
|
|
807
|
+
* opacity, put fx/mask on it, cut it with tl.set. */
|
|
808
|
+
three: ThreeKit
|
|
809
|
+
/** Variable-font axis animation — type whose WEIGHT is the thing that
|
|
810
|
+
* moves. After Effects cannot do this at all. `axis.animate(el, axis,
|
|
811
|
+
* effect, opts?, position?)`: axes `wght` (weight), `wdth` (width), `slnt`
|
|
812
|
+
* (slant), `opsz` (optical size), or any custom axis name; effects `ramp`
|
|
813
|
+
* (go and hold), `pulse` (one breath), `wave` (travels through the
|
|
814
|
+
* characters). Opts `{ from?, to?, duration?, ease?, stagger? }`. On a
|
|
815
|
+
* non-variable font it degrades honestly (wght -> weight staircase, wdth ->
|
|
816
|
+
* scaleX, slnt -> skewX) with a console warning, never a throw. Geist (the
|
|
817
|
+
* house family) IS variable — weight 100-900. */
|
|
818
|
+
axis: AxisKit
|
|
819
|
+
/** The soundtrack's offline analysis (music/analysis/<track>.json).
|
|
820
|
+
* `audio.beat(n)` / `audio.downbeats` place cuts on the REAL grid — the
|
|
821
|
+
* downbeat phase is measured, not assumed to start at 0. `audio.onsets`
|
|
822
|
+
* are the strongest hits; land impact beats on them. `audio.drive(el, map)`
|
|
823
|
+
* makes a property breathe with the track. When no analysis exists,
|
|
824
|
+
* `audio.live` is false and every envelope reads 0 — never throws. */
|
|
825
|
+
audio: AudioKit
|
|
826
|
+
/** Ask the exporter for TRUE temporal motion blur over an interval —
|
|
827
|
+
* sub-frame supersampling at render time, the thing a real-time tool
|
|
828
|
+
* cannot do. Put it on fast camera moves and whip cuts; the preview does
|
|
829
|
+
* not show it (it renders one sample per frame), the MP4 does.
|
|
830
|
+
* `ctx.blur(from, to)` marks [from, to] seconds. Opts: `shutter` degrees
|
|
831
|
+
* (0-360, default 180 — wider smears more), `tier` 'light' (4 samples) or
|
|
832
|
+
* 'full' (8, final renders). Costs render time roughly x samples over the
|
|
833
|
+
* marked intervals only, so mark moves, not holds. */
|
|
834
|
+
blur: (
|
|
835
|
+
from: number,
|
|
836
|
+
to: number,
|
|
837
|
+
opts?: { shutter?: number; tier?: 'light' | 'full' }
|
|
838
|
+
) => void
|
|
839
|
+
/** Register async prep (texture decode, analysis fetch) started inside the
|
|
840
|
+
* direction. buildScene awaits every registered promise before setting
|
|
841
|
+
* window.__ready, so the exporter never screenshots a half-loaded scene.
|
|
842
|
+
* For kit authors — a direction rarely needs it directly. */
|
|
843
|
+
__await: (work: Promise<unknown>) => void
|
|
844
|
+
/** Visual effects — CSS filters and SVG filter primitives, all
|
|
845
|
+
* deterministic. Three verbs, all `(target, effect, opts?, position?)`:
|
|
846
|
+
* `fx.apply` ramps an effect on and leaves it, `fx.clear` ramps it off,
|
|
847
|
+
* and `fx.spike` does both in ONE gesture (one Tweak row, not two).
|
|
848
|
+
*
|
|
849
|
+
* Effects: `blur` (gaussian), `glow` (halo in the element's own colour),
|
|
850
|
+
* `saturate`, `dim` (brightness), `hue` (rotation), `smear` (DIRECTIONAL
|
|
851
|
+
* blur along `angle` — CSS blur cannot do this), `chroma` (chromatic
|
|
852
|
+
* aberration, the glitch look), `displace` (turbulence warp: heat haze,
|
|
853
|
+
* liquid dissolve), `grain` (film grain), `bloom` (bright-pass glow),
|
|
854
|
+
* `threshold` (posterize to N levels).
|
|
855
|
+
*
|
|
856
|
+
* Opts `{ amount?, detail?, duration?, ease?, angle?, color? }` — `amount`
|
|
857
|
+
* is the effect's peak, `detail` its secondary constant (turbulence
|
|
858
|
+
* frequency, bloom radius), `angle` in degrees for smear/chroma.
|
|
859
|
+
*
|
|
860
|
+
* The whip-pan cut is `fx.spike(stage, 'blur', {}, 'whip1')` alongside a
|
|
861
|
+
* fast `camera.move` — do NOT hand-write a pair of
|
|
862
|
+
* `tl.to(stage, { filter: 'blur(12px)' })` tweens, which produces two
|
|
863
|
+
* unsynced rows in the Tweak panel. */
|
|
864
|
+
fx: FilterKit
|
|
865
|
+
/** Masks, mattes, and blend composition — the things `clip-path` cannot do.
|
|
866
|
+
*
|
|
867
|
+
* `mask.reveal(el, shape, opts?, position?)` and `mask.hide(...)` animate
|
|
868
|
+
* any element through a moving mask. Shapes: `wipe` (hard directional
|
|
869
|
+
* edge), `feather` (SOFT directional edge — clip-path cannot do this),
|
|
870
|
+
* `iris` (hard circle), `radial` (soft circle), `slit` (letterbox opening),
|
|
871
|
+
* `barn` (panels part from centre), `blinds` (N bands), `shape` (your own
|
|
872
|
+
* clip-path via `opts.shape`). Opts `{ direction?: 'left'|'right'|'up'|
|
|
873
|
+
* 'down', softness?: 0-1, bands?, at?: {x,y}, duration?, ease? }`.
|
|
874
|
+
* Prefer `feather` over `wipe` for anything soft or premium — a hard edge
|
|
875
|
+
* on a full-frame reveal reads as cheap.
|
|
876
|
+
*
|
|
877
|
+
* `mask.matte(target, source, opts?, position?)` makes `source`'s
|
|
878
|
+
* silhouette the target's visibility — v1 snapshots the source ONCE, so
|
|
879
|
+
* animating the source will not move the matte; use `{ sweep: true }` to
|
|
880
|
+
* travel it across the target instead.
|
|
881
|
+
*
|
|
882
|
+
* `mask.fill(el, { gradient? | image?, drift? })` fills an element's GLYPHS
|
|
883
|
+
* with a live layer and drifts it (background-clip: text).
|
|
884
|
+
*
|
|
885
|
+
* `mask.blend(el, mode, opts?, position?)` composites a layer against its
|
|
886
|
+
* beat. Modes: screen, multiply, overlay, difference, exclusion,
|
|
887
|
+
* color-dodge, soft-light, luminosity, plus-lighter, normal. `screen` for
|
|
888
|
+
* light on dark, `multiply` for ink on cream, `luminosity` for duotone. */
|
|
889
|
+
mask: MaskKit
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export type Direction = (ctx: DirectionContext) => void | Promise<void>
|
|
893
|
+
|
|
894
|
+
// ─── Named eases ──────────────────────────────────────────────────────
|
|
895
|
+
|
|
896
|
+
// ─── The promo easing set ─────────────────────────────────────────────────
|
|
897
|
+
// Named curves for the "product promo" motion language (the ginyboi /
|
|
898
|
+
// Apple-promo feel): elements snap in fast and settle long, cameras glide,
|
|
899
|
+
// exits accelerate. These are the exact cubic-beziers from the house
|
|
900
|
+
// animation guidelines, registered once so directions, blocks, and the
|
|
901
|
+
// Tweak panel can all reference them by name.
|
|
902
|
+
//
|
|
903
|
+
// promo-out ease-out-expo (.19, 1, .22, 1) — entrances, card pops
|
|
904
|
+
// promo-soft ease-out-quint (.23, 1, .32, 1) — gentler entrances, settles
|
|
905
|
+
// promo-inout in-out-quint (.86, 0, .07, 1) — camera pushes, recenters
|
|
906
|
+
// promo-in ease-in-quart (.895,.03,.685,.22) — exits, dives
|
|
907
|
+
//
|
|
908
|
+
// Rule of thumb from the reference promos: entrances ~0.4–0.6s on promo-out
|
|
909
|
+
// (visually settled inside 0.15s), line staggers ~0.2s, holds carry a slow
|
|
910
|
+
// linear scale drift (~4%/s, ease 'none'), pops that need bounce use
|
|
911
|
+
// back.out(1.8–2), and background/color cuts are HARD (no crossfade).
|
|
912
|
+
|
|
913
|
+
/** Registered ease names usable anywhere GSAP accepts an ease. */
|
|
914
|
+
export type PromoEase = 'promo-out' | 'promo-soft' | 'promo-inout' | 'promo-in' | 'launch-out'
|