@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/actions.ts
DELETED
|
@@ -1,803 +0,0 @@
|
|
|
1
|
-
// ─── Actions — cursor, click, drag, scroll, and UI state primitives ────────
|
|
2
|
-
//
|
|
3
|
-
// The interaction vocabulary that every scene used to hand-roll: cursors that
|
|
4
|
-
// travel and click, buttons that squash, containers that scroll, UI state that
|
|
5
|
-
// changes on the timeline. One call per action, one Tweak row per gesture,
|
|
6
|
-
// perceptually-tuned defaults where scenes used to carry magic numbers.
|
|
7
|
-
//
|
|
8
|
-
// The cursor is a STATEFUL ACTOR, not a per-call sprite. The kit tracks where
|
|
9
|
-
// every cursor is and whether it is on screen, so continuation is the default
|
|
10
|
-
// and entry/exit are explicit moments:
|
|
11
|
-
//
|
|
12
|
-
// cursor.click('[data-new]') // enters from the nearest edge,
|
|
13
|
-
// type('[data-input]', 'hello') // travels, morphs, clicks
|
|
14
|
-
// cursor.click('[data-send]') // continues FROM WHERE IT WAS
|
|
15
|
-
// cursor.exit() // leaves via the nearest edge
|
|
16
|
-
//
|
|
17
|
-
// Icons live in the engine with REGISTERED HOTSPOTS (where the tip actually
|
|
18
|
-
// is), so click math can never drift the way per-scene hand-rolled rigs did.
|
|
19
|
-
// Every action defaults to `ease-in-out-cubic` and takes a `duration`; the two
|
|
20
|
-
// deliberate exceptions are typing (stepped by definition, see typeInto) and
|
|
21
|
-
// `ui.set` (instant — a cut is hard).
|
|
22
|
-
//
|
|
23
|
-
// Everything obeys the kit conventions: factory, every tween through `deps.tl`,
|
|
24
|
-
// `(target, opts?, position?)`, returns `{ duration }`, deterministic — no
|
|
25
|
-
// clocks, no randomness; the idle "human" drift is a fixed sine.
|
|
26
|
-
|
|
27
|
-
import gsap from 'gsap'
|
|
28
|
-
import { createWarner, layoutRect as sharedLayoutRect, visualRect as sharedVisualRect, resolveTarget } from './kits'
|
|
29
|
-
|
|
30
|
-
type Rect = { left: number; top: number; width: number; height: number }
|
|
31
|
-
|
|
32
|
-
export type ActionsDeps = {
|
|
33
|
-
tl: gsap.core.Timeline
|
|
34
|
-
stage: HTMLElement
|
|
35
|
-
config: { width: number; height: number }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Built-in cursor icons. `collab` is the Figma-style colored arrow + name
|
|
39
|
-
* chip for multi-user scenes. Custom art passes `{ svg, hotspot }` — the
|
|
40
|
-
* hotspot (0..1 of the icon box, where the tip sits) is part of the contract,
|
|
41
|
-
* so brand cursors cannot reopen the misaligned-click bug. */
|
|
42
|
-
export type CursorIconName =
|
|
43
|
-
| 'arrow'
|
|
44
|
-
| 'pointer'
|
|
45
|
-
| 'ibeam'
|
|
46
|
-
| 'grab'
|
|
47
|
-
| 'grabbing'
|
|
48
|
-
| 'crosshair'
|
|
49
|
-
| 'collab'
|
|
50
|
-
export type CursorIcon = CursorIconName | { svg: string; hotspot: { x: number; y: number } }
|
|
51
|
-
|
|
52
|
-
export type CursorActorOpts = {
|
|
53
|
-
/** Icon set. Default 'arrow'; named collaborators usually want 'collab'. */
|
|
54
|
-
icon?: CursorIcon
|
|
55
|
-
/** Tint for the collab arrow and label chip. */
|
|
56
|
-
color?: string
|
|
57
|
-
/** Figma-style name tag riding the cursor. */
|
|
58
|
-
label?: string
|
|
59
|
-
/** Icon box size in scene px. Default 56 (at 1080p) — marketing videos
|
|
60
|
-
* oversize the cursor so the action reads at a glance. */
|
|
61
|
-
size?: number
|
|
62
|
-
/** 'still' (default) or 'human' — sub-pixel drift while parked between
|
|
63
|
-
* actions, so a resting cursor never reads as a freeze-frame. */
|
|
64
|
-
idle?: 'still' | 'human'
|
|
65
|
-
/** Auto-morph arrow → pointer over the last stretch of a click travel.
|
|
66
|
-
* Default true. */
|
|
67
|
-
morph?: boolean
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export type CursorMoveOpts = {
|
|
71
|
-
/** Travel time in seconds. Default 0.8. */
|
|
72
|
-
duration?: number
|
|
73
|
-
/** Default 'ease-in-out-cubic' — every action eases in-out. */
|
|
74
|
-
ease?: string
|
|
75
|
-
/** Where on the target to land, as fractions of its box. Default
|
|
76
|
-
* `{ x: 0.5, y: 0.52 }` — the corpus's observed natural click point. */
|
|
77
|
-
anchor?: { x?: number; y?: number }
|
|
78
|
-
/** Extra scene-px nudge after the anchor. */
|
|
79
|
-
offset?: { x?: number; y?: number }
|
|
80
|
-
/** First-action entrance: 'edge' (default — slides in from the nearest
|
|
81
|
-
* frame edge) or 'fade' (materializes just short of the target, for tight
|
|
82
|
-
* close-ups where the edge is far away). Ignored while already visible. */
|
|
83
|
-
enter?: 'edge' | 'fade'
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** What the cursor does after the action — the continuity contract.
|
|
87
|
-
* 'stay' (default) parks it where it is so the NEXT action continues from
|
|
88
|
-
* there; 'exit' leaves via the nearest edge; 'park' steps aside just below
|
|
89
|
-
* the target (clears it for typing or a reveal); a selector travels to that
|
|
90
|
-
* element and parks there. */
|
|
91
|
-
export type CursorThen = 'stay' | 'exit' | 'park' | string
|
|
92
|
-
|
|
93
|
-
export type CursorClickOpts = CursorMoveOpts & {
|
|
94
|
-
/** Target squash scale. 'auto' (default) derives from the element's size —
|
|
95
|
-
* big surfaces squash less. A number (e.g. 0.9) is used verbatim. */
|
|
96
|
-
press?: number | 'auto'
|
|
97
|
-
/** Squash time each way. Default 0.09. */
|
|
98
|
-
pressDuration?: number
|
|
99
|
-
then?: CursorThen
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export type CursorDragOpts = CursorMoveOpts & {
|
|
103
|
-
/** Subtle scale lift on the payload while it is held. Default true. */
|
|
104
|
-
lift?: boolean
|
|
105
|
-
then?: CursorThen
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export type CursorExitOpts = {
|
|
109
|
-
duration?: number
|
|
110
|
-
ease?: string
|
|
111
|
-
/** 'edge' (default) or 'fade'. */
|
|
112
|
-
via?: 'edge' | 'fade'
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export type CursorActor = {
|
|
116
|
-
/** Move to an element (or continue there from the last position). */
|
|
117
|
-
to: (
|
|
118
|
-
target: string | Element,
|
|
119
|
-
opts?: CursorMoveOpts,
|
|
120
|
-
position?: gsap.Position
|
|
121
|
-
) => { duration: number }
|
|
122
|
-
/** Travel + press: squashes the cursor AND the target, ordered so the
|
|
123
|
-
* click visibly precedes any result the direction schedules after it. */
|
|
124
|
-
click: (
|
|
125
|
-
target: string | Element,
|
|
126
|
-
opts?: CursorClickOpts,
|
|
127
|
-
position?: gsap.Position
|
|
128
|
-
) => { duration: number }
|
|
129
|
-
/** Grab a payload and carry it: cursor and payload move as ONE tween pair
|
|
130
|
-
* with identical timing, so they cannot desync. `to` is an element/selector
|
|
131
|
-
* (payload centers on it) or a scene-px delta `{ x, y }`. */
|
|
132
|
-
drag: (
|
|
133
|
-
payload: string | Element,
|
|
134
|
-
to: string | Element | { x: number; y: number },
|
|
135
|
-
opts?: CursorDragOpts,
|
|
136
|
-
position?: gsap.Position
|
|
137
|
-
) => { duration: number }
|
|
138
|
-
/** Leave the frame. The next action re-enters fresh. */
|
|
139
|
-
exit: (opts?: CursorExitOpts, position?: gsap.Position) => { duration: number }
|
|
140
|
-
/** Update the collab name chip on the timeline. */
|
|
141
|
-
label: (text: string, position?: gsap.Position) => void
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export type CursorKit = CursorActor & {
|
|
145
|
-
/** Declare a named cursor — an independent actor with its own icon, color,
|
|
146
|
-
* label chip, and continuity state. Two actors animate concurrently just by
|
|
147
|
-
* overlapping their positions. */
|
|
148
|
-
actor: (name: string, opts?: CursorActorOpts) => CursorActor
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export type PressOpts = {
|
|
152
|
-
/** Squash scale; 'auto' (default) derives from element size. */
|
|
153
|
-
amount?: number | 'auto'
|
|
154
|
-
/** Each way. Default 0.09. */
|
|
155
|
-
duration?: number
|
|
156
|
-
ease?: string
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export type ScrollOpts = {
|
|
160
|
-
duration?: number
|
|
161
|
-
ease?: string
|
|
162
|
-
/** Where the target settles in the container: 0 top, 0.5 center (default),
|
|
163
|
-
* 1 bottom. */
|
|
164
|
-
align?: number
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export type UiChange = {
|
|
168
|
-
/** Replace the element's text content. */
|
|
169
|
-
text?: string
|
|
170
|
-
/** Set an input's value. */
|
|
171
|
-
value?: string
|
|
172
|
-
/** Swap an image/video source. */
|
|
173
|
-
src?: string
|
|
174
|
-
/** Set attributes verbatim. */
|
|
175
|
-
attrs?: Record<string, string>
|
|
176
|
-
/** Add/remove classes (resolved against the authored class list). */
|
|
177
|
-
classes?: { add?: string[]; remove?: string[] }
|
|
178
|
-
/** Brief brightness pulse after the change, so the state change reads as an
|
|
179
|
-
* event rather than a glitch. */
|
|
180
|
-
flash?: boolean
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export type ActionsKit = {
|
|
184
|
-
cursor: CursorKit
|
|
185
|
-
/** Squash without a cursor — keyboard sends, programmatic actions. */
|
|
186
|
-
press: (
|
|
187
|
-
target: string | Element,
|
|
188
|
-
opts?: PressOpts,
|
|
189
|
-
position?: gsap.Position
|
|
190
|
-
) => { duration: number }
|
|
191
|
-
/** Animated scroll of a container until `to` (element, selector, or an
|
|
192
|
-
* absolute scrollTop) is in view. `duration: 0` sets it instantly for
|
|
193
|
-
* initial state. */
|
|
194
|
-
scroll: (
|
|
195
|
-
container: string | Element,
|
|
196
|
-
to: string | Element | number,
|
|
197
|
-
opts?: ScrollOpts,
|
|
198
|
-
position?: gsap.Position
|
|
199
|
-
) => { duration: number }
|
|
200
|
-
/** Declarative hard UI state change on the timeline (seek-reversible).
|
|
201
|
-
* Instant by design — a cut is hard; `flash` adds the one animated accent. */
|
|
202
|
-
ui: {
|
|
203
|
-
set: (
|
|
204
|
-
target: string | Element,
|
|
205
|
-
change: UiChange,
|
|
206
|
-
position?: gsap.Position
|
|
207
|
-
) => { duration: number }
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ─── icon library ─────────────────────────────────────────────────────────
|
|
212
|
-
// All 32x32 viewBox. White fill + ink outline reads on any background; the
|
|
213
|
-
// collab arrow takes the actor's color. Hotspots are FRACTIONS of the icon
|
|
214
|
-
// box marking where the functional tip sits.
|
|
215
|
-
|
|
216
|
-
const OUTLINE = 'stroke="#1a1a1f" stroke-width="1.6" stroke-linejoin="round"'
|
|
217
|
-
|
|
218
|
-
const ICONS: Record<CursorIconName, { svg: (color: string) => string; hotspot: { x: number; y: number } }> = {
|
|
219
|
-
arrow: {
|
|
220
|
-
svg: () =>
|
|
221
|
-
`<svg viewBox="0 0 32 32"><path d="M8 3 L8 24.5 L13.4 19.6 L16.9 27.2 L20.1 25.7 L16.6 18.3 L23.8 17.9 Z" fill="#fff" ${OUTLINE}/></svg>`,
|
|
222
|
-
hotspot: { x: 0.25, y: 0.09 },
|
|
223
|
-
},
|
|
224
|
-
pointer: {
|
|
225
|
-
svg: () =>
|
|
226
|
-
`<svg viewBox="0 0 32 32"><path d="M12.2 3.6 a1.7 1.7 0 0 1 3.4 0 V13.6 h1.1 V8.2 a1.7 1.7 0 0 1 3.4 0 v6 h1.1 V10.4 a1.7 1.7 0 0 1 3.4 0 v9.2 c0 4.6-3.1 8.2-7.7 8.2 h-1.2 c-3 0-5.2-1.6-6.6-4.1 l-3-5.3 c-0.6-1.1 0.7-2.3 1.8-1.6 l3.3 2 V3.6 z" fill="#fff" ${OUTLINE}/></svg>`,
|
|
227
|
-
hotspot: { x: 0.43, y: 0.06 },
|
|
228
|
-
},
|
|
229
|
-
ibeam: {
|
|
230
|
-
svg: () =>
|
|
231
|
-
`<svg viewBox="0 0 32 32"><path d="M11 4 h4 v1.8 h3 V4 h4 M16 5.8 V26.2 M11 28 h4 v-1.8 h3 V28 h4 M13 4.9 h6 M13 27.1 h6" fill="none" stroke="#fff" stroke-width="4"/><path d="M11 4 h10 M11 28 h10 M16 5 V27" fill="none" stroke="#1a1a1f" stroke-width="1.8"/></svg>`,
|
|
232
|
-
hotspot: { x: 0.5, y: 0.5 },
|
|
233
|
-
},
|
|
234
|
-
grab: {
|
|
235
|
-
svg: () =>
|
|
236
|
-
`<svg viewBox="0 0 32 32"><path d="M9.5 15 v-3 a1.7 1.7 0 0 1 3.4 0 v-2 a1.7 1.7 0 0 1 3.4 0 v-0.6 a1.7 1.7 0 0 1 3.4 0 v1.6 a1.7 1.7 0 0 1 3.4 0 v7.5 c0 4.4-3 7.5-7.4 7.5 h-0.8 c-2.9 0-4.9-1.5-6.3-3.9 l-1.7-3 c-0.6-1.1 0.6-2.3 1.7-1.7 z" fill="#fff" ${OUTLINE}/></svg>`,
|
|
237
|
-
hotspot: { x: 0.5, y: 0.45 },
|
|
238
|
-
},
|
|
239
|
-
grabbing: {
|
|
240
|
-
svg: () =>
|
|
241
|
-
`<svg viewBox="0 0 32 32"><path d="M9.5 17 v-1.5 a1.7 1.7 0 0 1 3.4 0 v-1 a1.7 1.7 0 0 1 3.4 0 v-0.4 a1.7 1.7 0 0 1 3.4 0 v0.8 a1.7 1.7 0 0 1 3.4 0 v5.6 c0 4.2-3 7-7.4 7 h-0.8 c-2.9 0-4.9-1.4-6.3-3.7 l-1.4-2.4 c-0.6-1.1 0.6-2.3 1.7-1.7 z" fill="#fff" ${OUTLINE}/></svg>`,
|
|
242
|
-
hotspot: { x: 0.5, y: 0.5 },
|
|
243
|
-
},
|
|
244
|
-
crosshair: {
|
|
245
|
-
svg: () =>
|
|
246
|
-
`<svg viewBox="0 0 32 32"><path d="M16 5 V27 M5 16 H27" fill="none" stroke="#fff" stroke-width="4"/><path d="M16 5 V27 M5 16 H27" fill="none" stroke="#1a1a1f" stroke-width="1.6"/></svg>`,
|
|
247
|
-
hotspot: { x: 0.5, y: 0.5 },
|
|
248
|
-
},
|
|
249
|
-
collab: {
|
|
250
|
-
svg: (color) =>
|
|
251
|
-
`<svg viewBox="0 0 32 32"><path d="M8 3 L8 24.5 L13.4 19.6 L16.9 27.2 L20.1 25.7 L16.6 18.3 L23.8 17.9 Z" fill="${color}" stroke="#fff" stroke-width="1.6" stroke-linejoin="round"/></svg>`,
|
|
252
|
-
hotspot: { x: 0.25, y: 0.09 },
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** The icon layers a built-in actor carries, so morphs are opacity crossfades
|
|
257
|
-
* between pre-rendered siblings (deterministic and seek-reversible). */
|
|
258
|
-
const MORPH_SET: CursorIconName[] = ['arrow', 'pointer', 'grab', 'grabbing']
|
|
259
|
-
|
|
260
|
-
const DEFAULTS = {
|
|
261
|
-
size: 56,
|
|
262
|
-
travel: 0.8,
|
|
263
|
-
ease: 'ease-in-out-cubic',
|
|
264
|
-
anchor: { x: 0.5, y: 0.52 },
|
|
265
|
-
pressDuration: 0.09,
|
|
266
|
-
exitDuration: 0.5,
|
|
267
|
-
enterFade: 0.18,
|
|
268
|
-
morphDuration: 0.12,
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** Target squash from its smallest dimension: a 48px chip squashes to 0.86,
|
|
272
|
-
* a 400px+ surface barely moves (0.97). This is the one press parameter the
|
|
273
|
-
* corpus varied LEGITIMATELY by element size — so it is a derivation, not a
|
|
274
|
-
* constant. */
|
|
275
|
-
export function autoPressAmount(rect: { width: number; height: number }): number {
|
|
276
|
-
const d = Math.min(rect.width, rect.height)
|
|
277
|
-
const t = Math.min(1, Math.max(0, (d - 48) / 352))
|
|
278
|
-
return Math.round((0.86 + t * 0.11) * 1000) / 1000
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/** Entry point just outside the nearest frame edge to `p`, so a cursor's
|
|
282
|
-
* first travel reads as "someone's hand arrived", not a teleport. */
|
|
283
|
-
export function nearestEdgeStart(
|
|
284
|
-
p: { x: number; y: number },
|
|
285
|
-
frame: { width: number; height: number },
|
|
286
|
-
margin: number
|
|
287
|
-
): { x: number; y: number } {
|
|
288
|
-
const candidates = [
|
|
289
|
-
{ d: p.x, x: -margin, y: p.y },
|
|
290
|
-
{ d: frame.width - p.x, x: frame.width + margin, y: p.y },
|
|
291
|
-
{ d: p.y, x: p.x, y: -margin },
|
|
292
|
-
{ d: frame.height - p.y, x: p.x, y: frame.height + margin },
|
|
293
|
-
]
|
|
294
|
-
candidates.sort((a, b) => a.d - b.d)
|
|
295
|
-
return { x: candidates[0].x, y: candidates[0].y }
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ─── the kit ──────────────────────────────────────────────────────────────
|
|
299
|
-
|
|
300
|
-
type ActorState = {
|
|
301
|
-
el: HTMLElement
|
|
302
|
-
layers: Partial<Record<string, HTMLElement>>
|
|
303
|
-
chip: HTMLElement | null
|
|
304
|
-
hotspot: { x: number; y: number }
|
|
305
|
-
size: number
|
|
306
|
-
idle: 'still' | 'human'
|
|
307
|
-
morph: boolean
|
|
308
|
-
baseIcon: string
|
|
309
|
-
activeIcon: string
|
|
310
|
-
/** Scene-px position of the HOTSPOT after the last action. Null = off. */
|
|
311
|
-
pos: { x: number; y: number } | null
|
|
312
|
-
visible: boolean
|
|
313
|
-
/** Timeline second at which this actor's last action ends. */
|
|
314
|
-
clock: number | null
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export function createActionsKit(deps: ActionsDeps): ActionsKit {
|
|
318
|
-
const { tl, stage, config } = deps
|
|
319
|
-
const warn = createWarner('actions')
|
|
320
|
-
let actorCount = 0
|
|
321
|
-
let labelCount = 0
|
|
322
|
-
|
|
323
|
-
/** Rect in authored-layout scene coords — see kits.ts `layoutRect`. */
|
|
324
|
-
const layoutRect = (el: HTMLElement): Rect => sharedLayoutRect(stage, el)
|
|
325
|
-
|
|
326
|
-
/** Rect where the element sits RIGHT NOW (authored transforms included) —
|
|
327
|
-
* see kits.ts `visualRect`. Aim points resolve through this at action time. */
|
|
328
|
-
const visualRect = (el: HTMLElement): Rect => sharedVisualRect(stage, el)
|
|
329
|
-
|
|
330
|
-
/** Resolve any gsap position expression to absolute seconds via a label. */
|
|
331
|
-
const resolvePosition = (position: gsap.Position | undefined, fallback: number): number => {
|
|
332
|
-
if (position === undefined) return fallback
|
|
333
|
-
if (typeof position === 'number') return position
|
|
334
|
-
const name = `__action-${labelCount++}`
|
|
335
|
-
tl.addLabel(name, position)
|
|
336
|
-
return tl.labels[name] ?? fallback
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const anchorPoint = (
|
|
340
|
-
target: string | Element,
|
|
341
|
-
opts: CursorMoveOpts,
|
|
342
|
-
kind: string
|
|
343
|
-
): { el: HTMLElement; rect: Rect; x: number; y: number; resolve: () => { x: number; y: number } } => {
|
|
344
|
-
const el = resolveTarget(stage, target, kind)
|
|
345
|
-
const rect = layoutRect(el)
|
|
346
|
-
const ax = opts.anchor?.x ?? DEFAULTS.anchor.x
|
|
347
|
-
const ay = opts.anchor?.y ?? DEFAULTS.anchor.y
|
|
348
|
-
const point = (r: Rect) => ({
|
|
349
|
-
x: r.left + r.width * ax + (opts.offset?.x ?? 0),
|
|
350
|
-
y: r.top + r.height * ay + (opts.offset?.y ?? 0),
|
|
351
|
-
})
|
|
352
|
-
const estimate = point(rect)
|
|
353
|
-
return {
|
|
354
|
-
el,
|
|
355
|
-
rect,
|
|
356
|
-
x: estimate.x,
|
|
357
|
-
y: estimate.y,
|
|
358
|
-
// Resolved when the travel tween first renders, not when the direction
|
|
359
|
-
// builds: a container the direction scales mid-timeline (a shell zoom)
|
|
360
|
-
// has moved the target by then, and only the at-render rect knows where.
|
|
361
|
-
resolve: () => point(visualRect(el)),
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const squash = (
|
|
366
|
-
el: HTMLElement,
|
|
367
|
-
amount: number,
|
|
368
|
-
each: number,
|
|
369
|
-
at: number,
|
|
370
|
-
origin = '50% 50%'
|
|
371
|
-
) => {
|
|
372
|
-
tl.to(
|
|
373
|
-
el,
|
|
374
|
-
{
|
|
375
|
-
scale: amount,
|
|
376
|
-
duration: each,
|
|
377
|
-
repeat: 1,
|
|
378
|
-
yoyo: true,
|
|
379
|
-
ease: 'ease-in-out-quad',
|
|
380
|
-
transformOrigin: origin,
|
|
381
|
-
},
|
|
382
|
-
at
|
|
383
|
-
)
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function buildActor(name: string, opts: CursorActorOpts): CursorActor {
|
|
387
|
-
const size = opts.size ?? DEFAULTS.size
|
|
388
|
-
const color = opts.color ?? '#5865F2'
|
|
389
|
-
const iconOpt = opts.icon ?? 'arrow'
|
|
390
|
-
const custom = typeof iconOpt === 'object' ? iconOpt : null
|
|
391
|
-
const baseIcon = custom ? 'custom' : (iconOpt as string)
|
|
392
|
-
const hotspot = custom ? custom.hotspot : ICONS[iconOpt as CursorIconName].hotspot
|
|
393
|
-
|
|
394
|
-
const el = document.createElement('div')
|
|
395
|
-
el.setAttribute(`data-cursor-${name}`, '')
|
|
396
|
-
el.style.cssText = `position:absolute;left:0;top:0;width:${size}px;height:${size}px;pointer-events:none;z-index:${9990 + actorCount};filter:drop-shadow(0 1.5px 3px rgba(0,0,0,0.35));`
|
|
397
|
-
|
|
398
|
-
const layers: Partial<Record<string, HTMLElement>> = {}
|
|
399
|
-
const addLayer = (key: string, svg: string, visible: boolean) => {
|
|
400
|
-
const layer = document.createElement('div')
|
|
401
|
-
layer.style.cssText = `position:absolute;inset:0;opacity:${visible ? 1 : 0};`
|
|
402
|
-
layer.innerHTML = svg
|
|
403
|
-
el.appendChild(layer)
|
|
404
|
-
layers[key] = layer
|
|
405
|
-
}
|
|
406
|
-
if (custom) {
|
|
407
|
-
addLayer('custom', custom.svg, true)
|
|
408
|
-
} else if (MORPH_SET.includes(baseIcon as CursorIconName)) {
|
|
409
|
-
// The morphing set is pre-rendered as stacked siblings so every morph is
|
|
410
|
-
// an opacity crossfade — deterministic and seek-reversible.
|
|
411
|
-
for (const key of MORPH_SET) addLayer(key, ICONS[key].svg(color), key === baseIcon)
|
|
412
|
-
} else {
|
|
413
|
-
addLayer(baseIcon, ICONS[baseIcon as CursorIconName].svg(color), true)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
let chip: HTMLElement | null = null
|
|
417
|
-
if (opts.label) {
|
|
418
|
-
chip = document.createElement('div')
|
|
419
|
-
chip.textContent = opts.label
|
|
420
|
-
chip.style.cssText = `position:absolute;left:${size * 0.62}px;top:${size * 0.72}px;background:${color};color:#fff;font:600 ${Math.round(size * 0.34)}px/1.25 system-ui,sans-serif;padding:2px 8px;border-radius:999px;white-space:nowrap;box-shadow:0 1px 3px rgba(0,0,0,0.25);`
|
|
421
|
-
el.appendChild(chip)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
gsap.set(el, { autoAlpha: 0, x: -size * 3, y: -size * 3, force3D: false })
|
|
425
|
-
stage.appendChild(el)
|
|
426
|
-
|
|
427
|
-
const state: ActorState = {
|
|
428
|
-
el,
|
|
429
|
-
layers,
|
|
430
|
-
chip,
|
|
431
|
-
hotspot,
|
|
432
|
-
size,
|
|
433
|
-
idle: opts.idle ?? 'still',
|
|
434
|
-
morph: opts.morph ?? true,
|
|
435
|
-
baseIcon,
|
|
436
|
-
activeIcon: baseIcon,
|
|
437
|
-
pos: null,
|
|
438
|
-
visible: false,
|
|
439
|
-
clock: null,
|
|
440
|
-
}
|
|
441
|
-
actorCount += 1
|
|
442
|
-
|
|
443
|
-
/** Wrapper x/y from a hotspot scene point. */
|
|
444
|
-
const place = (p: { x: number; y: number }) => ({
|
|
445
|
-
x: p.x - state.hotspot.x * state.size,
|
|
446
|
-
y: p.y - state.hotspot.y * state.size,
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
const morphTo = (icon: string, at: number) => {
|
|
450
|
-
if (!state.morph || icon === state.activeIcon) return
|
|
451
|
-
const fromLayer = state.layers[state.activeIcon]
|
|
452
|
-
const toLayer = state.layers[icon]
|
|
453
|
-
if (!fromLayer || !toLayer) return
|
|
454
|
-
tl.to(fromLayer, { opacity: 0, duration: DEFAULTS.morphDuration, ease: 'ease-in-out-quad' }, at)
|
|
455
|
-
tl.to(toLayer, { opacity: 1, duration: DEFAULTS.morphDuration, ease: 'ease-in-out-quad' }, at)
|
|
456
|
-
state.activeIcon = icon
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/** Common start-of-action bookkeeping: resolve the start second, warn on
|
|
460
|
-
* mid-travel overlap, and fill a long park with the idle drift. */
|
|
461
|
-
const begin = (position: gsap.Position | undefined): number => {
|
|
462
|
-
const start = resolvePosition(position, state.clock ?? tl.duration())
|
|
463
|
-
if (state.clock !== null && start < state.clock - 1e-6) {
|
|
464
|
-
warn(
|
|
465
|
-
`overlap-${name}`,
|
|
466
|
-
`cursor "${name}" is scheduled at ${start.toFixed(2)}s while still mid-action ` +
|
|
467
|
-
`until ${state.clock.toFixed(2)}s — author each cursor's actions in order.`
|
|
468
|
-
)
|
|
469
|
-
}
|
|
470
|
-
if (
|
|
471
|
-
state.idle === 'human' &&
|
|
472
|
-
state.visible &&
|
|
473
|
-
state.clock !== null &&
|
|
474
|
-
start - state.clock > 0.4
|
|
475
|
-
) {
|
|
476
|
-
const gap = start - state.clock
|
|
477
|
-
tl.to(
|
|
478
|
-
el,
|
|
479
|
-
{ x: '+=2.4', y: '-=1.8', duration: gap / 2, repeat: 1, yoyo: true, ease: 'sine.inOut' },
|
|
480
|
-
state.clock
|
|
481
|
-
)
|
|
482
|
-
}
|
|
483
|
-
return start
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/** The label chip rides along during travel and fades just after arrival —
|
|
487
|
-
* parked on target it occludes exactly the thing the cursor points at. */
|
|
488
|
-
const chipRide = (start: number, duration: number) => {
|
|
489
|
-
if (!chip) return
|
|
490
|
-
tl.set(chip, { autoAlpha: 1 }, start)
|
|
491
|
-
tl.to(chip, { autoAlpha: 0, duration: 0.18, ease: 'ease-in-quad' }, start + duration + 0.15)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/** Travel the hotspot to scene point `p`. Returns total travel duration. */
|
|
495
|
-
const travelTo = (
|
|
496
|
-
p: { x: number; y: number },
|
|
497
|
-
opts: CursorMoveOpts,
|
|
498
|
-
start: number,
|
|
499
|
-
resolve?: () => { x: number; y: number }
|
|
500
|
-
): number => {
|
|
501
|
-
const duration = opts.duration ?? DEFAULTS.travel
|
|
502
|
-
const ease = opts.ease ?? DEFAULTS.ease
|
|
503
|
-
const dest = place(p)
|
|
504
|
-
// Destination resolves when the tween first renders: by then every
|
|
505
|
-
// earlier tween (container zooms included) has applied, so the aim is
|
|
506
|
-
// taken from where the target actually sits, not its resting layout.
|
|
507
|
-
const destX = resolve ? () => place(resolve()).x : dest.x
|
|
508
|
-
const destY = resolve ? () => place(resolve()).y : dest.y
|
|
509
|
-
if (!state.visible) {
|
|
510
|
-
const enter = opts.enter ?? 'edge'
|
|
511
|
-
const from =
|
|
512
|
-
enter === 'fade'
|
|
513
|
-
? { x: dest.x - state.size * 0.7, y: dest.y - state.size * 0.5 }
|
|
514
|
-
: place(nearestEdgeStart(p, config, state.size * 1.5))
|
|
515
|
-
tl.fromTo(
|
|
516
|
-
el,
|
|
517
|
-
{ autoAlpha: 0 },
|
|
518
|
-
{ autoAlpha: 1, duration: DEFAULTS.enterFade, ease: 'ease-out-quad' },
|
|
519
|
-
start
|
|
520
|
-
)
|
|
521
|
-
tl.fromTo(el, { x: from.x, y: from.y }, { x: destX, y: destY, duration, ease }, start)
|
|
522
|
-
} else {
|
|
523
|
-
tl.to(el, { x: destX, y: destY, duration, ease }, start)
|
|
524
|
-
}
|
|
525
|
-
chipRide(start, duration)
|
|
526
|
-
state.pos = p
|
|
527
|
-
state.visible = true
|
|
528
|
-
return duration
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const doExit = (opts: CursorExitOpts, start: number): number => {
|
|
532
|
-
if (!state.visible || !state.pos) {
|
|
533
|
-
warn(`exit-${name}`, `cursor "${name}" exit was skipped — it is not on screen.`)
|
|
534
|
-
return 0
|
|
535
|
-
}
|
|
536
|
-
const duration = opts.duration ?? DEFAULTS.exitDuration
|
|
537
|
-
const ease = opts.ease ?? DEFAULTS.ease
|
|
538
|
-
if ((opts.via ?? 'edge') === 'edge') {
|
|
539
|
-
const out = place(nearestEdgeStart(state.pos, config, state.size * 1.5))
|
|
540
|
-
tl.to(el, { x: out.x, y: out.y, duration, ease }, start)
|
|
541
|
-
}
|
|
542
|
-
tl.to(
|
|
543
|
-
el,
|
|
544
|
-
{ autoAlpha: 0, duration: Math.min(0.15, duration), ease: 'ease-in-quad' },
|
|
545
|
-
start + duration - Math.min(0.15, duration)
|
|
546
|
-
)
|
|
547
|
-
morphTo(state.baseIcon, start + duration)
|
|
548
|
-
state.visible = false
|
|
549
|
-
state.pos = null
|
|
550
|
-
state.clock = start + duration
|
|
551
|
-
return duration
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const handleThen = (then: CursorThen | undefined, at: number): number => {
|
|
555
|
-
if (!then || then === 'stay') return 0
|
|
556
|
-
if (then === 'exit') return 0.25 + doExit({}, at + 0.25)
|
|
557
|
-
if (then === 'park') {
|
|
558
|
-
const p = { x: state.pos!.x + 56, y: state.pos!.y + 46 }
|
|
559
|
-
const dest = place(p)
|
|
560
|
-
tl.to(el, { x: dest.x, y: dest.y, duration: 0.35, ease: DEFAULTS.ease }, at + 0.2)
|
|
561
|
-
state.pos = p
|
|
562
|
-
return 0.55
|
|
563
|
-
}
|
|
564
|
-
// A selector: travel there and park.
|
|
565
|
-
const point = anchorPoint(then, {}, 'cursor.then')
|
|
566
|
-
tl.to(
|
|
567
|
-
el,
|
|
568
|
-
{
|
|
569
|
-
x: () => place(point.resolve()).x,
|
|
570
|
-
y: () => place(point.resolve()).y,
|
|
571
|
-
duration: 0.6,
|
|
572
|
-
ease: DEFAULTS.ease,
|
|
573
|
-
},
|
|
574
|
-
at + 0.3
|
|
575
|
-
)
|
|
576
|
-
state.pos = { x: point.x, y: point.y }
|
|
577
|
-
return 0.9
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const actor: CursorActor = {
|
|
581
|
-
to: (target, opts = {}, position) => {
|
|
582
|
-
const start = begin(position)
|
|
583
|
-
const point = anchorPoint(target, opts, 'cursor.to')
|
|
584
|
-
const duration = travelTo(point, opts, start, point.resolve)
|
|
585
|
-
state.clock = start + duration
|
|
586
|
-
return { duration }
|
|
587
|
-
},
|
|
588
|
-
|
|
589
|
-
click: (target, opts = {}, position) => {
|
|
590
|
-
const start = begin(position)
|
|
591
|
-
const point = anchorPoint(target, opts, 'cursor.click')
|
|
592
|
-
const travel = travelTo(point, opts, start, point.resolve)
|
|
593
|
-
// Arrow melts into the pointer hand over the last stretch of travel —
|
|
594
|
-
// the detail that makes a click read as intentional.
|
|
595
|
-
if (state.baseIcon === 'arrow') morphTo('pointer', start + travel * 0.85)
|
|
596
|
-
const each = opts.pressDuration ?? DEFAULTS.pressDuration
|
|
597
|
-
const arrive = start + travel
|
|
598
|
-
squash(
|
|
599
|
-
el,
|
|
600
|
-
0.9,
|
|
601
|
-
each,
|
|
602
|
-
arrive,
|
|
603
|
-
`${state.hotspot.x * 100}% ${state.hotspot.y * 100}%`
|
|
604
|
-
)
|
|
605
|
-
const amount =
|
|
606
|
-
typeof opts.press === 'number' ? opts.press : autoPressAmount(point.rect)
|
|
607
|
-
squash(point.el, amount, each, arrive)
|
|
608
|
-
if (state.baseIcon === 'arrow') morphTo('arrow', arrive + each * 2 + 0.3)
|
|
609
|
-
state.clock = arrive + each * 2
|
|
610
|
-
const tail = handleThen(opts.then, state.clock)
|
|
611
|
-
state.clock += tail
|
|
612
|
-
return { duration: travel + each * 2 + tail }
|
|
613
|
-
},
|
|
614
|
-
|
|
615
|
-
drag: (payload, to, opts = {}, position) => {
|
|
616
|
-
const start = begin(position)
|
|
617
|
-
const payloadEl = resolveTarget(stage, payload, 'cursor.drag')
|
|
618
|
-
const payloadRect = layoutRect(payloadEl)
|
|
619
|
-
const gax = opts.anchor?.x ?? 0.5
|
|
620
|
-
const gay = opts.anchor?.y ?? 0.5
|
|
621
|
-
const grabResolve = () => {
|
|
622
|
-
const r = visualRect(payloadEl)
|
|
623
|
-
return { x: r.left + r.width * gax, y: r.top + r.height * gay }
|
|
624
|
-
}
|
|
625
|
-
const grabPoint = {
|
|
626
|
-
el: payloadEl,
|
|
627
|
-
rect: payloadRect,
|
|
628
|
-
x: payloadRect.left + payloadRect.width * gax,
|
|
629
|
-
y: payloadRect.top + payloadRect.height * gay,
|
|
630
|
-
}
|
|
631
|
-
const approach = travelTo(
|
|
632
|
-
{ x: grabPoint.x, y: grabPoint.y },
|
|
633
|
-
{ ...opts, duration: 0.6 },
|
|
634
|
-
start,
|
|
635
|
-
grabResolve
|
|
636
|
-
)
|
|
637
|
-
const grabAt = start + approach
|
|
638
|
-
morphTo('grab', grabAt - 0.1)
|
|
639
|
-
morphTo('grabbing', grabAt + 0.08)
|
|
640
|
-
|
|
641
|
-
let delta: { x: number; y: number }
|
|
642
|
-
if (typeof to === 'object' && 'x' in to && !('nodeType' in (to as object))) {
|
|
643
|
-
delta = { x: (to as { x: number }).x, y: (to as { y: number }).y }
|
|
644
|
-
} else {
|
|
645
|
-
const dest = resolveTarget(stage, to as string | Element, 'cursor.drag destination')
|
|
646
|
-
const destRect = layoutRect(dest)
|
|
647
|
-
delta = {
|
|
648
|
-
x: destRect.left + destRect.width / 2 - (payloadRect.left + payloadRect.width / 2),
|
|
649
|
-
y: destRect.top + destRect.height / 2 - (payloadRect.top + payloadRect.height / 2),
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const carry = opts.duration ?? 1.0
|
|
654
|
-
const ease = opts.ease ?? DEFAULTS.ease
|
|
655
|
-
const holdBeat = 0.16
|
|
656
|
-
const carryAt = grabAt + holdBeat
|
|
657
|
-
if (opts.lift !== false) {
|
|
658
|
-
tl.to(payloadEl, { scale: 1.04, duration: holdBeat, ease: 'ease-out-quad' }, grabAt)
|
|
659
|
-
tl.to(payloadEl, { scale: 1, duration: 0.2, ease: 'ease-out-quad' }, carryAt + carry)
|
|
660
|
-
}
|
|
661
|
-
// One delta, two targets, identical timing — the pair cannot desync.
|
|
662
|
-
// The payload's delta is layout-space: payload and destination share a
|
|
663
|
-
// container, so any ancestor scale applies to both equally and the
|
|
664
|
-
// authored delta is exactly the transform the payload needs. The CURSOR
|
|
665
|
-
// lives in stage space, so its version of the same delta must be
|
|
666
|
-
// multiplied by whatever the container's ancestors scale it to on
|
|
667
|
-
// screen — measured at carry time, after any shell zoom has applied.
|
|
668
|
-
// This tween is inserted BEFORE the payload's, so its function values
|
|
669
|
-
// resolve while the payload still sits at the grab point.
|
|
670
|
-
const carryDest = () => {
|
|
671
|
-
const grab = grabResolve()
|
|
672
|
-
const scale = payloadRect.width ? visualRect(payloadEl).width / payloadRect.width : 1
|
|
673
|
-
return place({ x: grab.x + delta.x * scale, y: grab.y + delta.y * scale })
|
|
674
|
-
}
|
|
675
|
-
tl.to(el, { x: () => carryDest().x, y: () => carryDest().y, duration: carry, ease }, carryAt)
|
|
676
|
-
tl.to(payloadEl, { x: `+=${delta.x}`, y: `+=${delta.y}`, duration: carry, ease }, carryAt)
|
|
677
|
-
morphTo('grab', carryAt + carry + 0.02)
|
|
678
|
-
morphTo('arrow', carryAt + carry + 0.24)
|
|
679
|
-
state.pos = { x: state.pos!.x + delta.x, y: state.pos!.y + delta.y }
|
|
680
|
-
state.clock = carryAt + carry + 0.2
|
|
681
|
-
const tail = handleThen(opts.then, state.clock)
|
|
682
|
-
state.clock += tail
|
|
683
|
-
return { duration: state.clock - start }
|
|
684
|
-
},
|
|
685
|
-
|
|
686
|
-
exit: (opts = {}, position) => {
|
|
687
|
-
const start = begin(position)
|
|
688
|
-
return { duration: doExit(opts, start) }
|
|
689
|
-
},
|
|
690
|
-
|
|
691
|
-
label: (text, position) => {
|
|
692
|
-
if (!chip) {
|
|
693
|
-
warn(`chip-${name}`, `cursor "${name}" has no label chip — declare it with a label.`)
|
|
694
|
-
return
|
|
695
|
-
}
|
|
696
|
-
const at = typeof position === 'number' ? position : (state.clock ?? tl.duration())
|
|
697
|
-
tl.set(chip, { textContent: text, autoAlpha: 1 }, at)
|
|
698
|
-
tl.to(chip, { autoAlpha: 0, duration: 0.18, ease: 'ease-in-quad' }, at + 1.2)
|
|
699
|
-
},
|
|
700
|
-
}
|
|
701
|
-
return actor
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
const actors = new Map<string, CursorActor>()
|
|
705
|
-
const defaultActor = () => {
|
|
706
|
-
let actor = actors.get('__default')
|
|
707
|
-
if (!actor) {
|
|
708
|
-
actor = buildActor('main', {})
|
|
709
|
-
actors.set('__default', actor)
|
|
710
|
-
}
|
|
711
|
-
return actor
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const cursor: CursorKit = {
|
|
715
|
-
to: (target, opts, position) => defaultActor().to(target, opts, position),
|
|
716
|
-
click: (target, opts, position) => defaultActor().click(target, opts, position),
|
|
717
|
-
drag: (payload, to, opts, position) => defaultActor().drag(payload, to, opts, position),
|
|
718
|
-
exit: (opts, position) => defaultActor().exit(opts, position),
|
|
719
|
-
label: (text, position) => defaultActor().label(text, position),
|
|
720
|
-
actor: (name, opts = {}) => {
|
|
721
|
-
let actor = actors.get(name)
|
|
722
|
-
if (!actor) {
|
|
723
|
-
actor = buildActor(name, opts)
|
|
724
|
-
actors.set(name, actor)
|
|
725
|
-
}
|
|
726
|
-
return actor
|
|
727
|
-
},
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const press: ActionsKit['press'] = (target, opts = {}, position) => {
|
|
731
|
-
const el = resolveTarget(stage, target, 'press')
|
|
732
|
-
const rect = layoutRect(el)
|
|
733
|
-
const each = opts.duration ?? DEFAULTS.pressDuration
|
|
734
|
-
const amount = typeof opts.amount === 'number' ? opts.amount : autoPressAmount(rect)
|
|
735
|
-
const at = resolvePosition(position, tl.duration())
|
|
736
|
-
tl.to(
|
|
737
|
-
el,
|
|
738
|
-
{
|
|
739
|
-
scale: amount,
|
|
740
|
-
duration: each,
|
|
741
|
-
repeat: 1,
|
|
742
|
-
yoyo: true,
|
|
743
|
-
ease: opts.ease ?? 'ease-in-out-quad',
|
|
744
|
-
transformOrigin: '50% 50%',
|
|
745
|
-
},
|
|
746
|
-
at
|
|
747
|
-
)
|
|
748
|
-
return { duration: each * 2 }
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const scroll: ActionsKit['scroll'] = (container, to, opts = {}, position) => {
|
|
752
|
-
const el = resolveTarget(stage, container, 'scroll')
|
|
753
|
-
let desired: number
|
|
754
|
-
if (typeof to === 'number') {
|
|
755
|
-
desired = to
|
|
756
|
-
} else {
|
|
757
|
-
const targetEl = resolveTarget(stage, to, 'scroll target')
|
|
758
|
-
const crect = layoutRect(el)
|
|
759
|
-
const trect = layoutRect(targetEl)
|
|
760
|
-
const align = opts.align ?? 0.5
|
|
761
|
-
const clientH = el.clientHeight
|
|
762
|
-
desired = el.scrollTop + (trect.top - crect.top) - (clientH - trect.height) * align
|
|
763
|
-
}
|
|
764
|
-
desired = Math.max(0, Math.min(desired, el.scrollHeight - el.clientHeight))
|
|
765
|
-
const duration = opts.duration ?? 0.7
|
|
766
|
-
const at = resolvePosition(position, tl.duration())
|
|
767
|
-
if (duration <= 0) {
|
|
768
|
-
tl.set(el, { scrollTop: desired }, at)
|
|
769
|
-
return { duration: 0 }
|
|
770
|
-
}
|
|
771
|
-
tl.to(el, { scrollTop: desired, duration, ease: opts.ease ?? DEFAULTS.ease }, at)
|
|
772
|
-
return { duration }
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const uiSet: ActionsKit['ui']['set'] = (target, change, position) => {
|
|
776
|
-
const el = resolveTarget(stage, target, 'ui.set')
|
|
777
|
-
const at = resolvePosition(position, tl.duration())
|
|
778
|
-
const vars: gsap.TweenVars = {}
|
|
779
|
-
if (change.text !== undefined) vars.textContent = change.text
|
|
780
|
-
if (change.value !== undefined) (vars as Record<string, unknown>).value = change.value
|
|
781
|
-
if (change.src !== undefined) (vars as Record<string, unknown>).src = change.src
|
|
782
|
-
if (change.attrs) vars.attr = change.attrs
|
|
783
|
-
if (change.classes) {
|
|
784
|
-
const current = new Set((el.getAttribute('class') ?? '').split(/\s+/).filter(Boolean))
|
|
785
|
-
for (const cls of change.classes.add ?? []) current.add(cls)
|
|
786
|
-
for (const cls of change.classes.remove ?? []) current.delete(cls)
|
|
787
|
-
;(vars as Record<string, unknown>).className = Array.from(current).join(' ')
|
|
788
|
-
}
|
|
789
|
-
if (Object.keys(vars).length) tl.set(el, vars, at)
|
|
790
|
-
if (change.flash) {
|
|
791
|
-
tl.fromTo(
|
|
792
|
-
el,
|
|
793
|
-
{ filter: 'brightness(1)' },
|
|
794
|
-
{ filter: 'brightness(1.35)', duration: 0.12, repeat: 1, yoyo: true, ease: 'ease-in-out-quad' },
|
|
795
|
-
at
|
|
796
|
-
)
|
|
797
|
-
return { duration: 0.24 }
|
|
798
|
-
}
|
|
799
|
-
return { duration: 0 }
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
return { cursor, press, scroll, ui: { set: uiSet } }
|
|
803
|
-
}
|