@impractical-ai/motion 0.1.7 → 0.1.9
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 +65 -1
- package/assets/impractical/SKILL.md +35 -263
- package/dist/{api-DR3NJ7TI.js → api-NVZQJBVP.js} +2 -2
- package/dist/{chunk-Z74SLKEU.js → chunk-H5TR3QJN.js} +6 -3
- package/dist/{chunk-MQKW7NFI.js → chunk-KI5E2464.js} +1 -1
- package/dist/{chunk-M7SHFM25.js → chunk-NG3Q3BYW.js} +57 -4
- package/dist/{commands-R3LJU2BW.js → commands-56WH3ZVJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/{stdio-XUVQKDCB.js → stdio-MJ4LTXME.js} +2717 -1454
- 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/kits/ensemble.ts
DELETED
|
@@ -1,483 +0,0 @@
|
|
|
1
|
-
// Ensemble choreography — words that move MANY elements as one system.
|
|
2
|
-
// dropIn — elements arrive from BEHIND THE CAMERA onto the layout
|
|
3
|
-
// (reference: raylight.app drop-in film)
|
|
4
|
-
// assemble — elements fly in from scattered planar directions and settle
|
|
5
|
-
// into their exact layout slots (Apple tile wall, Firefly
|
|
6
|
-
// place-on-canvas, Slack puzzle-slots)
|
|
7
|
-
// scatter — the inverse: elements explode outward from their layout
|
|
8
|
-
// fountain — elements fly BALLISTICALLY through the frame: tossed up,
|
|
9
|
-
// raining down, or bursting radially (Pitch slide fountain,
|
|
10
|
-
// Apple card pile, Slack icon rain)
|
|
11
|
-
// confetti — generated celebration particles (Simar's wow-staging beat)
|
|
12
|
-
//
|
|
13
|
-
// SHOWPIECE DOCTRINE (fountain/confetti especially): these are BEAT-LEVEL
|
|
14
|
-
// events — at most 1–2 per film, and they must land on a story payoff (a
|
|
15
|
-
// launch, a celebration, a reveal). A showpiece on an ambient beat is noise.
|
|
16
|
-
//
|
|
17
|
-
// Generalized + deterministic: geometry is measured (layoutRect); poses are
|
|
18
|
-
// closed-form functions of the seeked time (ballistics = parabola, no
|
|
19
|
-
// integration); per-item variation comes from index hashing, never
|
|
20
|
-
// Math.random; every element ends at exact identity or fully exited.
|
|
21
|
-
import { resolveTargets, layoutRect } from './kits'
|
|
22
|
-
|
|
23
|
-
type Position = number | string | undefined
|
|
24
|
-
|
|
25
|
-
/** Deterministic per-index variation in [0, 1). */
|
|
26
|
-
function hash01(i: number, salt = 0): number {
|
|
27
|
-
let x = (i + 1) * 2654435761 + salt * 40503
|
|
28
|
-
x = ((x >>> 16) ^ x) * 0x45d9f3b
|
|
29
|
-
x = ((x >>> 16) ^ x) * 0x45d9f3b
|
|
30
|
-
return (((x >>> 16) ^ x) >>> 0) % 10000 / 10000
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type DropInOpts = {
|
|
34
|
-
/** Plane blow-up factor the elements start at (default 3.2). */
|
|
35
|
-
from?: number
|
|
36
|
-
/** Per-element flight time (default 0.75). */
|
|
37
|
-
duration?: number
|
|
38
|
-
/** Stagger between elements (default 0.1). */
|
|
39
|
-
stagger?: number
|
|
40
|
-
/** Arrival order (default 'layout' = top-to-bottom, left-to-right). */
|
|
41
|
-
order?: 'layout' | 'dom' | 'random-ish'
|
|
42
|
-
/** Soft focus during the approach: brief blur that resolves on landing
|
|
43
|
-
* (default true; capped well under the 20px ceiling). */
|
|
44
|
-
soft?: boolean
|
|
45
|
-
/** Landing overshoot (default 1.02 → 1). `false` disables. */
|
|
46
|
-
settle?: boolean
|
|
47
|
-
ease?: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function createEnsembleKit(deps: {
|
|
51
|
-
tl: any
|
|
52
|
-
gsap: any
|
|
53
|
-
stage: HTMLElement
|
|
54
|
-
config: { width: number; height: number }
|
|
55
|
-
}) {
|
|
56
|
-
const { tl, gsap, stage, config } = deps
|
|
57
|
-
const at = (position: Position): number => (typeof position === 'number' ? position : tl.duration())
|
|
58
|
-
|
|
59
|
-
const dropIn = (
|
|
60
|
-
targets: string | Element | Element[],
|
|
61
|
-
opts: DropInOpts = {},
|
|
62
|
-
position?: Position
|
|
63
|
-
): { duration: number } => {
|
|
64
|
-
const els = resolveTargets(stage, targets, 'dropIn')
|
|
65
|
-
const s0 = Math.max(1.2, opts.from ?? 3.2)
|
|
66
|
-
const duration = opts.duration ?? 0.75
|
|
67
|
-
const stagger = opts.stagger ?? 0.1
|
|
68
|
-
const ease = opts.ease ?? 'power3.out'
|
|
69
|
-
const start = at(position)
|
|
70
|
-
const cx = config.width / 2
|
|
71
|
-
const cy = config.height / 2
|
|
72
|
-
|
|
73
|
-
const entries = els.map((el, i) => {
|
|
74
|
-
const r = layoutRect(stage, el as HTMLElement)
|
|
75
|
-
return { el: el as HTMLElement, r, i }
|
|
76
|
-
})
|
|
77
|
-
if (opts.order !== 'dom') {
|
|
78
|
-
if (opts.order === 'random-ish') {
|
|
79
|
-
// Deterministic shuffle: order by a hash of layout position.
|
|
80
|
-
entries.sort(
|
|
81
|
-
(a, b) =>
|
|
82
|
-
((a.r.left * 7 + a.r.top * 13) % 97) - ((b.r.left * 7 + b.r.top * 13) % 97)
|
|
83
|
-
)
|
|
84
|
-
} else {
|
|
85
|
-
entries.sort((a, b) => a.r.top - b.r.top || a.r.left - b.r.left)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
for (const [order, { el, r }] of entries.entries()) {
|
|
90
|
-
// Start pose = the element's position if the whole plane were scaled
|
|
91
|
-
// s0 about the frame center: it sits further out along the radial
|
|
92
|
-
// direction and s0× larger — i.e., closer to the camera.
|
|
93
|
-
const ecx = r.left + r.width / 2
|
|
94
|
-
const ecy = r.top + r.height / 2
|
|
95
|
-
const x0 = (ecx - cx) * (s0 - 1)
|
|
96
|
-
const y0 = (ecy - cy) * (s0 - 1)
|
|
97
|
-
const t = start + order * stagger
|
|
98
|
-
gsap.set(el, { transformOrigin: '50% 50%' })
|
|
99
|
-
tl.fromTo(
|
|
100
|
-
el,
|
|
101
|
-
{ x: x0, y: y0, scale: s0, autoAlpha: 0 },
|
|
102
|
-
{ x: 0, y: 0, scale: opts.settle === false ? 1 : 1.02, autoAlpha: 1, duration, ease, force3D: false },
|
|
103
|
-
t
|
|
104
|
-
)
|
|
105
|
-
if (opts.settle !== false) {
|
|
106
|
-
tl.to(el, { scale: 1, duration: 0.22, ease: 'promo-soft', force3D: false }, t + duration)
|
|
107
|
-
}
|
|
108
|
-
if (opts.soft !== false) {
|
|
109
|
-
tl.fromTo(
|
|
110
|
-
el,
|
|
111
|
-
{ filter: 'blur(9px)' },
|
|
112
|
-
{ filter: 'blur(0px)', duration: duration * 0.85, ease: 'power2.out' },
|
|
113
|
-
t
|
|
114
|
-
)
|
|
115
|
-
tl.set(el, { clearProps: 'filter' }, t + duration)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const total = (entries.length - 1) * stagger + duration + (opts.settle === false ? 0 : 0.22)
|
|
119
|
-
return { duration: Math.max(0, total) }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Elements fly in from scattered planar directions and settle into their
|
|
123
|
-
* exact layout slots. `from`: 'edges' (each from its nearest frame edge),
|
|
124
|
-
* 'center' (burst outward-in), 'above'. Ends at exact identity. */
|
|
125
|
-
const assemble = (
|
|
126
|
-
targets: string | Element | Element[],
|
|
127
|
-
opts: { from?: 'edges' | 'center' | 'above'; distance?: number; duration?: number; stagger?: number; spin?: number; ease?: string } = {},
|
|
128
|
-
position?: Position
|
|
129
|
-
): { duration: number } => {
|
|
130
|
-
const els = resolveTargets(stage, targets, 'assemble')
|
|
131
|
-
const from = opts.from ?? 'edges'
|
|
132
|
-
const duration = opts.duration ?? 0.7
|
|
133
|
-
const stagger = opts.stagger ?? 0.07
|
|
134
|
-
const spin = opts.spin ?? 14
|
|
135
|
-
const ease = opts.ease ?? 'promo-out'
|
|
136
|
-
const start = at(position)
|
|
137
|
-
const cx = config.width / 2
|
|
138
|
-
const cy = config.height / 2
|
|
139
|
-
els.forEach((raw, i) => {
|
|
140
|
-
const el = raw as HTMLElement
|
|
141
|
-
const r = layoutRect(stage, el)
|
|
142
|
-
const ecx = r.left + r.width / 2
|
|
143
|
-
const ecy = r.top + r.height / 2
|
|
144
|
-
const d = opts.distance ?? Math.max(240, Math.hypot(cx, cy) * 0.5)
|
|
145
|
-
let dx = 0
|
|
146
|
-
let dy = 0
|
|
147
|
-
if (from === 'above') {
|
|
148
|
-
dx = (hash01(i, 1) - 0.5) * d * 0.5
|
|
149
|
-
dy = -d
|
|
150
|
-
} else {
|
|
151
|
-
// Outward along the radial from frame center (edges) or inward (center).
|
|
152
|
-
const len = Math.max(1, Math.hypot(ecx - cx, ecy - cy))
|
|
153
|
-
const sign = from === 'center' ? -1 : 1
|
|
154
|
-
dx = ((ecx - cx) / len) * d * sign
|
|
155
|
-
dy = ((ecy - cy) / len) * d * sign
|
|
156
|
-
}
|
|
157
|
-
const t = start + i * stagger
|
|
158
|
-
gsap.set(el, { transformOrigin: '50% 50%' })
|
|
159
|
-
tl.fromTo(
|
|
160
|
-
el,
|
|
161
|
-
{ x: dx, y: dy, rotation: (hash01(i, 2) - 0.5) * 2 * spin, autoAlpha: 0 },
|
|
162
|
-
{ x: 0, y: 0, rotation: 0, autoAlpha: 1, duration, ease, force3D: false },
|
|
163
|
-
t
|
|
164
|
-
)
|
|
165
|
-
})
|
|
166
|
-
return { duration: (els.length - 1) * stagger + duration }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** The inverse of assemble: elements explode outward from their layout
|
|
170
|
-
* (radially from frame center), fading as they go. They end hidden with
|
|
171
|
-
* transforms reset, so the layout is clean if re-shown. */
|
|
172
|
-
const scatter = (
|
|
173
|
-
targets: string | Element | Element[],
|
|
174
|
-
opts: { distance?: number; duration?: number; stagger?: number; spin?: number; ease?: string } = {},
|
|
175
|
-
position?: Position
|
|
176
|
-
): { duration: number } => {
|
|
177
|
-
const els = resolveTargets(stage, targets, 'scatter')
|
|
178
|
-
const duration = opts.duration ?? 0.6
|
|
179
|
-
const stagger = opts.stagger ?? 0.05
|
|
180
|
-
const spin = opts.spin ?? 24
|
|
181
|
-
const start = at(position)
|
|
182
|
-
const cx = config.width / 2
|
|
183
|
-
const cy = config.height / 2
|
|
184
|
-
els.forEach((raw, i) => {
|
|
185
|
-
const el = raw as HTMLElement
|
|
186
|
-
const r = layoutRect(stage, el)
|
|
187
|
-
const ecx = r.left + r.width / 2
|
|
188
|
-
const ecy = r.top + r.height / 2
|
|
189
|
-
const len = Math.max(1, Math.hypot(ecx - cx, ecy - cy))
|
|
190
|
-
const d = (opts.distance ?? 420) * (0.7 + hash01(i, 3) * 0.6)
|
|
191
|
-
const t = start + i * stagger
|
|
192
|
-
gsap.set(el, { transformOrigin: '50% 50%' })
|
|
193
|
-
tl.to(
|
|
194
|
-
el,
|
|
195
|
-
{
|
|
196
|
-
x: ((ecx - cx) / len) * d,
|
|
197
|
-
y: ((ecy - cy) / len) * d,
|
|
198
|
-
rotation: (hash01(i, 4) - 0.5) * 2 * spin,
|
|
199
|
-
autoAlpha: 0,
|
|
200
|
-
duration,
|
|
201
|
-
ease: opts.ease ?? 'promo-in',
|
|
202
|
-
force3D: false,
|
|
203
|
-
},
|
|
204
|
-
t
|
|
205
|
-
)
|
|
206
|
-
tl.set(el, { x: 0, y: 0, rotation: 0 }, t + duration)
|
|
207
|
-
})
|
|
208
|
-
return { duration: (els.length - 1) * stagger + duration }
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** SHOWPIECE. Elements fly BALLISTICALLY through the frame — tossed up and
|
|
212
|
-
* falling back past the bottom ('up'), raining through ('down'), or
|
|
213
|
-
* bursting radially from their own position ('burst'). Items EXIT: they
|
|
214
|
-
* end hidden with transforms reset. Closed-form parabola per item (pure
|
|
215
|
-
* function of seeked time), spin and velocity varied by index hash.
|
|
216
|
-
*
|
|
217
|
-
* WHEN NOT TO USE: on ambient beats. A fountain must be a story payoff —
|
|
218
|
-
* something launched, generated, or released — and at most one or two
|
|
219
|
-
* showpieces belong in a whole film. */
|
|
220
|
-
const fountain = (
|
|
221
|
-
targets: string | Element | Element[],
|
|
222
|
-
opts: {
|
|
223
|
-
direction?: 'up' | 'down' | 'burst'
|
|
224
|
-
duration?: number
|
|
225
|
-
stagger?: number
|
|
226
|
-
gravity?: number
|
|
227
|
-
power?: number
|
|
228
|
-
spin?: number
|
|
229
|
-
ease?: string
|
|
230
|
-
} = {},
|
|
231
|
-
position?: Position
|
|
232
|
-
): { duration: number } => {
|
|
233
|
-
const els = resolveTargets(stage, targets, 'fountain')
|
|
234
|
-
const direction = opts.direction ?? 'up'
|
|
235
|
-
const duration = opts.duration ?? 2.0
|
|
236
|
-
const stagger = opts.stagger ?? 0.08
|
|
237
|
-
const g = opts.gravity ?? 2600
|
|
238
|
-
const power = opts.power ?? 1
|
|
239
|
-
const spin = opts.spin ?? 260
|
|
240
|
-
const start = at(position)
|
|
241
|
-
const H = config.height
|
|
242
|
-
els.forEach((raw, i) => {
|
|
243
|
-
const el = raw as HTMLElement
|
|
244
|
-
const h1 = hash01(i, 5)
|
|
245
|
-
const h2 = hash01(i, 6)
|
|
246
|
-
const h3 = hash01(i, 7)
|
|
247
|
-
const vx = (h1 - 0.5) * 900 * power
|
|
248
|
-
const vy =
|
|
249
|
-
direction === 'down'
|
|
250
|
-
? 200 + h2 * 300
|
|
251
|
-
: direction === 'burst'
|
|
252
|
-
? -(600 + h2 * 900) * power
|
|
253
|
-
: -(1400 + h2 * 900) * power
|
|
254
|
-
const w = (h3 - 0.5) * 2 * spin
|
|
255
|
-
const t0 = start + i * stagger
|
|
256
|
-
gsap.set(el, { transformOrigin: '50% 50%' })
|
|
257
|
-
const proxy = { t: 0 }
|
|
258
|
-
tl.set(el, { autoAlpha: 1 }, t0)
|
|
259
|
-
tl.to(
|
|
260
|
-
proxy,
|
|
261
|
-
{
|
|
262
|
-
t: duration,
|
|
263
|
-
duration,
|
|
264
|
-
ease: 'none',
|
|
265
|
-
onUpdate: () => {
|
|
266
|
-
const t = proxy.t
|
|
267
|
-
gsap.set(el, {
|
|
268
|
-
x: vx * t,
|
|
269
|
-
y: vy * t + 0.5 * g * t * t + (direction === 'down' ? -H * 0.2 : 0),
|
|
270
|
-
rotation: w * t,
|
|
271
|
-
force3D: false,
|
|
272
|
-
})
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
t0
|
|
276
|
-
)
|
|
277
|
-
// Items exit; leave the layout clean behind them.
|
|
278
|
-
tl.set(el, { autoAlpha: 0, x: 0, y: 0, rotation: 0 }, t0 + duration)
|
|
279
|
-
})
|
|
280
|
-
return { duration: (els.length - 1) * stagger + duration }
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** SHOWPIECE. A generated celebration burst — no authored elements needed:
|
|
284
|
-
* particles are created, fly ballistically with spin and flutter, fade,
|
|
285
|
-
* and the whole layer ends hidden. `at` is the origin (element or scene
|
|
286
|
-
* point); `count` is capped for paint cost.
|
|
287
|
-
*
|
|
288
|
-
* WHEN NOT TO USE: anywhere except a genuine payoff beat (shipped,
|
|
289
|
-
* delivered, launched, won). One confetti per film, maximum. */
|
|
290
|
-
const confetti = (
|
|
291
|
-
opts: {
|
|
292
|
-
at?: string | Element | { x: number; y: number }
|
|
293
|
-
count?: number
|
|
294
|
-
palette?: string[]
|
|
295
|
-
power?: number
|
|
296
|
-
spread?: number
|
|
297
|
-
duration?: number
|
|
298
|
-
size?: number
|
|
299
|
-
} = {},
|
|
300
|
-
position?: Position
|
|
301
|
-
): { duration: number } => {
|
|
302
|
-
const count = Math.min(160, opts.count ?? 90)
|
|
303
|
-
const palette = opts.palette ?? ['#2f6bff', '#7c5cff', '#ff8a5c', '#ffd166', '#06d6a0', '#ef476f']
|
|
304
|
-
const duration = opts.duration ?? 2.4
|
|
305
|
-
const power = opts.power ?? 1
|
|
306
|
-
const spreadDeg = opts.spread ?? 70
|
|
307
|
-
const start = at(position)
|
|
308
|
-
let ox = config.width / 2
|
|
309
|
-
let oy = config.height * 0.6
|
|
310
|
-
if (opts.at && typeof opts.at === 'object' && !(opts.at instanceof Element) && 'x' in opts.at) {
|
|
311
|
-
ox = opts.at.x
|
|
312
|
-
oy = opts.at.y
|
|
313
|
-
} else if (opts.at) {
|
|
314
|
-
const el = resolveTargets(stage, opts.at as string | Element, 'confetti')[0] as HTMLElement
|
|
315
|
-
const r = layoutRect(stage, el)
|
|
316
|
-
ox = r.left + r.width / 2
|
|
317
|
-
oy = r.top + r.height / 2
|
|
318
|
-
}
|
|
319
|
-
const layer = document.createElement('div')
|
|
320
|
-
layer.dataset.confetti = ''
|
|
321
|
-
layer.style.cssText = `position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;z-index:70;visibility:hidden;`
|
|
322
|
-
stage.appendChild(layer)
|
|
323
|
-
type P = { el: HTMLElement; vx: number; vy: number; w: number; flutter: number; phase: number; delay: number }
|
|
324
|
-
const parts: P[] = []
|
|
325
|
-
for (let i = 0; i < count; i += 1) {
|
|
326
|
-
const p = document.createElement('div')
|
|
327
|
-
const s = (opts.size ?? 12) * (0.6 + hash01(i, 8) * 0.9)
|
|
328
|
-
p.style.cssText =
|
|
329
|
-
`position:absolute;left:${ox}px;top:${oy}px;width:${s}px;height:${s * (0.5 + hash01(i, 9) * 0.7)}px;` +
|
|
330
|
-
`border-radius:2px;background:${palette[i % palette.length]};will-change:transform;`
|
|
331
|
-
layer.appendChild(p)
|
|
332
|
-
const ang = ((hash01(i, 10) - 0.5) * 2 * spreadDeg * Math.PI) / 180
|
|
333
|
-
const speed = (900 + hash01(i, 11) * 1100) * power
|
|
334
|
-
parts.push({
|
|
335
|
-
el: p,
|
|
336
|
-
vx: Math.sin(ang) * speed,
|
|
337
|
-
vy: -Math.cos(ang) * speed,
|
|
338
|
-
w: (hash01(i, 12) - 0.5) * 1400,
|
|
339
|
-
flutter: 20 + hash01(i, 13) * 50,
|
|
340
|
-
phase: hash01(i, 14) * Math.PI * 2,
|
|
341
|
-
delay: hash01(i, 15) * 0.12,
|
|
342
|
-
})
|
|
343
|
-
}
|
|
344
|
-
const g = 2200
|
|
345
|
-
tl.set(layer, { visibility: 'visible' }, start)
|
|
346
|
-
const proxy = { t: 0 }
|
|
347
|
-
tl.to(
|
|
348
|
-
proxy,
|
|
349
|
-
{
|
|
350
|
-
t: duration,
|
|
351
|
-
duration,
|
|
352
|
-
ease: 'none',
|
|
353
|
-
onUpdate: () => {
|
|
354
|
-
const T = proxy.t
|
|
355
|
-
for (const p of parts) {
|
|
356
|
-
const t = Math.max(0, T - p.delay)
|
|
357
|
-
const fade = Math.max(0, Math.min(1, (duration - 0.35 - t) / 0.5 + 1))
|
|
358
|
-
gsap.set(p.el, {
|
|
359
|
-
x: p.vx * t + Math.sin(p.phase + t * 9) * p.flutter * t,
|
|
360
|
-
y: p.vy * t + 0.5 * g * t * t,
|
|
361
|
-
rotation: p.w * t,
|
|
362
|
-
opacity: t > 0 ? Math.min(1, fade) : 0,
|
|
363
|
-
force3D: false,
|
|
364
|
-
})
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
start
|
|
369
|
-
)
|
|
370
|
-
tl.set(layer, { visibility: 'hidden' }, start + duration)
|
|
371
|
-
return { duration }
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** A continuous belt of items scrolling through a clipped viewport — the
|
|
375
|
-
* gallery-scroll (Dash's file reels, Apple's tile river). The container's
|
|
376
|
-
* children are duplicated once for a seamless wrap and the row translates
|
|
377
|
-
* linearly; it ends reset to the authored position. Author the row inside
|
|
378
|
-
* an overflow:hidden viewport.
|
|
379
|
-
*
|
|
380
|
-
* WHEN NOT TO USE: as ambient filler behind a reading beat — a river IS a
|
|
381
|
-
* showcase beat (logos, covers, generated results). */
|
|
382
|
-
const river = (
|
|
383
|
-
container: string | Element,
|
|
384
|
-
opts: { speed?: number; direction?: 'left' | 'right'; duration?: number } = {},
|
|
385
|
-
position?: Position
|
|
386
|
-
): { duration: number } => {
|
|
387
|
-
const el = resolveTargets(stage, container, 'river')[0] as HTMLElement
|
|
388
|
-
const duration = opts.duration ?? 6
|
|
389
|
-
const speed = opts.speed ?? 140
|
|
390
|
-
const dir = opts.direction === 'right' ? 1 : -1
|
|
391
|
-
const start = at(position)
|
|
392
|
-
const kids = Array.from(el.children) as HTMLElement[]
|
|
393
|
-
const gapPx = parseFloat(getComputedStyle(el).columnGap) || 0
|
|
394
|
-
// One full period = authored content + the seam gap; translating by
|
|
395
|
-
// exactly this shows identical pixels, so the wrap is invisible.
|
|
396
|
-
const loop = el.scrollWidth + gapPx
|
|
397
|
-
for (const k of kids) el.appendChild(k.cloneNode(true))
|
|
398
|
-
const proxy = { t: 0 }
|
|
399
|
-
tl.to(
|
|
400
|
-
proxy,
|
|
401
|
-
{
|
|
402
|
-
t: duration,
|
|
403
|
-
duration,
|
|
404
|
-
ease: 'none',
|
|
405
|
-
onUpdate: () => {
|
|
406
|
-
const travel = (proxy.t * speed) % loop
|
|
407
|
-
// Leftward belts reveal their right edge (where the clone sits);
|
|
408
|
-
// rightward belts reveal their LEFT edge, so start one period back.
|
|
409
|
-
gsap.set(el, { x: dir < 0 ? -travel : -loop + travel, force3D: false })
|
|
410
|
-
},
|
|
411
|
-
},
|
|
412
|
-
start
|
|
413
|
-
)
|
|
414
|
-
tl.set(el, { x: 0 }, start + duration)
|
|
415
|
-
return { duration }
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Bento reflow: tiles in ONE grid continuously trade space — spans change
|
|
419
|
-
* state to state and every tile FLIPs (measured, exact) to its new slot,
|
|
420
|
-
* so the grid stays filled while its cells resize around each other.
|
|
421
|
-
* Author a CSS grid container (grid-auto-flow: dense recommended); state 0
|
|
422
|
-
* is the authored layout and the cycle ends exactly back on it. States are
|
|
423
|
-
* generated deterministically from index hashes. */
|
|
424
|
-
const bento = (
|
|
425
|
-
container: string | Element,
|
|
426
|
-
opts: { states?: number; interval?: number; duration?: number; ease?: string } = {},
|
|
427
|
-
position?: Position
|
|
428
|
-
): { duration: number } => {
|
|
429
|
-
const el = resolveTargets(stage, container, 'bento')[0] as HTMLElement
|
|
430
|
-
const tiles = Array.from(el.children) as HTMLElement[]
|
|
431
|
-
const nStates = Math.max(2, opts.states ?? 4)
|
|
432
|
-
const interval = opts.interval ?? 1.6
|
|
433
|
-
const flip = Math.min(opts.duration ?? 0.7, interval)
|
|
434
|
-
const ease = opts.ease ?? 'promo-inout'
|
|
435
|
-
const start = at(position)
|
|
436
|
-
const spansFor = (s: number, i: number): [number, number] =>
|
|
437
|
-
s === 0
|
|
438
|
-
? [1, 1]
|
|
439
|
-
: [1 + (hash01(i, s * 3 + 1) < 0.28 ? 1 : 0), 1 + (hash01(i, s * 3 + 2) < 0.22 ? 1 : 0)]
|
|
440
|
-
// Boxes RESIZE (width/height tweens), never scale — scaling squashes the
|
|
441
|
-
// tile's content (stretched labels). Border-box so measured rects map
|
|
442
|
-
// straight onto the tweened dimensions.
|
|
443
|
-
tiles.forEach((tile) => {
|
|
444
|
-
tile.style.boxSizing = 'border-box'
|
|
445
|
-
})
|
|
446
|
-
const apply = (s: number) => {
|
|
447
|
-
tiles.forEach((tile, i) => {
|
|
448
|
-
const [w, h] = spansFor(s, i)
|
|
449
|
-
tile.style.gridColumn = `span ${w}`
|
|
450
|
-
tile.style.gridRow = `span ${h}`
|
|
451
|
-
})
|
|
452
|
-
}
|
|
453
|
-
// Measure every state's true rects at build time, then restore.
|
|
454
|
-
const states: Array<Array<{ left: number; top: number; width: number; height: number }>> = []
|
|
455
|
-
for (let s = 0; s < nStates; s += 1) {
|
|
456
|
-
apply(s)
|
|
457
|
-
states.push(tiles.map((tile) => layoutRect(stage, tile)))
|
|
458
|
-
}
|
|
459
|
-
apply(0)
|
|
460
|
-
for (let s = 1; s <= nStates; s += 1) {
|
|
461
|
-
const to = s % nStates
|
|
462
|
-
const from = s - 1
|
|
463
|
-
const t0 = start + s * interval
|
|
464
|
-
tiles.forEach((tile, i) => {
|
|
465
|
-
const a = states[from][i]
|
|
466
|
-
const b = states[to][i]
|
|
467
|
-
const [w, h] = spansFor(to, i)
|
|
468
|
-
tl.set(tile, { gridColumn: `span ${w}`, gridRow: `span ${h}` }, t0)
|
|
469
|
-
tl.fromTo(
|
|
470
|
-
tile,
|
|
471
|
-
{ x: a.left - b.left, y: a.top - b.top, width: a.width, height: a.height },
|
|
472
|
-
{ x: 0, y: 0, width: b.width, height: b.height, duration: flip, ease, immediateRender: false },
|
|
473
|
-
t0
|
|
474
|
-
)
|
|
475
|
-
// Hand sizing back to the grid once the box has arrived.
|
|
476
|
-
tl.set(tile, { clearProps: 'width,height' }, t0 + flip)
|
|
477
|
-
})
|
|
478
|
-
}
|
|
479
|
-
return { duration: nStates * interval + flip }
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return { dropIn, assemble, scatter, fountain, confetti, river, bento }
|
|
483
|
-
}
|