kuinetic 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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/esm/chunk-LWS4OSLX.mjs +730 -0
- package/dist/esm/chunk-QZIJ7WZI.mjs +3312 -0
- package/dist/esm/chunk-R3TGDJKA.mjs +1264 -0
- package/dist/esm/core/index.mjs +34 -0
- package/dist/esm/effects/index.mjs +27 -0
- package/dist/esm/index.mjs +52 -0
- package/dist/kuinetic.all.js +5291 -0
- package/dist/kuinetic.css +2338 -0
- package/dist/kuinetic.js +5281 -0
- package/dist/types/core/activation.d.ts +21 -0
- package/dist/types/core/animator.d.ts +216 -0
- package/dist/types/core/attrs.d.ts +15 -0
- package/dist/types/core/capabilities.d.ts +19 -0
- package/dist/types/core/channels.d.ts +25 -0
- package/dist/types/core/compile.d.ts +49 -0
- package/dist/types/core/dom-watcher.d.ts +35 -0
- package/dist/types/core/effect-context.d.ts +49 -0
- package/dist/types/core/element-config.d.ts +52 -0
- package/dist/types/core/flip.d.ts +80 -0
- package/dist/types/core/gesture.d.ts +99 -0
- package/dist/types/core/index.d.ts +14 -0
- package/dist/types/core/instances.d.ts +98 -0
- package/dist/types/core/js-effect-preparer.d.ts +46 -0
- package/dist/types/core/js-params.d.ts +124 -0
- package/dist/types/core/owned-styles.d.ts +44 -0
- package/dist/types/core/params.d.ts +65 -0
- package/dist/types/core/parse.d.ts +22 -0
- package/dist/types/core/path-morph.d.ts +83 -0
- package/dist/types/core/play.d.ts +48 -0
- package/dist/types/core/registry.d.ts +38 -0
- package/dist/types/core/reporter.d.ts +35 -0
- package/dist/types/core/scroll-scheduler.d.ts +153 -0
- package/dist/types/core/spring.d.ts +83 -0
- package/dist/types/core/stagger.d.ts +19 -0
- package/dist/types/core/style-plan.d.ts +56 -0
- package/dist/types/core/types.d.ts +239 -0
- package/dist/types/effects/catalog/ambient.d.ts +18 -0
- package/dist/types/effects/catalog/core.d.ts +19 -0
- package/dist/types/effects/catalog/feedback.d.ts +21 -0
- package/dist/types/effects/catalog/index.d.ts +21 -0
- package/dist/types/effects/catalog/interaction-shared.d.ts +56 -0
- package/dist/types/effects/catalog/interaction.d.ts +22 -0
- package/dist/types/effects/catalog/media.d.ts +13 -0
- package/dist/types/effects/catalog/numbers-shared.d.ts +103 -0
- package/dist/types/effects/catalog/numbers.d.ts +31 -0
- package/dist/types/effects/catalog/shared.d.ts +3 -0
- package/dist/types/effects/catalog/text-shared.d.ts +180 -0
- package/dist/types/effects/catalog/text.d.ts +17 -0
- package/dist/types/effects/forms/index.d.ts +14 -0
- package/dist/types/effects/forms/primitives.d.ts +32 -0
- package/dist/types/effects/gestures/index.d.ts +19 -0
- package/dist/types/effects/gestures/primitives.d.ts +2 -0
- package/dist/types/effects/index.d.ts +19 -0
- package/dist/types/effects/layout/index.d.ts +12 -0
- package/dist/types/effects/layout/presets.d.ts +9 -0
- package/dist/types/effects/layout/primitives.d.ts +2 -0
- package/dist/types/effects/navigation/index.d.ts +30 -0
- package/dist/types/effects/scroll-mechanics/index.d.ts +14 -0
- package/dist/types/effects/scroll-mechanics/presets.d.ts +6 -0
- package/dist/types/effects/scroll-mechanics/primitives.d.ts +2 -0
- package/dist/types/effects/scroll-mechanics/tracker.d.ts +55 -0
- package/dist/types/effects/shared.d.ts +26 -0
- package/dist/types/effects/svg/index.d.ts +13 -0
- package/dist/types/effects/three-d/index.d.ts +20 -0
- package/dist/types/index.d.ts +16 -0
- package/package.json +78 -0
|
@@ -0,0 +1,3312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ABSOLUTE_BASIS,
|
|
3
|
+
CHANNEL,
|
|
4
|
+
Registry,
|
|
5
|
+
clamp01,
|
|
6
|
+
createAttributeLedger,
|
|
7
|
+
createMeasureCache,
|
|
8
|
+
createStyleLedger,
|
|
9
|
+
deferPrepare,
|
|
10
|
+
effectDurationMs,
|
|
11
|
+
inertInstance,
|
|
12
|
+
isSameOriginPath,
|
|
13
|
+
toPixels
|
|
14
|
+
} from "./chunk-LWS4OSLX.mjs";
|
|
15
|
+
|
|
16
|
+
// src/effects/shared.ts
|
|
17
|
+
var COMMON = {
|
|
18
|
+
duration: { type: "time", default: "600ms", cssProperty: "--kui-duration" },
|
|
19
|
+
delay: { type: "time", default: "0ms", cssProperty: "--kui-delay" },
|
|
20
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
21
|
+
stagger: { type: "time", default: "0ms", cssProperty: "--kui-stagger" }
|
|
22
|
+
};
|
|
23
|
+
function cssPrimitive(id, channels, options = {}) {
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
renderer: "css-keyframes",
|
|
27
|
+
channels,
|
|
28
|
+
parameters: { ...COMMON, ...options.parameters },
|
|
29
|
+
supportedTimelines: options.timelines ?? ["time"],
|
|
30
|
+
supportedActivations: options.activations ?? [
|
|
31
|
+
"load",
|
|
32
|
+
"enter",
|
|
33
|
+
"hover",
|
|
34
|
+
"focus",
|
|
35
|
+
"click",
|
|
36
|
+
"manual"
|
|
37
|
+
],
|
|
38
|
+
perfClass: options.perfClass ?? "compositor",
|
|
39
|
+
reducedMotion: options.reducedMotion ?? "shorten",
|
|
40
|
+
...options.defaultActivation ? { defaultActivation: options.defaultActivation } : {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/effects/catalog/ambient.ts
|
|
45
|
+
var drift = {
|
|
46
|
+
duration: { type: "time", default: "10s", cssProperty: "--kui-duration" },
|
|
47
|
+
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" }
|
|
48
|
+
};
|
|
49
|
+
var float = {
|
|
50
|
+
duration: { type: "time", default: "4s", cssProperty: "--kui-duration" },
|
|
51
|
+
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
52
|
+
distance: { type: "length", default: "14px", cssProperty: "--kui-distance" }
|
|
53
|
+
};
|
|
54
|
+
var pulse = {
|
|
55
|
+
duration: { type: "time", default: "2.2s", cssProperty: "--kui-duration" },
|
|
56
|
+
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" }
|
|
57
|
+
};
|
|
58
|
+
var AMBIENT_PRIMITIVES = [
|
|
59
|
+
cssPrimitive("ambient-gradient", [CHANNEL.background], {
|
|
60
|
+
parameters: drift,
|
|
61
|
+
defaultActivation: "load",
|
|
62
|
+
reducedMotion: "disable",
|
|
63
|
+
perfClass: "continuous"
|
|
64
|
+
}),
|
|
65
|
+
cssPrimitive("ambient-float", [CHANNEL.translate], {
|
|
66
|
+
parameters: float,
|
|
67
|
+
defaultActivation: "load",
|
|
68
|
+
reducedMotion: "disable",
|
|
69
|
+
perfClass: "continuous"
|
|
70
|
+
}),
|
|
71
|
+
cssPrimitive("ambient-pulse", [CHANNEL.scale, CHANNEL.opacity], {
|
|
72
|
+
parameters: pulse,
|
|
73
|
+
defaultActivation: "load",
|
|
74
|
+
reducedMotion: "disable",
|
|
75
|
+
perfClass: "continuous"
|
|
76
|
+
})
|
|
77
|
+
];
|
|
78
|
+
var AMBIENT_PRESETS = [
|
|
79
|
+
{ name: "gradient-mesh", primitive: "ambient-gradient", keyframes: "kui-gradient-mesh" },
|
|
80
|
+
{ name: "aurora", primitive: "ambient-gradient", keyframes: "kui-aurora" },
|
|
81
|
+
{
|
|
82
|
+
name: "gradient-rotate-border",
|
|
83
|
+
primitive: "ambient-gradient",
|
|
84
|
+
keyframes: "kui-gradient-rotate-border",
|
|
85
|
+
params: { duration: "6s", ease: "linear" }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "noise-overlay",
|
|
89
|
+
primitive: "ambient-gradient",
|
|
90
|
+
keyframes: "kui-noise-overlay",
|
|
91
|
+
params: { duration: "650ms", ease: "steps(6)" }
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "scanline",
|
|
95
|
+
primitive: "ambient-gradient",
|
|
96
|
+
keyframes: "kui-scanline",
|
|
97
|
+
params: { duration: "3.5s", ease: "linear" }
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "dot-grid-drift",
|
|
101
|
+
primitive: "ambient-gradient",
|
|
102
|
+
keyframes: "kui-dot-grid-drift",
|
|
103
|
+
params: { duration: "16s", ease: "linear" }
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "line-grid-drift",
|
|
107
|
+
primitive: "ambient-gradient",
|
|
108
|
+
keyframes: "kui-line-grid-drift",
|
|
109
|
+
params: { duration: "16s", ease: "linear" }
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "starfield",
|
|
113
|
+
primitive: "ambient-gradient",
|
|
114
|
+
keyframes: "kui-starfield",
|
|
115
|
+
params: { duration: "40s", ease: "linear" }
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "spotlight-follow",
|
|
119
|
+
primitive: "ambient-gradient",
|
|
120
|
+
keyframes: "kui-spotlight-follow",
|
|
121
|
+
params: { duration: "9s" }
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "wave-blob",
|
|
125
|
+
primitive: "ambient-gradient",
|
|
126
|
+
keyframes: "kui-wave-blob",
|
|
127
|
+
params: { duration: "12s" }
|
|
128
|
+
},
|
|
129
|
+
{ name: "float", primitive: "ambient-float", keyframes: "kui-float" },
|
|
130
|
+
{
|
|
131
|
+
name: "bob",
|
|
132
|
+
primitive: "ambient-float",
|
|
133
|
+
keyframes: "kui-bob",
|
|
134
|
+
params: { duration: "2s", distance: "8px" }
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "floating-shapes",
|
|
138
|
+
primitive: "ambient-float",
|
|
139
|
+
keyframes: "kui-floating-shapes",
|
|
140
|
+
params: { duration: "6s", distance: "10px" }
|
|
141
|
+
},
|
|
142
|
+
{ name: "glow-pulse", primitive: "ambient-pulse", keyframes: "kui-glow-pulse" }
|
|
143
|
+
];
|
|
144
|
+
function registerAmbient(registry) {
|
|
145
|
+
return registry.registerPrimitives(AMBIENT_PRIMITIVES).registerPresets(AMBIENT_PRESETS);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/effects/catalog/feedback.ts
|
|
149
|
+
var loop = {
|
|
150
|
+
duration: { type: "time", default: "1.6s", cssProperty: "--kui-duration" },
|
|
151
|
+
ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" }
|
|
152
|
+
};
|
|
153
|
+
var spin = {
|
|
154
|
+
duration: { type: "time", default: "900ms", cssProperty: "--kui-duration" },
|
|
155
|
+
ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" }
|
|
156
|
+
};
|
|
157
|
+
var dotPulse = {
|
|
158
|
+
duration: { type: "time", default: "1.2s", cssProperty: "--kui-duration" },
|
|
159
|
+
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" }
|
|
160
|
+
};
|
|
161
|
+
var progressTrack = {
|
|
162
|
+
duration: { type: "time", default: "1.4s", cssProperty: "--kui-duration" },
|
|
163
|
+
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" }
|
|
164
|
+
};
|
|
165
|
+
var toast = {
|
|
166
|
+
duration: { type: "time", default: "420ms", cssProperty: "--kui-duration" },
|
|
167
|
+
ease: { type: "easing", default: "back-out", cssProperty: "--kui-ease" },
|
|
168
|
+
distance: { type: "length", default: "24px", cssProperty: "--kui-distance" }
|
|
169
|
+
};
|
|
170
|
+
var shake = {
|
|
171
|
+
duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
|
|
172
|
+
ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" }
|
|
173
|
+
};
|
|
174
|
+
var pop = {
|
|
175
|
+
duration: { type: "time", default: "420ms", cssProperty: "--kui-duration" },
|
|
176
|
+
ease: { type: "easing", default: "back-out", cssProperty: "--kui-ease" }
|
|
177
|
+
};
|
|
178
|
+
var ripple = {
|
|
179
|
+
duration: { type: "time", default: "600ms", cssProperty: "--kui-duration" },
|
|
180
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
181
|
+
spread: { type: "number", default: "4", cssProperty: "--kui-ripple-scale", finite: true, minimum: 1 }
|
|
182
|
+
};
|
|
183
|
+
var confirm = {
|
|
184
|
+
duration: { type: "time", default: "1400ms", cssProperty: "--kui-duration" },
|
|
185
|
+
ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" }
|
|
186
|
+
};
|
|
187
|
+
var FEEDBACK_PRIMITIVES = [
|
|
188
|
+
cssPrimitive("feedback-shimmer", [CHANNEL.background], {
|
|
189
|
+
parameters: loop,
|
|
190
|
+
defaultActivation: "load",
|
|
191
|
+
reducedMotion: "disable",
|
|
192
|
+
perfClass: "continuous"
|
|
193
|
+
}),
|
|
194
|
+
cssPrimitive("feedback-fade", [CHANNEL.opacity], {
|
|
195
|
+
defaultActivation: "manual"
|
|
196
|
+
}),
|
|
197
|
+
cssPrimitive("feedback-spin", [CHANNEL.rotate], {
|
|
198
|
+
parameters: spin,
|
|
199
|
+
defaultActivation: "load",
|
|
200
|
+
reducedMotion: "disable",
|
|
201
|
+
perfClass: "continuous"
|
|
202
|
+
}),
|
|
203
|
+
// Declares every channel the preset's CSS actually paints, not just what the shared keyframe
|
|
204
|
+
// animates: spinner-dots' `[data-kui-fx~='spinner-dots']` rule also sets `background:
|
|
205
|
+
// currentColor` (the dot itself) and `box-shadow` (the other two dots), entirely outside
|
|
206
|
+
// `@keyframes kui-spinner-dots`. Declaring only [scale, opacity] let a composed
|
|
207
|
+
// `background`-writing effect (e.g. gradient-mesh) pass channel-collision detection and then
|
|
208
|
+
// have its gradient silently overwritten by this rule — see css-invariants.test.ts's "CSS
|
|
209
|
+
// static rules" describe block, which now catches this class of omission directly.
|
|
210
|
+
cssPrimitive("feedback-dot-pulse", [CHANNEL.scale, CHANNEL.opacity, CHANNEL.background, "shadow"], {
|
|
211
|
+
parameters: dotPulse,
|
|
212
|
+
defaultActivation: "load",
|
|
213
|
+
reducedMotion: "disable",
|
|
214
|
+
perfClass: "continuous"
|
|
215
|
+
}),
|
|
216
|
+
// Same shape as feedback-dot-pulse above: `[data-kui-fx~='progress-indeterminate']` sets
|
|
217
|
+
// `background: currentColor` unconditionally for the bar itself, outside the keyframe.
|
|
218
|
+
cssPrimitive("feedback-progress-track", [CHANNEL.translate, CHANNEL.scale, CHANNEL.background], {
|
|
219
|
+
parameters: progressTrack,
|
|
220
|
+
defaultActivation: "load",
|
|
221
|
+
reducedMotion: "disable",
|
|
222
|
+
perfClass: "continuous"
|
|
223
|
+
}),
|
|
224
|
+
cssPrimitive("feedback-toast", [CHANNEL.opacity, CHANNEL.translate], {
|
|
225
|
+
parameters: toast,
|
|
226
|
+
defaultActivation: "manual"
|
|
227
|
+
}),
|
|
228
|
+
cssPrimitive("feedback-shake", [CHANNEL.translate], {
|
|
229
|
+
parameters: shake,
|
|
230
|
+
defaultActivation: "manual"
|
|
231
|
+
}),
|
|
232
|
+
cssPrimitive("feedback-wobble", [CHANNEL.translate, CHANNEL.rotate], {
|
|
233
|
+
defaultActivation: "click"
|
|
234
|
+
}),
|
|
235
|
+
// `[data-kui-fx~='ripple']` sets `background: currentColor` unconditionally for the ripple
|
|
236
|
+
// disc itself, outside the keyframe — same gap as feedback-dot-pulse/feedback-progress-track.
|
|
237
|
+
cssPrimitive("feedback-ripple", [CHANNEL.scale, CHANNEL.opacity, CHANNEL.background], {
|
|
238
|
+
parameters: ripple,
|
|
239
|
+
defaultActivation: "click"
|
|
240
|
+
}),
|
|
241
|
+
cssPrimitive("feedback-pop", [CHANNEL.scale], {
|
|
242
|
+
parameters: pop,
|
|
243
|
+
defaultActivation: "manual"
|
|
244
|
+
}),
|
|
245
|
+
// Declares every channel any preset built on it actually paints, not just what the shared
|
|
246
|
+
// keyframes animate: heart-burst's CSS also sets `color`, confetti-burst's also sets
|
|
247
|
+
// `background-image`. Understating this let a composed `background`-writing effect (e.g.
|
|
248
|
+
// gradient-mesh) pass channel-collision detection and silently overwrite confetti-burst's dots.
|
|
249
|
+
cssPrimitive("feedback-burst", [CHANNEL.scale, CHANNEL.opacity, CHANNEL.background, CHANNEL.color], {
|
|
250
|
+
parameters: pop,
|
|
251
|
+
defaultActivation: "click"
|
|
252
|
+
}),
|
|
253
|
+
cssPrimitive("feedback-confirm", [CHANNEL.opacity], {
|
|
254
|
+
parameters: confirm,
|
|
255
|
+
defaultActivation: "click"
|
|
256
|
+
}),
|
|
257
|
+
cssPrimitive("feedback-pull", [CHANNEL.translate], {
|
|
258
|
+
defaultActivation: "manual"
|
|
259
|
+
})
|
|
260
|
+
];
|
|
261
|
+
var FEEDBACK_PRESETS = [
|
|
262
|
+
{ name: "skeleton-shimmer", primitive: "feedback-shimmer", keyframes: "kui-skeleton-shimmer" },
|
|
263
|
+
{ name: "skeleton-to-content", primitive: "feedback-fade", keyframes: "kui-skeleton-to-content" },
|
|
264
|
+
{ name: "spinner", primitive: "feedback-spin", keyframes: "kui-spinner-spin" },
|
|
265
|
+
{ name: "spinner-dots", primitive: "feedback-dot-pulse", keyframes: "kui-spinner-dots" },
|
|
266
|
+
{ name: "spinner-ring", primitive: "feedback-spin", keyframes: "kui-spinner-ring-spin" },
|
|
267
|
+
{
|
|
268
|
+
name: "progress-indeterminate",
|
|
269
|
+
primitive: "feedback-progress-track",
|
|
270
|
+
keyframes: "kui-progress-indeterminate"
|
|
271
|
+
},
|
|
272
|
+
{ name: "toast-slide-in", primitive: "feedback-toast", keyframes: "kui-toast-slide-in" },
|
|
273
|
+
{
|
|
274
|
+
name: "toast-slide-out",
|
|
275
|
+
primitive: "feedback-toast",
|
|
276
|
+
keyframes: "kui-toast-slide-out",
|
|
277
|
+
params: { ease: "ease-in" }
|
|
278
|
+
},
|
|
279
|
+
{ name: "shake-error", primitive: "feedback-shake", keyframes: "kui-shake-error" },
|
|
280
|
+
{
|
|
281
|
+
name: "wobble",
|
|
282
|
+
primitive: "feedback-wobble",
|
|
283
|
+
keyframes: "kui-wobble",
|
|
284
|
+
params: { duration: "600ms", ease: "ease-in-out" }
|
|
285
|
+
},
|
|
286
|
+
{ name: "ripple", primitive: "feedback-ripple", keyframes: "kui-ripple" },
|
|
287
|
+
{ name: "badge-pop", primitive: "feedback-pop", keyframes: "kui-badge-pop" },
|
|
288
|
+
{ name: "count-bump", primitive: "feedback-pop", keyframes: "kui-count-bump", params: { duration: "280ms" } },
|
|
289
|
+
{
|
|
290
|
+
name: "heart-burst",
|
|
291
|
+
primitive: "feedback-burst",
|
|
292
|
+
keyframes: "kui-heart-burst",
|
|
293
|
+
params: { duration: "700ms" }
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "confetti-burst",
|
|
297
|
+
primitive: "feedback-burst",
|
|
298
|
+
keyframes: "kui-confetti-burst",
|
|
299
|
+
params: { duration: "900ms" }
|
|
300
|
+
},
|
|
301
|
+
{ name: "copy-confirm", primitive: "feedback-confirm", keyframes: "kui-copy-confirm" },
|
|
302
|
+
{
|
|
303
|
+
name: "pull-to-refresh",
|
|
304
|
+
primitive: "feedback-pull",
|
|
305
|
+
keyframes: "kui-pull-to-refresh",
|
|
306
|
+
params: { duration: "900ms", ease: "ease-out" }
|
|
307
|
+
}
|
|
308
|
+
];
|
|
309
|
+
function registerFeedback(registry) {
|
|
310
|
+
return registry.registerPrimitives(FEEDBACK_PRIMITIVES).registerPresets(FEEDBACK_PRESETS);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/core/spring.ts
|
|
314
|
+
var DEFAULT_SPRING = {
|
|
315
|
+
stiffness: 180,
|
|
316
|
+
damping: 24,
|
|
317
|
+
mass: 1,
|
|
318
|
+
restVelocity: 0.05,
|
|
319
|
+
restDisplacement: 0.05
|
|
320
|
+
};
|
|
321
|
+
var SUBSTEP = 1 / 240;
|
|
322
|
+
var MAX_STEP = 0.25;
|
|
323
|
+
function stepSpring(state, target, config, dt) {
|
|
324
|
+
let { value, velocity } = state;
|
|
325
|
+
let remaining = Math.min(Math.max(dt, 0), MAX_STEP);
|
|
326
|
+
while (remaining > 0) {
|
|
327
|
+
const step = Math.min(SUBSTEP, remaining);
|
|
328
|
+
const force = -config.stiffness * (value - target) - config.damping * velocity;
|
|
329
|
+
velocity += force / config.mass * step;
|
|
330
|
+
value += velocity * step;
|
|
331
|
+
remaining -= step;
|
|
332
|
+
}
|
|
333
|
+
return { value, velocity };
|
|
334
|
+
}
|
|
335
|
+
function isSettled(state, target, config) {
|
|
336
|
+
return Math.abs(state.velocity) < config.restVelocity && Math.abs(state.value - target) < config.restDisplacement;
|
|
337
|
+
}
|
|
338
|
+
var MAX_SETTLE_MS = 1e4;
|
|
339
|
+
function createSpringRunner(config, onChange, deps) {
|
|
340
|
+
const safeConfig = validConfig(config) ? config : DEFAULT_SPRING;
|
|
341
|
+
if (safeConfig !== config) deps.warn?.("invalid spring configuration; using defaults");
|
|
342
|
+
let state = { value: 0, velocity: 0 };
|
|
343
|
+
let target = 0;
|
|
344
|
+
let handle = null;
|
|
345
|
+
let lastTime = 0;
|
|
346
|
+
let startedAt = 0;
|
|
347
|
+
function frame(time) {
|
|
348
|
+
handle = null;
|
|
349
|
+
const dt = (time - lastTime) / 1e3;
|
|
350
|
+
lastTime = time;
|
|
351
|
+
state = stepSpring(state, target, safeConfig, dt);
|
|
352
|
+
if (!finiteState(state) || !Number.isFinite(target)) {
|
|
353
|
+
abortRun("spring produced non-finite state");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (time - startedAt >= MAX_SETTLE_MS) {
|
|
357
|
+
abortRun(`spring exceeded ${MAX_SETTLE_MS}ms settle budget`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (isSettled(state, target, safeConfig)) {
|
|
361
|
+
state = { value: target, velocity: 0 };
|
|
362
|
+
onChange(state.value, true);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
onChange(state.value, false);
|
|
366
|
+
schedule();
|
|
367
|
+
}
|
|
368
|
+
function schedule() {
|
|
369
|
+
if (handle !== null) return;
|
|
370
|
+
handle = deps.requestFrame(frame);
|
|
371
|
+
}
|
|
372
|
+
function start() {
|
|
373
|
+
lastTime = deps.now();
|
|
374
|
+
startedAt = lastTime;
|
|
375
|
+
schedule();
|
|
376
|
+
}
|
|
377
|
+
function abortRun(message) {
|
|
378
|
+
state = { value: Number.isFinite(target) ? target : 0, velocity: 0 };
|
|
379
|
+
deps.warn?.(message);
|
|
380
|
+
onChange(state.value, true);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
to(next, velocity) {
|
|
384
|
+
target = next;
|
|
385
|
+
if (velocity !== void 0) state = { ...state, velocity };
|
|
386
|
+
start();
|
|
387
|
+
},
|
|
388
|
+
set(value, velocity = 0) {
|
|
389
|
+
state = { value, velocity };
|
|
390
|
+
},
|
|
391
|
+
current: () => ({ ...state }),
|
|
392
|
+
stop() {
|
|
393
|
+
if (handle !== null) deps.cancelFrame(handle);
|
|
394
|
+
handle = null;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function validConfig(config) {
|
|
399
|
+
return Object.values(config).every(Number.isFinite) && config.stiffness > 0 && config.damping > 0 && config.mass > 0 && config.restVelocity > 0 && config.restDisplacement > 0;
|
|
400
|
+
}
|
|
401
|
+
function finiteState(state) {
|
|
402
|
+
return Number.isFinite(state.value) && Number.isFinite(state.velocity);
|
|
403
|
+
}
|
|
404
|
+
function defaultSpringDeps() {
|
|
405
|
+
const raf = globalThis.requestAnimationFrame;
|
|
406
|
+
if (typeof raf !== "function") {
|
|
407
|
+
return {
|
|
408
|
+
requestFrame: (callback) => globalThis.setTimeout(() => callback(Date.now()), 16),
|
|
409
|
+
cancelFrame: (handle) => globalThis.clearTimeout(handle),
|
|
410
|
+
now: () => Date.now()
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
requestFrame: (callback) => raf(callback),
|
|
415
|
+
cancelFrame: (handle) => globalThis.cancelAnimationFrame(handle),
|
|
416
|
+
now: () => performance.now()
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/effects/catalog/interaction-shared.ts
|
|
421
|
+
function centeredOffset(point, size) {
|
|
422
|
+
return {
|
|
423
|
+
x: size.width > 0 ? point.x / size.width - 0.5 : 0,
|
|
424
|
+
y: size.height > 0 ? point.y / size.height - 0.5 : 0
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function tiltAngles(point, size, maxAngleDeg) {
|
|
428
|
+
const centered = centeredOffset(point, size);
|
|
429
|
+
return { rotateY: centered.x * maxAngleDeg * 2, rotateX: -centered.y * maxAngleDeg * 2 };
|
|
430
|
+
}
|
|
431
|
+
function parallaxOffset(point, size, strengthPx) {
|
|
432
|
+
const centered = centeredOffset(point, size);
|
|
433
|
+
return { x: centered.x * strengthPx * 2, y: centered.y * strengthPx * 2 };
|
|
434
|
+
}
|
|
435
|
+
function supportsFineHover(win) {
|
|
436
|
+
return win.matchMedia?.("(hover: hover) and (pointer: fine)").matches ?? true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/effects/catalog/interaction.ts
|
|
440
|
+
var hoverTiming = {
|
|
441
|
+
duration: { type: "time", default: "220ms", cssProperty: "--kui-duration" },
|
|
442
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
|
|
443
|
+
};
|
|
444
|
+
function hoverPrimitive(id, channels) {
|
|
445
|
+
return {
|
|
446
|
+
id,
|
|
447
|
+
renderer: "javascript",
|
|
448
|
+
channels,
|
|
449
|
+
parameters: hoverTiming,
|
|
450
|
+
supportedTimelines: ["time"],
|
|
451
|
+
supportedActivations: ["load"],
|
|
452
|
+
defaultActivation: "load",
|
|
453
|
+
perfClass: "compositor",
|
|
454
|
+
// Not 'disable': a translate/box-shadow/rotate hover micro-interaction at this scale is not a
|
|
455
|
+
// vestibular trigger the way parallax or continuous ambient motion is. The real motion lives in
|
|
456
|
+
// CSS transitions and `:hover`-scoped pseudo-element animations rather than this primitive's
|
|
457
|
+
// (here unused) compiled `animation-*` path, so the policy layer shortens it via the
|
|
458
|
+
// `transition-duration` and `::before`/`::after` rules in base.css.
|
|
459
|
+
reducedMotion: "shorten",
|
|
460
|
+
prepare: () => inertInstance()
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
var HOVER_PRIMITIVES = [
|
|
464
|
+
hoverPrimitive("lift", ["translate"]),
|
|
465
|
+
hoverPrimitive("lift-shadow", ["translate", "shadow"]),
|
|
466
|
+
hoverPrimitive("shine-sweep", ["sweep"]),
|
|
467
|
+
hoverPrimitive("split-flap", ["rotate"]),
|
|
468
|
+
hoverPrimitive("border-draw", ["border"]),
|
|
469
|
+
hoverPrimitive("border-glow", ["shadow"]),
|
|
470
|
+
hoverPrimitive("beam-border", ["border"]),
|
|
471
|
+
hoverPrimitive("underline-slide", ["scale"]),
|
|
472
|
+
hoverPrimitive("underline-center", ["scale"]),
|
|
473
|
+
hoverPrimitive("icon-wiggle", ["rotate"]),
|
|
474
|
+
hoverPrimitive("icon-spin", ["rotate"]),
|
|
475
|
+
hoverPrimitive("icon-bounce", ["translate"])
|
|
476
|
+
];
|
|
477
|
+
var HOVER_PRESETS = HOVER_PRIMITIVES.map((primitive) => ({
|
|
478
|
+
name: primitive.id,
|
|
479
|
+
primitive: primitive.id
|
|
480
|
+
}));
|
|
481
|
+
var CONTINUOUS_BORDER_PRIMITIVES = [
|
|
482
|
+
{
|
|
483
|
+
id: "beam-border-auto",
|
|
484
|
+
renderer: "javascript",
|
|
485
|
+
channels: ["border"],
|
|
486
|
+
parameters: hoverTiming,
|
|
487
|
+
supportedTimelines: ["time"],
|
|
488
|
+
supportedActivations: ["load"],
|
|
489
|
+
defaultActivation: "load",
|
|
490
|
+
perfClass: "compositor",
|
|
491
|
+
reducedMotion: "disable",
|
|
492
|
+
prepare: () => inertInstance()
|
|
493
|
+
}
|
|
494
|
+
];
|
|
495
|
+
var CONTINUOUS_BORDER_PRESETS = [
|
|
496
|
+
{ name: "beam-border-auto", primitive: "beam-border-auto" }
|
|
497
|
+
];
|
|
498
|
+
var springParams = {
|
|
499
|
+
stiffness: {
|
|
500
|
+
type: "number",
|
|
501
|
+
default: "260",
|
|
502
|
+
cssProperty: "--kui-stiffness",
|
|
503
|
+
finite: true,
|
|
504
|
+
minimum: 1,
|
|
505
|
+
maximum: 1e4
|
|
506
|
+
},
|
|
507
|
+
damping: {
|
|
508
|
+
type: "number",
|
|
509
|
+
default: "26",
|
|
510
|
+
cssProperty: "--kui-damping",
|
|
511
|
+
finite: true,
|
|
512
|
+
minimum: 0.1,
|
|
513
|
+
maximum: 1e3
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
function pointerPrimitive(id, channels, parameters, prepare) {
|
|
517
|
+
return {
|
|
518
|
+
id,
|
|
519
|
+
renderer: "javascript",
|
|
520
|
+
channels,
|
|
521
|
+
parameters,
|
|
522
|
+
supportedTimelines: ["time", "pointer"],
|
|
523
|
+
supportedActivations: ["load", "manual"],
|
|
524
|
+
defaultActivation: "load",
|
|
525
|
+
perfClass: "continuous",
|
|
526
|
+
// A tilt or cursor-follow effect only exists while the pointer is present; there is no
|
|
527
|
+
// meaningful "shortened" version of tracking a position. `prepare` itself checks
|
|
528
|
+
// `supportsFineHover` and no-ops on touch, which is the coarse-pointer half of this rule.
|
|
529
|
+
reducedMotion: "disable",
|
|
530
|
+
prepare
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function springFrom(params) {
|
|
534
|
+
return {
|
|
535
|
+
...DEFAULT_SPRING,
|
|
536
|
+
stiffness: params.num("stiffness", DEFAULT_SPRING.stiffness),
|
|
537
|
+
damping: params.num("damping", DEFAULT_SPRING.damping)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function springDepsFor(ctx) {
|
|
541
|
+
return { ...defaultSpringDeps(), warn: ctx.warn };
|
|
542
|
+
}
|
|
543
|
+
function prepareTilt3d(el, params, ctx) {
|
|
544
|
+
if (!supportsFineHover(ctx.win)) return () => {
|
|
545
|
+
};
|
|
546
|
+
const node = el;
|
|
547
|
+
const maxAngle = params.num("maxAngle", 14);
|
|
548
|
+
const perspective = params.text("perspective", "800px");
|
|
549
|
+
function writeAngles(rotateX, rotateY, transitionMs) {
|
|
550
|
+
ctx.style.set("transition", `transform ${transitionMs}ms ease-out`);
|
|
551
|
+
ctx.style.set(
|
|
552
|
+
"transform",
|
|
553
|
+
`perspective(${perspective}) rotateX(${rotateX.toFixed(2)}deg) rotateY(${rotateY.toFixed(2)}deg)`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
function onMove(event) {
|
|
557
|
+
const rect = node.getBoundingClientRect();
|
|
558
|
+
const point = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
559
|
+
const { rotateX, rotateY } = tiltAngles(point, rect, maxAngle);
|
|
560
|
+
writeAngles(rotateX, rotateY, 100);
|
|
561
|
+
}
|
|
562
|
+
function reset() {
|
|
563
|
+
writeAngles(0, 0, 400);
|
|
564
|
+
}
|
|
565
|
+
function onFocus() {
|
|
566
|
+
writeAngles(-maxAngle / 2, maxAngle / 2, 250);
|
|
567
|
+
}
|
|
568
|
+
node.addEventListener("pointermove", onMove, { passive: true });
|
|
569
|
+
node.addEventListener("pointerleave", reset, { passive: true });
|
|
570
|
+
node.addEventListener("focus", onFocus);
|
|
571
|
+
node.addEventListener("blur", reset);
|
|
572
|
+
return () => {
|
|
573
|
+
node.removeEventListener("pointermove", onMove);
|
|
574
|
+
node.removeEventListener("pointerleave", reset);
|
|
575
|
+
node.removeEventListener("focus", onFocus);
|
|
576
|
+
node.removeEventListener("blur", reset);
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function collectParallaxLayers(node) {
|
|
580
|
+
return [...node.querySelectorAll("[data-depth]")].map((layer) => ({
|
|
581
|
+
ledger: createStyleLedger(layer),
|
|
582
|
+
depth: Number(layer.dataset.depth) || 0
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
function writeParallaxLayers(layers, x, y, transitionMs) {
|
|
586
|
+
for (const layer of layers) {
|
|
587
|
+
layer.ledger.set("transition", `translate ${transitionMs}ms ease-out`);
|
|
588
|
+
layer.ledger.set("translate", `${(x * layer.depth).toFixed(2)}px ${(y * layer.depth).toFixed(2)}px`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function prepareTiltParallax(el, params, ctx) {
|
|
592
|
+
if (!supportsFineHover(ctx.win)) return () => {
|
|
593
|
+
};
|
|
594
|
+
const node = el;
|
|
595
|
+
const strength = params.num("strength", 24);
|
|
596
|
+
const layers = collectParallaxLayers(node);
|
|
597
|
+
function onMove(event) {
|
|
598
|
+
const rect = node.getBoundingClientRect();
|
|
599
|
+
const point = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
600
|
+
const offset = parallaxOffset(point, rect, strength);
|
|
601
|
+
writeParallaxLayers(layers, offset.x, offset.y, 120);
|
|
602
|
+
}
|
|
603
|
+
function reset() {
|
|
604
|
+
writeParallaxLayers(layers, 0, 0, 400);
|
|
605
|
+
}
|
|
606
|
+
node.addEventListener("pointermove", onMove, { passive: true });
|
|
607
|
+
node.addEventListener("pointerleave", reset, { passive: true });
|
|
608
|
+
node.addEventListener("focus", reset);
|
|
609
|
+
node.addEventListener("blur", reset);
|
|
610
|
+
return () => {
|
|
611
|
+
node.removeEventListener("pointermove", onMove);
|
|
612
|
+
node.removeEventListener("pointerleave", reset);
|
|
613
|
+
node.removeEventListener("focus", reset);
|
|
614
|
+
node.removeEventListener("blur", reset);
|
|
615
|
+
for (const layer of layers) layer.ledger.restore();
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function writeDotPosition(dot, x, y) {
|
|
619
|
+
dot.style.translate = `${x.toFixed(1)}px ${y.toFixed(1)}px`;
|
|
620
|
+
}
|
|
621
|
+
function createCursorDot(doc, dotClass, labelText) {
|
|
622
|
+
const dot = doc.createElement("span");
|
|
623
|
+
dot.className = `kui-cursor-dot ${dotClass}`;
|
|
624
|
+
dot.setAttribute("aria-hidden", "true");
|
|
625
|
+
if (labelText) dot.textContent = labelText;
|
|
626
|
+
doc.body.append(dot);
|
|
627
|
+
return dot;
|
|
628
|
+
}
|
|
629
|
+
function createCursorRunners(dot, config, deps) {
|
|
630
|
+
const position = { x: 0, y: 0 };
|
|
631
|
+
const onAxis = (axis) => (value) => {
|
|
632
|
+
position[axis] = value;
|
|
633
|
+
writeDotPosition(dot, position.x, position.y);
|
|
634
|
+
};
|
|
635
|
+
return {
|
|
636
|
+
x: createSpringRunner(config, onAxis("x"), deps),
|
|
637
|
+
y: createSpringRunner(config, onAxis("y"), deps)
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function prepareCursorDot(el, params, ctx, dotClass) {
|
|
641
|
+
if (!supportsFineHover(ctx.win)) return () => {
|
|
642
|
+
};
|
|
643
|
+
const doc = el.ownerDocument;
|
|
644
|
+
const node = el;
|
|
645
|
+
const dot = createCursorDot(doc, dotClass, params.text("label", ""));
|
|
646
|
+
const runners = createCursorRunners(dot, springFrom(params), springDepsFor(ctx));
|
|
647
|
+
function show() {
|
|
648
|
+
dot.classList.add("kui-cursor-dot-active");
|
|
649
|
+
}
|
|
650
|
+
function hide() {
|
|
651
|
+
dot.classList.remove("kui-cursor-dot-active");
|
|
652
|
+
}
|
|
653
|
+
function onMove(event) {
|
|
654
|
+
show();
|
|
655
|
+
runners.x.to(event.clientX);
|
|
656
|
+
runners.y.to(event.clientY);
|
|
657
|
+
}
|
|
658
|
+
function onFocus() {
|
|
659
|
+
const rect = node.getBoundingClientRect();
|
|
660
|
+
const cx = rect.left + rect.width / 2;
|
|
661
|
+
const cy = rect.top + rect.height / 2;
|
|
662
|
+
show();
|
|
663
|
+
runners.x.set(cx);
|
|
664
|
+
runners.y.set(cy);
|
|
665
|
+
writeDotPosition(dot, cx, cy);
|
|
666
|
+
}
|
|
667
|
+
node.addEventListener("pointermove", onMove, { passive: true });
|
|
668
|
+
node.addEventListener("pointerleave", hide, { passive: true });
|
|
669
|
+
node.addEventListener("focus", onFocus);
|
|
670
|
+
node.addEventListener("blur", hide);
|
|
671
|
+
return () => {
|
|
672
|
+
node.removeEventListener("pointermove", onMove);
|
|
673
|
+
node.removeEventListener("pointerleave", hide);
|
|
674
|
+
node.removeEventListener("focus", onFocus);
|
|
675
|
+
node.removeEventListener("blur", hide);
|
|
676
|
+
runners.x.stop();
|
|
677
|
+
runners.y.stop();
|
|
678
|
+
dot.remove();
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function prepareSpotlight(el, params, ctx) {
|
|
682
|
+
if (!supportsFineHover(ctx.win)) return () => {
|
|
683
|
+
};
|
|
684
|
+
const node = el;
|
|
685
|
+
if (ctx.win.getComputedStyle(node).position === "static") ctx.style.set("position", "relative");
|
|
686
|
+
function writeSpot(x, y, on) {
|
|
687
|
+
ctx.style.set("--kui-x", x);
|
|
688
|
+
ctx.style.set("--kui-y", y);
|
|
689
|
+
ctx.style.set("--kui-spotlight-opacity", on ? "1" : "0");
|
|
690
|
+
}
|
|
691
|
+
function onMove(event) {
|
|
692
|
+
const rect = node.getBoundingClientRect();
|
|
693
|
+
writeSpot(`${(event.clientX - rect.left).toFixed(1)}px`, `${(event.clientY - rect.top).toFixed(1)}px`, true);
|
|
694
|
+
}
|
|
695
|
+
function hide() {
|
|
696
|
+
writeSpot("50%", "50%", false);
|
|
697
|
+
}
|
|
698
|
+
function onFocus() {
|
|
699
|
+
writeSpot("50%", "50%", true);
|
|
700
|
+
}
|
|
701
|
+
node.addEventListener("pointermove", onMove, { passive: true });
|
|
702
|
+
node.addEventListener("pointerleave", hide, { passive: true });
|
|
703
|
+
node.addEventListener("focus", onFocus);
|
|
704
|
+
node.addEventListener("blur", hide);
|
|
705
|
+
return () => {
|
|
706
|
+
node.removeEventListener("pointermove", onMove);
|
|
707
|
+
node.removeEventListener("pointerleave", hide);
|
|
708
|
+
node.removeEventListener("focus", onFocus);
|
|
709
|
+
node.removeEventListener("blur", hide);
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
var tiltParams = {
|
|
713
|
+
maxAngle: { type: "number", default: "14", cssProperty: "--kui-max-angle" },
|
|
714
|
+
perspective: { type: "length", default: "800px", cssProperty: "--kui-perspective" }
|
|
715
|
+
};
|
|
716
|
+
var parallaxParams = {
|
|
717
|
+
strength: { type: "number", default: "24", cssProperty: "--kui-strength" }
|
|
718
|
+
};
|
|
719
|
+
var cursorDotParams = {
|
|
720
|
+
...springParams,
|
|
721
|
+
label: { type: "text", default: "", cssProperty: "--kui-label" }
|
|
722
|
+
};
|
|
723
|
+
var POINTER_PRIMITIVES = [
|
|
724
|
+
pointerPrimitive("tilt-3d", ["rotate"], tiltParams, deferPrepare(prepareTilt3d)),
|
|
725
|
+
pointerPrimitive("tilt-parallax", ["translate"], parallaxParams, deferPrepare(prepareTiltParallax)),
|
|
726
|
+
pointerPrimitive("cursor-follow", ["translate"], cursorDotParams, deferPrepare(prepareCursorFollow)),
|
|
727
|
+
pointerPrimitive("cursor-lag", ["translate"], cursorDotParams, deferPrepare(prepareCursorLag)),
|
|
728
|
+
pointerPrimitive("cursor-label", ["translate"], cursorDotParams, deferPrepare(prepareCursorLabel)),
|
|
729
|
+
pointerPrimitive("cursor-invert", ["translate"], cursorDotParams, deferPrepare(prepareCursorInvert)),
|
|
730
|
+
pointerPrimitive("cursor-spotlight", ["spotlight"], {}, deferPrepare(prepareSpotlight))
|
|
731
|
+
];
|
|
732
|
+
function prepareCursorFollow(el, params, ctx) {
|
|
733
|
+
return prepareCursorDot(el, params, ctx, "kui-cursor-dot-follow");
|
|
734
|
+
}
|
|
735
|
+
function prepareCursorLag(el, params, ctx) {
|
|
736
|
+
return prepareCursorDot(el, params, ctx, "kui-cursor-dot-lag");
|
|
737
|
+
}
|
|
738
|
+
function prepareCursorLabel(el, params, ctx) {
|
|
739
|
+
return prepareCursorDot(el, params, ctx, "kui-cursor-dot-label");
|
|
740
|
+
}
|
|
741
|
+
function prepareCursorInvert(el, params, ctx) {
|
|
742
|
+
return prepareCursorDot(el, params, ctx, "kui-cursor-dot-invert");
|
|
743
|
+
}
|
|
744
|
+
var POINTER_PRESETS = [
|
|
745
|
+
{ name: "tilt-3d", primitive: "tilt-3d" },
|
|
746
|
+
{ name: "tilt-parallax", primitive: "tilt-parallax" },
|
|
747
|
+
{ name: "cursor-follow", primitive: "cursor-follow", params: { stiffness: "300", damping: "30" } },
|
|
748
|
+
{ name: "cursor-lag", primitive: "cursor-lag", params: { stiffness: "80", damping: "14" } },
|
|
749
|
+
{ name: "cursor-label", primitive: "cursor-label", params: { stiffness: "260", damping: "26", label: "View" } },
|
|
750
|
+
{ name: "cursor-spotlight", primitive: "cursor-spotlight" },
|
|
751
|
+
{ name: "cursor-invert", primitive: "cursor-invert", params: { stiffness: "260", damping: "26" } }
|
|
752
|
+
];
|
|
753
|
+
var INTERACTION_PRIMITIVES = [
|
|
754
|
+
...HOVER_PRIMITIVES,
|
|
755
|
+
...POINTER_PRIMITIVES,
|
|
756
|
+
...CONTINUOUS_BORDER_PRIMITIVES
|
|
757
|
+
];
|
|
758
|
+
var INTERACTION_PRESETS = [
|
|
759
|
+
...HOVER_PRESETS,
|
|
760
|
+
...POINTER_PRESETS,
|
|
761
|
+
...CONTINUOUS_BORDER_PRESETS
|
|
762
|
+
];
|
|
763
|
+
function registerInteraction(registry) {
|
|
764
|
+
return registry.registerPrimitives(INTERACTION_PRIMITIVES).registerPresets(INTERACTION_PRESETS);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/effects/catalog/media.ts
|
|
768
|
+
var geometry = {
|
|
769
|
+
distance: { type: "length", default: "24px", cssProperty: "--kui-distance" },
|
|
770
|
+
scale: { type: "number", default: "1.12", cssProperty: "--kui-to-scale" }
|
|
771
|
+
};
|
|
772
|
+
var MEDIA_PRIMITIVES = [
|
|
773
|
+
cssPrimitive("media-wipe", [CHANNEL.clip]),
|
|
774
|
+
cssPrimitive("media-mask", ["mask"], { perfClass: "paint" }),
|
|
775
|
+
// Not `reducedMotion: 'disable'` — that policy means "no finite duration would make sense,
|
|
776
|
+
// because the animation never ends" (see `ambient.ts`/`feedback.ts`), and a Ken Burns pan/zoom
|
|
777
|
+
// is the opposite of that: a one-shot cinematic move with a real, shortenable duration. The demo
|
|
778
|
+
// authors it as `ken-burns 9000ms` (a still image, one slow zoom, then it holds) and
|
|
779
|
+
// `ken-burns 3000ms on:hover` (zooms in while hovered), and its complement `ken-burns-out` is a
|
|
780
|
+
// second one-shot preset for the reverse move — not a `-loop` variant the way `typewriter-loop`
|
|
781
|
+
// or `marquee`/`marquee-scroll-linked` are. `kui-ken-burns`'s keyframe (`scale: 1` to `1.12`,
|
|
782
|
+
// no loop-safe midpoint) is shaped for exactly that: run once, land on the zoomed frame, stay
|
|
783
|
+
// there. `'disable'` here previously looked like the same missing-`--kui-fx-*-iterations` bug as
|
|
784
|
+
// `marquee`/`gradient-shimmer`, but the actual defect was this policy — the default `'shorten'`
|
|
785
|
+
// is correct, so no `--kui-fx-ken-burns-iterations` is needed at all.
|
|
786
|
+
cssPrimitive("media-ken-burns", [CHANNEL.translate, CHANNEL.scale], {
|
|
787
|
+
parameters: geometry
|
|
788
|
+
}),
|
|
789
|
+
cssPrimitive("media-filter", [CHANNEL.filter], {
|
|
790
|
+
defaultActivation: "hover",
|
|
791
|
+
perfClass: "paint"
|
|
792
|
+
}),
|
|
793
|
+
cssPrimitive("media-blur-up", [CHANNEL.translate, CHANNEL.filter], {
|
|
794
|
+
parameters: geometry,
|
|
795
|
+
perfClass: "paint"
|
|
796
|
+
}),
|
|
797
|
+
cssPrimitive("media-parallax-frame", [CHANNEL.translate], {
|
|
798
|
+
parameters: geometry,
|
|
799
|
+
timelines: ["view", "scroll"],
|
|
800
|
+
activations: ["manual"],
|
|
801
|
+
defaultActivation: "manual",
|
|
802
|
+
reducedMotion: "disable"
|
|
803
|
+
}),
|
|
804
|
+
cssPrimitive("media-lightbox", [CHANNEL.opacity, CHANNEL.scale], {
|
|
805
|
+
parameters: geometry
|
|
806
|
+
})
|
|
807
|
+
];
|
|
808
|
+
var MEDIA_PRESETS = [
|
|
809
|
+
{ name: "wipe-up", primitive: "media-wipe", keyframes: "kui-wipe-up" },
|
|
810
|
+
{ name: "wipe-down", primitive: "media-wipe", keyframes: "kui-wipe-down" },
|
|
811
|
+
{ name: "wipe-left", primitive: "media-wipe", keyframes: "kui-wipe-left" },
|
|
812
|
+
{ name: "wipe-right", primitive: "media-wipe", keyframes: "kui-wipe-right" },
|
|
813
|
+
{ name: "wipe-circle", primitive: "media-wipe", keyframes: "kui-wipe-circle" },
|
|
814
|
+
{ name: "wipe-diagonal", primitive: "media-wipe", keyframes: "kui-wipe-diagonal" },
|
|
815
|
+
{ name: "mask-reveal", primitive: "media-mask", keyframes: "kui-mask-reveal" },
|
|
816
|
+
{ name: "curtain-reveal", primitive: "media-wipe", keyframes: "kui-curtain-reveal" },
|
|
817
|
+
{ name: "ken-burns", primitive: "media-ken-burns", keyframes: "kui-ken-burns" },
|
|
818
|
+
{ name: "ken-burns-out", primitive: "media-ken-burns", keyframes: "kui-ken-burns-out" },
|
|
819
|
+
{ name: "blur-up", primitive: "media-blur-up", keyframes: "kui-blur-up" },
|
|
820
|
+
{ name: "duotone-hover", primitive: "media-filter", keyframes: "kui-duotone-hover" },
|
|
821
|
+
{ name: "grayscale-hover", primitive: "media-filter", keyframes: "kui-grayscale-hover" },
|
|
822
|
+
{ name: "saturate-hover", primitive: "media-filter", keyframes: "kui-saturate-hover" },
|
|
823
|
+
{
|
|
824
|
+
name: "image-parallax-frame",
|
|
825
|
+
primitive: "media-parallax-frame",
|
|
826
|
+
keyframes: "kui-image-parallax-frame"
|
|
827
|
+
},
|
|
828
|
+
{ name: "before-after-wipe", primitive: "media-wipe", keyframes: "kui-before-after-wipe" },
|
|
829
|
+
{ name: "lightbox-open", primitive: "media-lightbox", keyframes: "kui-lightbox-open" }
|
|
830
|
+
];
|
|
831
|
+
function registerMedia(registry) {
|
|
832
|
+
return registry.registerPrimitives(MEDIA_PRIMITIVES).registerPresets(MEDIA_PRESETS);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/effects/catalog/numbers-shared.ts
|
|
836
|
+
var SR_ONLY_CLASS = "kui-sr-only";
|
|
837
|
+
var DECORATIVE_CLASS = "kui-count-decorative";
|
|
838
|
+
function installCountLayers(el, doc) {
|
|
839
|
+
const decorative = doc.createElement("span");
|
|
840
|
+
decorative.setAttribute("aria-hidden", "true");
|
|
841
|
+
decorative.className = DECORATIVE_CLASS;
|
|
842
|
+
const srOnly = doc.createElement("span");
|
|
843
|
+
srOnly.className = SR_ONLY_CLASS;
|
|
844
|
+
srOnly.setAttribute("aria-live", "polite");
|
|
845
|
+
el.textContent = "";
|
|
846
|
+
el.append(decorative, srOnly);
|
|
847
|
+
return {
|
|
848
|
+
decorative,
|
|
849
|
+
srOnly,
|
|
850
|
+
restore: () => {
|
|
851
|
+
el.textContent = srOnly.textContent;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function easeOutCubic(t) {
|
|
856
|
+
const clamped = Math.min(Math.max(t, 0), 1);
|
|
857
|
+
return 1 - (1 - clamped) ** 3;
|
|
858
|
+
}
|
|
859
|
+
function tweenValue(t, from, to) {
|
|
860
|
+
return from + (to - from) * t;
|
|
861
|
+
}
|
|
862
|
+
function cubicBezier(x1, y1, x2, y2) {
|
|
863
|
+
const cx = 3 * x1;
|
|
864
|
+
const bx = 3 * (x2 - x1) - cx;
|
|
865
|
+
const ax = 1 - cx - bx;
|
|
866
|
+
const cy = 3 * y1;
|
|
867
|
+
const by = 3 * (y2 - y1) - cy;
|
|
868
|
+
const ay = 1 - cy - by;
|
|
869
|
+
const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
|
|
870
|
+
const sampleY = (t) => ((ay * t + by) * t + cy) * t;
|
|
871
|
+
const slopeX = (t) => (3 * ax * t + 2 * bx) * t + cx;
|
|
872
|
+
function solveT(x) {
|
|
873
|
+
let t = x;
|
|
874
|
+
for (let i = 0; i < 8; i++) {
|
|
875
|
+
const slope = slopeX(t);
|
|
876
|
+
if (Math.abs(slope) < 1e-6) break;
|
|
877
|
+
const next = t - (sampleX(t) - x) / slope;
|
|
878
|
+
if (Math.abs(sampleX(next) - x) < 1e-6) return next;
|
|
879
|
+
t = next;
|
|
880
|
+
}
|
|
881
|
+
let lo = 0;
|
|
882
|
+
let hi = 1;
|
|
883
|
+
t = x;
|
|
884
|
+
while (hi - lo > 1e-6) {
|
|
885
|
+
if (sampleX(t) < x) lo = t;
|
|
886
|
+
else hi = t;
|
|
887
|
+
t = (lo + hi) / 2;
|
|
888
|
+
}
|
|
889
|
+
return t;
|
|
890
|
+
}
|
|
891
|
+
return (t) => sampleY(solveT(Math.min(Math.max(t, 0), 1)));
|
|
892
|
+
}
|
|
893
|
+
var EASING_KEYWORDS = {
|
|
894
|
+
ease: [0.25, 0.1, 0.25, 1],
|
|
895
|
+
"ease-in": [0.42, 0, 1, 1],
|
|
896
|
+
"ease-out": [0, 0, 0.58, 1],
|
|
897
|
+
"ease-in-out": [0.42, 0, 0.58, 1],
|
|
898
|
+
"expo-in": [0.7, 0, 0.84, 0],
|
|
899
|
+
"expo-out": [0.16, 1, 0.3, 1],
|
|
900
|
+
"expo-in-out": [0.87, 0, 0.13, 1],
|
|
901
|
+
"back-in": [0.36, 0, 0.66, -0.56],
|
|
902
|
+
"back-out": [0.34, 1.56, 0.64, 1],
|
|
903
|
+
"back-in-out": [0.68, -0.6, 0.32, 1.6],
|
|
904
|
+
"quart-out": [0.25, 1, 0.5, 1],
|
|
905
|
+
"circ-out": [0, 0.55, 0.45, 1]
|
|
906
|
+
};
|
|
907
|
+
var CUBIC_BEZIER_FN = /^cubic-bezier\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)$/i;
|
|
908
|
+
function resolveEasing(easing, warn) {
|
|
909
|
+
if (easing === void 0) return easeOutCubic;
|
|
910
|
+
if (easing === "linear") return (t) => Math.min(Math.max(t, 0), 1);
|
|
911
|
+
const keyword = EASING_KEYWORDS[easing];
|
|
912
|
+
if (keyword) return cubicBezier(...keyword);
|
|
913
|
+
const fn = CUBIC_BEZIER_FN.exec(easing);
|
|
914
|
+
if (fn) return cubicBezier(Number(fn[1]), Number(fn[2]), Number(fn[3]), Number(fn[4]));
|
|
915
|
+
warn(`easing "${easing}" has no JS equivalent for a counter tween \u2014 using the default ease-out`);
|
|
916
|
+
return easeOutCubic;
|
|
917
|
+
}
|
|
918
|
+
var COMPACT_OPTIONS = { notation: "compact", maximumFractionDigits: 1 };
|
|
919
|
+
function formatCount(value, options) {
|
|
920
|
+
const { format, decimals, currency } = options;
|
|
921
|
+
if (format === "currency") {
|
|
922
|
+
return new Intl.NumberFormat(void 0, {
|
|
923
|
+
style: "currency",
|
|
924
|
+
currency,
|
|
925
|
+
minimumFractionDigits: decimals,
|
|
926
|
+
maximumFractionDigits: decimals
|
|
927
|
+
}).format(value);
|
|
928
|
+
}
|
|
929
|
+
if (format === "percent") {
|
|
930
|
+
return new Intl.NumberFormat(void 0, {
|
|
931
|
+
style: "percent",
|
|
932
|
+
minimumFractionDigits: decimals,
|
|
933
|
+
maximumFractionDigits: decimals
|
|
934
|
+
}).format(value);
|
|
935
|
+
}
|
|
936
|
+
if (format === "compact") {
|
|
937
|
+
return new Intl.NumberFormat(void 0, COMPACT_OPTIONS).format(value);
|
|
938
|
+
}
|
|
939
|
+
return new Intl.NumberFormat(void 0, {
|
|
940
|
+
minimumFractionDigits: decimals,
|
|
941
|
+
maximumFractionDigits: decimals
|
|
942
|
+
}).format(value);
|
|
943
|
+
}
|
|
944
|
+
function paddedDigits(value, width) {
|
|
945
|
+
return Math.round(Math.max(0, value)).toString().padStart(width, "0");
|
|
946
|
+
}
|
|
947
|
+
function groupDigits(digits) {
|
|
948
|
+
const groups = [];
|
|
949
|
+
for (let end = digits.length; end > 0; end -= 3) {
|
|
950
|
+
groups.unshift(digits.slice(Math.max(0, end - 3), end));
|
|
951
|
+
}
|
|
952
|
+
return groups.join(",");
|
|
953
|
+
}
|
|
954
|
+
function odometerTokens(grouped) {
|
|
955
|
+
return [...grouped].map((char) => ({ char, digit: char >= "0" && char <= "9" }));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/effects/catalog/numbers.ts
|
|
959
|
+
var COUNT_STEP_MS = 16;
|
|
960
|
+
var REDUCED_MOTION_DURATION_MS = 1;
|
|
961
|
+
var countParams = {
|
|
962
|
+
duration: { type: "time", default: "1600ms", cssProperty: "--kui-duration" },
|
|
963
|
+
from: { type: "number", default: "0", cssProperty: "--kui-from" },
|
|
964
|
+
to: { type: "number", default: "100", cssProperty: "--kui-to" },
|
|
965
|
+
decimals: {
|
|
966
|
+
type: "number",
|
|
967
|
+
default: "0",
|
|
968
|
+
cssProperty: "--kui-decimals",
|
|
969
|
+
integer: true,
|
|
970
|
+
minimum: 0,
|
|
971
|
+
maximum: 6
|
|
972
|
+
},
|
|
973
|
+
format: {
|
|
974
|
+
type: "keyword",
|
|
975
|
+
default: "number",
|
|
976
|
+
cssProperty: "--kui-format",
|
|
977
|
+
values: ["number", "currency", "percent", "compact"]
|
|
978
|
+
},
|
|
979
|
+
currency: { type: "text", default: "USD", cssProperty: "--kui-currency" }
|
|
980
|
+
};
|
|
981
|
+
var odometerParams = {
|
|
982
|
+
duration: { type: "time", default: "1600ms", cssProperty: "--kui-duration" },
|
|
983
|
+
from: { type: "number", default: "0", cssProperty: "--kui-from" },
|
|
984
|
+
to: { type: "number", default: "100", cssProperty: "--kui-to" }
|
|
985
|
+
};
|
|
986
|
+
function countPrimitive(id, parameters, prepare) {
|
|
987
|
+
return {
|
|
988
|
+
id,
|
|
989
|
+
renderer: "javascript",
|
|
990
|
+
channels: ["content"],
|
|
991
|
+
parameters,
|
|
992
|
+
supportedTimelines: ["time"],
|
|
993
|
+
supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
|
|
994
|
+
defaultActivation: "enter",
|
|
995
|
+
perfClass: "dom-transform",
|
|
996
|
+
// Not 'disable': the animator would then skip activation entirely under reduced motion,
|
|
997
|
+
// leaving the counter permanently blank. `prepareTween` reads `ctx.reducedMotion` itself and
|
|
998
|
+
// collapses the ramp to one effectively-instant tick instead, so the final value still lands.
|
|
999
|
+
reducedMotion: "shorten",
|
|
1000
|
+
prepare
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function tweenDurationMs(params, ctx) {
|
|
1004
|
+
return ctx.reducedMotion ? REDUCED_MOTION_DURATION_MS : Math.max(0, effectDurationMs(params, 1600));
|
|
1005
|
+
}
|
|
1006
|
+
function tweenTimingFor(params, ctx) {
|
|
1007
|
+
return {
|
|
1008
|
+
durationMs: tweenDurationMs(params, ctx),
|
|
1009
|
+
delayMs: Math.max(0, params.timing.delayMs ?? 0),
|
|
1010
|
+
easing: resolveEasing(params.timing.easing, ctx.warn)
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function tweenNumber(ctx, tween) {
|
|
1014
|
+
const { from, to, durationMs, delayMs, easing, onTick } = tween;
|
|
1015
|
+
let elapsed = 0;
|
|
1016
|
+
let handle;
|
|
1017
|
+
let settle2;
|
|
1018
|
+
const finished = new Promise((resolve) => {
|
|
1019
|
+
settle2 = resolve;
|
|
1020
|
+
});
|
|
1021
|
+
const stop = () => {
|
|
1022
|
+
ctx.win.clearTimeout(start);
|
|
1023
|
+
if (handle !== void 0) ctx.win.clearInterval(handle);
|
|
1024
|
+
};
|
|
1025
|
+
const tick = () => {
|
|
1026
|
+
elapsed += COUNT_STEP_MS;
|
|
1027
|
+
const ratio = durationMs <= 0 ? 1 : Math.min(elapsed / durationMs, 1);
|
|
1028
|
+
const done = ratio >= 1;
|
|
1029
|
+
onTick(tweenValue(easing(ratio), from, to), done);
|
|
1030
|
+
if (done) {
|
|
1031
|
+
stop();
|
|
1032
|
+
settle2();
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
const start = ctx.win.setTimeout(() => {
|
|
1036
|
+
handle = ctx.win.setInterval(tick, COUNT_STEP_MS);
|
|
1037
|
+
}, delayMs);
|
|
1038
|
+
return {
|
|
1039
|
+
cleanup: stop,
|
|
1040
|
+
finished,
|
|
1041
|
+
finish: () => {
|
|
1042
|
+
stop();
|
|
1043
|
+
onTick(to, true);
|
|
1044
|
+
settle2();
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function prepareCount(el, params, ctx) {
|
|
1049
|
+
const doc = el.ownerDocument;
|
|
1050
|
+
const from = params.num("from", 0);
|
|
1051
|
+
const to = params.num("to", 100);
|
|
1052
|
+
const decimals = Math.max(0, Math.round(params.num("decimals", 0)));
|
|
1053
|
+
const format = params.text("format", "number");
|
|
1054
|
+
const currency = params.text("currency", "USD");
|
|
1055
|
+
const options = { format, decimals, currency };
|
|
1056
|
+
const layers = installCountLayers(el, doc);
|
|
1057
|
+
layers.decorative.textContent = formatCount(from, options);
|
|
1058
|
+
layers.srOnly.textContent = formatCount(from, options);
|
|
1059
|
+
const tween = tweenNumber(ctx, {
|
|
1060
|
+
from,
|
|
1061
|
+
to,
|
|
1062
|
+
...tweenTimingFor(params, ctx),
|
|
1063
|
+
onTick: (value, done) => {
|
|
1064
|
+
layers.decorative.textContent = formatCount(value, options);
|
|
1065
|
+
if (done) layers.srOnly.textContent = formatCount(to, options);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
return {
|
|
1069
|
+
cleanup: () => {
|
|
1070
|
+
tween.cleanup();
|
|
1071
|
+
layers.restore();
|
|
1072
|
+
},
|
|
1073
|
+
finished: tween.finished,
|
|
1074
|
+
finish: tween.finish
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function buildOdometerColumns(container, doc, grouped) {
|
|
1078
|
+
const strips = [];
|
|
1079
|
+
for (const token of odometerTokens(grouped)) {
|
|
1080
|
+
if (!token.digit) {
|
|
1081
|
+
container.append(doc.createTextNode(token.char));
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const column = doc.createElement("span");
|
|
1085
|
+
column.className = "kui-odometer-col";
|
|
1086
|
+
const strip = doc.createElement("span");
|
|
1087
|
+
strip.className = "kui-odometer-strip";
|
|
1088
|
+
for (let digit = 0; digit <= 9; digit++) {
|
|
1089
|
+
const row = doc.createElement("span");
|
|
1090
|
+
row.textContent = String(digit);
|
|
1091
|
+
strip.append(row);
|
|
1092
|
+
}
|
|
1093
|
+
strip.style.setProperty("--kui-o", token.char);
|
|
1094
|
+
column.append(strip);
|
|
1095
|
+
container.append(column);
|
|
1096
|
+
strips.push(strip);
|
|
1097
|
+
}
|
|
1098
|
+
return strips;
|
|
1099
|
+
}
|
|
1100
|
+
function updateOdometerColumns(strips, grouped) {
|
|
1101
|
+
let index = 0;
|
|
1102
|
+
for (const token of odometerTokens(grouped)) {
|
|
1103
|
+
if (!token.digit) continue;
|
|
1104
|
+
strips[index]?.style.setProperty("--kui-o", token.char);
|
|
1105
|
+
index++;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function prepareOdometer(el, params, ctx) {
|
|
1109
|
+
const doc = el.ownerDocument;
|
|
1110
|
+
const from = Math.max(0, params.num("from", 0));
|
|
1111
|
+
const to = Math.max(0, params.num("to", 100));
|
|
1112
|
+
const width = Math.max(String(Math.round(from)).length, String(Math.round(to)).length);
|
|
1113
|
+
const toGrouped = groupDigits(paddedDigits(to, width));
|
|
1114
|
+
const fromGrouped = groupDigits(paddedDigits(from, width));
|
|
1115
|
+
const layers = installCountLayers(el, doc);
|
|
1116
|
+
layers.srOnly.textContent = fromGrouped;
|
|
1117
|
+
const strips = buildOdometerColumns(layers.decorative, doc, fromGrouped);
|
|
1118
|
+
const tween = tweenNumber(ctx, {
|
|
1119
|
+
from,
|
|
1120
|
+
to,
|
|
1121
|
+
...tweenTimingFor(params, ctx),
|
|
1122
|
+
onTick: (value, done) => {
|
|
1123
|
+
updateOdometerColumns(strips, groupDigits(paddedDigits(value, width)));
|
|
1124
|
+
if (done) layers.srOnly.textContent = toGrouped;
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
return {
|
|
1128
|
+
cleanup: () => {
|
|
1129
|
+
tween.cleanup();
|
|
1130
|
+
layers.restore();
|
|
1131
|
+
},
|
|
1132
|
+
finished: tween.finished,
|
|
1133
|
+
finish: tween.finish
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
var COUNT_PRIMITIVES = [
|
|
1137
|
+
countPrimitive("count", countParams, deferPrepare(prepareCount)),
|
|
1138
|
+
countPrimitive("count-odometer", odometerParams, deferPrepare(prepareOdometer))
|
|
1139
|
+
];
|
|
1140
|
+
var COUNT_PRESETS = [
|
|
1141
|
+
{ name: "count-up", primitive: "count", params: { from: "0", to: "100" } },
|
|
1142
|
+
{ name: "count-down", primitive: "count", params: { from: "100", to: "0" } },
|
|
1143
|
+
{
|
|
1144
|
+
name: "count-currency",
|
|
1145
|
+
primitive: "count",
|
|
1146
|
+
params: { from: "0", to: "4820", format: "currency", currency: "USD", decimals: "0" }
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
name: "count-percent",
|
|
1150
|
+
primitive: "count",
|
|
1151
|
+
params: { from: "0", to: "0.82", format: "percent", decimals: "0" }
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
name: "count-compact",
|
|
1155
|
+
primitive: "count",
|
|
1156
|
+
params: { from: "0", to: "128400", format: "compact" }
|
|
1157
|
+
},
|
|
1158
|
+
{ name: "odometer-roll", primitive: "count-odometer", params: { from: "0", to: "4820" } }
|
|
1159
|
+
];
|
|
1160
|
+
var METER_PRIMITIVES = [
|
|
1161
|
+
cssPrimitive("stroke-sweep", [CHANNEL.stroke]),
|
|
1162
|
+
cssPrimitive("meter-bar", [CHANNEL.scale]),
|
|
1163
|
+
cssPrimitive("meter-segments", [CHANNEL.opacity]),
|
|
1164
|
+
cssPrimitive("meter-stars", [CHANNEL.clip])
|
|
1165
|
+
];
|
|
1166
|
+
var METER_PRESETS = [
|
|
1167
|
+
{ name: "progress-ring", primitive: "stroke-sweep", keyframes: "kui-progress-ring" },
|
|
1168
|
+
{ name: "gauge-sweep", primitive: "stroke-sweep", keyframes: "kui-gauge-sweep" },
|
|
1169
|
+
{ name: "donut-sweep", primitive: "stroke-sweep", keyframes: "kui-donut-sweep" },
|
|
1170
|
+
{ name: "sparkline-draw", primitive: "stroke-sweep", keyframes: "kui-sparkline-draw" },
|
|
1171
|
+
{ name: "progress-bar", primitive: "meter-bar", keyframes: "kui-progress-bar" },
|
|
1172
|
+
{ name: "progress-segments", primitive: "meter-segments", keyframes: "kui-progress-segments" },
|
|
1173
|
+
{ name: "star-rating-fill", primitive: "meter-stars", keyframes: "kui-star-rating-fill" }
|
|
1174
|
+
];
|
|
1175
|
+
var NUMBERS_PRIMITIVES = [...COUNT_PRIMITIVES, ...METER_PRIMITIVES];
|
|
1176
|
+
var NUMBERS_PRESETS = [...COUNT_PRESETS, ...METER_PRESETS];
|
|
1177
|
+
function registerNumbers(registry) {
|
|
1178
|
+
return registry.registerPrimitives(NUMBERS_PRIMITIVES).registerPresets(NUMBERS_PRESETS);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/effects/catalog/text-shared.ts
|
|
1182
|
+
var SR_ONLY_CLASS2 = "kui-sr-only";
|
|
1183
|
+
var DECORATIVE_CLASS2 = "kui-split-decorative";
|
|
1184
|
+
function installSplitLayers(el, doc) {
|
|
1185
|
+
const originalText = el.textContent.trim();
|
|
1186
|
+
const decorative = doc.createElement("span");
|
|
1187
|
+
decorative.setAttribute("aria-hidden", "true");
|
|
1188
|
+
decorative.className = DECORATIVE_CLASS2;
|
|
1189
|
+
const srOnly = doc.createElement("span");
|
|
1190
|
+
srOnly.className = SR_ONLY_CLASS2;
|
|
1191
|
+
srOnly.textContent = originalText;
|
|
1192
|
+
el.textContent = "";
|
|
1193
|
+
el.append(decorative, srOnly);
|
|
1194
|
+
return {
|
|
1195
|
+
decorative,
|
|
1196
|
+
originalText,
|
|
1197
|
+
restore: () => {
|
|
1198
|
+
el.textContent = originalText;
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function segmentGraphemes(text) {
|
|
1203
|
+
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
1204
|
+
return Array.from(segmenter.segment(text), (entry) => entry.segment);
|
|
1205
|
+
}
|
|
1206
|
+
function segmentWords(text) {
|
|
1207
|
+
const segmenter = new Intl.Segmenter(void 0, { granularity: "word" });
|
|
1208
|
+
return Array.from(segmenter.segment(text), (entry) => ({
|
|
1209
|
+
text: entry.segment,
|
|
1210
|
+
isWord: entry.isWordLike === true
|
|
1211
|
+
}));
|
|
1212
|
+
}
|
|
1213
|
+
function markItem(el, index, extraClass) {
|
|
1214
|
+
el.className = extraClass ? `kui-split-item ${extraClass}` : "kui-split-item";
|
|
1215
|
+
el.style.setProperty("--kui-i", String(index));
|
|
1216
|
+
}
|
|
1217
|
+
function applyStaggerVars(el, params) {
|
|
1218
|
+
const { durationMs, delayMs, easing } = params.timing;
|
|
1219
|
+
const duration = durationMs === void 0 ? params.text("duration", "500ms") : `${durationMs}ms`;
|
|
1220
|
+
const delay = delayMs === void 0 ? params.text("delay", "0ms") : `${delayMs}ms`;
|
|
1221
|
+
el.style.setProperty("--kui-duration", duration);
|
|
1222
|
+
el.style.setProperty("--kui-delay", delay);
|
|
1223
|
+
el.style.setProperty("--kui-ease", easing ?? params.text("ease", "ease-out"));
|
|
1224
|
+
el.style.setProperty("--kui-stagger", params.text("stagger", "30ms"));
|
|
1225
|
+
}
|
|
1226
|
+
function stepMsFor(params, ticks, fallback) {
|
|
1227
|
+
const total = params.timing.durationMs;
|
|
1228
|
+
if (total === void 0 || ticks <= 0) return params.ms("step", fallback);
|
|
1229
|
+
return Math.max(1, total / ticks);
|
|
1230
|
+
}
|
|
1231
|
+
function splitRevealFinishMs(params, itemCount) {
|
|
1232
|
+
if (itemCount === 0) return 0;
|
|
1233
|
+
const durationMs = params.timing.durationMs ?? params.ms("duration", 500);
|
|
1234
|
+
const delayMs = params.timing.delayMs ?? params.ms("delay", 0);
|
|
1235
|
+
const staggerMs = params.ms("stagger", 30);
|
|
1236
|
+
return delayMs + (itemCount - 1) * staggerMs + durationMs;
|
|
1237
|
+
}
|
|
1238
|
+
function createStepRunner(win, options) {
|
|
1239
|
+
let settle2;
|
|
1240
|
+
const finished = new Promise((resolve) => {
|
|
1241
|
+
settle2 = resolve;
|
|
1242
|
+
});
|
|
1243
|
+
let interval;
|
|
1244
|
+
function stop() {
|
|
1245
|
+
win.clearTimeout(start);
|
|
1246
|
+
if (interval !== void 0) win.clearInterval(interval);
|
|
1247
|
+
settle2?.();
|
|
1248
|
+
}
|
|
1249
|
+
const start = win.setTimeout(() => {
|
|
1250
|
+
interval = win.setInterval(() => {
|
|
1251
|
+
if (options.tick()) stop();
|
|
1252
|
+
}, options.stepMs);
|
|
1253
|
+
}, options.delayMs);
|
|
1254
|
+
return { finished, stop };
|
|
1255
|
+
}
|
|
1256
|
+
function appendCharSpans(container, doc, text) {
|
|
1257
|
+
const spans = [];
|
|
1258
|
+
let index = 0;
|
|
1259
|
+
for (const grapheme of segmentGraphemes(text)) {
|
|
1260
|
+
if (grapheme.trim() === "") {
|
|
1261
|
+
container.append(doc.createTextNode(grapheme));
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
const span = doc.createElement("span");
|
|
1265
|
+
markItem(span, index);
|
|
1266
|
+
span.textContent = grapheme;
|
|
1267
|
+
container.append(span);
|
|
1268
|
+
spans.push(span);
|
|
1269
|
+
index++;
|
|
1270
|
+
}
|
|
1271
|
+
return spans;
|
|
1272
|
+
}
|
|
1273
|
+
function appendWordSpans(container, doc, text) {
|
|
1274
|
+
const spans = [];
|
|
1275
|
+
let index = 0;
|
|
1276
|
+
for (const token of segmentWords(text)) {
|
|
1277
|
+
if (!token.isWord) {
|
|
1278
|
+
container.append(doc.createTextNode(token.text));
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
const span = doc.createElement("span");
|
|
1282
|
+
markItem(span, index);
|
|
1283
|
+
span.textContent = token.text;
|
|
1284
|
+
container.append(span);
|
|
1285
|
+
spans.push(span);
|
|
1286
|
+
index++;
|
|
1287
|
+
}
|
|
1288
|
+
return spans;
|
|
1289
|
+
}
|
|
1290
|
+
function bucketByLine(container) {
|
|
1291
|
+
const buckets = [];
|
|
1292
|
+
let currentTop = null;
|
|
1293
|
+
for (const node of Array.from(container.childNodes)) {
|
|
1294
|
+
if (node instanceof HTMLElement) {
|
|
1295
|
+
const top = node.offsetTop;
|
|
1296
|
+
if (currentTop === null || Math.abs(top - currentTop) > 1) {
|
|
1297
|
+
buckets.push([]);
|
|
1298
|
+
currentTop = top;
|
|
1299
|
+
}
|
|
1300
|
+
} else if (buckets.length === 0) {
|
|
1301
|
+
buckets.push([]);
|
|
1302
|
+
}
|
|
1303
|
+
buckets.at(-1).push(node);
|
|
1304
|
+
}
|
|
1305
|
+
return buckets;
|
|
1306
|
+
}
|
|
1307
|
+
function appendLineSpans(container, doc, text) {
|
|
1308
|
+
appendWordSpans(container, doc, text);
|
|
1309
|
+
const buckets = bucketByLine(container);
|
|
1310
|
+
container.replaceChildren();
|
|
1311
|
+
return buckets.map((nodes, index) => {
|
|
1312
|
+
const line = doc.createElement("span");
|
|
1313
|
+
markItem(line, index, "kui-split-line");
|
|
1314
|
+
for (const node of nodes) line.append(node);
|
|
1315
|
+
container.append(line);
|
|
1316
|
+
return line;
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
function appendSpansFor(unit, container, doc, text) {
|
|
1320
|
+
if (unit === "words") return appendWordSpans(container, doc, text);
|
|
1321
|
+
if (unit === "lines") return appendLineSpans(container, doc, text);
|
|
1322
|
+
return appendCharSpans(container, doc, text);
|
|
1323
|
+
}
|
|
1324
|
+
function nextTypeState(state, total, loop2) {
|
|
1325
|
+
if (!state.deleting) {
|
|
1326
|
+
const index2 = state.index + 1;
|
|
1327
|
+
if (index2 < total) return { index: index2, deleting: false, done: false };
|
|
1328
|
+
return loop2 ? { index: total, deleting: true, done: false } : { index: total, deleting: false, done: true };
|
|
1329
|
+
}
|
|
1330
|
+
const index = state.index - 1;
|
|
1331
|
+
if (index > 0) return { index, deleting: true, done: false };
|
|
1332
|
+
return { index: 0, deleting: false, done: false };
|
|
1333
|
+
}
|
|
1334
|
+
var SCRAMBLE_CHARSETS = {
|
|
1335
|
+
upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
1336
|
+
binary: "01",
|
|
1337
|
+
symbols: "!<>-_\\/[]{}=+*^?#~"
|
|
1338
|
+
};
|
|
1339
|
+
function scrambledFrame(graphemes, resolved, charset, random) {
|
|
1340
|
+
return graphemes.map((grapheme, index) => {
|
|
1341
|
+
if (index < resolved || grapheme.trim() === "") return grapheme;
|
|
1342
|
+
return charset[Math.floor(random() * charset.length)] ?? charset[0] ?? grapheme;
|
|
1343
|
+
}).join("");
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// src/effects/catalog/text.ts
|
|
1347
|
+
var fontWeightParams = {
|
|
1348
|
+
from: { type: "number", default: "100", cssProperty: "--kui-from-weight", minimum: 1, maximum: 1e3 },
|
|
1349
|
+
to: { type: "number", default: "800", cssProperty: "--kui-to-weight", minimum: 1, maximum: 1e3 }
|
|
1350
|
+
};
|
|
1351
|
+
var fontWidthParams = {
|
|
1352
|
+
from: { type: "percentage", default: "75%", cssProperty: "--kui-from-width" },
|
|
1353
|
+
to: { type: "percentage", default: "125%", cssProperty: "--kui-to-width" }
|
|
1354
|
+
};
|
|
1355
|
+
var fontSlantParams = {
|
|
1356
|
+
from: { type: "angle", default: "0deg", cssProperty: "--kui-from-slant" },
|
|
1357
|
+
to: { type: "angle", default: "-10deg", cssProperty: "--kui-to-slant" }
|
|
1358
|
+
};
|
|
1359
|
+
var TEXT_CSS_PRIMITIVES = [
|
|
1360
|
+
cssPrimitive("text-shimmer", [CHANNEL.background], { reducedMotion: "disable" }),
|
|
1361
|
+
cssPrimitive("text-sweep", [CHANNEL.background]),
|
|
1362
|
+
cssPrimitive("text-outline-fill", [CHANNEL.stroke, CHANNEL.color]),
|
|
1363
|
+
cssPrimitive("var-weight", ["font"], { parameters: fontWeightParams }),
|
|
1364
|
+
cssPrimitive("var-width", ["font"], { parameters: fontWidthParams }),
|
|
1365
|
+
cssPrimitive("var-slant", ["font"], { parameters: fontSlantParams }),
|
|
1366
|
+
cssPrimitive("text-marquee", [CHANNEL.translate], {
|
|
1367
|
+
timelines: ["time", "scroll"],
|
|
1368
|
+
reducedMotion: "disable"
|
|
1369
|
+
}),
|
|
1370
|
+
cssPrimitive("redaction-reveal", [CHANNEL.clip]),
|
|
1371
|
+
cssPrimitive("text-3d-extrude", [CHANNEL.rotate, CHANNEL.translate])
|
|
1372
|
+
];
|
|
1373
|
+
var TEXT_CSS_PRESETS = [
|
|
1374
|
+
{ name: "gradient-shimmer", primitive: "text-shimmer", keyframes: "kui-gradient-shimmer" },
|
|
1375
|
+
{ name: "gradient-sweep", primitive: "text-sweep", keyframes: "kui-gradient-sweep" },
|
|
1376
|
+
{ name: "highlight-sweep", primitive: "text-sweep", keyframes: "kui-highlight-sweep" },
|
|
1377
|
+
{ name: "underline-draw", primitive: "text-sweep", keyframes: "kui-underline-draw" },
|
|
1378
|
+
{ name: "text-outline-fill", primitive: "text-outline-fill", keyframes: "kui-text-outline-fill" },
|
|
1379
|
+
{ name: "var-weight", primitive: "var-weight", keyframes: "kui-var-weight" },
|
|
1380
|
+
{ name: "var-width", primitive: "var-width", keyframes: "kui-var-width" },
|
|
1381
|
+
{ name: "var-slant", primitive: "var-slant", keyframes: "kui-var-slant" },
|
|
1382
|
+
{ name: "marquee", primitive: "text-marquee", keyframes: "kui-marquee" },
|
|
1383
|
+
{ name: "marquee-scroll-linked", primitive: "text-marquee", keyframes: "kui-marquee" },
|
|
1384
|
+
{ name: "redaction-reveal", primitive: "redaction-reveal", keyframes: "kui-redaction-reveal" },
|
|
1385
|
+
{ name: "text-3d-extrude", primitive: "text-3d-extrude", keyframes: "kui-text-3d-extrude" }
|
|
1386
|
+
];
|
|
1387
|
+
function jsTextPrimitive(id, channels, options) {
|
|
1388
|
+
return {
|
|
1389
|
+
id,
|
|
1390
|
+
renderer: "javascript",
|
|
1391
|
+
channels,
|
|
1392
|
+
parameters: options.parameters,
|
|
1393
|
+
supportedTimelines: ["time"],
|
|
1394
|
+
supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
|
|
1395
|
+
defaultActivation: "enter",
|
|
1396
|
+
perfClass: options.perfClass ?? "dom-transform",
|
|
1397
|
+
// Every JS-rendered primitive in this catalog is `disable`: none of them declare a CSS
|
|
1398
|
+
// `animation-duration` the reduced-motion policy layer could shorten, so `shorten` would be a
|
|
1399
|
+
// silent no-op. `disable` is enforced upstream — the animator never calls `activate()` at all
|
|
1400
|
+
// — which is the only place that actually works for a timer- or DOM-surgery-driven effect.
|
|
1401
|
+
reducedMotion: "disable",
|
|
1402
|
+
prepare: options.prepare
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
var splitTiming = {
|
|
1406
|
+
duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
|
|
1407
|
+
delay: { type: "time", default: "0ms", cssProperty: "--kui-delay" },
|
|
1408
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
1409
|
+
stagger: { type: "time", default: "30ms", cssProperty: "--kui-stagger" },
|
|
1410
|
+
unit: {
|
|
1411
|
+
type: "keyword",
|
|
1412
|
+
default: "chars",
|
|
1413
|
+
cssProperty: "--kui-unit",
|
|
1414
|
+
values: ["chars", "words", "lines"]
|
|
1415
|
+
},
|
|
1416
|
+
direction: {
|
|
1417
|
+
type: "keyword",
|
|
1418
|
+
default: "fade",
|
|
1419
|
+
cssProperty: "--kui-direction",
|
|
1420
|
+
values: ["fade", "up", "down", "mask"]
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
var motionParams = {
|
|
1424
|
+
stagger: { type: "time", default: "40ms", cssProperty: "--kui-stagger" },
|
|
1425
|
+
motion: {
|
|
1426
|
+
type: "keyword",
|
|
1427
|
+
default: "wave",
|
|
1428
|
+
cssProperty: "--kui-motion",
|
|
1429
|
+
values: ["wave", "jitter"]
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
var typewriterParams = {
|
|
1433
|
+
step: { type: "time", default: "55ms", cssProperty: "--kui-step" },
|
|
1434
|
+
loop: { type: "keyword", default: "false", cssProperty: "--kui-loop", values: ["true", "false"] }
|
|
1435
|
+
};
|
|
1436
|
+
var scrambleParams = {
|
|
1437
|
+
step: { type: "time", default: "40ms", cssProperty: "--kui-step" },
|
|
1438
|
+
revealEvery: {
|
|
1439
|
+
type: "number",
|
|
1440
|
+
default: "2",
|
|
1441
|
+
cssProperty: "--kui-reveal-every",
|
|
1442
|
+
minimum: 1,
|
|
1443
|
+
integer: true
|
|
1444
|
+
},
|
|
1445
|
+
charset: {
|
|
1446
|
+
type: "keyword",
|
|
1447
|
+
default: "upper",
|
|
1448
|
+
cssProperty: "--kui-charset",
|
|
1449
|
+
values: ["upper", "binary", "symbols"]
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
var wordCyclerParams = {
|
|
1453
|
+
words: { type: "text", default: "", cssProperty: "--kui-words" },
|
|
1454
|
+
interval: { type: "time", default: "2200ms", cssProperty: "--kui-interval" }
|
|
1455
|
+
};
|
|
1456
|
+
function prepareSplitText(el, params, ctx) {
|
|
1457
|
+
const doc = el.ownerDocument;
|
|
1458
|
+
const unit = params.text("unit", "chars");
|
|
1459
|
+
const direction = params.text("direction", "fade");
|
|
1460
|
+
const layers = installSplitLayers(el, doc);
|
|
1461
|
+
layers.decorative.setAttribute("data-kui-split-fx", direction);
|
|
1462
|
+
applyStaggerVars(layers.decorative, params);
|
|
1463
|
+
const items = appendSpansFor(unit, layers.decorative, doc, layers.originalText);
|
|
1464
|
+
let settle2;
|
|
1465
|
+
const finished = new Promise((resolve) => {
|
|
1466
|
+
settle2 = resolve;
|
|
1467
|
+
});
|
|
1468
|
+
const timer = ctx.win.setTimeout(settle2, splitRevealFinishMs(params, items.length));
|
|
1469
|
+
return {
|
|
1470
|
+
cleanup: () => {
|
|
1471
|
+
ctx.win.clearTimeout(timer);
|
|
1472
|
+
layers.restore();
|
|
1473
|
+
},
|
|
1474
|
+
finished,
|
|
1475
|
+
finish: () => {
|
|
1476
|
+
ctx.win.clearTimeout(timer);
|
|
1477
|
+
settle2();
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
function prepareSplitMotion(el, params) {
|
|
1482
|
+
const doc = el.ownerDocument;
|
|
1483
|
+
const motion = params.text("motion", "wave");
|
|
1484
|
+
const layers = installSplitLayers(el, doc);
|
|
1485
|
+
layers.decorative.setAttribute("data-kui-split-fx", motion);
|
|
1486
|
+
applyStaggerVars(layers.decorative, params);
|
|
1487
|
+
appendCharSpans(layers.decorative, doc, layers.originalText);
|
|
1488
|
+
return layers.restore;
|
|
1489
|
+
}
|
|
1490
|
+
function prepareTypewriter(el, params, ctx) {
|
|
1491
|
+
const loop2 = params.is("loop");
|
|
1492
|
+
const layers = installSplitLayers(el, el.ownerDocument);
|
|
1493
|
+
layers.decorative.classList.add("kui-typewriter");
|
|
1494
|
+
const graphemes = segmentGraphemes(layers.originalText);
|
|
1495
|
+
let state = { index: 0, deleting: false };
|
|
1496
|
+
const render = (count) => {
|
|
1497
|
+
layers.decorative.textContent = graphemes.slice(0, count).join("");
|
|
1498
|
+
};
|
|
1499
|
+
const run = createStepRunner(ctx.win, {
|
|
1500
|
+
delayMs: params.timing.delayMs ?? 0,
|
|
1501
|
+
stepMs: stepMsFor(params, graphemes.length, 55),
|
|
1502
|
+
tick: () => {
|
|
1503
|
+
const step = nextTypeState(state, graphemes.length, loop2);
|
|
1504
|
+
state = step;
|
|
1505
|
+
render(step.index);
|
|
1506
|
+
return step.done;
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
return {
|
|
1510
|
+
cleanup: () => {
|
|
1511
|
+
run.stop();
|
|
1512
|
+
layers.restore();
|
|
1513
|
+
},
|
|
1514
|
+
finished: run.finished,
|
|
1515
|
+
finish: () => {
|
|
1516
|
+
run.stop();
|
|
1517
|
+
render(graphemes.length);
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
function prepareScramble(el, params, ctx) {
|
|
1522
|
+
const charset = SCRAMBLE_CHARSETS[params.text("charset", "upper")];
|
|
1523
|
+
const revealEvery = Math.max(1, Math.round(params.num("revealEvery", 2)));
|
|
1524
|
+
const layers = installSplitLayers(el, el.ownerDocument);
|
|
1525
|
+
layers.decorative.classList.add("kui-scramble");
|
|
1526
|
+
const graphemes = segmentGraphemes(layers.originalText);
|
|
1527
|
+
let resolved = 0;
|
|
1528
|
+
let ticks = 0;
|
|
1529
|
+
const render = () => {
|
|
1530
|
+
layers.decorative.textContent = scrambledFrame(graphemes, resolved, charset, Math.random);
|
|
1531
|
+
};
|
|
1532
|
+
render();
|
|
1533
|
+
const run = createStepRunner(ctx.win, {
|
|
1534
|
+
delayMs: params.timing.delayMs ?? 0,
|
|
1535
|
+
stepMs: stepMsFor(params, graphemes.length * revealEvery, 40),
|
|
1536
|
+
tick: () => {
|
|
1537
|
+
ticks++;
|
|
1538
|
+
if (ticks % revealEvery === 0) resolved++;
|
|
1539
|
+
render();
|
|
1540
|
+
return resolved >= graphemes.length;
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
return {
|
|
1544
|
+
cleanup: () => {
|
|
1545
|
+
run.stop();
|
|
1546
|
+
layers.restore();
|
|
1547
|
+
},
|
|
1548
|
+
finished: run.finished,
|
|
1549
|
+
finish: () => {
|
|
1550
|
+
run.stop();
|
|
1551
|
+
resolved = graphemes.length;
|
|
1552
|
+
render();
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
function prepareWordCycler(el, params, ctx) {
|
|
1557
|
+
const words = params.text("words", "").split("|").map((word) => word.trim()).filter(Boolean);
|
|
1558
|
+
if (words.length === 0) return () => {
|
|
1559
|
+
};
|
|
1560
|
+
const original = el.textContent;
|
|
1561
|
+
const swapMs = 150;
|
|
1562
|
+
let index = 0;
|
|
1563
|
+
el.textContent = words[0];
|
|
1564
|
+
const run = createStepRunner(ctx.win, {
|
|
1565
|
+
delayMs: params.timing.delayMs ?? 0,
|
|
1566
|
+
stepMs: params.ms("interval", 2200),
|
|
1567
|
+
tick: () => {
|
|
1568
|
+
el.classList.add("kui-word-cycler-swap");
|
|
1569
|
+
ctx.win.setTimeout(() => {
|
|
1570
|
+
index = (index + 1) % words.length;
|
|
1571
|
+
el.textContent = words[index];
|
|
1572
|
+
el.classList.remove("kui-word-cycler-swap");
|
|
1573
|
+
}, swapMs);
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
return {
|
|
1578
|
+
cleanup: () => {
|
|
1579
|
+
run.stop();
|
|
1580
|
+
el.classList.remove("kui-word-cycler-swap");
|
|
1581
|
+
el.textContent = original;
|
|
1582
|
+
},
|
|
1583
|
+
finished: run.finished,
|
|
1584
|
+
finish: () => run.stop()
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
var TEXT_JS_PRIMITIVES = [
|
|
1588
|
+
jsTextPrimitive("split-text", [CHANNEL.opacity, CHANNEL.translate, CHANNEL.clip], {
|
|
1589
|
+
parameters: splitTiming,
|
|
1590
|
+
prepare: deferPrepare(prepareSplitText)
|
|
1591
|
+
}),
|
|
1592
|
+
jsTextPrimitive("split-text-motion", [CHANNEL.translate, CHANNEL.rotate], {
|
|
1593
|
+
parameters: motionParams,
|
|
1594
|
+
prepare: deferPrepare(prepareSplitMotion),
|
|
1595
|
+
perfClass: "continuous"
|
|
1596
|
+
}),
|
|
1597
|
+
jsTextPrimitive("typewriter", [CHANNEL.clip], {
|
|
1598
|
+
parameters: typewriterParams,
|
|
1599
|
+
prepare: deferPrepare(prepareTypewriter),
|
|
1600
|
+
perfClass: "continuous"
|
|
1601
|
+
}),
|
|
1602
|
+
jsTextPrimitive("scramble-text", ["content"], {
|
|
1603
|
+
parameters: scrambleParams,
|
|
1604
|
+
prepare: deferPrepare(prepareScramble),
|
|
1605
|
+
perfClass: "continuous"
|
|
1606
|
+
}),
|
|
1607
|
+
jsTextPrimitive("word-cycler", ["content"], {
|
|
1608
|
+
parameters: wordCyclerParams,
|
|
1609
|
+
prepare: deferPrepare(prepareWordCycler),
|
|
1610
|
+
perfClass: "continuous"
|
|
1611
|
+
})
|
|
1612
|
+
];
|
|
1613
|
+
var TEXT_JS_PRESETS = [
|
|
1614
|
+
{ name: "split-chars", primitive: "split-text", params: { unit: "chars", direction: "fade" } },
|
|
1615
|
+
{ name: "split-words", primitive: "split-text", params: { unit: "words", direction: "fade" } },
|
|
1616
|
+
{ name: "split-lines", primitive: "split-text", params: { unit: "lines", direction: "fade" } },
|
|
1617
|
+
{ name: "text-reveal-up", primitive: "split-text", params: { unit: "words", direction: "up" } },
|
|
1618
|
+
{ name: "text-reveal-down", primitive: "split-text", params: { unit: "words", direction: "down" } },
|
|
1619
|
+
{ name: "text-reveal-mask", primitive: "split-text", params: { unit: "lines", direction: "mask" } },
|
|
1620
|
+
{ name: "text-wave", primitive: "split-text-motion", params: { motion: "wave" } },
|
|
1621
|
+
{ name: "text-jitter", primitive: "split-text-motion", params: { motion: "jitter" } },
|
|
1622
|
+
{ name: "typewriter", primitive: "typewriter", params: { loop: "false" } },
|
|
1623
|
+
{ name: "typewriter-loop", primitive: "typewriter", params: { loop: "true" } },
|
|
1624
|
+
{ name: "scramble", primitive: "scramble-text", params: { charset: "upper" } },
|
|
1625
|
+
{ name: "decode", primitive: "scramble-text", params: { charset: "binary" } },
|
|
1626
|
+
{ name: "glitch", primitive: "scramble-text", params: { charset: "symbols" } },
|
|
1627
|
+
{ name: "word-cycler", primitive: "word-cycler" }
|
|
1628
|
+
];
|
|
1629
|
+
var TEXT_PRIMITIVES = [...TEXT_CSS_PRIMITIVES, ...TEXT_JS_PRIMITIVES];
|
|
1630
|
+
var TEXT_PRESETS = [...TEXT_CSS_PRESETS, ...TEXT_JS_PRESETS];
|
|
1631
|
+
function registerText(registry) {
|
|
1632
|
+
return registry.registerPrimitives(TEXT_PRIMITIVES).registerPresets(TEXT_PRESETS);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/effects/catalog/index.ts
|
|
1636
|
+
function registerCatalog(registry) {
|
|
1637
|
+
registerMedia(registry);
|
|
1638
|
+
registerText(registry);
|
|
1639
|
+
registerAmbient(registry);
|
|
1640
|
+
registerFeedback(registry);
|
|
1641
|
+
registerNumbers(registry);
|
|
1642
|
+
registerInteraction(registry);
|
|
1643
|
+
return registry;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/effects/catalog/core.ts
|
|
1647
|
+
var distance = {
|
|
1648
|
+
distance: { type: "length", default: "24px", cssProperty: "--kui-distance" },
|
|
1649
|
+
opacity: { type: "number", default: "0", cssProperty: "--kui-from-opacity" }
|
|
1650
|
+
};
|
|
1651
|
+
var PRIMITIVES = [
|
|
1652
|
+
// --- entrance / exit -------------------------------------------------------------------
|
|
1653
|
+
cssPrimitive("reveal", [CHANNEL.opacity, CHANNEL.translate], { parameters: distance }),
|
|
1654
|
+
cssPrimitive("scale", [CHANNEL.scale], {
|
|
1655
|
+
parameters: { scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" } }
|
|
1656
|
+
}),
|
|
1657
|
+
// Separate from `scale` because it claims translate as well and so composes differently.
|
|
1658
|
+
cssPrimitive("scale-move", [CHANNEL.scale, CHANNEL.translate], {
|
|
1659
|
+
parameters: {
|
|
1660
|
+
...distance,
|
|
1661
|
+
scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" }
|
|
1662
|
+
}
|
|
1663
|
+
}),
|
|
1664
|
+
cssPrimitive("rotate", [CHANNEL.rotate], {
|
|
1665
|
+
parameters: { angle: { type: "angle", default: "-8deg", cssProperty: "--kui-from-angle" } }
|
|
1666
|
+
}),
|
|
1667
|
+
cssPrimitive("roll", [CHANNEL.rotate, CHANNEL.translate], {
|
|
1668
|
+
parameters: {
|
|
1669
|
+
...distance,
|
|
1670
|
+
angle: { type: "angle", default: "-120deg", cssProperty: "--kui-from-angle" }
|
|
1671
|
+
}
|
|
1672
|
+
}),
|
|
1673
|
+
cssPrimitive("flip-3d", [CHANNEL.rotate], {
|
|
1674
|
+
parameters: {
|
|
1675
|
+
angle: { type: "angle", default: "90deg", cssProperty: "--kui-from-angle" },
|
|
1676
|
+
perspective: { type: "length", default: "1200px", cssProperty: "--kui-perspective" }
|
|
1677
|
+
}
|
|
1678
|
+
}),
|
|
1679
|
+
cssPrimitive("blur", [CHANNEL.filter], {
|
|
1680
|
+
parameters: { blur: { type: "length", default: "12px", cssProperty: "--kui-blur" } }
|
|
1681
|
+
}),
|
|
1682
|
+
// Purpose-built combination: one keyframe, so opacity is written once instead of twice.
|
|
1683
|
+
cssPrimitive("reveal-blur", [CHANNEL.opacity, CHANNEL.translate, CHANNEL.filter], {
|
|
1684
|
+
parameters: { ...distance, blur: { type: "length", default: "12px", cssProperty: "--kui-blur" } }
|
|
1685
|
+
}),
|
|
1686
|
+
// --- scroll-linked ---------------------------------------------------------------------
|
|
1687
|
+
// These are progress-linked, not time-triggered: they reverse as the user scrolls back.
|
|
1688
|
+
// That is by design and is why `timeline:` is a different axis from `on:`.
|
|
1689
|
+
cssPrimitive("parallax", [CHANNEL.translate], {
|
|
1690
|
+
parameters: distance,
|
|
1691
|
+
timelines: ["view", "scroll"],
|
|
1692
|
+
activations: ["manual"],
|
|
1693
|
+
reducedMotion: "disable",
|
|
1694
|
+
perfClass: "compositor"
|
|
1695
|
+
}),
|
|
1696
|
+
cssPrimitive("parallax-scale", [CHANNEL.scale], {
|
|
1697
|
+
parameters: { scale: { type: "number", default: "1.2", cssProperty: "--kui-to-scale" } },
|
|
1698
|
+
timelines: ["view", "scroll"],
|
|
1699
|
+
activations: ["manual"],
|
|
1700
|
+
reducedMotion: "disable"
|
|
1701
|
+
}),
|
|
1702
|
+
cssPrimitive("parallax-rotate", [CHANNEL.rotate], {
|
|
1703
|
+
parameters: { angle: { type: "angle", default: "12deg", cssProperty: "--kui-to-angle" } },
|
|
1704
|
+
timelines: ["view", "scroll"],
|
|
1705
|
+
activations: ["manual"],
|
|
1706
|
+
reducedMotion: "disable"
|
|
1707
|
+
}),
|
|
1708
|
+
cssPrimitive("scroll-fade", [CHANNEL.opacity], {
|
|
1709
|
+
parameters: { opacity: { type: "number", default: "0", cssProperty: "--kui-from-opacity" } },
|
|
1710
|
+
timelines: ["view", "scroll"],
|
|
1711
|
+
activations: ["manual"],
|
|
1712
|
+
reducedMotion: "disable"
|
|
1713
|
+
}),
|
|
1714
|
+
cssPrimitive("progress", [CHANNEL.scale], {
|
|
1715
|
+
timelines: ["scroll", "view"],
|
|
1716
|
+
activations: ["manual"],
|
|
1717
|
+
reducedMotion: "disable"
|
|
1718
|
+
}),
|
|
1719
|
+
// Separate primitive because it writes `stroke-dashoffset`, not `scale` — declaring the
|
|
1720
|
+
// wrong channel would let it silently compose with an effect it actually collides with.
|
|
1721
|
+
cssPrimitive("progress-stroke", [CHANNEL.stroke], {
|
|
1722
|
+
parameters: { length: { type: "number", default: "100", cssProperty: "--kui-path-length" } },
|
|
1723
|
+
timelines: ["scroll", "view"],
|
|
1724
|
+
activations: ["manual"],
|
|
1725
|
+
reducedMotion: "disable",
|
|
1726
|
+
perfClass: "paint"
|
|
1727
|
+
})
|
|
1728
|
+
];
|
|
1729
|
+
var p = (name, primitive, keyframes, params) => ({ name, primitive, keyframes, ...params ? { params } : {} });
|
|
1730
|
+
var FADE = [
|
|
1731
|
+
p("fade-in", "reveal", "kui-in"),
|
|
1732
|
+
p("fade-out", "reveal", "kui-out"),
|
|
1733
|
+
p("fade-up", "reveal", "kui-in-up"),
|
|
1734
|
+
p("fade-down", "reveal", "kui-in-down"),
|
|
1735
|
+
p("fade-left", "reveal", "kui-in-left"),
|
|
1736
|
+
p("fade-right", "reveal", "kui-in-right"),
|
|
1737
|
+
p("fade-out-up", "reveal", "kui-out-up"),
|
|
1738
|
+
p("fade-out-down", "reveal", "kui-out-down"),
|
|
1739
|
+
p("fade-out-left", "reveal", "kui-out-left"),
|
|
1740
|
+
p("fade-out-right", "reveal", "kui-out-right")
|
|
1741
|
+
];
|
|
1742
|
+
var SLIDE_PARAMS = { distance: "100px", opacity: "1" };
|
|
1743
|
+
var SLIDE = [
|
|
1744
|
+
p("slide-up", "reveal", "kui-in-up", SLIDE_PARAMS),
|
|
1745
|
+
p("slide-down", "reveal", "kui-in-down", SLIDE_PARAMS),
|
|
1746
|
+
p("slide-left", "reveal", "kui-in-left", SLIDE_PARAMS),
|
|
1747
|
+
p("slide-right", "reveal", "kui-in-right", SLIDE_PARAMS),
|
|
1748
|
+
p("slide-out-up", "reveal", "kui-out-up", SLIDE_PARAMS),
|
|
1749
|
+
p("slide-out-down", "reveal", "kui-out-down", SLIDE_PARAMS),
|
|
1750
|
+
p("slide-out-left", "reveal", "kui-out-left", SLIDE_PARAMS),
|
|
1751
|
+
p("slide-out-right", "reveal", "kui-out-right", SLIDE_PARAMS)
|
|
1752
|
+
];
|
|
1753
|
+
var LOGICAL = [
|
|
1754
|
+
p("slide-inline-start", "reveal", "kui-in-inline-start", SLIDE_PARAMS),
|
|
1755
|
+
p("slide-inline-end", "reveal", "kui-in-inline-end", SLIDE_PARAMS),
|
|
1756
|
+
p("slide-block-start", "reveal", "kui-in-up", SLIDE_PARAMS),
|
|
1757
|
+
p("slide-block-end", "reveal", "kui-in-down", SLIDE_PARAMS)
|
|
1758
|
+
];
|
|
1759
|
+
var ZOOM = [
|
|
1760
|
+
p("zoom-in", "scale", "kui-zoom-in"),
|
|
1761
|
+
p("zoom-out", "scale", "kui-zoom-out"),
|
|
1762
|
+
p("pop-in", "scale", "kui-zoom-in", { scale: "0.6", ease: "back-out" }),
|
|
1763
|
+
p("pop-out", "scale", "kui-zoom-out", { scale: "0.6", ease: "back-in" }),
|
|
1764
|
+
p("zoom-in-up", "scale-move", "kui-zoom-in-up"),
|
|
1765
|
+
p("zoom-in-down", "scale-move", "kui-zoom-in-down")
|
|
1766
|
+
];
|
|
1767
|
+
var FLIP = [
|
|
1768
|
+
p("flip-in-x", "flip-3d", "kui-flip-in-x"),
|
|
1769
|
+
p("flip-in-y", "flip-3d", "kui-flip-in-y"),
|
|
1770
|
+
p("flip-out-x", "flip-3d", "kui-flip-out-x"),
|
|
1771
|
+
p("flip-out-y", "flip-3d", "kui-flip-out-y")
|
|
1772
|
+
];
|
|
1773
|
+
var ROTATE = [
|
|
1774
|
+
p("rotate-in", "rotate", "kui-rotate-in"),
|
|
1775
|
+
p("rotate-out", "rotate", "kui-rotate-out"),
|
|
1776
|
+
p("rotate-in-left", "rotate", "kui-rotate-in", { angle: "-45deg" }),
|
|
1777
|
+
p("rotate-in-right", "rotate", "kui-rotate-in", { angle: "45deg" }),
|
|
1778
|
+
p("roll-in", "roll", "kui-roll-in"),
|
|
1779
|
+
p("roll-out", "roll", "kui-roll-out"),
|
|
1780
|
+
p("swing-in", "rotate", "kui-swing-in", { angle: "-15deg", ease: "back-out" })
|
|
1781
|
+
];
|
|
1782
|
+
var BLUR = [
|
|
1783
|
+
p("blur-in", "blur", "kui-blur-in"),
|
|
1784
|
+
p("blur-out", "blur", "kui-blur-out"),
|
|
1785
|
+
p("fade-blur-up", "reveal-blur", "kui-fade-blur-up"),
|
|
1786
|
+
p("fade-blur-in", "reveal-blur", "kui-fade-blur-in")
|
|
1787
|
+
];
|
|
1788
|
+
var CHARACTER = [
|
|
1789
|
+
p("bounce-in", "scale", "kui-zoom-in", { scale: "0.3", ease: "back-out" }),
|
|
1790
|
+
p("bounce-in-up", "reveal", "kui-in-up", { distance: "60px", ease: "back-out" }),
|
|
1791
|
+
p("bounce-in-down", "reveal", "kui-in-down", { distance: "60px", ease: "back-out" }),
|
|
1792
|
+
p("back-in-up", "reveal", "kui-in-up", { distance: "120px", ease: "expo-out" }),
|
|
1793
|
+
p("back-in-down", "reveal", "kui-in-down", { distance: "120px", ease: "expo-out" })
|
|
1794
|
+
];
|
|
1795
|
+
var SCROLL = [
|
|
1796
|
+
p("parallax-y", "parallax", "kui-parallax-y"),
|
|
1797
|
+
p("parallax-x", "parallax", "kui-parallax-x"),
|
|
1798
|
+
p("parallax-scale", "parallax-scale", "kui-parallax-scale"),
|
|
1799
|
+
p("parallax-rotate", "parallax-rotate", "kui-parallax-rotate"),
|
|
1800
|
+
p("depth-layer", "parallax", "kui-parallax-y", { distance: "200px" }),
|
|
1801
|
+
p("scroll-fade", "scroll-fade", "kui-scroll-fade"),
|
|
1802
|
+
p("scroll-progress-bar", "progress", "kui-progress-x"),
|
|
1803
|
+
p("scroll-progress-ring", "progress-stroke", "kui-progress-ring"),
|
|
1804
|
+
// `reveal-repeat` was removed: it was byte-identical to `reveal-once`, and the activation
|
|
1805
|
+
// binder unobserves after first entry, so a repeating reveal is not implementable yet.
|
|
1806
|
+
p("reveal-once", "reveal", "kui-in-up")
|
|
1807
|
+
];
|
|
1808
|
+
var PRESETS = [
|
|
1809
|
+
...FADE,
|
|
1810
|
+
...SLIDE,
|
|
1811
|
+
...LOGICAL,
|
|
1812
|
+
...ZOOM,
|
|
1813
|
+
...FLIP,
|
|
1814
|
+
...ROTATE,
|
|
1815
|
+
...BLUR,
|
|
1816
|
+
...CHARACTER,
|
|
1817
|
+
...SCROLL
|
|
1818
|
+
];
|
|
1819
|
+
var COMBOS = [
|
|
1820
|
+
[["fade-up", "blur-in"], "fade-blur-up"],
|
|
1821
|
+
[["fade-in", "blur-in"], "fade-blur-in"]
|
|
1822
|
+
];
|
|
1823
|
+
function registerCore(registry) {
|
|
1824
|
+
registry.registerPrimitives(PRIMITIVES).registerPresets(PRESETS);
|
|
1825
|
+
for (const [names, preset] of COMBOS) registry.registerCombo(names, preset);
|
|
1826
|
+
return registry;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/effects/forms/primitives.ts
|
|
1830
|
+
var timing = {
|
|
1831
|
+
duration: { type: "time", default: "400ms", cssProperty: "--kui-duration" },
|
|
1832
|
+
delay: { type: "time", default: "0ms", cssProperty: "--kui-delay" },
|
|
1833
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
|
|
1834
|
+
};
|
|
1835
|
+
var NATIVE_STATE_PRIMITIVE = {
|
|
1836
|
+
id: "native-state",
|
|
1837
|
+
renderer: "javascript",
|
|
1838
|
+
channels: [CHANNEL.translate, CHANNEL.scale, CHANNEL.opacity, CHANNEL.stroke, CHANNEL.color],
|
|
1839
|
+
parameters: {},
|
|
1840
|
+
supportedTimelines: ["time"],
|
|
1841
|
+
supportedActivations: ["load"],
|
|
1842
|
+
defaultActivation: "load",
|
|
1843
|
+
perfClass: "compositor",
|
|
1844
|
+
// Native pseudo-classes drive these and the motion lands on a sibling, not on the control that
|
|
1845
|
+
// carries the attribute, so base.css's policy layer enforces this through its sibling
|
|
1846
|
+
// `transition-duration` rules rather than the `animation-*` ones.
|
|
1847
|
+
reducedMotion: "disable",
|
|
1848
|
+
prepare: () => inertInstance()
|
|
1849
|
+
};
|
|
1850
|
+
var FOCUS_RING_PRIMITIVE = cssPrimitive("focus-ring", ["shadow"], {
|
|
1851
|
+
activations: ["focus", "manual"],
|
|
1852
|
+
defaultActivation: "focus"
|
|
1853
|
+
});
|
|
1854
|
+
var VALIDATE_SHAKE_PRIMITIVE = cssPrimitive("validate-shake", [CHANNEL.translate], {
|
|
1855
|
+
activations: ["click", "manual"],
|
|
1856
|
+
defaultActivation: "click"
|
|
1857
|
+
});
|
|
1858
|
+
var VALIDATE_CHECK_PRIMITIVE = cssPrimitive("validate-check", [CHANNEL.stroke], {
|
|
1859
|
+
activations: ["click", "manual"],
|
|
1860
|
+
defaultActivation: "click"
|
|
1861
|
+
});
|
|
1862
|
+
function jsInputPrimitive(id, channels, parameters, prepare) {
|
|
1863
|
+
return {
|
|
1864
|
+
id,
|
|
1865
|
+
renderer: "javascript",
|
|
1866
|
+
channels,
|
|
1867
|
+
parameters: { ...timing, ...parameters },
|
|
1868
|
+
supportedTimelines: ["time"],
|
|
1869
|
+
supportedActivations: ["load"],
|
|
1870
|
+
defaultActivation: "load",
|
|
1871
|
+
perfClass: "compositor",
|
|
1872
|
+
reducedMotion: "disable",
|
|
1873
|
+
prepare
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
function computeStrength(value) {
|
|
1877
|
+
let score = 0;
|
|
1878
|
+
if (value.length >= 6) score++;
|
|
1879
|
+
if (value.length >= 10) score++;
|
|
1880
|
+
if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score++;
|
|
1881
|
+
if (/\d/.test(value)) score++;
|
|
1882
|
+
if (/[^A-Za-z0-9]/.test(value)) score++;
|
|
1883
|
+
return Math.min(4, score);
|
|
1884
|
+
}
|
|
1885
|
+
function prepareStrengthMeter(el) {
|
|
1886
|
+
const input = el;
|
|
1887
|
+
const update = () => {
|
|
1888
|
+
el.setAttribute("data-kui-strength-level", String(computeStrength(input.value)));
|
|
1889
|
+
};
|
|
1890
|
+
input.addEventListener("input", update);
|
|
1891
|
+
update();
|
|
1892
|
+
return () => {
|
|
1893
|
+
input.removeEventListener("input", update);
|
|
1894
|
+
el.removeAttribute("data-kui-strength-level");
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function prepareRangeFill(el, params, ctx) {
|
|
1898
|
+
const input = el;
|
|
1899
|
+
const update = () => {
|
|
1900
|
+
const min = Number(input.min || "0");
|
|
1901
|
+
const max = Number(input.max || "100");
|
|
1902
|
+
const pct = max > min ? (Number(input.value) - min) / (max - min) * 100 : 0;
|
|
1903
|
+
ctx.style.set("--kui-fill", `${pct.toFixed(2)}%`);
|
|
1904
|
+
};
|
|
1905
|
+
input.addEventListener("input", update);
|
|
1906
|
+
update();
|
|
1907
|
+
return () => input.removeEventListener("input", update);
|
|
1908
|
+
}
|
|
1909
|
+
var STRENGTH_METER_PRIMITIVE = jsInputPrimitive(
|
|
1910
|
+
"strength-meter",
|
|
1911
|
+
["meter"],
|
|
1912
|
+
{},
|
|
1913
|
+
deferPrepare(prepareStrengthMeter)
|
|
1914
|
+
);
|
|
1915
|
+
var RANGE_FILL_PRIMITIVE = jsInputPrimitive(
|
|
1916
|
+
"range-fill",
|
|
1917
|
+
[CHANNEL.background],
|
|
1918
|
+
{},
|
|
1919
|
+
deferPrepare(prepareRangeFill)
|
|
1920
|
+
);
|
|
1921
|
+
function nextStep(step, total) {
|
|
1922
|
+
return total > 0 ? (step + 1) % total : 0;
|
|
1923
|
+
}
|
|
1924
|
+
function prepareStepProgress(el, params) {
|
|
1925
|
+
const total = Math.max(1, Math.round(params.num("steps", 4)));
|
|
1926
|
+
let step = 0;
|
|
1927
|
+
const render = () => el.setAttribute("data-kui-step", String(step));
|
|
1928
|
+
const advance = () => {
|
|
1929
|
+
step = nextStep(step, total);
|
|
1930
|
+
render();
|
|
1931
|
+
};
|
|
1932
|
+
el.addEventListener("click", advance);
|
|
1933
|
+
render();
|
|
1934
|
+
return () => {
|
|
1935
|
+
el.removeEventListener("click", advance);
|
|
1936
|
+
el.removeAttribute("data-kui-step");
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
var STEP_PROGRESS_PRIMITIVE = jsInputPrimitive(
|
|
1940
|
+
"step-progress",
|
|
1941
|
+
["state"],
|
|
1942
|
+
{ steps: { type: "number", default: "4", cssProperty: "--kui-steps", minimum: 1, integer: true } },
|
|
1943
|
+
deferPrepare(prepareStepProgress)
|
|
1944
|
+
);
|
|
1945
|
+
function nextSubmitStage(stage) {
|
|
1946
|
+
if (stage === "idle") return "loading";
|
|
1947
|
+
if (stage === "loading") return "done";
|
|
1948
|
+
return "idle";
|
|
1949
|
+
}
|
|
1950
|
+
function prepareSubmitFlow(el, params, ctx) {
|
|
1951
|
+
const loadMs = params.ms("load", 1200);
|
|
1952
|
+
const holdMs = params.ms("hold", 1500);
|
|
1953
|
+
let stage = "idle";
|
|
1954
|
+
let handle;
|
|
1955
|
+
const render = () => el.setAttribute("data-kui-stage", stage);
|
|
1956
|
+
const toIdle = () => {
|
|
1957
|
+
stage = nextSubmitStage(stage);
|
|
1958
|
+
render();
|
|
1959
|
+
};
|
|
1960
|
+
const toDone = () => {
|
|
1961
|
+
stage = nextSubmitStage(stage);
|
|
1962
|
+
render();
|
|
1963
|
+
handle = ctx.win.setTimeout(toIdle, holdMs);
|
|
1964
|
+
};
|
|
1965
|
+
const advance = () => {
|
|
1966
|
+
if (stage !== "idle") return;
|
|
1967
|
+
stage = nextSubmitStage(stage);
|
|
1968
|
+
render();
|
|
1969
|
+
handle = ctx.win.setTimeout(toDone, loadMs);
|
|
1970
|
+
};
|
|
1971
|
+
el.addEventListener("click", advance);
|
|
1972
|
+
render();
|
|
1973
|
+
return () => {
|
|
1974
|
+
el.removeEventListener("click", advance);
|
|
1975
|
+
if (handle !== void 0) ctx.win.clearTimeout(handle);
|
|
1976
|
+
el.removeAttribute("data-kui-stage");
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
var SUBMIT_FLOW_PRIMITIVE = jsInputPrimitive(
|
|
1980
|
+
"submit-flow",
|
|
1981
|
+
["state"],
|
|
1982
|
+
{
|
|
1983
|
+
load: { type: "time", default: "1200ms", cssProperty: "--kui-load" },
|
|
1984
|
+
hold: { type: "time", default: "1500ms", cssProperty: "--kui-hold" }
|
|
1985
|
+
},
|
|
1986
|
+
deferPrepare(prepareSubmitFlow)
|
|
1987
|
+
);
|
|
1988
|
+
|
|
1989
|
+
// src/effects/forms/index.ts
|
|
1990
|
+
var FORMS_PRIMITIVES = [
|
|
1991
|
+
NATIVE_STATE_PRIMITIVE,
|
|
1992
|
+
FOCUS_RING_PRIMITIVE,
|
|
1993
|
+
VALIDATE_SHAKE_PRIMITIVE,
|
|
1994
|
+
VALIDATE_CHECK_PRIMITIVE,
|
|
1995
|
+
STRENGTH_METER_PRIMITIVE,
|
|
1996
|
+
RANGE_FILL_PRIMITIVE,
|
|
1997
|
+
STEP_PROGRESS_PRIMITIVE,
|
|
1998
|
+
SUBMIT_FLOW_PRIMITIVE
|
|
1999
|
+
];
|
|
2000
|
+
var FORMS_PRESETS = [
|
|
2001
|
+
{ name: "label-float", primitive: "native-state" },
|
|
2002
|
+
{ name: "input-underline-grow", primitive: "native-state" },
|
|
2003
|
+
{ name: "focus-ring-grow", primitive: "focus-ring", keyframes: "kui-focus-ring-grow" },
|
|
2004
|
+
{ name: "validate-shake", primitive: "validate-shake", keyframes: "kui-validate-shake" },
|
|
2005
|
+
{ name: "validate-check", primitive: "validate-check", keyframes: "kui-validate-check" },
|
|
2006
|
+
{ name: "strength-meter", primitive: "strength-meter" },
|
|
2007
|
+
{ name: "toggle-morph", primitive: "native-state" },
|
|
2008
|
+
{ name: "checkbox-draw", primitive: "native-state" },
|
|
2009
|
+
{ name: "radio-fill", primitive: "native-state" },
|
|
2010
|
+
{ name: "range-fill", primitive: "range-fill" },
|
|
2011
|
+
{ name: "submit-to-spinner-to-check", primitive: "submit-flow" },
|
|
2012
|
+
{ name: "step-progress", primitive: "step-progress" }
|
|
2013
|
+
];
|
|
2014
|
+
function registerForms(registry) {
|
|
2015
|
+
return registry.registerPrimitives(FORMS_PRIMITIVES).registerPresets(FORMS_PRESETS);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// src/core/gesture.ts
|
|
2019
|
+
var VELOCITY_WINDOW_MS = 100;
|
|
2020
|
+
var MAX_SAMPLES = 12;
|
|
2021
|
+
function velocityFrom(samples) {
|
|
2022
|
+
const last = samples[samples.length - 1];
|
|
2023
|
+
if (!last || samples.length < 2) return { vx: 0, vy: 0 };
|
|
2024
|
+
const cutoff = last.time - VELOCITY_WINDOW_MS;
|
|
2025
|
+
const first = samples.find((sample) => sample.time >= cutoff) ?? samples[0];
|
|
2026
|
+
const span = (last.time - first.time) / 1e3;
|
|
2027
|
+
if (span <= 0) return { vx: 0, vy: 0 };
|
|
2028
|
+
return { vx: (last.x - first.x) / span, vy: (last.y - first.y) / span };
|
|
2029
|
+
}
|
|
2030
|
+
function swipeDirection(vector, minVelocity) {
|
|
2031
|
+
const { vx, vy } = vector;
|
|
2032
|
+
if (Math.abs(vx) < minVelocity && Math.abs(vy) < minVelocity) return null;
|
|
2033
|
+
if (Math.abs(vx) >= Math.abs(vy)) return vx > 0 ? "right" : "left";
|
|
2034
|
+
return vy > 0 ? "down" : "up";
|
|
2035
|
+
}
|
|
2036
|
+
function applyAxis(vector, axis) {
|
|
2037
|
+
if (axis === "x") return { ...vector, dy: 0, vy: 0 };
|
|
2038
|
+
if (axis === "y") return { ...vector, dx: 0, vx: 0 };
|
|
2039
|
+
return vector;
|
|
2040
|
+
}
|
|
2041
|
+
function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
|
|
2042
|
+
const threshold = options.threshold ?? 4;
|
|
2043
|
+
const axis = options.axis ?? "both";
|
|
2044
|
+
const swipeVelocity = options.swipeVelocity ?? 300;
|
|
2045
|
+
const longPressMs = options.longPressMs ?? 0;
|
|
2046
|
+
let samples = [];
|
|
2047
|
+
let origin = null;
|
|
2048
|
+
let active = false;
|
|
2049
|
+
let longPressTimer = null;
|
|
2050
|
+
function sampleOf(event) {
|
|
2051
|
+
return { x: event.clientX, y: event.clientY, time: deps.now() };
|
|
2052
|
+
}
|
|
2053
|
+
function vectorNow(sample) {
|
|
2054
|
+
const start = origin;
|
|
2055
|
+
const { vx, vy } = velocityFrom(samples);
|
|
2056
|
+
return applyAxis({ dx: sample.x - start.x, dy: sample.y - start.y, vx, vy }, axis);
|
|
2057
|
+
}
|
|
2058
|
+
function clearLongPress() {
|
|
2059
|
+
if (longPressTimer !== null) deps.clearTimer(longPressTimer);
|
|
2060
|
+
longPressTimer = null;
|
|
2061
|
+
}
|
|
2062
|
+
function onDown(event) {
|
|
2063
|
+
origin = sampleOf(event);
|
|
2064
|
+
samples = [origin];
|
|
2065
|
+
active = false;
|
|
2066
|
+
el.setPointerCapture?.(event.pointerId);
|
|
2067
|
+
if (longPressMs > 0) {
|
|
2068
|
+
longPressTimer = deps.setTimer(() => handlers.onLongPress?.(origin), longPressMs);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
function onMove(event) {
|
|
2072
|
+
if (!origin) return;
|
|
2073
|
+
const sample = sampleOf(event);
|
|
2074
|
+
samples.push(sample);
|
|
2075
|
+
if (samples.length > MAX_SAMPLES) samples.shift();
|
|
2076
|
+
const vector = vectorNow(sample);
|
|
2077
|
+
if (!active && Math.hypot(vector.dx, vector.dy) < threshold) return;
|
|
2078
|
+
if (!active) {
|
|
2079
|
+
active = true;
|
|
2080
|
+
clearLongPress();
|
|
2081
|
+
handlers.onStart?.(origin);
|
|
2082
|
+
}
|
|
2083
|
+
handlers.onMove?.(vector, sample);
|
|
2084
|
+
}
|
|
2085
|
+
function onUp(event) {
|
|
2086
|
+
clearLongPress();
|
|
2087
|
+
el.releasePointerCapture?.(event.pointerId);
|
|
2088
|
+
if (!origin) return;
|
|
2089
|
+
const sample = sampleOf(event);
|
|
2090
|
+
samples.push(sample);
|
|
2091
|
+
const vector = vectorNow(sample);
|
|
2092
|
+
if (active) {
|
|
2093
|
+
handlers.onEnd?.(vector, sample);
|
|
2094
|
+
const direction = swipeDirection(vector, swipeVelocity);
|
|
2095
|
+
if (direction) handlers.onSwipe?.(direction, vector);
|
|
2096
|
+
}
|
|
2097
|
+
origin = null;
|
|
2098
|
+
active = false;
|
|
2099
|
+
samples = [];
|
|
2100
|
+
}
|
|
2101
|
+
el.addEventListener("pointerdown", onDown);
|
|
2102
|
+
el.addEventListener("pointermove", onMove, { passive: true });
|
|
2103
|
+
el.addEventListener("pointerup", onUp, { passive: true });
|
|
2104
|
+
el.addEventListener("pointercancel", onUp, { passive: true });
|
|
2105
|
+
return () => {
|
|
2106
|
+
clearLongPress();
|
|
2107
|
+
el.removeEventListener("pointerdown", onDown);
|
|
2108
|
+
el.removeEventListener("pointermove", onMove);
|
|
2109
|
+
el.removeEventListener("pointerup", onUp);
|
|
2110
|
+
el.removeEventListener("pointercancel", onUp);
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
function defaultGestureDeps() {
|
|
2114
|
+
return {
|
|
2115
|
+
now: () => typeof performance === "undefined" ? Date.now() : performance.now(),
|
|
2116
|
+
setTimer: (callback, ms) => globalThis.setTimeout(callback, ms),
|
|
2117
|
+
clearTimer: (handle) => globalThis.clearTimeout(handle)
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
function rubberBand(offset, limit, tension = 0.55) {
|
|
2121
|
+
if (limit <= 0) return 0;
|
|
2122
|
+
const sign = Math.sign(offset);
|
|
2123
|
+
const magnitude = Math.abs(offset);
|
|
2124
|
+
return sign * (1 - 1 / (magnitude / limit / tension + 1)) * limit;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/effects/gestures/primitives.ts
|
|
2128
|
+
var springParams2 = {
|
|
2129
|
+
stiffness: {
|
|
2130
|
+
type: "number",
|
|
2131
|
+
default: "180",
|
|
2132
|
+
cssProperty: "--kui-stiffness",
|
|
2133
|
+
finite: true,
|
|
2134
|
+
minimum: 1,
|
|
2135
|
+
maximum: 1e4
|
|
2136
|
+
},
|
|
2137
|
+
damping: {
|
|
2138
|
+
type: "number",
|
|
2139
|
+
default: "24",
|
|
2140
|
+
cssProperty: "--kui-damping",
|
|
2141
|
+
finite: true,
|
|
2142
|
+
minimum: 0.1,
|
|
2143
|
+
maximum: 1e3
|
|
2144
|
+
}
|
|
2145
|
+
};
|
|
2146
|
+
function gesturePrimitive(id, channels, parameters, prepare) {
|
|
2147
|
+
return {
|
|
2148
|
+
id,
|
|
2149
|
+
renderer: "javascript",
|
|
2150
|
+
channels,
|
|
2151
|
+
parameters,
|
|
2152
|
+
supportedTimelines: ["time", "pointer"],
|
|
2153
|
+
supportedActivations: ["load", "manual"],
|
|
2154
|
+
defaultActivation: "load",
|
|
2155
|
+
perfClass: "continuous",
|
|
2156
|
+
reducedMotion: "disable",
|
|
2157
|
+
prepare
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function springFrom2(params) {
|
|
2161
|
+
return {
|
|
2162
|
+
...DEFAULT_SPRING,
|
|
2163
|
+
stiffness: params.num("stiffness", DEFAULT_SPRING.stiffness),
|
|
2164
|
+
damping: params.num("damping", DEFAULT_SPRING.damping)
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
function springDeps(ctx) {
|
|
2168
|
+
return { ...defaultSpringDeps(), warn: ctx.warn };
|
|
2169
|
+
}
|
|
2170
|
+
function writeOffset(ctx, x, y) {
|
|
2171
|
+
ctx.style.set("translate", `${x.toFixed(2)}px ${y.toFixed(2)}px`);
|
|
2172
|
+
}
|
|
2173
|
+
function prepareDraggable(el, params, ctx) {
|
|
2174
|
+
const config = springFrom2(params);
|
|
2175
|
+
const bounds = params.num("bounds", 0);
|
|
2176
|
+
const returns = params.is("return");
|
|
2177
|
+
const inertia = params.is("inertia");
|
|
2178
|
+
const offset = { x: 0, y: 0 };
|
|
2179
|
+
const deps = springDeps(ctx);
|
|
2180
|
+
const runners = {
|
|
2181
|
+
x: createSpringRunner(config, (value) => write({ ...offset, x: value }), deps),
|
|
2182
|
+
y: createSpringRunner(config, (value) => write({ ...offset, y: value }), deps)
|
|
2183
|
+
};
|
|
2184
|
+
function write(next) {
|
|
2185
|
+
offset.x = next.x;
|
|
2186
|
+
offset.y = next.y;
|
|
2187
|
+
writeOffset(ctx, offset.x, offset.y);
|
|
2188
|
+
}
|
|
2189
|
+
const stopRecognising = recognise(
|
|
2190
|
+
el,
|
|
2191
|
+
{
|
|
2192
|
+
onStart() {
|
|
2193
|
+
runners.x.stop();
|
|
2194
|
+
runners.y.stop();
|
|
2195
|
+
el.setAttribute("data-kui-dragging", "true");
|
|
2196
|
+
},
|
|
2197
|
+
onMove(vector) {
|
|
2198
|
+
write({ x: resist(vector.dx, bounds), y: resist(vector.dy, bounds) });
|
|
2199
|
+
},
|
|
2200
|
+
onEnd(vector) {
|
|
2201
|
+
el.setAttribute("data-kui-dragging", "false");
|
|
2202
|
+
settle(runners, offset, vector, { returns, inertia });
|
|
2203
|
+
}
|
|
2204
|
+
},
|
|
2205
|
+
{ axis: params.text("axis", "both") }
|
|
2206
|
+
);
|
|
2207
|
+
ctx.invalidate();
|
|
2208
|
+
return () => {
|
|
2209
|
+
stopRecognising();
|
|
2210
|
+
runners.x.stop();
|
|
2211
|
+
runners.y.stop();
|
|
2212
|
+
el.removeAttribute("data-kui-dragging");
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
function resist(delta, bounds) {
|
|
2216
|
+
return bounds > 0 ? rubberBand(delta, bounds) : delta;
|
|
2217
|
+
}
|
|
2218
|
+
function settle(runners, offset, vector, mode) {
|
|
2219
|
+
runners.x.set(offset.x, mode.inertia ? vector.vx : 0);
|
|
2220
|
+
runners.y.set(offset.y, mode.inertia ? vector.vy : 0);
|
|
2221
|
+
if (mode.returns) {
|
|
2222
|
+
runners.x.to(0);
|
|
2223
|
+
runners.y.to(0);
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
runners.x.to(offset.x + vector.vx * 0.2);
|
|
2227
|
+
runners.y.to(offset.y + vector.vy * 0.2);
|
|
2228
|
+
}
|
|
2229
|
+
function prepareSwipeable(el, params) {
|
|
2230
|
+
const stop = recognise(
|
|
2231
|
+
el,
|
|
2232
|
+
{
|
|
2233
|
+
onSwipe(direction) {
|
|
2234
|
+
el.setAttribute("data-kui-swipe", direction);
|
|
2235
|
+
}
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
axis: params.text("axis", "both"),
|
|
2239
|
+
swipeVelocity: params.num("velocity", 300)
|
|
2240
|
+
}
|
|
2241
|
+
);
|
|
2242
|
+
return () => {
|
|
2243
|
+
stop();
|
|
2244
|
+
el.removeAttribute("data-kui-swipe");
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
function preparePressable(el, params) {
|
|
2248
|
+
const stop = recognise(
|
|
2249
|
+
el,
|
|
2250
|
+
{
|
|
2251
|
+
onLongPress() {
|
|
2252
|
+
el.setAttribute("data-kui-pressed", "true");
|
|
2253
|
+
},
|
|
2254
|
+
onEnd() {
|
|
2255
|
+
el.setAttribute("data-kui-pressed", "false");
|
|
2256
|
+
}
|
|
2257
|
+
},
|
|
2258
|
+
{ longPressMs: effectDurationMs(params, 500) }
|
|
2259
|
+
);
|
|
2260
|
+
return () => {
|
|
2261
|
+
stop();
|
|
2262
|
+
el.removeAttribute("data-kui-pressed");
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
function prepareMagnetic(el, params, ctx) {
|
|
2266
|
+
const node = el;
|
|
2267
|
+
const radius = params.num("radius", 120);
|
|
2268
|
+
const strength = params.num("strength", 0.35);
|
|
2269
|
+
const config = springFrom2(params);
|
|
2270
|
+
const deps = springDeps(ctx);
|
|
2271
|
+
const offset = { x: 0, y: 0 };
|
|
2272
|
+
const runnerX = createSpringRunner(config, (value) => {
|
|
2273
|
+
offset.x = value;
|
|
2274
|
+
writeOffset(ctx, offset.x, offset.y);
|
|
2275
|
+
}, deps);
|
|
2276
|
+
const runnerY = createSpringRunner(config, (value) => {
|
|
2277
|
+
offset.y = value;
|
|
2278
|
+
writeOffset(ctx, offset.x, offset.y);
|
|
2279
|
+
}, deps);
|
|
2280
|
+
const onPointerMove = (event) => {
|
|
2281
|
+
const box = node.getBoundingClientRect();
|
|
2282
|
+
const dx = event.clientX - (box.left + box.width / 2);
|
|
2283
|
+
const dy = event.clientY - (box.top + box.height / 2);
|
|
2284
|
+
const inRange = Math.hypot(dx, dy) < radius;
|
|
2285
|
+
runnerX.to(inRange ? dx * strength : 0);
|
|
2286
|
+
runnerY.to(inRange ? dy * strength : 0);
|
|
2287
|
+
};
|
|
2288
|
+
ctx.win.addEventListener("pointermove", onPointerMove, { passive: true });
|
|
2289
|
+
return () => {
|
|
2290
|
+
ctx.win.removeEventListener("pointermove", onPointerMove);
|
|
2291
|
+
runnerX.stop();
|
|
2292
|
+
runnerY.stop();
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
var GESTURE_PRIMITIVES = [
|
|
2296
|
+
gesturePrimitive(
|
|
2297
|
+
"draggable",
|
|
2298
|
+
["translate"],
|
|
2299
|
+
{
|
|
2300
|
+
...springParams2,
|
|
2301
|
+
axis: { type: "keyword", default: "both", cssProperty: "--kui-axis", values: ["x", "y", "both"] },
|
|
2302
|
+
bounds: { type: "number", default: "0", cssProperty: "--kui-bounds" },
|
|
2303
|
+
return: {
|
|
2304
|
+
type: "keyword",
|
|
2305
|
+
default: "false",
|
|
2306
|
+
cssProperty: "--kui-return",
|
|
2307
|
+
values: ["true", "false"]
|
|
2308
|
+
},
|
|
2309
|
+
inertia: {
|
|
2310
|
+
type: "keyword",
|
|
2311
|
+
default: "false",
|
|
2312
|
+
cssProperty: "--kui-inertia",
|
|
2313
|
+
values: ["true", "false"]
|
|
2314
|
+
}
|
|
2315
|
+
},
|
|
2316
|
+
deferPrepare(prepareDraggable)
|
|
2317
|
+
),
|
|
2318
|
+
gesturePrimitive(
|
|
2319
|
+
"swipeable",
|
|
2320
|
+
["state"],
|
|
2321
|
+
{
|
|
2322
|
+
axis: { type: "keyword", default: "both", cssProperty: "--kui-axis", values: ["x", "y", "both"] },
|
|
2323
|
+
velocity: { type: "number", default: "300", cssProperty: "--kui-velocity" }
|
|
2324
|
+
},
|
|
2325
|
+
deferPrepare(prepareSwipeable)
|
|
2326
|
+
),
|
|
2327
|
+
gesturePrimitive(
|
|
2328
|
+
"pressable",
|
|
2329
|
+
["state"],
|
|
2330
|
+
{ duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" } },
|
|
2331
|
+
deferPrepare(preparePressable)
|
|
2332
|
+
),
|
|
2333
|
+
gesturePrimitive(
|
|
2334
|
+
"magnetic",
|
|
2335
|
+
["translate"],
|
|
2336
|
+
{
|
|
2337
|
+
...springParams2,
|
|
2338
|
+
radius: { type: "number", default: "120", cssProperty: "--kui-radius" },
|
|
2339
|
+
strength: { type: "number", default: "0.35", cssProperty: "--kui-strength" }
|
|
2340
|
+
},
|
|
2341
|
+
deferPrepare(prepareMagnetic)
|
|
2342
|
+
)
|
|
2343
|
+
];
|
|
2344
|
+
|
|
2345
|
+
// src/effects/gestures/index.ts
|
|
2346
|
+
var GESTURE_PRESETS = [
|
|
2347
|
+
{ name: "drag", primitive: "draggable" },
|
|
2348
|
+
{ name: "drag-x", primitive: "draggable", params: { axis: "x" } },
|
|
2349
|
+
{ name: "drag-y", primitive: "draggable", params: { axis: "y" } },
|
|
2350
|
+
{ name: "drag-inertia", primitive: "draggable", params: { inertia: "true" } },
|
|
2351
|
+
{ name: "throwable", primitive: "draggable", params: { inertia: "true", damping: "18" } },
|
|
2352
|
+
{ name: "elastic-pull", primitive: "draggable", params: { return: "true", bounds: "80" } },
|
|
2353
|
+
{ name: "rubber-band", primitive: "draggable", params: { return: "true", bounds: "120" } },
|
|
2354
|
+
{ name: "snap-back", primitive: "draggable", params: { return: "true", stiffness: "260" } },
|
|
2355
|
+
{ name: "swipe", primitive: "swipeable" },
|
|
2356
|
+
{ name: "swipe-x", primitive: "swipeable", params: { axis: "x" } },
|
|
2357
|
+
{ name: "long-press", primitive: "pressable" },
|
|
2358
|
+
{ name: "magnetic", primitive: "magnetic" },
|
|
2359
|
+
{ name: "magnetic-snap", primitive: "magnetic", params: { strength: "0.6", radius: "160" } }
|
|
2360
|
+
];
|
|
2361
|
+
function registerGestures(registry) {
|
|
2362
|
+
return registry.registerPrimitives(GESTURE_PRIMITIVES).registerPresets(GESTURE_PRESETS);
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// src/core/flip.ts
|
|
2366
|
+
var EPSILON = 0.5;
|
|
2367
|
+
function domMeasure(el) {
|
|
2368
|
+
const rect = el.getBoundingClientRect();
|
|
2369
|
+
return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
|
|
2370
|
+
}
|
|
2371
|
+
function domAnimate(el, keyframes, options) {
|
|
2372
|
+
const animate = el.animate;
|
|
2373
|
+
return typeof animate === "function" ? animate.call(el, keyframes, options) : null;
|
|
2374
|
+
}
|
|
2375
|
+
function createFlipEngine(deps = {}) {
|
|
2376
|
+
const measure = deps.measure ?? domMeasure;
|
|
2377
|
+
const animate = deps.animate ?? domAnimate;
|
|
2378
|
+
return {
|
|
2379
|
+
snapshot(elements) {
|
|
2380
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
2381
|
+
for (const el of elements) boxes.set(el, measure(el));
|
|
2382
|
+
return { boxes };
|
|
2383
|
+
},
|
|
2384
|
+
play(before, elements, options = {}) {
|
|
2385
|
+
const deltas = collectDeltas(before, elements, measure, options.scale ?? false);
|
|
2386
|
+
if (deltas.length === 0) {
|
|
2387
|
+
return { moved: [], finished: Promise.resolve(), cancel: () => {
|
|
2388
|
+
} };
|
|
2389
|
+
}
|
|
2390
|
+
return runDeltas(deltas, animate, options);
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
function collectDeltas(before, elements, measure, scale) {
|
|
2395
|
+
const deltas = [];
|
|
2396
|
+
for (const el of elements) {
|
|
2397
|
+
const first = before.boxes.get(el);
|
|
2398
|
+
if (!first) continue;
|
|
2399
|
+
const last = measure(el);
|
|
2400
|
+
const delta = deltaFor(el, first, last, scale);
|
|
2401
|
+
if (delta) deltas.push(delta);
|
|
2402
|
+
}
|
|
2403
|
+
return deltas;
|
|
2404
|
+
}
|
|
2405
|
+
function deltaFor(el, first, last, scale) {
|
|
2406
|
+
const dx = first.x - last.x;
|
|
2407
|
+
const dy = first.y - last.y;
|
|
2408
|
+
const sx = scale && last.width > 0 ? first.width / last.width : 1;
|
|
2409
|
+
const sy = scale && last.height > 0 ? first.height / last.height : 1;
|
|
2410
|
+
const still = Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON && Math.abs(sx - 1) < 1e-3 && Math.abs(sy - 1) < 1e-3;
|
|
2411
|
+
return still ? null : { el, dx, dy, sx, sy };
|
|
2412
|
+
}
|
|
2413
|
+
function runDeltas(deltas, animate, options) {
|
|
2414
|
+
const duration = options.durationMs ?? 400;
|
|
2415
|
+
const easing = options.easing ?? "cubic-bezier(0.2, 0, 0, 1)";
|
|
2416
|
+
const animations = [];
|
|
2417
|
+
for (const { el, dx, dy, sx, sy } of deltas) {
|
|
2418
|
+
const animation = animate(
|
|
2419
|
+
el,
|
|
2420
|
+
[
|
|
2421
|
+
{ translate: `${dx}px ${dy}px`, scale: `${sx} ${sy}` },
|
|
2422
|
+
{ translate: "0px 0px", scale: "1 1" }
|
|
2423
|
+
],
|
|
2424
|
+
{ duration, easing, fill: "none" }
|
|
2425
|
+
);
|
|
2426
|
+
if (animation) animations.push(animation);
|
|
2427
|
+
}
|
|
2428
|
+
const finished = Promise.all(
|
|
2429
|
+
animations.map((animation) => animation.finished.catch(() => void 0))
|
|
2430
|
+
).then(() => void 0);
|
|
2431
|
+
return {
|
|
2432
|
+
moved: deltas.map((delta) => delta.el),
|
|
2433
|
+
finished,
|
|
2434
|
+
cancel() {
|
|
2435
|
+
for (const animation of animations) animation.cancel();
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
function observeLayout(container, engine, options, observe) {
|
|
2440
|
+
let before = engine.snapshot(container.children);
|
|
2441
|
+
const cleanup = observe(() => {
|
|
2442
|
+
engine.play(before, container.children, options);
|
|
2443
|
+
before = engine.snapshot(container.children);
|
|
2444
|
+
});
|
|
2445
|
+
return cleanup;
|
|
2446
|
+
}
|
|
2447
|
+
function mutationWatcher(container) {
|
|
2448
|
+
return (callback) => {
|
|
2449
|
+
if (typeof MutationObserver === "undefined") return () => {
|
|
2450
|
+
};
|
|
2451
|
+
const observer = new MutationObserver(callback);
|
|
2452
|
+
observer.observe(container, {
|
|
2453
|
+
childList: true,
|
|
2454
|
+
subtree: true,
|
|
2455
|
+
attributes: true,
|
|
2456
|
+
attributeFilter: ["hidden"]
|
|
2457
|
+
});
|
|
2458
|
+
return () => observer.disconnect();
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// src/effects/layout/primitives.ts
|
|
2463
|
+
var timing2 = {
|
|
2464
|
+
duration: { type: "time", default: "400ms", cssProperty: "--kui-duration" },
|
|
2465
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
|
|
2466
|
+
};
|
|
2467
|
+
function layoutPrimitive(id, channels, parameters, prepare) {
|
|
2468
|
+
return {
|
|
2469
|
+
id,
|
|
2470
|
+
renderer: "javascript",
|
|
2471
|
+
channels,
|
|
2472
|
+
parameters: { ...timing2, ...parameters },
|
|
2473
|
+
supportedTimelines: ["time"],
|
|
2474
|
+
supportedActivations: ["load", "manual", "click"],
|
|
2475
|
+
defaultActivation: "load",
|
|
2476
|
+
perfClass: "layout",
|
|
2477
|
+
// A layout transition that is merely faster is still a layout transition; under reduced
|
|
2478
|
+
// motion the correct behaviour is for elements to appear in place.
|
|
2479
|
+
reducedMotion: "disable",
|
|
2480
|
+
prepare
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
function prepareFlipContainer(el, params) {
|
|
2484
|
+
const engine = createFlipEngine();
|
|
2485
|
+
return observeLayout(
|
|
2486
|
+
el,
|
|
2487
|
+
engine,
|
|
2488
|
+
{
|
|
2489
|
+
durationMs: effectDurationMs(params, 400),
|
|
2490
|
+
easing: params.text("ease"),
|
|
2491
|
+
scale: params.is("scale")
|
|
2492
|
+
},
|
|
2493
|
+
mutationWatcher(el)
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
function prepareAutoHeight(el, params, ctx) {
|
|
2497
|
+
const node = el;
|
|
2498
|
+
ctx.style.set("overflow", "hidden");
|
|
2499
|
+
ctx.style.claim("height");
|
|
2500
|
+
const duration = effectDurationMs(params, 400);
|
|
2501
|
+
let animation = null;
|
|
2502
|
+
const observer = watchAttribute(node, params.text("attribute"), () => {
|
|
2503
|
+
animation?.cancel();
|
|
2504
|
+
animation = animateHeight(node, duration, params.text("ease"));
|
|
2505
|
+
});
|
|
2506
|
+
ctx.invalidate();
|
|
2507
|
+
return () => {
|
|
2508
|
+
observer();
|
|
2509
|
+
animation?.cancel();
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
function animateHeight(node, duration, easing) {
|
|
2513
|
+
const from = `${node.getBoundingClientRect().height}px`;
|
|
2514
|
+
node.style.removeProperty("height");
|
|
2515
|
+
const to = `${node.scrollHeight}px`;
|
|
2516
|
+
const animate = node.animate;
|
|
2517
|
+
if (typeof animate !== "function") return null;
|
|
2518
|
+
return animate.call(node, [{ height: from }, { height: to }], { duration, easing, fill: "none" });
|
|
2519
|
+
}
|
|
2520
|
+
function prepareIndicator(el, params, ctx) {
|
|
2521
|
+
const node = el;
|
|
2522
|
+
const engine = createFlipEngine();
|
|
2523
|
+
const selector = params.text("follow");
|
|
2524
|
+
let currentShift = 0;
|
|
2525
|
+
const move = () => {
|
|
2526
|
+
if (!selector) return;
|
|
2527
|
+
const target = ctx.doc.querySelector(selector);
|
|
2528
|
+
if (!(target instanceof Element)) return;
|
|
2529
|
+
const before = engine.snapshot([node]);
|
|
2530
|
+
const box = target.getBoundingClientRect();
|
|
2531
|
+
const current = node.getBoundingClientRect();
|
|
2532
|
+
const shift = box.left - current.left + currentShift;
|
|
2533
|
+
currentShift = shift;
|
|
2534
|
+
ctx.style.set("width", `${box.width}px`);
|
|
2535
|
+
ctx.style.set("translate", `${shift}px 0`);
|
|
2536
|
+
engine.play(before, [node], {
|
|
2537
|
+
durationMs: effectDurationMs(params, 400),
|
|
2538
|
+
easing: params.text("ease"),
|
|
2539
|
+
scale: true
|
|
2540
|
+
});
|
|
2541
|
+
};
|
|
2542
|
+
move();
|
|
2543
|
+
return watchAttribute(ctx.doc.documentElement, params.text("attribute"), move);
|
|
2544
|
+
}
|
|
2545
|
+
function watchAttribute(root, attribute, onChange) {
|
|
2546
|
+
if (typeof MutationObserver === "undefined" || !attribute) return () => {
|
|
2547
|
+
};
|
|
2548
|
+
const observer = new MutationObserver(onChange);
|
|
2549
|
+
observer.observe(root, { subtree: true, attributes: true, attributeFilter: [attribute] });
|
|
2550
|
+
return () => observer.disconnect();
|
|
2551
|
+
}
|
|
2552
|
+
var LAYOUT_PRIMITIVES = [
|
|
2553
|
+
layoutPrimitive(
|
|
2554
|
+
"flip-container",
|
|
2555
|
+
["translate", "scale"],
|
|
2556
|
+
{
|
|
2557
|
+
scale: {
|
|
2558
|
+
type: "keyword",
|
|
2559
|
+
default: "false",
|
|
2560
|
+
cssProperty: "--kui-flip-scale",
|
|
2561
|
+
values: ["true", "false"]
|
|
2562
|
+
}
|
|
2563
|
+
},
|
|
2564
|
+
deferPrepare(prepareFlipContainer)
|
|
2565
|
+
),
|
|
2566
|
+
layoutPrimitive(
|
|
2567
|
+
"auto-height",
|
|
2568
|
+
["layout"],
|
|
2569
|
+
{ attribute: { type: "text", default: "data-open", cssProperty: "--kui-attribute" } },
|
|
2570
|
+
deferPrepare(prepareAutoHeight)
|
|
2571
|
+
),
|
|
2572
|
+
layoutPrimitive(
|
|
2573
|
+
"flip-indicator",
|
|
2574
|
+
["translate", "layout"],
|
|
2575
|
+
{
|
|
2576
|
+
follow: { type: "text", default: "", cssProperty: "--kui-follow" },
|
|
2577
|
+
attribute: { type: "text", default: "aria-selected", cssProperty: "--kui-attribute" }
|
|
2578
|
+
},
|
|
2579
|
+
deferPrepare(prepareIndicator)
|
|
2580
|
+
)
|
|
2581
|
+
];
|
|
2582
|
+
|
|
2583
|
+
// src/effects/layout/presets.ts
|
|
2584
|
+
var LAYOUT_PRESETS = [
|
|
2585
|
+
{ name: "flip-reorder", primitive: "flip-container" },
|
|
2586
|
+
{ name: "flip-filter", primitive: "flip-container" },
|
|
2587
|
+
{ name: "flip-sort", primitive: "flip-container" },
|
|
2588
|
+
{ name: "flip-shuffle", primitive: "flip-container" },
|
|
2589
|
+
// Cards changing aspect between layouts need their size interpolated, not just their position.
|
|
2590
|
+
{ name: "grid-to-list", primitive: "flip-container", params: { scale: "true" } },
|
|
2591
|
+
{ name: "masonry-reflow", primitive: "flip-container" },
|
|
2592
|
+
{ name: "expand-to-modal", primitive: "flip-container", params: { scale: "true", duration: "500ms" } },
|
|
2593
|
+
{ name: "accordion-height", primitive: "auto-height" },
|
|
2594
|
+
{ name: "tab-indicator-slide", primitive: "flip-indicator", params: { duration: "300ms" } }
|
|
2595
|
+
];
|
|
2596
|
+
|
|
2597
|
+
// src/effects/layout/index.ts
|
|
2598
|
+
function registerLayout(registry) {
|
|
2599
|
+
return registry.registerPrimitives(LAYOUT_PRIMITIVES).registerPresets(LAYOUT_PRESETS);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// src/effects/navigation/index.ts
|
|
2603
|
+
var NAV_CSS_PRIMITIVES = [
|
|
2604
|
+
cssPrimitive("nav-reveal", [CHANNEL.opacity, CHANNEL.translate]),
|
|
2605
|
+
cssPrimitive("menu-fullscreen", [CHANNEL.clip, CHANNEL.opacity]),
|
|
2606
|
+
cssPrimitive("panel-reveal", [CHANNEL.opacity, CHANNEL.translate]),
|
|
2607
|
+
cssPrimitive("drawer-slide", [CHANNEL.translate], {
|
|
2608
|
+
defaultActivation: "click"
|
|
2609
|
+
})
|
|
2610
|
+
];
|
|
2611
|
+
var NAV_CSS_PRESETS = [
|
|
2612
|
+
{ name: "menu-stagger-open", primitive: "nav-reveal", keyframes: "kui-nav-reveal" },
|
|
2613
|
+
{ name: "menu-fullscreen", primitive: "menu-fullscreen", keyframes: "kui-menu-fullscreen" },
|
|
2614
|
+
{ name: "dropdown-open", primitive: "panel-reveal", keyframes: "kui-panel-reveal" },
|
|
2615
|
+
{
|
|
2616
|
+
name: "mega-menu-drop",
|
|
2617
|
+
primitive: "panel-reveal",
|
|
2618
|
+
keyframes: "kui-panel-reveal",
|
|
2619
|
+
params: { duration: "550ms" }
|
|
2620
|
+
},
|
|
2621
|
+
{
|
|
2622
|
+
name: "drawer-slide",
|
|
2623
|
+
primitive: "drawer-slide",
|
|
2624
|
+
keyframes: "kui-drawer-slide-right"
|
|
2625
|
+
}
|
|
2626
|
+
];
|
|
2627
|
+
function navPrimitive(id, channels, parameters, prepare) {
|
|
2628
|
+
return {
|
|
2629
|
+
id,
|
|
2630
|
+
renderer: "javascript",
|
|
2631
|
+
channels,
|
|
2632
|
+
parameters,
|
|
2633
|
+
supportedTimelines: ["time"],
|
|
2634
|
+
supportedActivations: ["load"],
|
|
2635
|
+
defaultActivation: "load",
|
|
2636
|
+
perfClass: "compositor",
|
|
2637
|
+
// A scroll-position reaction, like the scroll-mechanics category: shortening its "duration"
|
|
2638
|
+
// is meaningless because the position, not a clock, drives it.
|
|
2639
|
+
reducedMotion: "disable",
|
|
2640
|
+
prepare
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
function subscribeScrollTop(el, ctx, onScrollTop) {
|
|
2644
|
+
return ctx.scheduler.subscribe(ctx.rootFor(el), (frame) => onScrollTop(frame.metrics.scrollTop));
|
|
2645
|
+
}
|
|
2646
|
+
function prepareHeaderShrink(el, params, ctx) {
|
|
2647
|
+
const offset = params.num("offset", 120);
|
|
2648
|
+
return subscribeScrollTop(el, ctx, (top) => {
|
|
2649
|
+
const progress = offset > 0 ? Math.min(1, Math.max(0, top / offset)) : 1;
|
|
2650
|
+
ctx.style.set("--kui-shrink", progress.toFixed(4));
|
|
2651
|
+
el.setAttribute("data-kui-shrunk", String(progress >= 1));
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
function prepareHeaderHide(el, params, ctx) {
|
|
2655
|
+
const minDelta = params.num("offset", 8);
|
|
2656
|
+
let last = 0;
|
|
2657
|
+
return subscribeScrollTop(el, ctx, (top) => {
|
|
2658
|
+
const delta = top - last;
|
|
2659
|
+
if (Math.abs(delta) < minDelta) return;
|
|
2660
|
+
el.setAttribute("data-kui-hidden", String(delta > 0 && top > minDelta));
|
|
2661
|
+
last = top;
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
function prepareBackToTop(el, params, ctx) {
|
|
2665
|
+
const offset = params.num("offset", 400);
|
|
2666
|
+
return subscribeScrollTop(el, ctx, (top) => {
|
|
2667
|
+
el.setAttribute("data-kui-visible", String(top > offset));
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
var NAV_JS_PRIMITIVES = [
|
|
2671
|
+
navPrimitive(
|
|
2672
|
+
"header-shrink",
|
|
2673
|
+
["layout"],
|
|
2674
|
+
{ offset: { type: "number", default: "120", cssProperty: "--kui-offset" } },
|
|
2675
|
+
deferPrepare(prepareHeaderShrink)
|
|
2676
|
+
),
|
|
2677
|
+
navPrimitive(
|
|
2678
|
+
"header-hide-on-scroll",
|
|
2679
|
+
[CHANNEL.translate],
|
|
2680
|
+
{ offset: { type: "number", default: "8", cssProperty: "--kui-offset" } },
|
|
2681
|
+
deferPrepare(prepareHeaderHide)
|
|
2682
|
+
),
|
|
2683
|
+
navPrimitive(
|
|
2684
|
+
"back-to-top-fade",
|
|
2685
|
+
[CHANNEL.opacity, CHANNEL.translate],
|
|
2686
|
+
{ offset: { type: "number", default: "400", cssProperty: "--kui-offset" } },
|
|
2687
|
+
deferPrepare(prepareBackToTop)
|
|
2688
|
+
)
|
|
2689
|
+
];
|
|
2690
|
+
var NAV_JS_PRESETS = [
|
|
2691
|
+
{ name: "header-shrink", primitive: "header-shrink" },
|
|
2692
|
+
{ name: "header-hide-on-scroll", primitive: "header-hide-on-scroll" },
|
|
2693
|
+
{ name: "back-to-top-fade", primitive: "back-to-top-fade" }
|
|
2694
|
+
];
|
|
2695
|
+
var NAVIGATION_PRIMITIVES = [...NAV_CSS_PRIMITIVES, ...NAV_JS_PRIMITIVES];
|
|
2696
|
+
var NAVIGATION_PRESETS = [...NAV_CSS_PRESETS, ...NAV_JS_PRESETS];
|
|
2697
|
+
function registerNavigation(registry) {
|
|
2698
|
+
return registry.registerPrimitives(NAVIGATION_PRIMITIVES).registerPresets(NAVIGATION_PRESETS);
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// src/effects/scroll-mechanics/tracker.ts
|
|
2702
|
+
var domGeometry = (el) => {
|
|
2703
|
+
const rect = el.getBoundingClientRect();
|
|
2704
|
+
return { top: rect.top, height: rect.height };
|
|
2705
|
+
};
|
|
2706
|
+
function trackProgress(el, ctx, options, onProgress) {
|
|
2707
|
+
const measure = options.measure ?? domGeometry;
|
|
2708
|
+
let scrollTop = 0;
|
|
2709
|
+
let scrollportTop = 0;
|
|
2710
|
+
const geometry2 = createMeasureCache(() => {
|
|
2711
|
+
const box = measure(el);
|
|
2712
|
+
return { contentTop: box.top - scrollportTop + scrollTop, height: box.height };
|
|
2713
|
+
});
|
|
2714
|
+
return ctx.scheduler.subscribe(ctx.rootFor(el), (frame) => {
|
|
2715
|
+
scrollTop = frame.metrics.scrollTop;
|
|
2716
|
+
scrollportTop = frame.metrics.viewportTop;
|
|
2717
|
+
const box = geometry2.read(frame.epoch);
|
|
2718
|
+
const span = resolveDistance(options.distance, { top: 0, height: box.height }, frame);
|
|
2719
|
+
onProgress(progressFrom(box.contentTop - scrollTop, span), frame);
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
function resolveDistance(distance2, box, frame) {
|
|
2723
|
+
if (!distance2) return box.height;
|
|
2724
|
+
const basis = {
|
|
2725
|
+
viewportWidth: frame.metrics.viewportWidth,
|
|
2726
|
+
viewportHeight: frame.metrics.viewportHeight,
|
|
2727
|
+
percentBasis: box.height,
|
|
2728
|
+
fontSize: 16,
|
|
2729
|
+
rootFontSize: 16
|
|
2730
|
+
};
|
|
2731
|
+
return toPixels(distance2, basis, box.height);
|
|
2732
|
+
}
|
|
2733
|
+
function progressFrom(top, span) {
|
|
2734
|
+
if (span <= 0) return 0;
|
|
2735
|
+
return clamp01(-top / span);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// src/effects/scroll-mechanics/primitives.ts
|
|
2739
|
+
var PROGRESS_VAR = "--kui-progress";
|
|
2740
|
+
var distanceParam = {
|
|
2741
|
+
distance: { type: "length", default: "100vh", cssProperty: "--kui-distance" }
|
|
2742
|
+
};
|
|
2743
|
+
function scrollPrimitive(spec) {
|
|
2744
|
+
const { id, channels, parameters, prepare, perfClass = "compositor" } = spec;
|
|
2745
|
+
return {
|
|
2746
|
+
id,
|
|
2747
|
+
renderer: "javascript",
|
|
2748
|
+
channels,
|
|
2749
|
+
parameters,
|
|
2750
|
+
supportedTimelines: ["time", "view", "scroll"],
|
|
2751
|
+
supportedActivations: ["manual", "load", "enter"],
|
|
2752
|
+
defaultActivation: "load",
|
|
2753
|
+
perfClass,
|
|
2754
|
+
reducedMotion: "disable",
|
|
2755
|
+
prepare
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
function writeProgress(ctx, progress) {
|
|
2759
|
+
ctx.style.set(PROGRESS_VAR, progress.toFixed(4));
|
|
2760
|
+
}
|
|
2761
|
+
function preparePin(el, params, ctx) {
|
|
2762
|
+
const node = el;
|
|
2763
|
+
ctx.style.set("position", "sticky");
|
|
2764
|
+
ctx.style.set("top", params.text("offset", "0px"));
|
|
2765
|
+
const removeSpacer = params.is("spacer") ? insertSpacer(node, params.text("distance"), ctx) : null;
|
|
2766
|
+
const tracked = node.parentElement ?? el;
|
|
2767
|
+
const untrack = trackProgress(tracked, ctx, { distance: params.text("distance") }, (progress) => {
|
|
2768
|
+
writeProgress(ctx, progress);
|
|
2769
|
+
el.setAttribute("data-kui-pinned", progress > 0 && progress < 1 ? "true" : "false");
|
|
2770
|
+
});
|
|
2771
|
+
return () => {
|
|
2772
|
+
untrack();
|
|
2773
|
+
removeSpacer?.();
|
|
2774
|
+
el.removeAttribute("data-kui-pinned");
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
function insertSpacer(node, distance2, ctx) {
|
|
2778
|
+
const spacer = ctx.doc.createElement("div");
|
|
2779
|
+
spacer.setAttribute("data-kui-spacer", "");
|
|
2780
|
+
spacer.setAttribute("aria-hidden", "true");
|
|
2781
|
+
spacer.style.height = distance2;
|
|
2782
|
+
spacer.style.pointerEvents = "none";
|
|
2783
|
+
node.after(spacer);
|
|
2784
|
+
ctx.invalidate();
|
|
2785
|
+
return () => {
|
|
2786
|
+
spacer.remove();
|
|
2787
|
+
ctx.invalidate();
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
function prepareProgress(el, params, ctx) {
|
|
2791
|
+
const steps = Math.max(0, Math.round(params.num("steps", 0)));
|
|
2792
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
2793
|
+
writeProgress(ctx, progress);
|
|
2794
|
+
if (steps > 0) {
|
|
2795
|
+
const index = Math.min(steps - 1, Math.floor(progress * steps));
|
|
2796
|
+
el.setAttribute("data-kui-step", String(index));
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
return () => {
|
|
2800
|
+
untrack();
|
|
2801
|
+
el.removeAttribute("data-kui-step");
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
function prepareHorizontal(el, params, ctx) {
|
|
2805
|
+
const node = el;
|
|
2806
|
+
const authored = params.text("travel", "auto");
|
|
2807
|
+
const travel = createMeasureCache(() => trackTravel(node, authored, node.ownerDocument));
|
|
2808
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress, frame) => {
|
|
2809
|
+
ctx.style.set("translate", `${-progress * travel.read(frame.epoch)}px 0`);
|
|
2810
|
+
writeProgress(ctx, progress);
|
|
2811
|
+
});
|
|
2812
|
+
return untrack;
|
|
2813
|
+
}
|
|
2814
|
+
function trackTravel(node, authored, doc) {
|
|
2815
|
+
if (authored && authored !== "auto") return toPixels(authored, ABSOLUTE_BASIS, 0);
|
|
2816
|
+
const selfOverflow = node.scrollWidth - node.clientWidth;
|
|
2817
|
+
if (selfOverflow > 0) return selfOverflow;
|
|
2818
|
+
const viewportWidth = node.parentElement?.clientWidth || doc.documentElement.clientWidth;
|
|
2819
|
+
return Math.max(0, node.scrollWidth - viewportWidth);
|
|
2820
|
+
}
|
|
2821
|
+
function prepareMediaScrub(el, params, ctx) {
|
|
2822
|
+
const frames = Math.max(1, Math.round(params.num("frames", 1)));
|
|
2823
|
+
const pattern = mediaSrcPattern(params.text("src"), ctx);
|
|
2824
|
+
const media = el;
|
|
2825
|
+
let lastIndex = -1;
|
|
2826
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
2827
|
+
writeProgress(ctx, progress);
|
|
2828
|
+
const index = Math.min(frames - 1, Math.floor(progress * frames));
|
|
2829
|
+
if (index === lastIndex) return;
|
|
2830
|
+
lastIndex = index;
|
|
2831
|
+
applyFrame(media, { index, frames, progress, pattern });
|
|
2832
|
+
});
|
|
2833
|
+
return untrack;
|
|
2834
|
+
}
|
|
2835
|
+
function mediaSrcPattern(pattern, ctx) {
|
|
2836
|
+
if (!pattern || isSameOriginPath(pattern)) return pattern;
|
|
2837
|
+
ctx.warn(`media-scrub "src" must be a same-origin path, got "${pattern}" \u2014 ignoring`);
|
|
2838
|
+
return "";
|
|
2839
|
+
}
|
|
2840
|
+
function applyFrame(media, write) {
|
|
2841
|
+
const { index, frames, progress, pattern } = write;
|
|
2842
|
+
if (media.tagName === "VIDEO") {
|
|
2843
|
+
const duration = Number.isFinite(media.duration) ? media.duration : 0;
|
|
2844
|
+
if (duration > 0) media.currentTime = duration * progress;
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
if (media.tagName !== "IMG") return;
|
|
2848
|
+
if (pattern) media.src = pattern.replace("{i}", String(index).padStart(String(frames).length, "0"));
|
|
2849
|
+
}
|
|
2850
|
+
function prepareScrollSpy(el, params, ctx) {
|
|
2851
|
+
const selector = spyTarget(params.text("target"), ctx);
|
|
2852
|
+
const links = /* @__PURE__ */ new Map();
|
|
2853
|
+
const self = createAttributeLedger(el);
|
|
2854
|
+
let last;
|
|
2855
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
2856
|
+
const active = progress > 0 && progress < 1;
|
|
2857
|
+
if (active === last) return;
|
|
2858
|
+
last = active;
|
|
2859
|
+
self.set("data-kui-active", String(active));
|
|
2860
|
+
if (selector) markLinks(ctx.doc, selector, active, links);
|
|
2861
|
+
});
|
|
2862
|
+
return () => {
|
|
2863
|
+
untrack();
|
|
2864
|
+
self.restore();
|
|
2865
|
+
for (const ledger of links.values()) ledger.restore();
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
function spyTarget(selector, ctx) {
|
|
2869
|
+
if (!selector) return selector;
|
|
2870
|
+
const breadth = selectorBreadth(selector, ctx.doc);
|
|
2871
|
+
if (breadth === "invalid") {
|
|
2872
|
+
ctx.warn(`scroll-spy target "${selector}" is not a valid selector and will be ignored`);
|
|
2873
|
+
return "";
|
|
2874
|
+
}
|
|
2875
|
+
if (breadth === "document-wide") {
|
|
2876
|
+
ctx.warn(`scroll-spy target "${selector}" matches the whole document and will be ignored`);
|
|
2877
|
+
return "";
|
|
2878
|
+
}
|
|
2879
|
+
return selector;
|
|
2880
|
+
}
|
|
2881
|
+
function selectorBreadth(selector, doc) {
|
|
2882
|
+
try {
|
|
2883
|
+
if (doc.documentElement.matches(selector)) return "document-wide";
|
|
2884
|
+
if (doc.body?.matches(selector)) return "document-wide";
|
|
2885
|
+
return "ok";
|
|
2886
|
+
} catch {
|
|
2887
|
+
return "invalid";
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
function markLinks(doc, selector, active, links) {
|
|
2891
|
+
for (const link of doc.querySelectorAll(selector)) {
|
|
2892
|
+
let ledger = links.get(link);
|
|
2893
|
+
if (!ledger) {
|
|
2894
|
+
ledger = createAttributeLedger(link);
|
|
2895
|
+
links.set(link, ledger);
|
|
2896
|
+
}
|
|
2897
|
+
ledger.set("data-kui-active", String(active));
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
function prepareSnap(el, params, ctx) {
|
|
2901
|
+
ctx.style.set(
|
|
2902
|
+
"scroll-snap-type",
|
|
2903
|
+
`${params.is("axis", "x") ? "x" : "y"} ${params.text("strictness", "mandatory")}`
|
|
2904
|
+
);
|
|
2905
|
+
const childLedgers = [...el.children].map((child) => createStyleLedger(child));
|
|
2906
|
+
for (const ledger of childLedgers) ledger.set("scroll-snap-align", params.text("align", "start"));
|
|
2907
|
+
return () => {
|
|
2908
|
+
for (const ledger of childLedgers) ledger.restore();
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
var SCROLL_PRIMITIVES = [
|
|
2912
|
+
scrollPrimitive({
|
|
2913
|
+
id: "pin",
|
|
2914
|
+
channels: ["layout", "progress"],
|
|
2915
|
+
parameters: {
|
|
2916
|
+
...distanceParam,
|
|
2917
|
+
offset: { type: "length", default: "0px", cssProperty: "--kui-offset" },
|
|
2918
|
+
spacer: {
|
|
2919
|
+
type: "keyword",
|
|
2920
|
+
default: "false",
|
|
2921
|
+
cssProperty: "--kui-spacer",
|
|
2922
|
+
values: ["true", "false"]
|
|
2923
|
+
}
|
|
2924
|
+
},
|
|
2925
|
+
prepare: deferPrepare(preparePin),
|
|
2926
|
+
perfClass: "layout"
|
|
2927
|
+
}),
|
|
2928
|
+
scrollPrimitive({
|
|
2929
|
+
id: "scroll-progress",
|
|
2930
|
+
channels: ["progress"],
|
|
2931
|
+
parameters: {
|
|
2932
|
+
...distanceParam,
|
|
2933
|
+
steps: { type: "number", default: "0", cssProperty: "--kui-steps" }
|
|
2934
|
+
},
|
|
2935
|
+
prepare: deferPrepare(prepareProgress)
|
|
2936
|
+
}),
|
|
2937
|
+
scrollPrimitive({
|
|
2938
|
+
id: "horizontal-track",
|
|
2939
|
+
channels: ["translate", "progress"],
|
|
2940
|
+
parameters: {
|
|
2941
|
+
...distanceParam,
|
|
2942
|
+
travel: { type: "text", default: "auto", cssProperty: "--kui-travel" }
|
|
2943
|
+
},
|
|
2944
|
+
prepare: deferPrepare(prepareHorizontal)
|
|
2945
|
+
}),
|
|
2946
|
+
scrollPrimitive({
|
|
2947
|
+
id: "media-scrub",
|
|
2948
|
+
channels: ["media", "progress"],
|
|
2949
|
+
parameters: {
|
|
2950
|
+
...distanceParam,
|
|
2951
|
+
frames: { type: "number", default: "1", cssProperty: "--kui-frames" },
|
|
2952
|
+
src: { type: "text", default: "", cssProperty: "--kui-src" }
|
|
2953
|
+
},
|
|
2954
|
+
prepare: deferPrepare(prepareMediaScrub),
|
|
2955
|
+
perfClass: "paint"
|
|
2956
|
+
}),
|
|
2957
|
+
scrollPrimitive({
|
|
2958
|
+
id: "scroll-spy",
|
|
2959
|
+
channels: ["state"],
|
|
2960
|
+
parameters: {
|
|
2961
|
+
...distanceParam,
|
|
2962
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
2963
|
+
},
|
|
2964
|
+
prepare: deferPrepare(prepareScrollSpy)
|
|
2965
|
+
}),
|
|
2966
|
+
scrollPrimitive({
|
|
2967
|
+
id: "scroll-snap",
|
|
2968
|
+
channels: ["layout"],
|
|
2969
|
+
parameters: {
|
|
2970
|
+
axis: { type: "keyword", default: "y", cssProperty: "--kui-axis", values: ["x", "y"] },
|
|
2971
|
+
strictness: {
|
|
2972
|
+
type: "keyword",
|
|
2973
|
+
default: "mandatory",
|
|
2974
|
+
cssProperty: "--kui-snap-strictness",
|
|
2975
|
+
values: ["mandatory", "proximity"]
|
|
2976
|
+
},
|
|
2977
|
+
align: {
|
|
2978
|
+
type: "keyword",
|
|
2979
|
+
default: "start",
|
|
2980
|
+
cssProperty: "--kui-snap-align",
|
|
2981
|
+
values: ["start", "center", "end"]
|
|
2982
|
+
}
|
|
2983
|
+
},
|
|
2984
|
+
prepare: deferPrepare(prepareSnap),
|
|
2985
|
+
perfClass: "layout"
|
|
2986
|
+
})
|
|
2987
|
+
];
|
|
2988
|
+
|
|
2989
|
+
// src/effects/scroll-mechanics/presets.ts
|
|
2990
|
+
var SCROLL_PRESETS = [
|
|
2991
|
+
// --- pinning ---------------------------------------------------------------------------
|
|
2992
|
+
// The default carries a spacer: a pin longer than its containing block silently does nothing,
|
|
2993
|
+
// and that is the single most common way authors get sticky wrong.
|
|
2994
|
+
{ name: "pin-section", primitive: "pin", params: { distance: "100vh", spacer: "true" } },
|
|
2995
|
+
{ name: "pin-until", primitive: "pin", params: { spacer: "false" } },
|
|
2996
|
+
{ name: "pin-spacer", primitive: "pin", params: { spacer: "true" } },
|
|
2997
|
+
// Applied per card; each card sticks at its own offset and the stack builds up naturally.
|
|
2998
|
+
{ name: "stacking-cards", primitive: "pin", params: { spacer: "false" } },
|
|
2999
|
+
// --- progress publishing ---------------------------------------------------------------
|
|
3000
|
+
{ name: "scroll-progress", primitive: "scroll-progress" },
|
|
3001
|
+
{ name: "scrollytelling-step", primitive: "scroll-progress", params: { steps: "4" } },
|
|
3002
|
+
// --- travel ------------------------------------------------------------------------------
|
|
3003
|
+
{ name: "horizontal-scroll", primitive: "horizontal-track" },
|
|
3004
|
+
// --- media -------------------------------------------------------------------------------
|
|
3005
|
+
{ name: "sequence-scrub", primitive: "media-scrub" },
|
|
3006
|
+
{ name: "video-scrub", primitive: "media-scrub" },
|
|
3007
|
+
// --- navigation --------------------------------------------------------------------------
|
|
3008
|
+
{ name: "scroll-spy", primitive: "scroll-spy" },
|
|
3009
|
+
// --- native CSS passthroughs ---------------------------------------------------------------
|
|
3010
|
+
{ name: "scroll-snap-x", primitive: "scroll-snap", params: { axis: "x" } },
|
|
3011
|
+
{ name: "scroll-snap-y", primitive: "scroll-snap", params: { axis: "y" } }
|
|
3012
|
+
];
|
|
3013
|
+
|
|
3014
|
+
// src/effects/scroll-mechanics/index.ts
|
|
3015
|
+
function registerScrollMechanics(registry) {
|
|
3016
|
+
return registry.registerPrimitives(SCROLL_PRIMITIVES).registerPresets(SCROLL_PRESETS);
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
// src/core/path-morph.ts
|
|
3020
|
+
var COMMAND = /([mlhvcz])|(-?(?:\d+(?:\.\d+)?|\.\d+))/gi;
|
|
3021
|
+
var UNSUPPORTED = /[AaSsQqTt]/;
|
|
3022
|
+
function parsePath(d) {
|
|
3023
|
+
if (UNSUPPORTED.test(d)) {
|
|
3024
|
+
return { segments: [], reason: "arc and shorthand commands (A S Q T) are not supported" };
|
|
3025
|
+
}
|
|
3026
|
+
const tokens = [...d.matchAll(COMMAND)].map((m) => m[1] ?? m[2]);
|
|
3027
|
+
const state = {
|
|
3028
|
+
segments: [],
|
|
3029
|
+
current: { x: 0, y: 0 },
|
|
3030
|
+
start: { x: 0, y: 0 },
|
|
3031
|
+
command: ""
|
|
3032
|
+
};
|
|
3033
|
+
let index = 0;
|
|
3034
|
+
while (index < tokens.length) {
|
|
3035
|
+
const token = tokens[index];
|
|
3036
|
+
if (/[a-z]/i.test(token)) {
|
|
3037
|
+
state.command = token;
|
|
3038
|
+
if (token.toLowerCase() === "z") closeSubpath(state);
|
|
3039
|
+
index++;
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
if (!state.command) return { segments: [], reason: "path must start with a command letter" };
|
|
3043
|
+
index = consume(tokens, index, state);
|
|
3044
|
+
}
|
|
3045
|
+
if (state.segments.length === 0) return { segments: [], reason: "no drawable segments" };
|
|
3046
|
+
return { segments: state.segments };
|
|
3047
|
+
}
|
|
3048
|
+
var ARITY = { m: 2, l: 2, h: 1, v: 1, c: 6, z: 0 };
|
|
3049
|
+
function closeSubpath(state) {
|
|
3050
|
+
const { current, start } = state;
|
|
3051
|
+
if (Math.abs(current.x - start.x) < 1e-6 && Math.abs(current.y - start.y) < 1e-6) return;
|
|
3052
|
+
state.segments.push(lineToCubic(current, start));
|
|
3053
|
+
state.current = { ...start };
|
|
3054
|
+
}
|
|
3055
|
+
function consume(tokens, index, state) {
|
|
3056
|
+
const key = state.command.toLowerCase();
|
|
3057
|
+
const relative = state.command === key;
|
|
3058
|
+
const arity = ARITY[key];
|
|
3059
|
+
const args = tokens.slice(index, index + arity).map(Number);
|
|
3060
|
+
if (args.length < arity) return tokens.length;
|
|
3061
|
+
const next = endpointFor(key, args, state.current, relative);
|
|
3062
|
+
if (key === "m") {
|
|
3063
|
+
state.current = next;
|
|
3064
|
+
state.start = next;
|
|
3065
|
+
state.command = relative ? "l" : "L";
|
|
3066
|
+
return index + arity;
|
|
3067
|
+
}
|
|
3068
|
+
state.segments.push(straightOrCubic({ key, args, from: state.current, relative }, next));
|
|
3069
|
+
state.current = next;
|
|
3070
|
+
return index + arity;
|
|
3071
|
+
}
|
|
3072
|
+
function endpointFor(key, args, current, relative) {
|
|
3073
|
+
const base = relative ? current : { x: 0, y: 0 };
|
|
3074
|
+
if (key === "h") return { x: base.x + args[0], y: current.y };
|
|
3075
|
+
if (key === "v") return { x: current.x, y: base.y + args[0] };
|
|
3076
|
+
if (key === "c") return { x: base.x + args[4], y: base.y + args[5] };
|
|
3077
|
+
return { x: base.x + args[0], y: base.y + args[1] };
|
|
3078
|
+
}
|
|
3079
|
+
function straightOrCubic(command, to) {
|
|
3080
|
+
const { key, args, from, relative } = command;
|
|
3081
|
+
if (key !== "c") return lineToCubic(from, to);
|
|
3082
|
+
const base = relative ? from : { x: 0, y: 0 };
|
|
3083
|
+
return {
|
|
3084
|
+
from,
|
|
3085
|
+
c1: { x: base.x + args[0], y: base.y + args[1] },
|
|
3086
|
+
c2: { x: base.x + args[2], y: base.y + args[3] },
|
|
3087
|
+
to
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
function lineToCubic(from, to) {
|
|
3091
|
+
return {
|
|
3092
|
+
from,
|
|
3093
|
+
c1: lerpPoint(from, to, 1 / 3),
|
|
3094
|
+
c2: lerpPoint(from, to, 2 / 3),
|
|
3095
|
+
to
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
function lerp(a, b, t) {
|
|
3099
|
+
return a + (b - a) * t;
|
|
3100
|
+
}
|
|
3101
|
+
function lerpPoint(a, b, t) {
|
|
3102
|
+
return { x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t) };
|
|
3103
|
+
}
|
|
3104
|
+
function splitCubic(segment, t) {
|
|
3105
|
+
const p01 = lerpPoint(segment.from, segment.c1, t);
|
|
3106
|
+
const p12 = lerpPoint(segment.c1, segment.c2, t);
|
|
3107
|
+
const p23 = lerpPoint(segment.c2, segment.to, t);
|
|
3108
|
+
const p012 = lerpPoint(p01, p12, t);
|
|
3109
|
+
const p123 = lerpPoint(p12, p23, t);
|
|
3110
|
+
const mid = lerpPoint(p012, p123, t);
|
|
3111
|
+
return [
|
|
3112
|
+
{ from: segment.from, c1: p01, c2: p012, to: mid },
|
|
3113
|
+
{ from: mid, c1: p123, c2: p23, to: segment.to }
|
|
3114
|
+
];
|
|
3115
|
+
}
|
|
3116
|
+
function normaliseCount(segments, target) {
|
|
3117
|
+
const out = [...segments];
|
|
3118
|
+
while (out.length < target && out.length > 0) {
|
|
3119
|
+
let longest = 0;
|
|
3120
|
+
for (let i = 1; i < out.length; i++) {
|
|
3121
|
+
if (chordLength(out[i]) > chordLength(out[longest])) longest = i;
|
|
3122
|
+
}
|
|
3123
|
+
const [a, b] = splitCubic(out[longest], 0.5);
|
|
3124
|
+
out.splice(longest, 1, a, b);
|
|
3125
|
+
}
|
|
3126
|
+
return out;
|
|
3127
|
+
}
|
|
3128
|
+
function chordLength(segment) {
|
|
3129
|
+
return Math.hypot(segment.to.x - segment.from.x, segment.to.y - segment.from.y);
|
|
3130
|
+
}
|
|
3131
|
+
function toPathData(segments) {
|
|
3132
|
+
if (segments.length === 0) return "";
|
|
3133
|
+
const head = segments[0];
|
|
3134
|
+
const parts = [`M${round(head.from.x)},${round(head.from.y)}`];
|
|
3135
|
+
for (const s of segments) {
|
|
3136
|
+
parts.push(
|
|
3137
|
+
`C${round(s.c1.x)},${round(s.c1.y)} ${round(s.c2.x)},${round(s.c2.y)} ${round(s.to.x)},${round(s.to.y)}`
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
3140
|
+
return parts.join(" ");
|
|
3141
|
+
}
|
|
3142
|
+
function round(value) {
|
|
3143
|
+
return Math.round(value * 100) / 100;
|
|
3144
|
+
}
|
|
3145
|
+
function createMorph(fromPath, toPath) {
|
|
3146
|
+
const a = parsePath(fromPath);
|
|
3147
|
+
const b = parsePath(toPath);
|
|
3148
|
+
if (a.reason) return { reason: `start path: ${a.reason}` };
|
|
3149
|
+
if (b.reason) return { reason: `end path: ${b.reason}` };
|
|
3150
|
+
const count = Math.max(a.segments.length, b.segments.length);
|
|
3151
|
+
const from = normaliseCount(a.segments, count);
|
|
3152
|
+
const to = normaliseCount(b.segments, count);
|
|
3153
|
+
return {
|
|
3154
|
+
morph: {
|
|
3155
|
+
segmentCount: count,
|
|
3156
|
+
at(t) {
|
|
3157
|
+
const clamped = Math.min(1, Math.max(0, t));
|
|
3158
|
+
return toPathData(from.map((segment, i) => lerpCubic(segment, to[i], clamped)));
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
function lerpCubic(a, b, t) {
|
|
3164
|
+
return {
|
|
3165
|
+
from: lerpPoint(a.from, b.from, t),
|
|
3166
|
+
c1: lerpPoint(a.c1, b.c1, t),
|
|
3167
|
+
c2: lerpPoint(a.c2, b.c2, t),
|
|
3168
|
+
to: lerpPoint(a.to, b.to, t)
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// src/effects/svg/index.ts
|
|
3173
|
+
function prepareMorph(el, params, ctx) {
|
|
3174
|
+
const path = el;
|
|
3175
|
+
const startPath = params.text("from") || path.getAttribute("d") || "";
|
|
3176
|
+
const { morph, reason } = createMorph(startPath, params.text("to"));
|
|
3177
|
+
if (!morph) {
|
|
3178
|
+
ctx.warn(`cannot morph: ${reason}`);
|
|
3179
|
+
return () => {
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
const duration = effectDurationMs(params, 300);
|
|
3183
|
+
let frame = 0;
|
|
3184
|
+
let cancelled = false;
|
|
3185
|
+
const drive = (target) => {
|
|
3186
|
+
cancelAnimationFrame(frame);
|
|
3187
|
+
const startedAt = performance.now();
|
|
3188
|
+
const startValue = current;
|
|
3189
|
+
const step = (now) => {
|
|
3190
|
+
if (cancelled) return;
|
|
3191
|
+
const t = duration > 0 ? Math.min(1, (now - startedAt) / duration) : 1;
|
|
3192
|
+
current = startValue + (target - startValue) * t;
|
|
3193
|
+
path.setAttribute("d", morph.at(current));
|
|
3194
|
+
if (t < 1) frame = requestAnimationFrame(step);
|
|
3195
|
+
};
|
|
3196
|
+
frame = requestAnimationFrame(step);
|
|
3197
|
+
};
|
|
3198
|
+
let current = 0;
|
|
3199
|
+
const enter = () => drive(1);
|
|
3200
|
+
const leave = () => drive(0);
|
|
3201
|
+
el.addEventListener("pointerenter", enter, { passive: true });
|
|
3202
|
+
el.addEventListener("focusin", enter, { passive: true });
|
|
3203
|
+
el.addEventListener("pointerleave", leave, { passive: true });
|
|
3204
|
+
el.addEventListener("focusout", leave, { passive: true });
|
|
3205
|
+
return () => {
|
|
3206
|
+
cancelled = true;
|
|
3207
|
+
cancelAnimationFrame(frame);
|
|
3208
|
+
el.removeEventListener("pointerenter", enter);
|
|
3209
|
+
el.removeEventListener("focusin", enter);
|
|
3210
|
+
el.removeEventListener("pointerleave", leave);
|
|
3211
|
+
el.removeEventListener("focusout", leave);
|
|
3212
|
+
path.setAttribute("d", startPath);
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
var SVG_PRIMITIVES = [
|
|
3216
|
+
{
|
|
3217
|
+
id: "path-morph",
|
|
3218
|
+
renderer: "javascript",
|
|
3219
|
+
// `d` is its own channel: nothing else in the catalog writes path geometry, so a morph can
|
|
3220
|
+
// safely compose with a fade or a rotation on the same element.
|
|
3221
|
+
channels: ["path"],
|
|
3222
|
+
parameters: {
|
|
3223
|
+
from: { type: "text", default: "", cssProperty: "--kui-path-from" },
|
|
3224
|
+
to: { type: "text", default: "", cssProperty: "--kui-path-to" },
|
|
3225
|
+
duration: { type: "time", default: "300ms", cssProperty: "--kui-duration" }
|
|
3226
|
+
},
|
|
3227
|
+
supportedTimelines: ["time"],
|
|
3228
|
+
supportedActivations: ["hover", "focus", "manual", "load"],
|
|
3229
|
+
// The morph attaches its own hover listeners, so it must be wired up on load, not on enter.
|
|
3230
|
+
defaultActivation: "load",
|
|
3231
|
+
perfClass: "paint",
|
|
3232
|
+
reducedMotion: "disable",
|
|
3233
|
+
prepare: deferPrepare(prepareMorph)
|
|
3234
|
+
}
|
|
3235
|
+
];
|
|
3236
|
+
var SVG_PRESETS = [
|
|
3237
|
+
{ name: "icon-morph", primitive: "path-morph" },
|
|
3238
|
+
{ name: "blob-morph", primitive: "path-morph", params: { duration: "800ms" } }
|
|
3239
|
+
];
|
|
3240
|
+
function registerSvg(registry) {
|
|
3241
|
+
return registry.registerPrimitives(SVG_PRIMITIVES).registerPresets(SVG_PRESETS);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
// src/effects/three-d/index.ts
|
|
3245
|
+
var THREE_D_PRIMITIVES = [
|
|
3246
|
+
cssPrimitive("flip-face", [CHANNEL.rotate], {
|
|
3247
|
+
parameters: {
|
|
3248
|
+
angle: { type: "angle", default: "180deg", cssProperty: "--kui-from-angle" },
|
|
3249
|
+
perspective: { type: "length", default: "1200px", cssProperty: "--kui-perspective" }
|
|
3250
|
+
}
|
|
3251
|
+
}),
|
|
3252
|
+
cssPrimitive("page-reveal", [CHANNEL.opacity, CHANNEL.translate], {
|
|
3253
|
+
parameters: { distance: { type: "length", default: "40px", cssProperty: "--kui-distance" } }
|
|
3254
|
+
}),
|
|
3255
|
+
cssPrimitive("wipe", [CHANNEL.clip]),
|
|
3256
|
+
cssPrimitive("bar", [CHANNEL.scale])
|
|
3257
|
+
];
|
|
3258
|
+
var THREE_D_PRESETS = [
|
|
3259
|
+
// --- 3D & perspective ---
|
|
3260
|
+
{ name: "card-flip-y", primitive: "flip-face", keyframes: "kui-card-flip-y" },
|
|
3261
|
+
{ name: "card-flip-x", primitive: "flip-face", keyframes: "kui-card-flip-x" },
|
|
3262
|
+
{ name: "cube-rotate", primitive: "flip-face", keyframes: "kui-cube-rotate", params: { angle: "90deg" } },
|
|
3263
|
+
{
|
|
3264
|
+
name: "book-page-turn",
|
|
3265
|
+
primitive: "flip-face",
|
|
3266
|
+
keyframes: "kui-book-page-turn",
|
|
3267
|
+
params: { angle: "-160deg", duration: "900ms" }
|
|
3268
|
+
},
|
|
3269
|
+
{
|
|
3270
|
+
name: "fold-panel",
|
|
3271
|
+
primitive: "flip-face",
|
|
3272
|
+
keyframes: "kui-fold-panel",
|
|
3273
|
+
params: { angle: "-90deg" }
|
|
3274
|
+
},
|
|
3275
|
+
// --- page transitions ---
|
|
3276
|
+
{ name: "page-fade", primitive: "page-reveal", keyframes: "kui-page-fade" },
|
|
3277
|
+
{ name: "page-slide", primitive: "page-reveal", keyframes: "kui-page-slide" },
|
|
3278
|
+
{ name: "curtain-wipe", primitive: "wipe", keyframes: "kui-curtain-wipe", params: { duration: "800ms" } },
|
|
3279
|
+
{ name: "loading-bar", primitive: "bar", keyframes: "kui-loading-bar" }
|
|
3280
|
+
];
|
|
3281
|
+
function registerThreeD(registry) {
|
|
3282
|
+
return registry.registerPrimitives(THREE_D_PRIMITIVES).registerPresets(THREE_D_PRESETS);
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
// src/effects/index.ts
|
|
3286
|
+
function createRegistry() {
|
|
3287
|
+
const registry = new Registry();
|
|
3288
|
+
registerCore(registry);
|
|
3289
|
+
registerScrollMechanics(registry);
|
|
3290
|
+
registerLayout(registry);
|
|
3291
|
+
registerSvg(registry);
|
|
3292
|
+
registerGestures(registry);
|
|
3293
|
+
registerThreeD(registry);
|
|
3294
|
+
registerCatalog(registry);
|
|
3295
|
+
registerNavigation(registry);
|
|
3296
|
+
registerForms(registry);
|
|
3297
|
+
return registry;
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
export {
|
|
3301
|
+
registerCatalog,
|
|
3302
|
+
PRIMITIVES,
|
|
3303
|
+
PRESETS,
|
|
3304
|
+
COMBOS,
|
|
3305
|
+
registerCore,
|
|
3306
|
+
registerGestures,
|
|
3307
|
+
registerLayout,
|
|
3308
|
+
registerScrollMechanics,
|
|
3309
|
+
registerSvg,
|
|
3310
|
+
registerThreeD,
|
|
3311
|
+
createRegistry
|
|
3312
|
+
};
|