kuinetic 0.1.2 → 0.1.3
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-5DON3UFQ.mjs} +1195 -214
- package/dist/esm/{chunk-LWS4OSLX.mjs → chunk-JT4PZL3A.mjs} +50 -7
- package/dist/esm/{chunk-5EVG7CIY.mjs → chunk-QUVFODSQ.mjs} +27 -13
- 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 +1269 -233
- package/dist/kuinetic.css +1392 -52
- package/dist/kuinetic.js +1268 -232
- 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 +93 -1
- 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 +0 -1
- package/dist/types/effects/catalog/subtree-capture.d.ts +23 -0
- package/dist/types/effects/catalog/text-shared.d.ts +5 -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/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. */
|
|
@@ -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
1
|
/** Re-exported for same-directory catalog siblings; the canonical definition lives in `effects/shared.ts` — neutral ground for every CSS-keyframe category, not just `catalog/`. */
|
|
2
2
|
export { cssPrimitive } from '../shared.js';
|
|
3
|
-
export type { CssPrimitiveOptions } 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
|
/**
|
|
@@ -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
|
*
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PrepareContext } from '../../core/effect-context.js';
|
|
2
|
+
import type { EffectParams } from '../../core/types.js';
|
|
3
|
+
import type { ContinuousSetup } from '../../core/instances.js';
|
|
4
|
+
/**
|
|
5
|
+
* `scroll-spy`'s two authoring shapes.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `primitives.ts` once the container form pushed that file over its own line cap —
|
|
8
|
+
* same reason `stacking-cards` and `scroll-snap` got their own *test* files in commit 69253cf, one
|
|
9
|
+
* level up: a category that outgrows one file gets a file of its own rather than a bigger one.
|
|
10
|
+
*
|
|
11
|
+
* `prepareScrollSpy` is the only export `primitives.ts` needs; everything else here is either mode
|
|
12
|
+
* is private to how it marks `data-kui-active`.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Mark the navigation link pointing at the most recently entered section.
|
|
16
|
+
*
|
|
17
|
+
* Two authoring shapes share this primitive. `sections:` selects the container form — one
|
|
18
|
+
* instance on the element that has both the nav and the sections as descendants, which measures
|
|
19
|
+
* every section itself instead of taking an authored `distance:` per section. Its absence keeps
|
|
20
|
+
* the original per-section form running exactly as shipped, `distance:` and all: that form is
|
|
21
|
+
* public API and this dispatch is the whole of what changes for it, nothing inside it does.
|
|
22
|
+
*
|
|
23
|
+
* @complexity Dispatch only; see whichever form runs.
|
|
24
|
+
* @overallScore 100
|
|
25
|
+
*/
|
|
26
|
+
export declare function prepareScrollSpy(el: Element, params: EffectParams, ctx: PrepareContext): ContinuousSetup;
|
|
@@ -18,6 +18,19 @@ export interface ElementGeometry {
|
|
|
18
18
|
}
|
|
19
19
|
export type Measurer = (el: Element) => ElementGeometry;
|
|
20
20
|
export declare const domGeometry: Measurer;
|
|
21
|
+
/** Resolved `position` for one element. Injected for the same reason `Measurer` is. */
|
|
22
|
+
export type PositionReader = (el: Element) => string;
|
|
23
|
+
export declare const domPosition: PositionReader;
|
|
24
|
+
/**
|
|
25
|
+
* Resolved `top`, in pixels, for whichever element is actually `position: sticky`. Injected for
|
|
26
|
+
* the same reason `Measurer` and `PositionReader` are, and for an extra one of its own: the value
|
|
27
|
+
* is routinely an unresolved CSS expression — `demo/system.css` sets `--kui-pin-offset: 5.5rem`
|
|
28
|
+
* and every pin's `offset-top` defaults to `var(--kui-pin-offset, 0px)` — so a static parse of the
|
|
29
|
+
* authored string cannot recover it. `getComputedStyle` can, because the browser has already
|
|
30
|
+
* resolved the `var()`, the `calc()`, and any `vh` in it.
|
|
31
|
+
*/
|
|
32
|
+
export type OffsetReader = (el: Element) => number;
|
|
33
|
+
export declare const domOffsetTop: OffsetReader;
|
|
21
34
|
export interface TrackOptions {
|
|
22
35
|
/**
|
|
23
36
|
* Scroll distance the effect spans, as an authored CSS length. Defaults to the element's own
|
|
@@ -25,14 +38,59 @@ export interface TrackOptions {
|
|
|
25
38
|
*/
|
|
26
39
|
distance?: string;
|
|
27
40
|
measure?: Measurer;
|
|
41
|
+
positionOf?: PositionReader;
|
|
42
|
+
/**
|
|
43
|
+
* A spacer the caller inserted immediately after `el`, used instead of `geometrySource` to say
|
|
44
|
+
* where `el` really sits in the content.
|
|
45
|
+
*
|
|
46
|
+
* `geometrySource` escapes a sticky subtree by taking its parent, which is only honest when the
|
|
47
|
+
* parent is a tight box around the effect. Pages used to guarantee that by hand-writing a
|
|
48
|
+
* wrapper — `demo/scroll.html` carried a `.scrub-stage` whose entire job was `height: 260vh`.
|
|
49
|
+
* Delete the wrapper and the parent becomes whatever section happens to contain the scrub:
|
|
50
|
+
* measured there, the parent started 926px above it against a 1817px distance, so progress read
|
|
51
|
+
* 51% before the element had even stuck and half the sequence played off screen.
|
|
52
|
+
*
|
|
53
|
+
* The spacer has neither problem. The library inserts it, it is exactly `distance` tall, it is
|
|
54
|
+
* never sticky, and it moves with the content — so it describes the scroll the effect spans
|
|
55
|
+
* without depending on how the author nested anything.
|
|
56
|
+
*
|
|
57
|
+
* It sits *after* `el`, so its top is `el`'s bottom; subtracting `el`'s own height recovers the
|
|
58
|
+
* flow position sticky is hiding. That assumes no margin between the two, which holds because
|
|
59
|
+
* both boxes are the library's own.
|
|
60
|
+
*/
|
|
61
|
+
contentAnchor?: Element;
|
|
62
|
+
/**
|
|
63
|
+
* The element that is actually `position: sticky`, so its resolved `top` can be read back and
|
|
64
|
+
* subtracted from progress.
|
|
65
|
+
*
|
|
66
|
+
* Every caller above computes "the flow position sticky is hiding" — `sourceTop` or
|
|
67
|
+
* `contentAnchor` — but that flow position is where the element's top reaches the *viewport's*
|
|
68
|
+
* top edge, y = 0. Sticky does not wait for that: it engages the instant the flow top reaches
|
|
69
|
+
* `top: <offset>`, `offset` pixels earlier. Without this, progress read 0 for the first `offset`
|
|
70
|
+
* pixels the element was visibly stuck (an author with `--kui-pin-offset: 5.5rem` set — the
|
|
71
|
+
* showcase default — got 88px of dead pin at the start of every `pin-*` and managed
|
|
72
|
+
* `media-scrub`/`horizontal-scroll` effect, and 88px of dead pin at the end, since the same
|
|
73
|
+
* untouched flow top also gated when progress reached 1). Measured on `demo/scroll.html`'s
|
|
74
|
+
* `pin-spacer` card: 71 of those 88px, the rest eaten by an unrelated layout shift in the probe
|
|
75
|
+
* — see `docs/live-testing-backlog.md` D8.
|
|
76
|
+
*
|
|
77
|
+
* Left undefined for primitives that never call `installSticky` themselves (`scroll-progress`,
|
|
78
|
+
* `scroll-spy`, the bare `horizontal-scroll` form, `video-scrub`) — a page-authored sticky
|
|
79
|
+
* ancestor is still escaped by `geometrySource`, but its offset is unknowable from here, so
|
|
80
|
+
* progress there is unchanged: 0 at the sticky ancestor's untouched flow top, same as before.
|
|
81
|
+
*/
|
|
82
|
+
stickyEl?: Element;
|
|
83
|
+
/** Injected for the same reason `measure`/`positionOf` are; see `OffsetReader`. */
|
|
84
|
+
offsetOf?: OffsetReader;
|
|
28
85
|
}
|
|
29
86
|
export type ProgressHandler = (progress: number, frame: ScrollFrame) => void;
|
|
30
87
|
/**
|
|
31
88
|
* Drive `onProgress` with the element's scroll progress in [0, 1].
|
|
32
89
|
*
|
|
33
|
-
* Progress is 0 when the
|
|
34
|
-
*
|
|
35
|
-
*
|
|
90
|
+
* Progress is 0 when the tracked flow position reaches `options.stickyEl`'s sticky offset (the
|
|
91
|
+
* scrollport's top edge, y = 0, when there is no `stickyEl`) and 1 once the scroll has advanced by
|
|
92
|
+
* `distance`. Measurements are cached against the scheduler's epoch, so a frame costs arithmetic
|
|
93
|
+
* rather than a layout flush.
|
|
36
94
|
*
|
|
37
95
|
* @param el - Element whose position drives progress.
|
|
38
96
|
* @param ctx - Prepare context supplying the scheduler and root resolver.
|