@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/kits/highlight.ts
DELETED
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
// ─── Highlight — annotation primitives for pre-existing components ─────────
|
|
2
|
-
//
|
|
3
|
-
// The "look here" vocabulary: rings that draw themselves around an element,
|
|
4
|
-
// hand-drawn circles, highlighter-marker swipes, underlines, glow pulses, and
|
|
5
|
-
// the inverse spotlight (everything EXCEPT the target dims). One call per
|
|
6
|
-
// annotation, one Tweak row, geometry derived from the target (ring padding
|
|
7
|
-
// from element size, marker height from line height), colors overridable per
|
|
8
|
-
// call. Every highlight is STATE with an explicit end: `persist` says how long
|
|
9
|
-
// it holds, `release` says how it leaves — nothing loiters.
|
|
10
|
-
//
|
|
11
|
-
// Overlays are SVG/div siblings appended to the stage (auto-cleaned per
|
|
12
|
-
// rebuild), positioned from the shared authored-layout measurement, so they
|
|
13
|
-
// land correctly even when authored after entrance tweens.
|
|
14
|
-
|
|
15
|
-
import gsap from 'gsap'
|
|
16
|
-
import { layoutRect, layoutTextRect, visualRect, resolveTarget } from './kits'
|
|
17
|
-
|
|
18
|
-
export type HighlightRelease = 'fade' | 'undraw' | 'hold'
|
|
19
|
-
|
|
20
|
-
export type HighlightOpts = {
|
|
21
|
-
/** Stroke/fill color. Each primitive has its own sensible default. */
|
|
22
|
-
color?: string
|
|
23
|
-
/** Draw-on time in seconds. */
|
|
24
|
-
duration?: number
|
|
25
|
-
/** Default 'ease-in-out-cubic'. */
|
|
26
|
-
ease?: string
|
|
27
|
-
/** Seconds the highlight holds after drawing, or 'hold' to stay to the end.
|
|
28
|
-
* Default 'hold' — releasing is a deliberate choice. */
|
|
29
|
-
persist?: number | 'hold'
|
|
30
|
-
/** How it leaves when persist is a number: 'fade' (default) or 'undraw'. */
|
|
31
|
-
release?: Exclude<HighlightRelease, 'hold'>
|
|
32
|
-
/** Extra space around the target in scene px. Default derives from size. */
|
|
33
|
-
padding?: number
|
|
34
|
-
/** Stroke thickness in px. */
|
|
35
|
-
thickness?: number
|
|
36
|
-
/** What to fit: 'text' hugs the glyphs (a block element's box spans its
|
|
37
|
-
* full column — a marker on "99.99%" must not swallow the empty half of
|
|
38
|
-
* the card); 'box' takes the element box. Marker, circle, and underline
|
|
39
|
-
* default to 'text'; ring, glow, and spotlight default to 'box'. */
|
|
40
|
-
fit?: 'text' | 'box'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type SpotlightOpts = {
|
|
44
|
-
/** How dark the rest of the frame goes (0..1). Default 0.55. */
|
|
45
|
-
dim?: number
|
|
46
|
-
/** Defocus everything OUTSIDE the cutout by this many px (the target stays
|
|
47
|
-
* sharp). 0 (default) dims only. Engine ceiling: 20. */
|
|
48
|
-
blur?: number
|
|
49
|
-
/** Gaussian softness of the cutout edge in px. Default 26. */
|
|
50
|
-
feather?: number
|
|
51
|
-
/** Breathing room between the element's edge and the cutout, px. Default 16. */
|
|
52
|
-
padding?: number
|
|
53
|
-
duration?: number
|
|
54
|
-
ease?: string
|
|
55
|
-
persist?: number | 'hold'
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type HighlightKit = {
|
|
59
|
-
/** Rounded ring draws itself around the element, following its corner
|
|
60
|
-
* radius. The workhorse product-tour annotation. */
|
|
61
|
-
ring: (target: string | Element, opts?: HighlightOpts, position?: gsap.Position) => { duration: number }
|
|
62
|
-
/** Hand-drawn ellipse loop with overshoot — red-pen annotation energy. */
|
|
63
|
-
circle: (target: string | Element, opts?: HighlightOpts, position?: gsap.Position) => { duration: number }
|
|
64
|
-
/** Highlighter-pen swipe behind the glyphs: a skewed band wipes across in
|
|
65
|
-
* multiply blend, sized from the line height. */
|
|
66
|
-
marker: (target: string | Element, opts?: HighlightOpts, position?: gsap.Position) => { duration: number }
|
|
67
|
-
/** Underline draw. Styles: 'straight' | 'marker' | 'sketch'. */
|
|
68
|
-
underline: (
|
|
69
|
-
target: string | Element,
|
|
70
|
-
style?: 'straight' | 'marker' | 'sketch',
|
|
71
|
-
opts?: HighlightOpts,
|
|
72
|
-
position?: gsap.Position
|
|
73
|
-
) => { duration: number }
|
|
74
|
-
/** One soft glow pulse as a beat lands — light, not opacity. */
|
|
75
|
-
glow: (target: string | Element, opts?: HighlightOpts, position?: gsap.Position) => { duration: number }
|
|
76
|
-
/** Inverse move: everything EXCEPT the target dims behind a feathered
|
|
77
|
-
* cutout. The strongest "look here" in the vocabulary. */
|
|
78
|
-
spotlight: (target: string | Element, opts?: SpotlightOpts, position?: gsap.Position) => { duration: number }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export type HighlightDeps = {
|
|
82
|
-
tl: gsap.core.Timeline
|
|
83
|
-
stage: HTMLElement
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const EASE = 'ease-in-out-cubic'
|
|
87
|
-
|
|
88
|
-
/** Ring padding derived from target size — small chips get tight rings, large
|
|
89
|
-
* panels get room to breathe. */
|
|
90
|
-
export function autoPadding(rect: { width: number; height: number }): number {
|
|
91
|
-
const d = Math.min(rect.width, rect.height)
|
|
92
|
-
return Math.round(Math.min(18, Math.max(6, d * 0.12)))
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const NS = 'http://www.w3.org/2000/svg'
|
|
96
|
-
|
|
97
|
-
export function createHighlightKit(deps: HighlightDeps): HighlightKit {
|
|
98
|
-
const { tl, stage } = deps
|
|
99
|
-
|
|
100
|
-
const rectFor = (
|
|
101
|
-
target: string | Element,
|
|
102
|
-
kind: string,
|
|
103
|
-
fit: 'text' | 'box' = 'box'
|
|
104
|
-
) => {
|
|
105
|
-
const el = resolveTarget(stage, target, kind)
|
|
106
|
-
return {
|
|
107
|
-
el,
|
|
108
|
-
rect: fit === 'text' ? layoutTextRect(stage, el) : layoutRect(stage, el),
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const overlaySvg = (
|
|
113
|
-
left: number,
|
|
114
|
-
top: number,
|
|
115
|
-
width: number,
|
|
116
|
-
height: number,
|
|
117
|
-
label: string
|
|
118
|
-
): SVGSVGElement => {
|
|
119
|
-
const svg = document.createElementNS(NS, 'svg')
|
|
120
|
-
svg.setAttribute(`data-highlight-${label}`, '')
|
|
121
|
-
svg.setAttribute('viewBox', `0 0 ${width} ${height}`)
|
|
122
|
-
svg.style.cssText = `position:absolute;left:${left}px;top:${top}px;width:${width}px;height:${height}px;pointer-events:none;overflow:visible;z-index:9900;`
|
|
123
|
-
stage.appendChild(svg)
|
|
124
|
-
return svg
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Draw-on via dash offset, then apply the persist/release contract. */
|
|
128
|
-
const drawAndRelease = (
|
|
129
|
-
strokeEl: SVGElement,
|
|
130
|
-
host: Element,
|
|
131
|
-
length: number,
|
|
132
|
-
opts: HighlightOpts,
|
|
133
|
-
at: number
|
|
134
|
-
): number => {
|
|
135
|
-
const duration = opts.duration ?? 0.5
|
|
136
|
-
const ease = opts.ease ?? EASE
|
|
137
|
-
strokeEl.setAttribute('stroke-dasharray', String(length))
|
|
138
|
-
strokeEl.setAttribute('stroke-dashoffset', String(length))
|
|
139
|
-
tl.to(strokeEl, { strokeDashoffset: 0, duration, ease }, at)
|
|
140
|
-
const persist = opts.persist ?? 'hold'
|
|
141
|
-
if (persist === 'hold') return duration
|
|
142
|
-
const releaseAt = at + duration + persist
|
|
143
|
-
if ((opts.release ?? 'fade') === 'undraw') {
|
|
144
|
-
tl.to(strokeEl, { strokeDashoffset: -length, duration: duration * 0.8, ease }, releaseAt)
|
|
145
|
-
return duration + persist + duration * 0.8
|
|
146
|
-
}
|
|
147
|
-
tl.to(host, { autoAlpha: 0, duration: 0.25, ease: 'ease-in-quad' }, releaseAt)
|
|
148
|
-
return duration + persist + 0.25
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const fadeRelease = (host: Element, opts: { persist?: number | 'hold' }, endOfIn: number): number => {
|
|
152
|
-
const persist = opts.persist ?? 'hold'
|
|
153
|
-
if (persist === 'hold') return 0
|
|
154
|
-
tl.to(host, { autoAlpha: 0, duration: 0.25, ease: 'ease-in-quad' }, endOfIn + persist)
|
|
155
|
-
return persist + 0.25
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const at = (position: gsap.Position | undefined): number =>
|
|
159
|
-
typeof position === 'number' ? position : tl.duration()
|
|
160
|
-
|
|
161
|
-
/** Reposition a stage-space overlay from its build-time layout frame to
|
|
162
|
-
* wherever its target actually sits when the highlight begins — a container
|
|
163
|
-
* the direction scales mid-timeline has moved the UI off its resting rects
|
|
164
|
-
* by then. Resolved via function values at render time; uses css box props,
|
|
165
|
-
* never transforms, so it cannot fight the draw/skew animations. */
|
|
166
|
-
const syncToTarget = (
|
|
167
|
-
host: HTMLElement | SVGSVGElement,
|
|
168
|
-
el: HTMLElement,
|
|
169
|
-
frame: { left: number; top: number; width: number; height: number },
|
|
170
|
-
rect: { left: number; top: number; width: number; height: number },
|
|
171
|
-
position: number
|
|
172
|
-
): void => {
|
|
173
|
-
const map = () => {
|
|
174
|
-
const v = visualRect(stage, el)
|
|
175
|
-
const s = rect.width ? v.width / rect.width : 1
|
|
176
|
-
return {
|
|
177
|
-
left: v.left + (frame.left - rect.left) * s,
|
|
178
|
-
top: v.top + (frame.top - rect.top) * s,
|
|
179
|
-
width: frame.width * s,
|
|
180
|
-
height: frame.height * s,
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
tl.set(
|
|
184
|
-
host,
|
|
185
|
-
{
|
|
186
|
-
left: () => `${map().left}px`,
|
|
187
|
-
top: () => `${map().top}px`,
|
|
188
|
-
width: () => `${map().width}px`,
|
|
189
|
-
height: () => `${map().height}px`,
|
|
190
|
-
},
|
|
191
|
-
position
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
ring: (target, opts = {}, position) => {
|
|
197
|
-
const { el, rect } = rectFor(target, 'highlight.ring')
|
|
198
|
-
const pad = opts.padding ?? autoPadding(rect)
|
|
199
|
-
const thickness = opts.thickness ?? 3
|
|
200
|
-
const w = rect.width + pad * 2
|
|
201
|
-
const h = rect.height + pad * 2
|
|
202
|
-
const radius = Math.min(
|
|
203
|
-
parseFloat(getComputedStyle(el).borderRadius) + pad || 10,
|
|
204
|
-
Math.min(w, h) / 2
|
|
205
|
-
)
|
|
206
|
-
const svg = overlaySvg(rect.left - pad, rect.top - pad, w, h, 'ring')
|
|
207
|
-
const inset = thickness / 2
|
|
208
|
-
const r = document.createElementNS(NS, 'rect')
|
|
209
|
-
r.setAttribute('x', String(inset))
|
|
210
|
-
r.setAttribute('y', String(inset))
|
|
211
|
-
r.setAttribute('width', String(w - thickness))
|
|
212
|
-
r.setAttribute('height', String(h - thickness))
|
|
213
|
-
r.setAttribute('rx', String(radius))
|
|
214
|
-
r.setAttribute('fill', 'none')
|
|
215
|
-
r.setAttribute('stroke', opts.color ?? '#4c8dff')
|
|
216
|
-
r.setAttribute('stroke-width', String(thickness))
|
|
217
|
-
r.setAttribute('stroke-linecap', 'round')
|
|
218
|
-
svg.appendChild(r)
|
|
219
|
-
const length = r.getTotalLength()
|
|
220
|
-
const start = at(position)
|
|
221
|
-
syncToTarget(svg, el, { left: rect.left - pad, top: rect.top - pad, width: w, height: h }, rect, start)
|
|
222
|
-
return { duration: drawAndRelease(r, svg, length, opts, start) }
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
circle: (target, opts = {}, position) => {
|
|
226
|
-
const { el, rect } = rectFor(target, 'highlight.circle', opts.fit ?? 'text')
|
|
227
|
-
const pad = (opts.padding ?? autoPadding(rect)) + 6
|
|
228
|
-
const w = rect.width + pad * 2
|
|
229
|
-
const h = rect.height + pad * 2
|
|
230
|
-
const svg = overlaySvg(rect.left - pad, rect.top - pad, w, h, 'circle')
|
|
231
|
-
svg.style.transform = 'rotate(-3deg)'
|
|
232
|
-
const e = document.createElementNS(NS, 'ellipse')
|
|
233
|
-
e.setAttribute('cx', String(w / 2))
|
|
234
|
-
e.setAttribute('cy', String(h / 2))
|
|
235
|
-
e.setAttribute('rx', String(w / 2 - 3))
|
|
236
|
-
e.setAttribute('ry', String(h / 2 - 3))
|
|
237
|
-
e.setAttribute('fill', 'none')
|
|
238
|
-
e.setAttribute('stroke', opts.color ?? '#ff3b30')
|
|
239
|
-
e.setAttribute('stroke-width', String(opts.thickness ?? 3.5))
|
|
240
|
-
e.setAttribute('stroke-linecap', 'round')
|
|
241
|
-
svg.appendChild(e)
|
|
242
|
-
// 1.15 loops: the pen overshoots past its starting point — the detail
|
|
243
|
-
// that reads as hand-drawn rather than geometric.
|
|
244
|
-
const length = e.getTotalLength()
|
|
245
|
-
const start = at(position)
|
|
246
|
-
syncToTarget(svg, el, { left: rect.left - pad, top: rect.top - pad, width: w, height: h }, rect, start)
|
|
247
|
-
const drawn = drawAndRelease(e, svg, length * 1.15, { duration: 0.6, ...opts }, start)
|
|
248
|
-
return { duration: drawn }
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
marker: (target, opts = {}, position) => {
|
|
252
|
-
const { el, rect } = rectFor(target, 'highlight.marker', opts.fit ?? 'text')
|
|
253
|
-
const band = document.createElement('div')
|
|
254
|
-
band.setAttribute('data-highlight-marker', '')
|
|
255
|
-
const h = rect.height * 0.72
|
|
256
|
-
band.style.cssText =
|
|
257
|
-
`position:absolute;left:${rect.left - 6}px;top:${rect.top + (rect.height - h) / 2}px;` +
|
|
258
|
-
`width:${rect.width + 14}px;height:${h}px;background:${opts.color ?? '#ffe066'};` +
|
|
259
|
-
`opacity:0.55;mix-blend-mode:multiply;border-radius:${h * 0.28}px;` +
|
|
260
|
-
`transform:skewX(-8deg);transform-origin:0 50%;pointer-events:none;z-index:9890;`
|
|
261
|
-
stage.appendChild(band)
|
|
262
|
-
const duration = opts.duration ?? 0.45
|
|
263
|
-
const start = at(position)
|
|
264
|
-
syncToTarget(
|
|
265
|
-
band,
|
|
266
|
-
el,
|
|
267
|
-
{ left: rect.left - 6, top: rect.top + (rect.height - h) / 2, width: rect.width + 14, height: h },
|
|
268
|
-
rect,
|
|
269
|
-
start
|
|
270
|
-
)
|
|
271
|
-
tl.fromTo(band, { scaleX: 0 }, { scaleX: 1, duration, ease: opts.ease ?? 'ease-out-quart' }, start)
|
|
272
|
-
const tail = fadeRelease(band, opts, start + duration)
|
|
273
|
-
return { duration: duration + tail }
|
|
274
|
-
},
|
|
275
|
-
|
|
276
|
-
underline: (target, style = 'sketch', opts = {}, position) => {
|
|
277
|
-
const { el, rect } = rectFor(target, 'highlight.underline', opts.fit ?? 'text')
|
|
278
|
-
const thickness = opts.thickness ?? (style === 'marker' ? 7 : 3)
|
|
279
|
-
const y = rect.top + rect.height + (opts.padding ?? 4)
|
|
280
|
-
const svg = overlaySvg(rect.left, y, rect.width, thickness * 3, 'underline')
|
|
281
|
-
const path = document.createElementNS(NS, 'path')
|
|
282
|
-
const w = rect.width
|
|
283
|
-
const mid = thickness * 1.5
|
|
284
|
-
const d =
|
|
285
|
-
style === 'sketch'
|
|
286
|
-
? // A slight double-back wave: the pen presses through and returns.
|
|
287
|
-
`M2 ${mid} Q ${w * 0.3} ${mid - 3}, ${w * 0.55} ${mid} T ${w - 2} ${mid - 1}`
|
|
288
|
-
: `M2 ${mid} L ${w - 2} ${mid}`
|
|
289
|
-
path.setAttribute('d', d)
|
|
290
|
-
path.setAttribute('fill', 'none')
|
|
291
|
-
path.setAttribute('stroke', opts.color ?? (style === 'marker' ? '#ffe066' : '#ff3b30'))
|
|
292
|
-
path.setAttribute('stroke-width', String(thickness))
|
|
293
|
-
path.setAttribute('stroke-linecap', 'round')
|
|
294
|
-
if (style === 'marker') {
|
|
295
|
-
path.style.mixBlendMode = 'multiply'
|
|
296
|
-
path.style.opacity = '0.6'
|
|
297
|
-
}
|
|
298
|
-
svg.appendChild(path)
|
|
299
|
-
const start = at(position)
|
|
300
|
-
syncToTarget(svg, el, { left: rect.left, top: y, width: rect.width, height: thickness * 3 }, rect, start)
|
|
301
|
-
return { duration: drawAndRelease(path, svg, path.getTotalLength(), { duration: 0.4, ...opts }, start) }
|
|
302
|
-
},
|
|
303
|
-
|
|
304
|
-
glow: (target, opts = {}, position) => {
|
|
305
|
-
const { el, rect } = rectFor(target, 'highlight.glow')
|
|
306
|
-
const color = opts.color ?? '#ffb020'
|
|
307
|
-
const duration = opts.duration ?? 0.7
|
|
308
|
-
const start = at(position)
|
|
309
|
-
// A shadow-halo overlay, not a filter on the element — so it cannot
|
|
310
|
-
// clobber an fx chain already driving the element's filter.
|
|
311
|
-
const halo = document.createElement('div')
|
|
312
|
-
halo.setAttribute('data-highlight-glow', '')
|
|
313
|
-
halo.style.cssText =
|
|
314
|
-
`position:absolute;left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;` +
|
|
315
|
-
`height:${rect.height}px;border-radius:${getComputedStyle(el).borderRadius || '8px'};` +
|
|
316
|
-
`box-shadow:0 0 ${Math.max(24, rect.height * 0.5)}px ${color};opacity:0;` +
|
|
317
|
-
`pointer-events:none;z-index:9880;`
|
|
318
|
-
stage.appendChild(halo)
|
|
319
|
-
syncToTarget(halo, el, { left: rect.left, top: rect.top, width: rect.width, height: rect.height }, rect, start)
|
|
320
|
-
tl.to(halo, { opacity: 0.85, duration: duration / 2, repeat: 1, yoyo: true, ease: 'ease-in-out-quad' }, start)
|
|
321
|
-
return { duration }
|
|
322
|
-
},
|
|
323
|
-
|
|
324
|
-
spotlight: (target, opts = {}, position) => {
|
|
325
|
-
const { el, rect } = rectFor(target, 'highlight.spotlight')
|
|
326
|
-
const dim = opts.dim ?? 0.55
|
|
327
|
-
const blur = Math.min(20, opts.blur ?? 0)
|
|
328
|
-
// Legacy fractional feather values map into px so old calls stay sane.
|
|
329
|
-
const rawFeather = opts.feather ?? 26
|
|
330
|
-
const feather = rawFeather <= 1 ? rawFeather * 80 : rawFeather
|
|
331
|
-
const pad = opts.padding ?? 16
|
|
332
|
-
const duration = opts.duration ?? 0.4
|
|
333
|
-
const overlay = document.createElement('div')
|
|
334
|
-
overlay.setAttribute('data-highlight-spotlight', '')
|
|
335
|
-
// The cutout is the ELEMENT'S OWN SHAPE — a rounded rect at its
|
|
336
|
-
// border-radius plus breathing room — feathered by a true Gaussian in
|
|
337
|
-
// an SVG mask. An elliptical gradient cutout protects a huge oval of
|
|
338
|
-
// background around a rectangular card and shows a visible ring where
|
|
339
|
-
// the gradient ends; a blurred shape-hugging hole does neither.
|
|
340
|
-
const sw = stage.offsetWidth
|
|
341
|
-
const sh = stage.offsetHeight
|
|
342
|
-
const radius = (parseFloat(getComputedStyle(el).borderRadius) || 12) + pad
|
|
343
|
-
const hx = (rect.left - pad).toFixed(1)
|
|
344
|
-
const hy = (rect.top - pad).toFixed(1)
|
|
345
|
-
const hw = (rect.width + pad * 2).toFixed(1)
|
|
346
|
-
const hh = (rect.height + pad * 2).toFixed(1)
|
|
347
|
-
const svg =
|
|
348
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${sw}" height="${sh}">` +
|
|
349
|
-
`<defs><filter id="f" x="-50%" y="-50%" width="200%" height="200%">` +
|
|
350
|
-
`<feGaussianBlur stdDeviation="${feather}"/></filter>` +
|
|
351
|
-
`<mask id="m"><rect width="100%" height="100%" fill="white"/>` +
|
|
352
|
-
`<rect x="${hx}" y="${hy}" width="${hw}" height="${hh}" rx="${radius}" fill="black" filter="url(#f)"/>` +
|
|
353
|
-
`</mask></defs>` +
|
|
354
|
-
`<rect width="100%" height="100%" fill="white" mask="url(#m)"/></svg>`
|
|
355
|
-
const maskValue = `url("data:image/svg+xml,${encodeURIComponent(svg)}")`
|
|
356
|
-
// With `blur`, the overlay defocuses what's BEHIND it (backdrop-filter)
|
|
357
|
-
// outside the cutout — the target stays sharp while its surroundings
|
|
358
|
-
// fall out of focus, on top of the dim.
|
|
359
|
-
overlay.style.cssText =
|
|
360
|
-
`position:absolute;inset:0;background:rgba(8,8,12,${dim});opacity:0;` +
|
|
361
|
-
`pointer-events:none;z-index:9870;` +
|
|
362
|
-
(blur > 0 ? `backdrop-filter:blur(${blur}px);-webkit-backdrop-filter:blur(${blur}px);` : '') +
|
|
363
|
-
`mask-image:${maskValue};-webkit-mask-image:${maskValue};` +
|
|
364
|
-
`mask-size:100% 100%;-webkit-mask-size:100% 100%;`
|
|
365
|
-
stage.appendChild(overlay)
|
|
366
|
-
const start = at(position)
|
|
367
|
-
tl.to(overlay, { opacity: 1, duration, ease: opts.ease ?? EASE }, start)
|
|
368
|
-
const persist = opts.persist ?? 'hold'
|
|
369
|
-
if (persist === 'hold') return { duration }
|
|
370
|
-
tl.to(overlay, { opacity: 0, duration: 0.3, ease: 'ease-in-quad' }, start + duration + persist)
|
|
371
|
-
return { duration: duration + persist + 0.3 }
|
|
372
|
-
},
|
|
373
|
-
}
|
|
374
|
-
}
|
package/assets/kits/impact.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// Impact moves — the "slam": type or a card that hits the frame hard, or
|
|
2
|
-
// tips away out of it.
|
|
3
|
-
//
|
|
4
|
-
// THE RULE THIS KIT EXISTS TO ENFORCE: distortion belongs to MOTION, never to
|
|
5
|
-
// rest. A slam is legible because the element is perspective-turned only while
|
|
6
|
-
// it is travelling too fast to read, and lands (or leaves) undistorted. The
|
|
7
|
-
// failure mode — seen in a shipped film — is an element that POPS into a
|
|
8
|
-
// sheared pose in one frame and then sits there: the viewer reads a broken
|
|
9
|
-
// font, not a moving object. So:
|
|
10
|
-
// · the big change is the visible travel, not a jump into the end pose
|
|
11
|
-
// · 'in' ends at exact identity (rotation 0, scale 1) — never mid-turn
|
|
12
|
-
// · 'away' ends fully invisible, so the turned pose is never a resting one
|
|
13
|
-
// · pair with ctx.blur() across the fast window: real smear sells the speed
|
|
14
|
-
// that a 30fps frame cannot show on its own
|
|
15
|
-
//
|
|
16
|
-
// Perspective is applied to the ELEMENT, never the camera-scaled stage
|
|
17
|
-
// (promoting the whole scaled stage to a 3D layer blows Chromium's texture
|
|
18
|
-
// budget — see camera.set).
|
|
19
|
-
import { resolveTargets } from './kits'
|
|
20
|
-
|
|
21
|
-
type Position = number | string | undefined
|
|
22
|
-
|
|
23
|
-
export type SlamOpts = {
|
|
24
|
-
/** 'in' = arrives and lands flat (the impact). 'away' = tips back and
|
|
25
|
-
* leaves (the hand-off). Default 'in'. */
|
|
26
|
-
direction?: 'in' | 'away'
|
|
27
|
-
/** Perspective turn in degrees at the extreme of the move. Default 34. */
|
|
28
|
-
tilt?: number
|
|
29
|
-
/** Travel along Y in px. Default 240 ('in' comes from above, 'away' falls). */
|
|
30
|
-
distance?: number
|
|
31
|
-
/** Scale at the extreme. Default 1.55 for 'in', 0.86 for 'away'. */
|
|
32
|
-
scale?: number
|
|
33
|
-
/** Default 0.34s — a slam is fast; past ~0.5s it stops reading as impact. */
|
|
34
|
-
duration?: number
|
|
35
|
-
/** Hinge. Default '50% 100%' (the baseline) so the plane TIPS rather than
|
|
36
|
-
* spins; use '50% 50%' for a card, '0% 50%' to swing from the left edge. */
|
|
37
|
-
origin?: string
|
|
38
|
-
perspective?: number
|
|
39
|
-
ease?: string
|
|
40
|
-
/** Stagger when several elements are handed in. Default 0.06. */
|
|
41
|
-
stagger?: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type ImpactKit = {
|
|
45
|
-
slam: (target: string | Element | Element[], opts?: SlamOpts, position?: Position) => { duration: number }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function createImpactKit(deps: { tl: any; gsap: any; stage: HTMLElement }): ImpactKit {
|
|
49
|
-
const { tl, gsap, stage } = deps
|
|
50
|
-
const at = (position: Position): number => (typeof position === 'number' ? position : tl.duration())
|
|
51
|
-
|
|
52
|
-
const slam: ImpactKit['slam'] = (target, opts = {}, position) => {
|
|
53
|
-
const els = resolveTargets(stage, target, 'slam')
|
|
54
|
-
const away = opts.direction === 'away'
|
|
55
|
-
const tilt = opts.tilt ?? 34
|
|
56
|
-
const distance = opts.distance ?? 240
|
|
57
|
-
const scale = opts.scale ?? (away ? 0.86 : 1.55)
|
|
58
|
-
const duration = opts.duration ?? 0.34
|
|
59
|
-
const origin = opts.origin ?? '50% 100%'
|
|
60
|
-
const perspective = opts.perspective ?? 900
|
|
61
|
-
const stagger = opts.stagger ?? 0.06
|
|
62
|
-
// Entrances land hard (launch-out puts ~80% of the travel in the first
|
|
63
|
-
// ~20% of the time); exits accelerate away.
|
|
64
|
-
const ease = opts.ease ?? (away ? 'ease-in-quart' : 'launch-out')
|
|
65
|
-
const start = at(position)
|
|
66
|
-
|
|
67
|
-
els.forEach((raw, i) => {
|
|
68
|
-
const el = raw as HTMLElement
|
|
69
|
-
const t = start + i * stagger
|
|
70
|
-
gsap.set(el, { transformOrigin: origin, transformPerspective: perspective })
|
|
71
|
-
if (away) {
|
|
72
|
-
tl.to(
|
|
73
|
-
el,
|
|
74
|
-
{
|
|
75
|
-
rotationX: -tilt,
|
|
76
|
-
y: distance,
|
|
77
|
-
scale,
|
|
78
|
-
autoAlpha: 0,
|
|
79
|
-
duration,
|
|
80
|
-
ease,
|
|
81
|
-
force3D: false,
|
|
82
|
-
},
|
|
83
|
-
t
|
|
84
|
-
)
|
|
85
|
-
// Ends fully gone AND reset: the turned pose is never a resting one,
|
|
86
|
-
// and a cold-seeking export worker lands on a clean state.
|
|
87
|
-
tl.set(el, { rotationX: 0, y: 0, scale: 1, autoAlpha: 0 }, t + duration)
|
|
88
|
-
} else {
|
|
89
|
-
tl.fromTo(
|
|
90
|
-
el,
|
|
91
|
-
{ rotationX: tilt, y: -distance, scale, autoAlpha: 0 },
|
|
92
|
-
{ rotationX: 0, y: 0, scale: 1, autoAlpha: 1, duration, ease, force3D: false },
|
|
93
|
-
t
|
|
94
|
-
)
|
|
95
|
-
// Exact identity at the landing frame.
|
|
96
|
-
tl.set(el, { rotationX: 0, y: 0, scale: 1 }, t + duration)
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
return { duration: (els.length - 1) * stagger + duration }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { slam }
|
|
103
|
-
}
|