kuinetic 0.1.2 → 0.1.4
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/README.md +3 -3
- package/dist/esm/{chunk-WPU33HDL.mjs → chunk-7WMNPIOZ.mjs} +1724 -240
- package/dist/esm/{chunk-LWS4OSLX.mjs → chunk-JT4PZL3A.mjs} +50 -7
- package/dist/esm/{chunk-5EVG7CIY.mjs → chunk-TJDDIQRG.mjs} +42 -16
- package/dist/esm/core/index.mjs +2 -2
- package/dist/esm/effects/index.mjs +2 -2
- package/dist/esm/index.mjs +3 -3
- package/dist/kuinetic.all.js +1813 -262
- package/dist/kuinetic.css +1491 -94
- package/dist/kuinetic.js +1812 -261
- package/dist/types/core/instances.d.ts +34 -3
- package/dist/types/core/path-morph.d.ts +19 -0
- package/dist/types/core/style-plan.d.ts +6 -1
- package/dist/types/core/types.d.ts +99 -1
- package/dist/types/effects/catalog/background-media.d.ts +152 -0
- package/dist/types/effects/catalog/index.d.ts +0 -6
- package/dist/types/effects/catalog/media-shared.d.ts +124 -0
- package/dist/types/effects/catalog/media.d.ts +4 -0
- package/dist/types/effects/catalog/numbers-shared.d.ts +5 -1
- package/dist/types/effects/catalog/shared.d.ts +2 -3
- package/dist/types/effects/catalog/subtree-capture.d.ts +23 -0
- package/dist/types/effects/catalog/text-shared.d.ts +11 -2
- package/dist/types/effects/gestures/index.d.ts +0 -1
- package/dist/types/effects/layout/index.d.ts +0 -2
- package/dist/types/effects/scroll-mechanics/index.d.ts +0 -4
- package/dist/types/effects/scroll-mechanics/scroll-spy.d.ts +26 -0
- package/dist/types/effects/scroll-mechanics/tracker.d.ts +61 -3
- package/dist/types/effects/shared.d.ts +40 -0
- package/dist/types/effects/step-marking.d.ts +99 -0
- package/dist/types/effects/three-d/index.d.ts +0 -7
- package/package.json +9 -2
|
@@ -23,7 +23,7 @@ import type { Cleanup, EffectInstance } from './types.js';
|
|
|
23
23
|
* @complexity O(a) per call in the number of running animations; O(1) space.
|
|
24
24
|
* @overallScore 100
|
|
25
25
|
*/
|
|
26
|
-
export declare function createCssInstance(el: Element, ledger: StyleLedger, animationNames: readonly string[]): EffectInstance;
|
|
26
|
+
export declare function createCssInstance(el: Element, ledger: StyleLedger, animationNames: readonly string[], scrubbed?: boolean): EffectInstance;
|
|
27
27
|
/**
|
|
28
28
|
* What a *finite* JS primitive's setup returns instead of a bare teardown.
|
|
29
29
|
*
|
|
@@ -43,8 +43,34 @@ export interface TimedSetup {
|
|
|
43
43
|
/** Jump to the end state, for `EffectInstance.finish()`. */
|
|
44
44
|
finish?: () => void;
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* What a setup returns when its effect genuinely never ends — a pin, a scroll progress track, a
|
|
48
|
+
* media scrub, a drag handler.
|
|
49
|
+
*
|
|
50
|
+
* This exists because a bare `Cleanup` is ambiguous, and the ambiguity matters. Ten setups in the
|
|
51
|
+
* catalog return one meaning *"there was nothing to do"* — no words to cycle, no fine pointer to
|
|
52
|
+
* follow, a stage that could not be built — and for those, "no completion pending" is the honest
|
|
53
|
+
* report. A pin returns the same shape meaning *"this will still be running an hour from now"*.
|
|
54
|
+
* Reading the two as one thing is what made the fix for D9 regress the other ten; saying which you
|
|
55
|
+
* are is cheap and the only thing that actually distinguishes them.
|
|
56
|
+
*/
|
|
57
|
+
export interface ContinuousSetup {
|
|
58
|
+
cleanup: Cleanup;
|
|
59
|
+
continuous: true;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Mark a teardown as belonging to an effect that never ends.
|
|
63
|
+
*
|
|
64
|
+
* Wrap only the return that means the effect actually started something perpetual — leave an early
|
|
65
|
+
* "nothing to do" bail-out as a bare `Cleanup`, which is what keeps the two distinguishable.
|
|
66
|
+
*
|
|
67
|
+
* @param cleanup - The effect's own teardown.
|
|
68
|
+
* @complexity O(1) time and space.
|
|
69
|
+
* @overallScore 100
|
|
70
|
+
*/
|
|
71
|
+
export declare function continuousSetup(cleanup: Cleanup): ContinuousSetup;
|
|
72
|
+
/** A deferred setup's return: a teardown, optionally with a completion or a never-ends marker. */
|
|
73
|
+
export type SetupResult = Cleanup | TimedSetup | ContinuousSetup;
|
|
48
74
|
/**
|
|
49
75
|
* Turn setup-that-also-starts into a properly gated instance.
|
|
50
76
|
*
|
|
@@ -85,6 +111,11 @@ export interface JsInstanceHooks {
|
|
|
85
111
|
* `finished` after `activate()`, never before.
|
|
86
112
|
*/
|
|
87
113
|
finished?(): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Whether this effect never ends. Read on every access, for the same reason as `finished`: a
|
|
116
|
+
* deferred setup only learns which kind it is once it has run. See `EffectInstance.continuous`.
|
|
117
|
+
*/
|
|
118
|
+
continuous?(): boolean;
|
|
88
119
|
}
|
|
89
120
|
/**
|
|
90
121
|
* Build a JS-rendered instance from a primitive's hooks, filling in the optional operations.
|
|
@@ -21,8 +21,27 @@ export interface Cubic {
|
|
|
21
21
|
c2: Point;
|
|
22
22
|
to: Point;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* One subpath: a run of segments, plus whether the author closed it.
|
|
26
|
+
*
|
|
27
|
+
* A `d` string is not one contour — `M ... Z M ... Z` is a shape with a hole, and an icon with a
|
|
28
|
+
* counter (the inside of an `o`, `a`, `e`) is the common case. Flattening the runs together turns
|
|
29
|
+
* a hole into a detour line across the glyph, so the boundary has to survive parsing.
|
|
30
|
+
*/
|
|
31
|
+
export interface Subpath {
|
|
32
|
+
segments: Cubic[];
|
|
33
|
+
/**
|
|
34
|
+
* True when the author wrote an explicit `Z`/`z`. Kept separate from the geometry because
|
|
35
|
+
* `closeSubpath` already materialises the closing side as a real segment — this flag is what
|
|
36
|
+
* decides whether `Z` is re-emitted, which is what `fill-rule` needs to punch the hole.
|
|
37
|
+
*/
|
|
38
|
+
closed: boolean;
|
|
39
|
+
}
|
|
24
40
|
export interface ParseResult {
|
|
41
|
+
/** Every segment in document order, with subpath boundaries flattened away. */
|
|
25
42
|
segments: Cubic[];
|
|
43
|
+
/** The same segments grouped by subpath. This is the form morphing needs. */
|
|
44
|
+
subpaths: Subpath[];
|
|
26
45
|
/** Present when the path could not be normalised; the caller should warn and not morph. */
|
|
27
46
|
reason?: string;
|
|
28
47
|
}
|
|
@@ -7,10 +7,15 @@ import type { Activation } from './types.js';
|
|
|
7
7
|
* How the animation is started.
|
|
8
8
|
*
|
|
9
9
|
* - `native-timeline` — a scroll/view timeline drives progress; nothing to start.
|
|
10
|
+
* - `scrubbed` — `timeline: pin`. Held paused forever and seeked by `--kui-progress` through the
|
|
11
|
+
* compiled `animation-delay`. Distinct from `deferred`, which is also paused but is waiting to
|
|
12
|
+
* be *released* by an activation: a scrubbed animation must never be released, because
|
|
13
|
+
* `animation-play-state: running` hands it back to the document timeline and it would play
|
|
14
|
+
* forward in real time instead of tracking scroll.
|
|
10
15
|
* - `immediate` — runs as soon as it is applied.
|
|
11
16
|
* - `deferred` — held at its from-state until an activation fires.
|
|
12
17
|
*/
|
|
13
|
-
export type Gate = 'native-timeline' | 'immediate' | 'deferred';
|
|
18
|
+
export type Gate = 'native-timeline' | 'scrubbed' | 'immediate' | 'deferred';
|
|
14
19
|
export interface StylePlan {
|
|
15
20
|
/** Custom properties and animation longhands to write, in application order. */
|
|
16
21
|
properties: Record<string, string>;
|
|
@@ -29,6 +29,17 @@ export declare const CHANNEL: {
|
|
|
29
29
|
readonly color: "color";
|
|
30
30
|
readonly stroke: "stroke";
|
|
31
31
|
readonly text: "text";
|
|
32
|
+
/**
|
|
33
|
+
* Skew is the one transform CSS never gave an independent property to — there is no `skew:`
|
|
34
|
+
* beside `translate:`/`rotate:`/`scale:`, so it can only be written through the `transform`
|
|
35
|
+
* shorthand. That makes it its own channel: anything writing `transform` clobbers the whole
|
|
36
|
+
* shorthand, so every primitive that does is on this channel, whatever the transform is for.
|
|
37
|
+
* `scroll-skew` is one member; `flip-face` (`effects/three-d`) and `flip-3d`
|
|
38
|
+
* (`effects/catalog/core`, the `flip-in-*`/`flip-out-*` family) are the other two — both need
|
|
39
|
+
* the `perspective()` transform *function* for an element to have depth on itself, which
|
|
40
|
+
* likewise only exists inside `transform`.
|
|
41
|
+
*/
|
|
42
|
+
readonly skew: "skew";
|
|
32
43
|
};
|
|
33
44
|
/**
|
|
34
45
|
* A CSS property group an effect writes to. Two effects may only be composed in one `data-kui`
|
|
@@ -51,7 +62,18 @@ export type Activation = 'load' | 'enter' | 'hover' | 'focus' | 'click' | 'manua
|
|
|
51
62
|
* *reverse when the user scrolls back*, which a time-based reveal does not. These are different
|
|
52
63
|
* animation models, not fallbacks for each other. See docs/design.md §5.
|
|
53
64
|
*/
|
|
54
|
-
|
|
65
|
+
/**
|
|
66
|
+
* `pin` is the odd one out and deliberately so. `view`/`scroll` map to a native CSS
|
|
67
|
+
* `animation-timeline`; `pin` has no native equivalent because CSS has no way to say "drive this
|
|
68
|
+
* from another element's published progress". A pinned element's `view()` timeline *stalls* — it
|
|
69
|
+
* stops travelling through the viewport, so its progress freezes for the whole hold, which is
|
|
70
|
+
* exactly the span an author wants to animate across. `pin` fills that hole: the animation is
|
|
71
|
+
* held paused and seeked with a negative `animation-delay` proportional to `--kui-progress`,
|
|
72
|
+
* which the scroll-mechanics primitives already publish every frame. Verified in Chrome:
|
|
73
|
+
* progress 0/0.25/0.5/0.75/1 renders -180/-135/-90/-45/0deg on a half-turn keyframe, re-seeks
|
|
74
|
+
* live on every variable change, and composes with `--kui-stagger`.
|
|
75
|
+
*/
|
|
76
|
+
export type Timeline = 'time' | 'view' | 'scroll' | 'pointer' | 'pin';
|
|
55
77
|
export type Renderer = 'css-keyframes' | 'waapi' | 'javascript';
|
|
56
78
|
/** Governs the performance budget a primitive is held to in tests. */
|
|
57
79
|
export type PerfClass = 'compositor' | 'paint' | 'layout' | 'continuous' | 'dom-transform';
|
|
@@ -106,6 +128,20 @@ export interface EffectInstance {
|
|
|
106
128
|
finish(): void;
|
|
107
129
|
/** Resolves when the effect completes. Resolves — never rejects — on cancel. */
|
|
108
130
|
readonly finished: Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Whether this effect has no end — a pin, a scroll progress track, a media scrub.
|
|
133
|
+
*
|
|
134
|
+
* Such an effect keeps an already-resolved `finished` on purpose, so that composing it with a
|
|
135
|
+
* one-shot on the same element does not stop the one-shot ever reporting complete. That is
|
|
136
|
+
* right for composition and wrong for the element's own reported state: an element whose *only*
|
|
137
|
+
* effects are continuous would otherwise read `data-kui-state="finished"` from the first
|
|
138
|
+
* microtask onwards and never say anything else, which is a lie an author cannot style around.
|
|
139
|
+
*
|
|
140
|
+
* The animator therefore reads this to decide whether there is any completion to wait for at
|
|
141
|
+
* all. Read after `activate()`, never before — a deferred setup only learns which kind it is
|
|
142
|
+
* once it has actually run.
|
|
143
|
+
*/
|
|
144
|
+
readonly continuous?: boolean;
|
|
109
145
|
/** Release every listener, observer, subscription, and inserted node. */
|
|
110
146
|
destroy(): void;
|
|
111
147
|
}
|
|
@@ -182,6 +218,43 @@ export interface Primitive {
|
|
|
182
218
|
defaultActivation?: Activation;
|
|
183
219
|
perfClass: PerfClass;
|
|
184
220
|
reducedMotion: ReducedMotionPolicy;
|
|
221
|
+
/**
|
|
222
|
+
* Whether this primitive, left alone to finish with nobody calling `reset()`/`destroy()`, ends
|
|
223
|
+
* with the element's markup indistinguishable from what the author wrote.
|
|
224
|
+
*
|
|
225
|
+
* There are two separate teardown contracts, and `test/browser/teardown-sweep.test.mjs` used to
|
|
226
|
+
* conflate them into one check:
|
|
227
|
+
*
|
|
228
|
+
* - **Interruption restores.** Calling `reset()` mid-effect puts the markup back byte-for-byte.
|
|
229
|
+
* This is unconditional and every primitive owes it — it is not gated by this field at all, and
|
|
230
|
+
* never will be, because a caller can `reset()` anything at any moment.
|
|
231
|
+
* - **Natural finish restores** (this field). The effect, left alone to run to completion, ends
|
|
232
|
+
* with the markup back as authored. This is `slat-assemble`'s whole point: it disassembles a
|
|
233
|
+
* picture into slats and reassembles it, and the slats are scaffolding that must be gone once
|
|
234
|
+
* the illusion lands — not just when someone happens to call `reset()`.
|
|
235
|
+
*
|
|
236
|
+
* **Defaults to `false` — a primitive has to opt in.** The tempting default is `true` ("assume it
|
|
237
|
+
* restores unless told otherwise"), on the theory that a forgotten declaration should never
|
|
238
|
+
* silently skip a check. That is the wrong default in this codebase specifically: the majority of
|
|
239
|
+
* the catalog is JS/CSS-driven effects that either never resolve `finished` at all (ambient
|
|
240
|
+
* loops, hover/pointer effects, anything `reducedMotion: 'disable'` marks as unboundable) or
|
|
241
|
+
* finish *by design* in a state that differs from authored markup — `data-kui-state="finished"`
|
|
242
|
+
* and the `--kui-*` custom properties `createCssInstance` leaves in place are the point, not a
|
|
243
|
+
* bug, and every `css-keyframes` primitive in the catalog would need an explicit opt-out under a
|
|
244
|
+
* `true` default. Defaulting to `true` would have meant "almost every primitive needs a
|
|
245
|
+
* non-default declaration," which is the sign from the design notes above that a default is
|
|
246
|
+
* wrong. `false` means "no claim is being made" for the ordinary case, and only the small set of
|
|
247
|
+
* primitives that build their own temporary DOM as scaffolding — `slat-assemble` today — assert
|
|
248
|
+
* this explicitly. The unconditional interruption check above is what actually closes the "a new
|
|
249
|
+
* scaffold-building primitive forgets to opt in" gap: `reset()` is still guaranteed to restore it
|
|
250
|
+
* even if nobody ever wrote `restoresOnFinish: true` for it.
|
|
251
|
+
*
|
|
252
|
+
* `test/browser/teardown-sweep.test.mjs` reads this to decide whether a primitive owes a
|
|
253
|
+
* natural-finish check at all, and its failure messages name which law was broken instead of the
|
|
254
|
+
* generic "leaves synthetic nodes behind" that previously misdescribed a `style=""` leak as extra
|
|
255
|
+
* DOM nodes for a whole debugging session.
|
|
256
|
+
*/
|
|
257
|
+
restoresOnFinish?: boolean;
|
|
185
258
|
/**
|
|
186
259
|
* JS-side setup. Returns a lifecycle handle, **not** a teardown function.
|
|
187
260
|
*
|
|
@@ -202,6 +275,25 @@ export interface Preset {
|
|
|
202
275
|
params?: Record<string, string>;
|
|
203
276
|
/** CSS `@keyframes` name this preset animates, when renderer is `css-keyframes`. */
|
|
204
277
|
keyframes?: string;
|
|
278
|
+
/**
|
|
279
|
+
* This effect begins from a state the visitor must not see — invisible, displaced, unsplit, or
|
|
280
|
+
* un-assembled — so painting the element at its rest state before the runtime installs that
|
|
281
|
+
* from-state is a visible flash. `scripts/generate-preset-css.mjs` emits a pre-JS cloak rule for
|
|
282
|
+
* every preset that declares it; see the `kui.cloak` layer in `src/css/base.css`.
|
|
283
|
+
*
|
|
284
|
+
* **This has to be declared, not derived.** Both plausible signals were measured and both are
|
|
285
|
+
* wrong. Keying on "owns the opacity channel" cloaks `hamburger-to-x`, `checkbox-draw` and
|
|
286
|
+
* `dropdown-open` — stateful controls with no entrance — while missing `blur-in` and `bounce-in`.
|
|
287
|
+
* Keying on "supports the entrance timelines" cloaks `pin-section`, `scroll-spy` and
|
|
288
|
+
* `horizontal-scroll`, and hiding a pinned section at opacity 0 is precisely the failure the
|
|
289
|
+
* opt-in rule in `base.css` was written to avoid. Whether a name is an entrance with a
|
|
290
|
+
* from-state worth hiding is a fact only its author knows, which is why it sits here beside
|
|
291
|
+
* `params` rather than being inferred from either.
|
|
292
|
+
*
|
|
293
|
+
* Exits are deliberately excluded: `fade-out` starts at the rest state, so there is nothing to
|
|
294
|
+
* hide and cloaking it would blank an element that should be visible until it leaves.
|
|
295
|
+
*/
|
|
296
|
+
cloak?: boolean;
|
|
205
297
|
}
|
|
206
298
|
export type ResolvedParams = Record<string, string>;
|
|
207
299
|
/** One effect segment parsed out of a `data-kui` value. */
|
|
@@ -235,5 +327,11 @@ export interface InstanceState {
|
|
|
235
327
|
attributes: AttributeLedger;
|
|
236
328
|
/** Aborted on release, detaching bindings and primitive listeners. */
|
|
237
329
|
controller: AbortController;
|
|
330
|
+
/**
|
|
331
|
+
* Releases a one-shot `enter` binding, set only for that activation and cleared once spent.
|
|
332
|
+
* Present so `Animator.activate` can disarm an observer a programmatic activation just made
|
|
333
|
+
* redundant; a toggle activation (`hover`/`focus`/`click`) deliberately leaves this undefined.
|
|
334
|
+
*/
|
|
335
|
+
releaseActivation?: Cleanup;
|
|
238
336
|
status: 'pending' | 'ready' | 'running' | 'finished' | 'failed';
|
|
239
337
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { PrepareContext } from '../../core/effect-context.js';
|
|
2
|
+
import type { Cleanup } from '../../core/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether an authored `src` names a video platform's web page instead of a media file.
|
|
5
|
+
*
|
|
6
|
+
* @complexity O(n) time in value length; O(1) space.
|
|
7
|
+
* @overallScore 100
|
|
8
|
+
*/
|
|
9
|
+
export declare function isVideoPageUrl(value: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Named focal points, as `object-position` percentage pairs.
|
|
12
|
+
*
|
|
13
|
+
* A `cover` fit crops, and which part it crops away is the whole editorial decision — the classic
|
|
14
|
+
* failure is a portrait cropped through the subject's chin. Percentages rather than the equivalent
|
|
15
|
+
* CSS keywords (`top`, `left top`) because the pair is written into one property from two halves,
|
|
16
|
+
* and `50% 0%` composes where `center top` would need its own keyword-order rules.
|
|
17
|
+
*/
|
|
18
|
+
declare const FOCAL_POINTS: {
|
|
19
|
+
readonly center: "50% 50%";
|
|
20
|
+
readonly top: "50% 0%";
|
|
21
|
+
readonly bottom: "50% 100%";
|
|
22
|
+
readonly left: "0% 50%";
|
|
23
|
+
readonly right: "100% 50%";
|
|
24
|
+
readonly 'top-left': "0% 0%";
|
|
25
|
+
readonly 'top-right': "100% 0%";
|
|
26
|
+
readonly 'bottom-left': "0% 100%";
|
|
27
|
+
readonly 'bottom-right': "100% 100%";
|
|
28
|
+
};
|
|
29
|
+
export type FocalPoint = keyof typeof FOCAL_POINTS;
|
|
30
|
+
/** The nine named focal points, in the order the parameter schema declares them. */
|
|
31
|
+
export declare const FOCAL_POINT_NAMES: FocalPoint[];
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a focal point name to an `object-position` value.
|
|
34
|
+
*
|
|
35
|
+
* @complexity O(1) time and space.
|
|
36
|
+
* @overallScore 100
|
|
37
|
+
*/
|
|
38
|
+
export declare function focalPosition(focus: string): string;
|
|
39
|
+
/** How a background clip decides when to run. */
|
|
40
|
+
export type AutoplayMode = 'in-view' | 'always' | 'never';
|
|
41
|
+
export interface BackgroundMediaOptions {
|
|
42
|
+
/** URL or path to the image or video, already screened by `mediaSource`. */
|
|
43
|
+
src: string;
|
|
44
|
+
/** Still shown before a video's first frame; `''` for none. */
|
|
45
|
+
poster: string;
|
|
46
|
+
/** `object-fit` for the layer. */
|
|
47
|
+
fit: 'cover' | 'contain';
|
|
48
|
+
/** `object-position` for the layer, already resolved from a named focal point. */
|
|
49
|
+
position: string;
|
|
50
|
+
/** Scrim colour painted over the media and under the author's children. */
|
|
51
|
+
overlay: string;
|
|
52
|
+
/** Scrim opacity, `0`–`1`. */
|
|
53
|
+
overlayOpacity: number;
|
|
54
|
+
/** When a clip plays. Ignored for an image. */
|
|
55
|
+
autoplay: AutoplayMode;
|
|
56
|
+
/** `playbackRate` for a clip. Ignored for an image. */
|
|
57
|
+
rate: number;
|
|
58
|
+
/** Whether a clip restarts when it ends. Ignored for an image. */
|
|
59
|
+
loop: boolean;
|
|
60
|
+
/** Suppresses autoplay, leaving the poster frame standing. */
|
|
61
|
+
reducedMotion: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface BackgroundLayer {
|
|
64
|
+
/** Stop observing and delete every node this effect added, leaving the author's own as written. */
|
|
65
|
+
remove: Cleanup;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether a scrim colour is actually asking for a scrim.
|
|
69
|
+
*
|
|
70
|
+
* `transparent` is the default so that the parameter stays a real `type: 'color'` — validated by
|
|
71
|
+
* the same screen every other colour goes through — rather than an empty string that no colour
|
|
72
|
+
* grammar accepts. It also happens to be the honest spelling of "no scrim", so an author who
|
|
73
|
+
* writes it explicitly gets exactly what the default gives them.
|
|
74
|
+
*
|
|
75
|
+
* @complexity O(1) time and space.
|
|
76
|
+
* @overallScore 100
|
|
77
|
+
*/
|
|
78
|
+
export declare function paintsOverlay(options: BackgroundMediaOptions): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Whether an authored `src` names a video rather than an image.
|
|
81
|
+
*
|
|
82
|
+
* @param src - Author-supplied path, already accepted as same-origin.
|
|
83
|
+
* @returns `true` when the path's final extension is a known video container.
|
|
84
|
+
* @complexity O(n) time in path length; O(1) space.
|
|
85
|
+
* @overallScore 100
|
|
86
|
+
*/
|
|
87
|
+
export declare function isVideoSource(src: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Screen an authored media URL before it can reach `src`/`poster`.
|
|
90
|
+
*
|
|
91
|
+
* **Cross-origin URLs are allowed here, unlike `media-scrub`'s frame pattern, and that is a
|
|
92
|
+
* deliberate divergence rather than an oversight.** `isSameOriginPath` (`core/params.ts`) refuses
|
|
93
|
+
* every absolute URL on the grounds that `data-kui` is not always written by the site owner — a CMS
|
|
94
|
+
* field, a comment — so an unconstrained `src` turns the visitor's browser into an
|
|
95
|
+
* attacker-directed request tool. That reasoning still stands, and it is why the scheme allowlist
|
|
96
|
+
* above is kept tight.
|
|
97
|
+
*
|
|
98
|
+
* It is not the right trade for *this* parameter, though. A background is the one media case where
|
|
99
|
+
* the file routinely does not live on the page's own origin — an S3 bucket, a CDN, an asset host —
|
|
100
|
+
* and `media-scrub`'s `src:` is materially more dangerous than this one for a second reason: it is
|
|
101
|
+
* a `{i}` *template* that fires one request per frame, so a two-hundred-frame sequence is a
|
|
102
|
+
* two-hundred-request channel where this is a single fixed fetch. A site that does accept untrusted
|
|
103
|
+
* `data-kui` should set a `Content-Security-Policy` with `img-src`/`media-src`, which is the
|
|
104
|
+
* control actually designed for this and the one thing a library cannot do on the consumer's
|
|
105
|
+
* behalf.
|
|
106
|
+
*
|
|
107
|
+
* @param authored - Author-supplied value; `''` when none was written.
|
|
108
|
+
* @param name - Parameter name, for the diagnostic.
|
|
109
|
+
* @returns The URL, or `''` when it was rejected or absent.
|
|
110
|
+
* @complexity O(n) time in value length; O(1) space.
|
|
111
|
+
* @overallScore 100
|
|
112
|
+
*/
|
|
113
|
+
export declare function mediaSource(authored: string, name: string, ctx: PrepareContext): string;
|
|
114
|
+
/**
|
|
115
|
+
* Apply the authored playback policy to a clip.
|
|
116
|
+
*
|
|
117
|
+
* `autoplay:` is the opt-out the three real cases need. `in-view` (default) pairs the clip with the
|
|
118
|
+
* viewport. `always` is for a short hero clip that must never be caught mid-stall by a visibility
|
|
119
|
+
* heuristic. `never` installs the clip and leaves it on its poster, which is also what any mode
|
|
120
|
+
* degrades to under a reduced-motion preference.
|
|
121
|
+
*
|
|
122
|
+
* @returns A teardown that stops any observer and leaves the clip paused.
|
|
123
|
+
* @complexity O(1) time and space.
|
|
124
|
+
* @overallScore 100
|
|
125
|
+
*/
|
|
126
|
+
export declare function startPlayback(video: HTMLVideoElement, win: Window, options: BackgroundMediaOptions): Cleanup;
|
|
127
|
+
/**
|
|
128
|
+
* Play a background clip while any part of it is on screen, and pause it the rest of the time.
|
|
129
|
+
*
|
|
130
|
+
* This is the primitive's own observer rather than the animator's `on:enter` gate, because the two
|
|
131
|
+
* answer different questions: `on:enter` fires once and stays fired, which is right for a reveal
|
|
132
|
+
* and wrong for a clip that should stop costing decode budget the moment it scrolls away.
|
|
133
|
+
*
|
|
134
|
+
* `threshold: 0` — any intersection at all — rather than the `0.25` a card-sized video wants.
|
|
135
|
+
* `intersectionRatio` is measured against the *target*, and this target is usually a full section:
|
|
136
|
+
* one taller than four viewports can never reach a ratio of 0.25 no matter where it is scrolled,
|
|
137
|
+
* so a fractional threshold is a clip that silently never plays.
|
|
138
|
+
*
|
|
139
|
+
* @returns A teardown that disconnects the observer and leaves the clip paused.
|
|
140
|
+
* @complexity O(1) time and space.
|
|
141
|
+
* @overallScore 100
|
|
142
|
+
*/
|
|
143
|
+
export declare function autoplayInView(video: HTMLVideoElement, win: Window): Cleanup;
|
|
144
|
+
/**
|
|
145
|
+
* Create the backdrop layer and hand back the handle that deletes it again.
|
|
146
|
+
*
|
|
147
|
+
* @param el - The host element, which keeps every child the author wrote.
|
|
148
|
+
* @complexity O(1) time and space.
|
|
149
|
+
* @overallScore 100
|
|
150
|
+
*/
|
|
151
|
+
export declare function installBackgroundMedia(el: Element, ctx: PrepareContext, options: BackgroundMediaOptions): BackgroundLayer;
|
|
152
|
+
export {};
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { Registry } from '../../core/registry.js';
|
|
2
|
-
export { AMBIENT_PRESETS, AMBIENT_PRIMITIVES } from './ambient.js';
|
|
3
|
-
export { FEEDBACK_PRESETS, FEEDBACK_PRIMITIVES } from './feedback.js';
|
|
4
|
-
export { INTERACTION_PRESETS, INTERACTION_PRIMITIVES } from './interaction.js';
|
|
5
|
-
export { MEDIA_PRESETS, MEDIA_PRIMITIVES } from './media.js';
|
|
6
|
-
export { NUMBERS_PRESETS, NUMBERS_PRIMITIVES } from './numbers.js';
|
|
7
|
-
export { TEXT_PRESETS, TEXT_PRIMITIVES } from './text.js';
|
|
8
2
|
/**
|
|
9
3
|
* Register the CSS-oriented catalog sections.
|
|
10
4
|
*
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Cleanup, EffectParams } from '../../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* DOM-surgery helpers behind `slat-assemble` (catalog section G).
|
|
4
|
+
*
|
|
5
|
+
* `text-shared.ts` is the template this follows: build a synthetic child tree once at activation,
|
|
6
|
+
* index each child with a plain custom property, and let CSS (`media.css`) own every frame from
|
|
7
|
+
* there — this module never animates anything itself, it only builds the tree and computes the
|
|
8
|
+
* per-child numbers CSS reads.
|
|
9
|
+
*/
|
|
10
|
+
/** Which way the image is cut into strips. `angle:` generalises this to any degree. */
|
|
11
|
+
export type SlatAxis = 'vertical' | 'horizontal';
|
|
12
|
+
/** How the per-slat stagger order relates to a slat's geometric position. */
|
|
13
|
+
export type SlatFrom = 'alternate' | 'start' | 'end' | 'edges' | 'random-ish';
|
|
14
|
+
/**
|
|
15
|
+
* Map a slat's geometric position to its stagger rank.
|
|
16
|
+
*
|
|
17
|
+
* The rank is what `--kui-i` carries into `media.css`'s `animation-delay: calc(delay + i *
|
|
18
|
+
* stagger)` — the same formula `text.css` already uses for `.kui-split-item` — so `from:` only
|
|
19
|
+
* ever changes which number lands in that one slot, never the delay formula itself. Ties are
|
|
20
|
+
* fine: two slats sharing a rank simply animate on the same beat, which is a legitimate look
|
|
21
|
+
* (`edges`'s two starting slats are meant to move together), not a bug.
|
|
22
|
+
*
|
|
23
|
+
* - `start` — geometric order, left-to-right or top-to-bottom.
|
|
24
|
+
* - `end` — the reverse.
|
|
25
|
+
* - `edges` — outside-in: both ends rank 0, the middle ranks highest, so the seam closes last.
|
|
26
|
+
* - `alternate` (default) — a zig-zag across the whole run (0, N-1, 1, N-2, 2, …), so slats land
|
|
27
|
+
* from alternating ends instead of sweeping one direction.
|
|
28
|
+
* - `random-ish` — a deterministic scatter (see `GOLDEN_RATIO_CONJUGATE`), not real randomness.
|
|
29
|
+
*
|
|
30
|
+
* @complexity O(1) time and space.
|
|
31
|
+
* @overallScore 100
|
|
32
|
+
*/
|
|
33
|
+
export declare function slatOrder(index: number, count: number, from: SlatFrom): number;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the authored `angle:` to degrees, falling back to whatever `axis:` names.
|
|
36
|
+
*
|
|
37
|
+
* `angle:` is the general parameter and `axis:` the two-keyword shorthand that predates it, so an
|
|
38
|
+
* authored angle wins and an absent one reads the axis. Both `45` and `45deg` are accepted, as are
|
|
39
|
+
* `turn` and `rad`, because an author who writes an angle should not have to remember which of the
|
|
40
|
+
* three this particular parameter takes.
|
|
41
|
+
*
|
|
42
|
+
* Normalised to `[0, 180)`: a band at 200° is the same set of bands as one at 20°, only numbered
|
|
43
|
+
* from the other end, and collapsing that here means the geometry below never has to think about
|
|
44
|
+
* sign or wrap.
|
|
45
|
+
*
|
|
46
|
+
* @complexity O(1) time and space.
|
|
47
|
+
* @overallScore 100
|
|
48
|
+
*/
|
|
49
|
+
export declare function slatAngleDegrees(authored: string, axis: SlatAxis): number;
|
|
50
|
+
/**
|
|
51
|
+
* The unit vector each slat travels along in its from-state: the band's own long axis.
|
|
52
|
+
*
|
|
53
|
+
* At 0° this is `(0, 1)` — vertical columns sliding up and down, which is exactly what the two
|
|
54
|
+
* hand-written axis keyframes did before this generalised them. At 90° it is `(-1, 0)`, horizontal
|
|
55
|
+
* rows sliding sideways. Perpendicular travel is deliberately not offered: a band moving across its
|
|
56
|
+
* own width exposes the gap it left, and the point of the effect is strips that are *out of line*,
|
|
57
|
+
* not strips that are missing.
|
|
58
|
+
*
|
|
59
|
+
* @complexity O(1) time and space.
|
|
60
|
+
* @overallScore 100
|
|
61
|
+
*/
|
|
62
|
+
export declare function slatTravelVector(angleDegrees: number): {
|
|
63
|
+
x: number;
|
|
64
|
+
y: number;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The `clip-path` for one band of an angled slice, in pixels against the stage's measured box.
|
|
68
|
+
*
|
|
69
|
+
* Pixels, not percentages, on purpose. A percentage polygon is resolved against each axis
|
|
70
|
+
* separately, so "45°" on a 3:4 card would paint at some other angle entirely — the author asked
|
|
71
|
+
* for a diagonal and would get a diagonal that changes as the card resizes. Measuring means the
|
|
72
|
+
* angle is the angle, at any aspect ratio, and the caller re-runs this whenever the box changes.
|
|
73
|
+
*
|
|
74
|
+
* The band is the region between two parallel lines perpendicular to `angleDegrees`, extended far
|
|
75
|
+
* enough sideways to cover the stage from any rotation; `overflow: clip` on the stage trims the
|
|
76
|
+
* overhang. Each boundary is pushed out by {@link BAND_OVERLAP_PX} for the same reason the axis
|
|
77
|
+
* version added a pixel to its width: `span / count` almost never lands on a whole pixel, and the
|
|
78
|
+
* remainder renders as a hairline of backdrop between neighbours once they have landed.
|
|
79
|
+
*
|
|
80
|
+
* @param index - Band number, `0` to `count - 1`, in geometric order along the cut normal.
|
|
81
|
+
* @param box - The stage's measured pixel size, re-read by the caller on every layout change.
|
|
82
|
+
* @complexity O(1) time and space.
|
|
83
|
+
* @overallScore 100
|
|
84
|
+
*/
|
|
85
|
+
export declare function slatBandClip(index: number, count: number, angleDegrees: number, box: {
|
|
86
|
+
width: number;
|
|
87
|
+
height: number;
|
|
88
|
+
}): string;
|
|
89
|
+
export interface SlatStage {
|
|
90
|
+
/** The `aria-hidden` wrapper holding every slat, positioned over the source `<img>`. */
|
|
91
|
+
stage: HTMLElement;
|
|
92
|
+
/** One element per slat, in geometric order. */
|
|
93
|
+
slats: HTMLElement[];
|
|
94
|
+
/** Remove every generated node and stop watching the image's box. The source `<img>` was never modified, so nothing to restore on it. */
|
|
95
|
+
restore: Cleanup;
|
|
96
|
+
}
|
|
97
|
+
/** Validated build options for {@link installSlatStage}, grouped to keep its own signature small. */
|
|
98
|
+
export interface SlatBuildOptions {
|
|
99
|
+
count: number;
|
|
100
|
+
/** Resolved cut angle in degrees, `[0, 180)`. See {@link slatAngleDegrees}. */
|
|
101
|
+
angleDegrees: number;
|
|
102
|
+
from: SlatFrom;
|
|
103
|
+
fold: boolean;
|
|
104
|
+
}
|
|
105
|
+
export declare function installSlatStage(el: Element, doc: Document, win: Window, options: SlatBuildOptions): SlatStage | null;
|
|
106
|
+
/**
|
|
107
|
+
* Forward validated timing params onto the stage, so `media.css`'s `--kui-i * --kui-stagger`
|
|
108
|
+
* delay formula (the same shape `text-shared.ts`'s `applyStaggerVars` writes for split-text) can
|
|
109
|
+
* read them by inheritance into every `.kui-slat-item`.
|
|
110
|
+
*
|
|
111
|
+
* @complexity O(1) time and space.
|
|
112
|
+
* @overallScore 100
|
|
113
|
+
*/
|
|
114
|
+
export declare function applySlatTimingVars(stage: HTMLElement, params: EffectParams): void;
|
|
115
|
+
/**
|
|
116
|
+
* Total time before every staggered slat has finished landing: the last slat's `--kui-i *
|
|
117
|
+
* --kui-stagger` delay, plus its own duration — `splitRevealFinishMs`'s reasoning, over slat
|
|
118
|
+
* count instead of grapheme/word/line count.
|
|
119
|
+
*
|
|
120
|
+
* @param count - Slat count; zero means nothing was ever built, so nothing is animating.
|
|
121
|
+
* @complexity O(1) time and space.
|
|
122
|
+
* @overallScore 100
|
|
123
|
+
*/
|
|
124
|
+
export declare function slatAssembleFinishMs(params: EffectParams, count: number): number;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Preset, Primitive } from '../../core/types.js';
|
|
2
2
|
import type { Registry } from '../../core/registry.js';
|
|
3
|
+
export declare const MEDIA_CSS_PRIMITIVES: Primitive[];
|
|
4
|
+
export declare const MEDIA_CSS_PRESETS: Preset[];
|
|
5
|
+
export declare const MEDIA_JS_PRIMITIVES: Primitive[];
|
|
6
|
+
export declare const MEDIA_JS_PRESETS: Preset[];
|
|
3
7
|
export declare const MEDIA_PRIMITIVES: Primitive[];
|
|
4
8
|
export declare const MEDIA_PRESETS: Preset[];
|
|
5
9
|
/**
|
|
@@ -14,7 +14,11 @@ export interface CountLayers {
|
|
|
14
14
|
* mid-tick — so a screen reader is told the final value and nothing in between.
|
|
15
15
|
*/
|
|
16
16
|
srOnly: HTMLElement;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Remove both layers, putting the authored children back as they were. Not the value the
|
|
19
|
+
* counter reached: that number is the library's, and teardown gives the element back to the
|
|
20
|
+
* author — including the `42` that was there as a no-JS fallback.
|
|
21
|
+
*/
|
|
18
22
|
restore: Cleanup;
|
|
19
23
|
}
|
|
20
24
|
/**
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
/** Re-exported for same-directory catalog siblings; the canonical
|
|
2
|
-
export { cssPrimitive } from '../shared.js';
|
|
3
|
-
export type { CssPrimitiveOptions } from '../shared.js';
|
|
1
|
+
/** Re-exported for same-directory catalog siblings; the canonical definitions live in `effects/shared.ts` — neutral ground for every effect category, not just `catalog/`. */
|
|
2
|
+
export { cssPrimitive, TIMELINE_AGNOSTIC, TRIGGER_DELAY_PARAM } from '../shared.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Cleanup } from '../../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot an element's children so teardown can put back exactly what the author wrote.
|
|
4
|
+
*
|
|
5
|
+
* Every DOM-rewriting primitive in this folder used to restore by assigning `textContent`, which
|
|
6
|
+
* is faithful only when the authored content was plain text — the one case every restore test
|
|
7
|
+
* happened to author. Given `<p><strong>one</strong> two</p>` it flattened the `<strong>`; given
|
|
8
|
+
* `<span>42</span>` a counter wrote back the value it had counted to instead of the author's own
|
|
9
|
+
* content. Both are invisible on a page that never tears down, and both destroy author markup in
|
|
10
|
+
* any app that unmounts.
|
|
11
|
+
*
|
|
12
|
+
* The captured nodes are the live originals, merely detached by the caller's own overwrite.
|
|
13
|
+
* Restoring re-inserts those same node objects, so nested structure, attributes, and any listeners
|
|
14
|
+
* bound to them survive — none of which would come back from re-parsing a string.
|
|
15
|
+
*
|
|
16
|
+
* Capture before overwriting: this reads the children as they stand when it is called.
|
|
17
|
+
*
|
|
18
|
+
* @param el - Element whose children are about to be replaced.
|
|
19
|
+
* @returns A cleanup that discards whatever is there now and reinstates the captured children.
|
|
20
|
+
* @complexity O(n) time and space in child-node count.
|
|
21
|
+
* @overallScore 100
|
|
22
|
+
*/
|
|
23
|
+
export declare function captureChildren(el: Element): Cleanup;
|
|
@@ -4,9 +4,12 @@ export type SplitUnit = 'chars' | 'words' | 'lines';
|
|
|
4
4
|
export interface SplitLayers {
|
|
5
5
|
/** `aria-hidden` container the caller populates with decorative markup or text. */
|
|
6
6
|
decorative: HTMLElement;
|
|
7
|
-
/** The element's text at the moment splitting began. */
|
|
7
|
+
/** The element's text at the moment splitting began, trimmed for display. */
|
|
8
8
|
originalText: string;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Remove both layers and put the authored children back exactly as they were — element nodes
|
|
11
|
+
* and untrimmed whitespace included, not the trimmed text `originalText` renders.
|
|
12
|
+
*/
|
|
10
13
|
restore: Cleanup;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
@@ -114,6 +117,12 @@ export declare function createStepRunner(win: Window, options: StepRunOptions):
|
|
|
114
117
|
* wrapping the space itself, the same way `appendWordSpans` already leaves inter-word whitespace
|
|
115
118
|
* unwrapped.
|
|
116
119
|
*
|
|
120
|
+
* Per the CSS Text line-breaking spec, browsers insert a line-break opportunity between any two
|
|
121
|
+
* adjacent atomic inline-level boxes, even without whitespace between them — so a bare run of
|
|
122
|
+
* `.kui-split-item` spans could wrap mid-word. Each word's char-spans are nested inside a shared
|
|
123
|
+
* `.kui-split-word` wrapper (itself `display: inline-block`) so the only break opportunities left
|
|
124
|
+
* are between words, same as the source text.
|
|
125
|
+
*
|
|
117
126
|
* @complexity O(n) time and space in grapheme count.
|
|
118
127
|
* @overallScore 100
|
|
119
128
|
*/
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { Registry } from '../../core/registry.js';
|
|
2
|
-
export { SCROLL_PRIMITIVES } from './primitives.js';
|
|
3
|
-
export { SCROLL_PRESETS } from './presets.js';
|
|
4
|
-
export { domGeometry, progressFrom, trackProgress } from './tracker.js';
|
|
5
|
-
export type { ElementGeometry, Measurer, TrackOptions } from './tracker.js';
|
|
6
2
|
/**
|
|
7
3
|
* Register the scroll-mechanics catalog.
|
|
8
4
|
*
|