kuinetic 0.2.2 → 0.2.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 +2 -2
- package/dist/esm/{chunk-XDDJVANU.mjs → chunk-EHS45PIK.mjs} +2346 -536
- package/dist/esm/{chunk-K2M7O6W5.mjs → chunk-FASAEQDR.mjs} +32 -1
- package/dist/esm/{chunk-BBQ22D7A.mjs → chunk-MZQG2VMG.mjs} +202 -2
- package/dist/esm/core/index.mjs +21 -3
- package/dist/esm/effects/index.mjs +2 -2
- package/dist/esm/index.mjs +35 -4
- package/dist/esm/notes/index.mjs +1083 -0
- package/dist/kuinetic.all.js +2547 -509
- package/dist/kuinetic.all.min.js +2 -2
- package/dist/kuinetic.css +338 -76
- package/dist/kuinetic.js +2546 -508
- package/dist/kuinetic.min.css +1 -1
- package/dist/kuinetic.min.js +1 -1
- package/dist/kuinetic.notes.js +1 -0
- package/dist/types/3d/flattening.d.ts +8 -7
- package/dist/types/core/deck-viewer.d.ts +87 -0
- package/dist/types/core/describe.d.ts +174 -0
- package/dist/types/core/gesture.d.ts +10 -0
- package/dist/types/core/index.d.ts +6 -2
- package/dist/types/core/target.d.ts +28 -0
- package/dist/types/core/timing-contract.d.ts +61 -0
- package/dist/types/core/types.d.ts +59 -0
- package/dist/types/effects/auto-motion.d.ts +169 -0
- package/dist/types/effects/carousel/deck.d.ts +170 -0
- package/dist/types/effects/carousel/drag.d.ts +6 -0
- package/dist/types/effects/carousel/fit.d.ts +130 -0
- package/dist/types/effects/carousel/index.d.ts +13 -23
- package/dist/types/effects/carousel/stack.d.ts +77 -0
- package/dist/types/effects/catalog/interaction-reveal.d.ts +69 -9
- package/dist/types/effects/forms/primitives.d.ts +1 -2
- package/dist/types/effects/gestures/index.d.ts +3 -4
- package/dist/types/effects/shared.d.ts +3 -30
- package/dist/types/effects/step-index.d.ts +48 -10
- package/dist/types/effects/swipe-event.d.ts +75 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/notes/catalog-core.d.ts +3 -0
- package/dist/types/notes/catalog.d.ts +3 -0
- package/dist/types/notes/index.d.ts +13 -0
- package/dist/types/notes/interaction-reveal.d.ts +3 -0
- package/dist/types/notes/interaction.d.ts +6 -0
- package/dist/types/notes/keys.d.ts +7 -0
- package/dist/types/notes/mechanics.d.ts +7 -0
- package/dist/types/notes/scroll-nav-forms.d.ts +6 -0
- package/dist/types/notes/shared.d.ts +6 -0
- package/dist/types/notes/showcase.d.ts +7 -0
- package/dist/types/notes/text.d.ts +3 -0
- package/dist/types/showcase/compare.d.ts +9 -0
- package/dist/types/showcase/index.d.ts +3 -0
- package/dist/types/showcase/lightbox.d.ts +24 -1
- package/dist/types/showcase/modal-shell.d.ts +11 -0
- package/package.json +7 -3
|
@@ -53,6 +53,42 @@ export declare const CHANNEL: {
|
|
|
53
53
|
* the union documents the built-ins without closing the set.
|
|
54
54
|
*/
|
|
55
55
|
export type Channel = (typeof CHANNEL)[keyof typeof CHANNEL] | (string & {});
|
|
56
|
+
/**
|
|
57
|
+
* The channel for an effect whose output is an attribute it writes on its own element.
|
|
58
|
+
*
|
|
59
|
+
* Named by the attribute, not by the kind of output. These primitives used to share one bucket
|
|
60
|
+
* channel, `'state'`, and since a channel is exclusive, any two of them in one comma list were
|
|
61
|
+
* refused: `swipe-y, carousel` compiled to `swipe-y` alone, with the deck dropped, though
|
|
62
|
+
* `data-kui-swipe` and `data-kui-step` never touch. What two attribute writers can actually fight
|
|
63
|
+
* over is one attribute, so that is what the claim says — `swipe-x, swipe-y` still collide on
|
|
64
|
+
* `data-kui-swipe`, and `carousel, step-progress` on `data-kui-step`.
|
|
65
|
+
*
|
|
66
|
+
* Claim every attribute the effect writes on its host; attributes written on other elements (a
|
|
67
|
+
* deck's slides, a spy's links) are not this element's to contend for.
|
|
68
|
+
*
|
|
69
|
+
* @complexity O(1) time and space.
|
|
70
|
+
* @overallScore 100
|
|
71
|
+
*/
|
|
72
|
+
export declare function attributeChannel(attribute: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* The channel for a widget that owns its host's children: it inserts elements of its own into the
|
|
75
|
+
* host (a slideshow's controls, a compare's range and handle, hotspot markers, the slow-mo toggle),
|
|
76
|
+
* its stylesheet lays the host's children out by where they sit (`scroll-story`'s sticky
|
|
77
|
+
* `:first-child` beside a scrolling `:last-child`, `compare`'s stacked layers), or it takes the
|
|
78
|
+
* host's children as its items (a step deck's slides when no `target:` names them).
|
|
79
|
+
*
|
|
80
|
+
* This is the one thing two such widgets genuinely fight over, so it is exclusive like any channel:
|
|
81
|
+
* `slow-mo`'s toggle prepended into a `scroll-story` becomes the sticky first child, and a
|
|
82
|
+
* `compare` range appended into `hotspots` sits under the markers it places in percent of the host,
|
|
83
|
+
* and the same toggle inside a `carousel` becomes one of its slides.
|
|
84
|
+
* It replaces the bucket the showcase used to share, `'widget'`, which refused every pair of
|
|
85
|
+
* widgets — `lightbox` beside a slideshow included, though the lightbox only listens on the links
|
|
86
|
+
* already there. Attributes a widget writes on its host are claimed separately, with
|
|
87
|
+
* {@link attributeChannel}.
|
|
88
|
+
*
|
|
89
|
+
* Local to the showcase rather than a row of `CHANNEL`: it names a subtree, not a CSS property.
|
|
90
|
+
*/
|
|
91
|
+
export declare const SUBTREE_CHANNEL = "subtree";
|
|
56
92
|
/**
|
|
57
93
|
* The activations the library gives a name of its own, because the DOM has no event for them or
|
|
58
94
|
* because the name bundles more than one event. See `core/activation.ts` for what each
|
|
@@ -215,6 +251,29 @@ interface ParamSpecBase {
|
|
|
215
251
|
maximum?: number;
|
|
216
252
|
/** Require a numeric parameter to have no fractional part. */
|
|
217
253
|
integer?: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* Plain-words documentation for a reader, surfaced by `describeEffect` — never read at runtime.
|
|
256
|
+
*
|
|
257
|
+
* Optional, and absent on every built-in primitive on purpose: the catalog's notes live in
|
|
258
|
+
* `src/notes/`, a separate entry only documentation pages load, so the prose does not ship in
|
|
259
|
+
* the core bundle. This field is the inline route for a third-party primitive, whose notes have
|
|
260
|
+
* nowhere else to live. Where both exist, this one wins.
|
|
261
|
+
*/
|
|
262
|
+
note?: ParamNote;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* What a reader needs to know about one parameter that its type and default cannot say.
|
|
266
|
+
*
|
|
267
|
+
* A bare string is shorthand for `{ why }`.
|
|
268
|
+
*/
|
|
269
|
+
export type ParamNote = string | ParamNoteDetail;
|
|
270
|
+
export interface ParamNoteDetail {
|
|
271
|
+
/** One short sentence, for a non-specialist: why would you set this? */
|
|
272
|
+
why: string;
|
|
273
|
+
/** What happens when the author leaves it out, where the default value alone does not say. */
|
|
274
|
+
whenOmitted?: string;
|
|
275
|
+
/** The effect does nothing useful without it. Documentation only; nothing enforces it. */
|
|
276
|
+
required?: boolean;
|
|
218
277
|
}
|
|
219
278
|
/**
|
|
220
279
|
* A parameter with a keyword half: either nothing but words (`'keyword'`), or words beside a
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { PrepareContext } from '../core/effect-context.js';
|
|
2
|
+
import type { Cleanup, EffectParams, ParamSpec } from '../core/types.js';
|
|
3
|
+
import type { ControlGroup } from './step-index.js';
|
|
4
|
+
/**
|
|
5
|
+
* A deck that moves on its own: continuous `spin:` and stepped `autoplay:`.
|
|
6
|
+
*
|
|
7
|
+
* Shared by every deck in the library — the spatial ones (`carousel/deck.ts`) and the flat
|
|
8
|
+
* `carousel`/`step-progress` index (`forms/primitives.ts`). It lived in `carousel/` while only the
|
|
9
|
+
* spatial decks had motion; it moved here, beside `step-index.ts` and `swipe-event.ts`, when the
|
|
10
|
+
* flat deck gained `autoplay:`, because a second copy of the pause rules below is exactly the drift
|
|
11
|
+
* this module exists to prevent. Nothing in it knows about positions or shapes: the deck hands in
|
|
12
|
+
* how to `step` (and, for a spatial deck, how to `advance` by a fraction).
|
|
13
|
+
*
|
|
14
|
+
* Both are the same question asked at two grains — "when nobody is touching the deck, where does it
|
|
15
|
+
* go next?" — and both have to answer the same four follow-ups identically, which is why they share
|
|
16
|
+
* one scheduler rather than each growing its own: when to stop (hover, keyboard focus, a hidden tab,
|
|
17
|
+
* offscreen, reduced motion, the author's pause control), how to yield to a person (a drag or a
|
|
18
|
+
* control press takes over, then the motion resumes from wherever the deck was left), how to hand
|
|
19
|
+
* the transition back and forth (a frame-driven spin must not also be eased), and how to tear down.
|
|
20
|
+
*
|
|
21
|
+
* ## Why `requestAnimationFrame` writing the deck's own position, and not a CSS animation
|
|
22
|
+
*
|
|
23
|
+
* The obvious CSS answer — register `--kui-step-position` with `@property` and run an infinite
|
|
24
|
+
* linear `@keyframes` over it — fails the one requirement that matters most here: a drag, a click
|
|
25
|
+
* and the spin must all move *one* number. A CSS animation owns its value; the drag would have to
|
|
26
|
+
* read the animated position back out of `getComputedStyle` at `pointerdown`, cancel the animation,
|
|
27
|
+
* drive the deck from JavaScript, and then restart the keyframe at a negative `animation-delay`
|
|
28
|
+
* computed to land on wherever the release settled. That is two sources of truth reconciled by
|
|
29
|
+
* arithmetic on every hand-off, and the "no snap-back when spin resumes" requirement is precisely
|
|
30
|
+
* the bug that reconciliation produces when it is off by one frame.
|
|
31
|
+
*
|
|
32
|
+
* Writing the continuous `position` the deck already has (the same number the drag moves and the
|
|
33
|
+
* controls set) makes the hand-off free: pausing is "stop adding to it", resuming is "start adding
|
|
34
|
+
* to it again", and a drag in between simply moved it. It costs nothing the CSS route would have
|
|
35
|
+
* saved — an animated custom property is recomputed on the main thread either way, because the
|
|
36
|
+
* geometry reading it is a `calc()` the compositor cannot run.
|
|
37
|
+
*
|
|
38
|
+
* `autoplay:` does not need frames at all: it is a timer that presses "next", and the stylesheet's
|
|
39
|
+
* own transition does the travel, exactly as it does for a click. Same model as
|
|
40
|
+
* `showcase/slideshow.ts`'s `autoplay:`, whose pause rules this mirrors.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* The shortest period either kind of motion may be set to, in milliseconds.
|
|
44
|
+
*
|
|
45
|
+
* The same floor `slideshow`'s `autoplay:` has and for the same reason: below it, content moves
|
|
46
|
+
* faster than it can be read, and a whole ring turning in under two seconds is a strobe rather than
|
|
47
|
+
* a carousel. Clamped with a warning rather than refused, so a typo still produces a working deck.
|
|
48
|
+
*/
|
|
49
|
+
export declare const MIN_PERIOD_MS = 2000;
|
|
50
|
+
/**
|
|
51
|
+
* The longest frame a spin will integrate over, in milliseconds.
|
|
52
|
+
*
|
|
53
|
+
* A dropped frame or a debugger pause otherwise arrives as one enormous `dt` and the ring leaps.
|
|
54
|
+
* Capping it turns a hitch into a brief slow-down, which is the less surprising failure. Resuming
|
|
55
|
+
* from a pause never relies on this — the first frame after a resume only records its timestamp.
|
|
56
|
+
*/
|
|
57
|
+
export declare const MAX_FRAME_MS = 100;
|
|
58
|
+
/**
|
|
59
|
+
* An authored signed period, floored to {@link MIN_PERIOD_MS} in magnitude.
|
|
60
|
+
*
|
|
61
|
+
* Pure and exported so the clamp and the sign rule are assertable without a clock. `0` means off
|
|
62
|
+
* and stays `0`; the sign is the direction and survives the clamp.
|
|
63
|
+
*
|
|
64
|
+
* @param ms - Authored period in milliseconds; negative runs backwards.
|
|
65
|
+
* @returns The period to run at, or `0` for off.
|
|
66
|
+
* @complexity O(1) time and space.
|
|
67
|
+
*/
|
|
68
|
+
export declare function clampPeriod(ms: number): number;
|
|
69
|
+
/**
|
|
70
|
+
* How far one frame advances the deck, in whole cycles of it.
|
|
71
|
+
*
|
|
72
|
+
* A cycle is every slide passing once — one revolution of a full ring. Expressed in cycles rather
|
|
73
|
+
* than places so the rate an author writes (`spin:40s`) is independent of how many slides there
|
|
74
|
+
* are: adding a card to a 40-second ring keeps it a 40-second ring rather than slowing each card.
|
|
75
|
+
*
|
|
76
|
+
* @param dtMs - Milliseconds since the previous frame, before capping.
|
|
77
|
+
* @param periodMs - Signed milliseconds per cycle; non-zero.
|
|
78
|
+
* @complexity O(1) time and space.
|
|
79
|
+
*/
|
|
80
|
+
export declare function cyclesFor(dtMs: number, periodMs: number): number;
|
|
81
|
+
export interface AutoMotionRequest {
|
|
82
|
+
el: Element;
|
|
83
|
+
ctx: PrepareContext;
|
|
84
|
+
/** Signed milliseconds per full cycle of continuous spin, already clamped; `0` is off. */
|
|
85
|
+
spinMs: number;
|
|
86
|
+
/** Signed milliseconds between autoplay steps, already clamped; `0` is off. */
|
|
87
|
+
autoplayMs: number;
|
|
88
|
+
/** How long a manual move takes to settle, so the motion does not resume mid-transition. */
|
|
89
|
+
settleMs: number;
|
|
90
|
+
/** Whether the pointer resting on the deck pauses it. Omitted, it does. See `HOVER_PARAM`. */
|
|
91
|
+
pauseOnHover?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Move the deck on by a (signed) fraction of a full cycle. Absent on a deck that cannot spin; such
|
|
94
|
+
* a deck always has `spinMs` 0 (`motionPeriods`), so nothing ever calls it.
|
|
95
|
+
*/
|
|
96
|
+
advance?(cycles: number): void;
|
|
97
|
+
/** Step the deck one place. */
|
|
98
|
+
step(direction: 1 | -1): void;
|
|
99
|
+
/**
|
|
100
|
+
* Publish whether frames, rather than the stylesheet's transition, are driving the deck. Absent,
|
|
101
|
+
* and never called, on a deck that cannot spin — see `advance`.
|
|
102
|
+
*/
|
|
103
|
+
setSpinning?(spinning: boolean): void;
|
|
104
|
+
/** Called whenever the author-facing paused state changes, to mirror it onto pause controls. */
|
|
105
|
+
onPausedChange(paused: boolean): void;
|
|
106
|
+
}
|
|
107
|
+
export interface AutoMotion {
|
|
108
|
+
/** Whether any motion was configured at all. */
|
|
109
|
+
readonly enabled: boolean;
|
|
110
|
+
/** A person moved the deck: hand the transition back and hold off for one settle period. */
|
|
111
|
+
interrupt(): void;
|
|
112
|
+
/** A gesture is (or is no longer) in progress; nothing moves on its own while one is. */
|
|
113
|
+
hold(holding: boolean): void;
|
|
114
|
+
/** The author's pause control was pressed. */
|
|
115
|
+
toggle(): void;
|
|
116
|
+
release: Cleanup;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Start the deck's own motion and return the handle its manual inputs talk to.
|
|
120
|
+
*
|
|
121
|
+
* @returns A controller; `enabled` is false (and every method inert) when neither motion was set.
|
|
122
|
+
* @complexity O(1) per event and per frame; O(1) space.
|
|
123
|
+
*/
|
|
124
|
+
export declare function createAutoMotion(request: AutoMotionRequest): AutoMotion;
|
|
125
|
+
export declare const AUTOPLAY_PARAM: ParamSpec;
|
|
126
|
+
export declare const PAUSE_PARAM: ParamSpec;
|
|
127
|
+
export declare const HOVER_PARAM: ParamSpec;
|
|
128
|
+
/** What {@link createDeckMotion} needs from a deck. */
|
|
129
|
+
export interface DeckMotionRequest {
|
|
130
|
+
el: Element;
|
|
131
|
+
ctx: PrepareContext;
|
|
132
|
+
params: EffectParams;
|
|
133
|
+
/** Prefix for warnings, the word an author would recognise. */
|
|
134
|
+
label: string;
|
|
135
|
+
/** How long a manual move takes to settle, so the motion does not resume mid-transition. */
|
|
136
|
+
settleMs: number;
|
|
137
|
+
/** Step the deck one place. */
|
|
138
|
+
step(direction: 1 | -1): void;
|
|
139
|
+
/**
|
|
140
|
+
* The continuous half, for a deck that has a continuous position to spin (`spin:`). Omitted, the
|
|
141
|
+
* deck has no `spin:` parameter and only `autoplay:` is read.
|
|
142
|
+
*/
|
|
143
|
+
spin?: Pick<AutoMotionRequest, 'advance' | 'setSpinning'>;
|
|
144
|
+
}
|
|
145
|
+
/** What {@link createDeckMotion} returns. */
|
|
146
|
+
export interface DeckMotion {
|
|
147
|
+
/** The motion handle the deck's manual inputs talk to (`interrupt`, `hold`). */
|
|
148
|
+
motion: AutoMotion;
|
|
149
|
+
/**
|
|
150
|
+
* The `pause:` control as a group for the deck's own delegated listener, or `null` when none was
|
|
151
|
+
* named or there is nothing to pause. Bound by the deck, so a press on it goes through the same
|
|
152
|
+
* one listener — and the same inside-first search (`core/target.ts`'s `queryControls`) — as its
|
|
153
|
+
* arrows and dots. A pause button in a band header outside the ring needs no `scope:`.
|
|
154
|
+
*/
|
|
155
|
+
pause: ControlGroup | null;
|
|
156
|
+
release: Cleanup;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A deck's `autoplay:` (and, for a spatial deck, `spin:`) with its `pause:` control: the periods,
|
|
160
|
+
* the floor and its warning, the scheduler, and the `aria-pressed` the control carries.
|
|
161
|
+
*
|
|
162
|
+
* The deck-facing half of {@link createAutoMotion}, so the spatial decks and the flat `carousel`
|
|
163
|
+
* read the same parameters, warn the same way and reflect pause the same way — the only things a
|
|
164
|
+
* deck supplies are how it steps and, if it spins, how it advances.
|
|
165
|
+
*
|
|
166
|
+
* @complexity O(1) at setup plus one scoped query for a named `pause:`; O(m) per pause toggle in the
|
|
167
|
+
* matched controls.
|
|
168
|
+
*/
|
|
169
|
+
export declare function createDeckMotion(request: DeckMotionRequest): DeckMotion;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Cleanup, EffectParams, ParameterSchema } from '../../core/types.js';
|
|
2
|
+
import type { PrepareContext } from '../../core/effect-context.js';
|
|
3
|
+
import type { StyleLedger } from '../../core/owned-styles.js';
|
|
4
|
+
/**
|
|
5
|
+
* The spatial deck: the index, the continuous position, the controls, the grab and the auto-motion
|
|
6
|
+
* that every carousel in this folder shares, whatever shape it lays its slides out in.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from what was `prepareSpatialRing` when a second shape arrived. The ring (`index.ts`)
|
|
9
|
+
* and the depth stack (`stack.ts`) differ in exactly two things — the stylesheet that turns
|
|
10
|
+
* `--kui-offset` and `--kui-step-position` into places, and which slots count as presented to the
|
|
11
|
+
* viewer — and agree on everything else: one real-valued position both a drag and a click move,
|
|
12
|
+
* the integer step derived from it, the marking, `next:`/`prev:`/`jump:`/`pause:`, the keyboard,
|
|
13
|
+
* `spin:` and `autoplay:`. Two primitives each owning a copy of that would be two answers to "where
|
|
14
|
+
* is the deck" that could drift apart, which is the bug this file's single `position` exists to
|
|
15
|
+
* make impossible. So the shape is a {@link DeckLayout} handed in, and the rest lives here once.
|
|
16
|
+
*/
|
|
17
|
+
/** Attribute this module owns on the host while a pointer is dragging the deck. */
|
|
18
|
+
export declare const DRAGGING_ATTR = "data-kui-ring-dragging";
|
|
19
|
+
/**
|
|
20
|
+
* The composition channels for what {@link prepareSpatialDeck} writes on the host, whichever
|
|
21
|
+
* layout it is handed: the children it takes as cards, the step index, the drag flag, and the
|
|
22
|
+
* `tabindex` the drag grants a host that had none. Each spatial primitive spreads this beside its
|
|
23
|
+
* own geometry's channels, so a second writer of any of them is refused rather than overwritten.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SPATIAL_DECK_CHANNELS: readonly string[];
|
|
26
|
+
/**
|
|
27
|
+
* Attribute this module owns on the host while frames — not the stylesheet — are moving the deck.
|
|
28
|
+
*
|
|
29
|
+
* The CSS transition that eases a click from one place to the next is exactly wrong under a spin:
|
|
30
|
+
* each frame retargets it, so the deck trails its own position by the transition's length and
|
|
31
|
+
* visibly accelerates out of every pause. Same reason, same fix, as {@link DRAGGING_ATTR}: while it
|
|
32
|
+
* is `true` the stylesheet suspends the transition, and the moment a person takes over it comes
|
|
33
|
+
* back, so their click still travels.
|
|
34
|
+
*/
|
|
35
|
+
export declare const SPINNING_ATTR = "data-kui-ring-spinning";
|
|
36
|
+
/**
|
|
37
|
+
* Attribute this module owns on each slot: whether the layout currently presents it to the viewer.
|
|
38
|
+
*
|
|
39
|
+
* A published fact rather than a CSS derivation, because it is a branch on the sign or magnitude of
|
|
40
|
+
* a `calc()`, which CSS cannot take. The ring answers it with "is this slot's plane turned more
|
|
41
|
+
* than a quarter turn away" (`faceAt`); the stack with "has it left the visible depth". The
|
|
42
|
+
* stylesheet hangs `pointer-events` on it for every shape, and `visibility` where a hidden slot
|
|
43
|
+
* must also leave the tab order.
|
|
44
|
+
*
|
|
45
|
+
* That matters for more than tidiness. A card turned away from the viewer still occupies its
|
|
46
|
+
* projected screen rectangle for hit-testing purposes — `backface-visibility: hidden` stops it
|
|
47
|
+
* *painting* but is not reliably a hit-testing rule, WebKit in particular — so the back of a ring
|
|
48
|
+
* swallows clicks meant for the front of it. Publishing the fact is what lets one stylesheet rule
|
|
49
|
+
* fix that at any deck size.
|
|
50
|
+
*/
|
|
51
|
+
export declare const FACE_ATTR = "data-kui-ring-face";
|
|
52
|
+
export type Face = 'front' | 'back';
|
|
53
|
+
/**
|
|
54
|
+
* Attribute this module sets on a slot for exactly one style resolution, when its offset wrapped.
|
|
55
|
+
*
|
|
56
|
+
* `--kui-offset` is circular, so on every step one slot is re-numbered from one end of the deck to
|
|
57
|
+
* the other: on a full ring its angle jumps by a whole turn (`-180deg` to `+180deg`) while its place
|
|
58
|
+
* on screen does not move at all. A transition interpolates the number, not the place, so that card
|
|
59
|
+
* would sweep the long way round, across the front of every other card, on every click. The flag
|
|
60
|
+
* suspends its transition (`carousel.css`), the style is resolved once with it present so the jump
|
|
61
|
+
* is committed without a transition, and the flag is removed in the same task — nothing ever sees
|
|
62
|
+
* it, and the next change to that slot transitions normally.
|
|
63
|
+
*/
|
|
64
|
+
export declare const WRAP_ATTR = "data-kui-ring-wrap";
|
|
65
|
+
/**
|
|
66
|
+
* Which way a deck's cards travel across the screen as its position grows, as the sign a drag's
|
|
67
|
+
* pixels are multiplied by: `1` when the next card sits to the right of the live one (so advancing
|
|
68
|
+
* pulls the cards left, and a leftward drag advances), `-1` when it sits to the left.
|
|
69
|
+
*
|
|
70
|
+
* Read off the rendered geometry rather than off a name or a parameter, because no one parameter
|
|
71
|
+
* owns it. Outside a ring (`translateZ(+r)`) the next card is on the right; from the centre of one
|
|
72
|
+
* (`carousel-3d-inside`, `translateZ(-r)`) the same angle puts it on the left, and a drag that
|
|
73
|
+
* ignored that moved the cards against the finger. A flat ring's next card is clockwise of twelve,
|
|
74
|
+
* so right; a stack's is wherever `shift:` steps it. The one rule that is right for all of them is
|
|
75
|
+
* "the card under the pointer follows the pointer", and that is a question about where the cards
|
|
76
|
+
* are on screen.
|
|
77
|
+
*
|
|
78
|
+
* Pure and exported so the rule is assertable without a layout engine. `null` for a side that has
|
|
79
|
+
* no card (a one-card deck) and a tie (a realm with no layout, where every box is at 0) both answer
|
|
80
|
+
* `1`, the outside ring's long-standing direction.
|
|
81
|
+
*
|
|
82
|
+
* @param liveX - Screen x of the live card's centre.
|
|
83
|
+
* @param nextX - Screen x of the next card's centre, or `null` if there is none.
|
|
84
|
+
* @param previousX - Screen x of the previous card's centre, consulted only without a next card.
|
|
85
|
+
* @complexity O(1) time and space.
|
|
86
|
+
*/
|
|
87
|
+
export declare function travelSign(liveX: number, nextX: number | null, previousX: number | null): 1 | -1;
|
|
88
|
+
/**
|
|
89
|
+
* Whether a slot's offset moved further than any real step can: it wrapped round the deck.
|
|
90
|
+
*
|
|
91
|
+
* A step moves every offset by one place (a jump by at most half the deck, because offsets are
|
|
92
|
+
* circular); only a re-numbering from one end to the other moves one by more than half. Pure and
|
|
93
|
+
* exported so the threshold is assertable without a DOM.
|
|
94
|
+
*
|
|
95
|
+
* @complexity O(1) time and space.
|
|
96
|
+
*/
|
|
97
|
+
export declare function offsetWrapped(previous: number, next: number, count: number): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Where the deck's continuous position rounds to, and how far past it the deck currently sits.
|
|
100
|
+
*
|
|
101
|
+
* The pair the stylesheet needs and the reason both tokens are published rather than one: the
|
|
102
|
+
* integer decides which slot is *live* (and therefore what `--kui-offset` means for every other
|
|
103
|
+
* slot), and the remainder is the sub-step motion on top of it. Re-snapping the integer whenever
|
|
104
|
+
* the remainder passes a half step is what keeps the remainder inside ±0.5, so the deck never has
|
|
105
|
+
* to travel back across the whole strip to wrap from the last slide to the first — the same
|
|
106
|
+
* property `circularOffset` gives the discrete case.
|
|
107
|
+
*
|
|
108
|
+
* @param position - Continuous position, in places. May be negative or beyond the count.
|
|
109
|
+
* @param total - How many places the deck has.
|
|
110
|
+
* @returns The live integer step, wrapped into range, and the signed remainder in `[-0.5, 0.5)`.
|
|
111
|
+
* @complexity O(1) time and space.
|
|
112
|
+
*/
|
|
113
|
+
export declare function snapPosition(position: number, total: number): {
|
|
114
|
+
step: number;
|
|
115
|
+
drift: number;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* The parameters every spatial deck takes, whatever its shape.
|
|
119
|
+
*
|
|
120
|
+
* Spread into each primitive's schema so the controls, the grab and the motion are spelled — and
|
|
121
|
+
* documented — once.
|
|
122
|
+
*/
|
|
123
|
+
export declare const DECK_PARAMETERS: ParameterSchema;
|
|
124
|
+
/** What a deck's shape contributes; everything else is shared. */
|
|
125
|
+
export interface DeckLayout {
|
|
126
|
+
/** The primitive's id: names the mirrored timing properties (`--kui-<id>-duration`). */
|
|
127
|
+
id: string;
|
|
128
|
+
/** Prefix for warnings, the word an author would recognise. */
|
|
129
|
+
label: string;
|
|
130
|
+
/** Whether a slot at this place is presented to the viewer. `place` is `offset - drift`. */
|
|
131
|
+
faceOf(place: number, offset: number, count: number): Face;
|
|
132
|
+
/**
|
|
133
|
+
* The shape's own reading of a circular offset, when it is not the offset itself.
|
|
134
|
+
*
|
|
135
|
+
* Only used to spot a wrap ({@link WRAP_ATTR}): a slot wraps where *its shape's* numbering jumps,
|
|
136
|
+
* which for a ring is at the circular seam and for a stack is between "just left" and "at the
|
|
137
|
+
* back". Omitted means the circular offset is the shape's numbering.
|
|
138
|
+
*/
|
|
139
|
+
placeOf?(offset: number, count: number): number;
|
|
140
|
+
/** Whether to publish the first slot's measured size for a stylesheet that derives from it. */
|
|
141
|
+
measure: boolean;
|
|
142
|
+
/** Attributes the shape publishes on the host, so its selectors can branch on parameters. */
|
|
143
|
+
hostAttributes?: Record<string, string>;
|
|
144
|
+
/**
|
|
145
|
+
* Anything else the shape keeps current on the host, handed the deck's own host style ledger.
|
|
146
|
+
*
|
|
147
|
+
* The ledger rather than a second one of the shape's own: a second ledger over the host would
|
|
148
|
+
* snapshot the deck's writes as the author's and "restore" to them (`LedgerSet` in
|
|
149
|
+
* `core/owned-styles.ts`). `render` runs after every full render — never per spin frame — with
|
|
150
|
+
* the slides it rendered; `release` runs before the host's styles are given back.
|
|
151
|
+
*/
|
|
152
|
+
attach?(styles: StyleLedger): DeckAttachment;
|
|
153
|
+
}
|
|
154
|
+
/** What {@link DeckLayout.attach} returns. */
|
|
155
|
+
export interface DeckAttachment {
|
|
156
|
+
render(nodes: readonly Element[], count: number): void;
|
|
157
|
+
release: Cleanup;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Drive a deck of slides from one continuous position.
|
|
161
|
+
*
|
|
162
|
+
* Reuses `step-progress`'s marking, counting and control delegation wholesale — the index half of a
|
|
163
|
+
* carousel is the same problem whether the slides sit in a row, on a ring or in a stack, and
|
|
164
|
+
* `createStepMarker` was extracted precisely so a second consumer would not re-derive it. What is
|
|
165
|
+
* new here is the continuous position, the measurement, the face marking, the grab and the motion.
|
|
166
|
+
*
|
|
167
|
+
* @complexity O(n) per flip and per drag frame in the slot count; O(n) per spin frame for the face
|
|
168
|
+
* marks only; O(n) space in slots ever touched.
|
|
169
|
+
*/
|
|
170
|
+
export declare function prepareSpatialDeck(el: Element, params: EffectParams, ctx: PrepareContext, layout: DeckLayout): Cleanup;
|
|
@@ -11,6 +11,12 @@ export interface RingDragRequest {
|
|
|
11
11
|
total(): number;
|
|
12
12
|
/** The ring's current continuous position, in places. */
|
|
13
13
|
positionOf(): number;
|
|
14
|
+
/**
|
|
15
|
+
* Which way the cards travel on screen as the position grows (`deck.ts`'s `travelSign`): `1` when
|
|
16
|
+
* a leftward drag advances, `-1` when a rightward one does. Asked once per drag, at its start, so
|
|
17
|
+
* the card under the pointer follows it whatever the shape.
|
|
18
|
+
*/
|
|
19
|
+
travelSign(): 1 | -1;
|
|
14
20
|
/** Move the ring to a continuous position and re-render. */
|
|
15
21
|
moveTo(position: number): void;
|
|
16
22
|
/** Publish or clear the dragging state the stylesheet suspends its transition on. */
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Cleanup } from '../../core/types.js';
|
|
2
|
+
import type { PrepareContext } from '../../core/effect-context.js';
|
|
3
|
+
import type { StyleLedger } from '../../core/owned-styles.js';
|
|
4
|
+
/**
|
|
5
|
+
* The largest radius at which a depth ring still fits the page it is on.
|
|
6
|
+
*
|
|
7
|
+
* A depth ring's radius is derived from its cards (or authored) and knows nothing about the width
|
|
8
|
+
* it got, so on a phone its edge cards project hundreds of pixels past the screen and push the page
|
|
9
|
+
* sideways. This file finds the radius at which that stops happening, and `carousel.css` uses
|
|
10
|
+
* `min(wanted, fit)`: a ring that already fits — every desktop ring — is untouched to the pixel,
|
|
11
|
+
* and only a ring that would overflow is pulled in, exactly as far as it has to be.
|
|
12
|
+
*
|
|
13
|
+
* ## Why a solver in JavaScript and not a formula in the stylesheet
|
|
14
|
+
*
|
|
15
|
+
* The first attempt was CSS: a size container on the host and one closed-form bound,
|
|
16
|
+
* `R = D * cos(atan2(D, P))`, with `D` the host's half-width less half a card. It was correct and
|
|
17
|
+
* far too cautious, because a closed form has to assume the worst case: a card at *any* angle, seen
|
|
18
|
+
* from the side, at full width. Real rings reach much less than that. A 180-degree inside slice
|
|
19
|
+
* never shows a card past a quarter turn; a card at the silhouette is turned nearly edge-on; the
|
|
20
|
+
* tilt, the facing and the perspective all move where a card's corners land. The bound shrank the
|
|
21
|
+
* showcase's rows at 1440px from radius 805 to 500 — rings that were not overflowing anything.
|
|
22
|
+
*
|
|
23
|
+
* The exact answer is a maximum over the angles the ring can actually reach, of where each card's
|
|
24
|
+
* *corners* project under the ring's own perspective, tilt and facing — and a stylesheet cannot
|
|
25
|
+
* take a maximum over a continuous range. It also needs two facts CSS cannot see at all: where the
|
|
26
|
+
* host sits on the page (a ring off-centre has less room on one side), and whether anything
|
|
27
|
+
* between it and the page already clips it. So the geometry is solved here, once per resize, and
|
|
28
|
+
* published as one length (`--kui-ring-fit`) the stylesheet spends.
|
|
29
|
+
*
|
|
30
|
+
* The size container went with the formula. It had a side effect the solver does not: a host sized
|
|
31
|
+
* by its content (a flex or grid item with no width) took its container's width instead. Nothing
|
|
32
|
+
* here changes how any host is sized — the solver measures the page and writes one custom property.
|
|
33
|
+
*
|
|
34
|
+
* ## What counts as "the page"
|
|
35
|
+
*
|
|
36
|
+
* The nearest ancestor that contains horizontal overflow decides, walking up to (not including)
|
|
37
|
+
* `<body>`:
|
|
38
|
+
*
|
|
39
|
+
* - `overflow-x: clip` or `hidden` — the author has already decided what happens past that edge:
|
|
40
|
+
* the ring bleeds and is cut, which is a composition (the showcase's rows run off both sides of
|
|
41
|
+
* their band on purpose). Nothing is fitted; the ring keeps the radius it asked for.
|
|
42
|
+
* - `auto` or `scroll` — overflow would scroll that box sideways, so the ring fits its padding box.
|
|
43
|
+
* - none — the ring fits the page's own width, `0..clientWidth` in document coordinates.
|
|
44
|
+
*
|
|
45
|
+
* `<html>` and `<body>` are skipped because a page-level `overflow-x: hidden` is on a large fraction of all sites as a blanket guard, not a
|
|
46
|
+
* statement that this ring should be cut off at the screen edge.
|
|
47
|
+
*
|
|
48
|
+
* ## Which cards count
|
|
49
|
+
*
|
|
50
|
+
* Every card on the reachable arc, as it moves: a card sits within half a place of its offset
|
|
51
|
+
* while it is dragged or spun, so the reachable angles are the offsets' range widened by half a
|
|
52
|
+
* spacing each side — the whole circle for a full ring. On an *inside* ring a card at or past a
|
|
53
|
+
* quarter turn is `visibility: hidden` (`carousel.css`), so only the front half counts. On an
|
|
54
|
+
* outside ring every angle counts: the far half is content there, and its cards sit further from
|
|
55
|
+
* the camera than the near half's, so they never set the width anyway.
|
|
56
|
+
*/
|
|
57
|
+
/** Custom property this module owns on the ring host. */
|
|
58
|
+
export declare const RING_FIT_PROPERTY = "--kui-ring-fit";
|
|
59
|
+
/** Everything the projection needs, in pixels and degrees. */
|
|
60
|
+
export interface RingFitGeometry {
|
|
61
|
+
/** A card's laid-out size. */
|
|
62
|
+
width: number;
|
|
63
|
+
height: number;
|
|
64
|
+
/** The host's `perspective`; `Infinity` for `none`. */
|
|
65
|
+
perspective: number;
|
|
66
|
+
/** The `rotateX` every slot is placed under — the stylesheet's `--kui-ring-tilt`, not the param. */
|
|
67
|
+
tiltDeg: number;
|
|
68
|
+
/** Whether cards sit behind the host's plane (`carousel-3d-inside`). */
|
|
69
|
+
inside: boolean;
|
|
70
|
+
/** Whether each card is counter-rotated to face the viewer. */
|
|
71
|
+
facing: 'radial' | 'camera';
|
|
72
|
+
/** The reachable ring angles (see {@link reachableAngles}). */
|
|
73
|
+
fromDeg: number;
|
|
74
|
+
toDeg: number;
|
|
75
|
+
/** The slot centre's horizontal distance from the perspective origin. */
|
|
76
|
+
originOffset: number;
|
|
77
|
+
/** The page's edges, as horizontal distances from the perspective origin (min < 0 < max). */
|
|
78
|
+
minX: number;
|
|
79
|
+
maxX: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The ring angles a card can occupy, in the stylesheet's own terms.
|
|
83
|
+
*
|
|
84
|
+
* `carousel.css` places a slot at `(offset - drift) * arc / count`. Offsets are circular, from
|
|
85
|
+
* `floor(count/2) - count + 1` to `floor(count/2)` (`circularOffset`), and the drift stays within
|
|
86
|
+
* half a place either way (`snapPosition`), so the union over every card and every moment is that
|
|
87
|
+
* range widened by half a spacing each side. A full ring reaches the whole circle.
|
|
88
|
+
*
|
|
89
|
+
* @complexity O(1) time and space.
|
|
90
|
+
*/
|
|
91
|
+
export declare function reachableAngles(arcDeg: number, count: number): {
|
|
92
|
+
fromDeg: number;
|
|
93
|
+
toDeg: number;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The largest radius at which the ring fits, or `Infinity` when every radius does.
|
|
97
|
+
*
|
|
98
|
+
* Bisection, because the extent grows with the radius: every card moves outward along its own
|
|
99
|
+
* direction, and the perspective only magnifies that on the near side and shrinks it less than
|
|
100
|
+
* linearly on the far side. Floored to a hundredth of a pixel so rounding never lands outside.
|
|
101
|
+
*
|
|
102
|
+
* @complexity O(s * k) — samples of the arc times {@link SEARCH_STEPS}; runs on resize, never per
|
|
103
|
+
* frame.
|
|
104
|
+
*/
|
|
105
|
+
export declare function solveRingFit(g: RingFitGeometry): number;
|
|
106
|
+
/** The box a ring has to fit, in viewport coordinates, or `null` when an ancestor clips it. */
|
|
107
|
+
export declare function fitBounds(el: Element, win: Window): {
|
|
108
|
+
left: number;
|
|
109
|
+
right: number;
|
|
110
|
+
} | null;
|
|
111
|
+
/** What the ring tells the fit that the page cannot: its parameters. */
|
|
112
|
+
export interface RingFitOptions {
|
|
113
|
+
el: Element;
|
|
114
|
+
ctx: PrepareContext;
|
|
115
|
+
styles: StyleLedger;
|
|
116
|
+
arcDeg: number;
|
|
117
|
+
/** The `tilt:` parameter in degrees, before the stylesheet's negation and clamp. */
|
|
118
|
+
tiltParamDeg: number;
|
|
119
|
+
facing: 'radial' | 'camera';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Keep a depth ring's `--kui-ring-fit` current: at every full render whose cards changed, and on
|
|
123
|
+
* every resize of the host or the page (coalesced to one solve per frame).
|
|
124
|
+
*
|
|
125
|
+
* @complexity O(s * k) per solve (see {@link solveRingFit}); O(1) space.
|
|
126
|
+
*/
|
|
127
|
+
export declare function createRingFit(options: RingFitOptions): {
|
|
128
|
+
render(nodes: readonly Element[], count: number): void;
|
|
129
|
+
release: Cleanup;
|
|
130
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Preset, Primitive } from '../../core/types.js';
|
|
2
2
|
import type { Registry } from '../../core/registry.js';
|
|
3
|
+
export { snapPosition } from './deck.js';
|
|
3
4
|
/**
|
|
4
5
|
* Whether a slot at this ring angle has its own plane turned away from the camera.
|
|
5
6
|
*
|
|
@@ -24,25 +25,6 @@ export declare function faceAt(angleDeg: number): 'front' | 'back';
|
|
|
24
25
|
* @complexity O(1) time and space.
|
|
25
26
|
*/
|
|
26
27
|
export declare function normaliseDegrees(angleDeg: number): number;
|
|
27
|
-
/**
|
|
28
|
-
* Where the ring's continuous position rounds to, and how far past it the ring currently sits.
|
|
29
|
-
*
|
|
30
|
-
* The pair the stylesheet needs and the reason both tokens are published rather than one: the
|
|
31
|
-
* integer decides which slot is *live* (and therefore what `--kui-offset` means for every other
|
|
32
|
-
* slot), and the remainder is the sub-step rotation on top of it. Re-snapping the integer whenever
|
|
33
|
-
* the remainder passes a half step is what keeps the remainder inside ±0.5, so the ring never has
|
|
34
|
-
* to travel back across the whole strip to wrap from the last slide to the first — the same
|
|
35
|
-
* property `circularOffset` gives the discrete case.
|
|
36
|
-
*
|
|
37
|
-
* @param position - Continuous ring position, in places. May be negative or beyond the count.
|
|
38
|
-
* @param total - How many places the ring has.
|
|
39
|
-
* @returns The live integer step, wrapped into range, and the signed remainder in `[-0.5, 0.5)`.
|
|
40
|
-
* @complexity O(1) time and space.
|
|
41
|
-
*/
|
|
42
|
-
export declare function snapPosition(position: number, total: number): {
|
|
43
|
-
step: number;
|
|
44
|
-
drift: number;
|
|
45
|
-
};
|
|
46
28
|
/**
|
|
47
29
|
* An authored `<angle>` as a number of degrees.
|
|
48
30
|
*
|
|
@@ -65,8 +47,9 @@ export declare function degreesOf(value: string, fallbackDeg: number): number;
|
|
|
65
47
|
/**
|
|
66
48
|
* The ring primitive.
|
|
67
49
|
*
|
|
68
|
-
* `renderer: 'javascript'` and
|
|
69
|
-
* nothing itself — it publishes numbers
|
|
50
|
+
* `renderer: 'javascript'` and its channels together are the honest declaration: this file paints
|
|
51
|
+
* nothing itself — it publishes numbers (and the `data-kui-step` index, its third channel) — but
|
|
52
|
+
* `carousel.css` does, and both of the things that stylesheet writes
|
|
70
53
|
* have to be declared or the conflict detector waves through a composition that silently loses one
|
|
71
54
|
* effect's output.
|
|
72
55
|
*
|
|
@@ -116,7 +99,7 @@ export declare const CAROUSEL_PRIMITIVES: Primitive[];
|
|
|
116
99
|
*
|
|
117
100
|
* `phase: 'idle'` on all of them too, and for the same reason `target` does not change the answer:
|
|
118
101
|
* `channelsFor`/`findConflicts` (`core/channels.ts`) reason about the channels *this preset
|
|
119
|
-
* declares* on the host it is authored on (`CHANNEL.skew` and `
|
|
102
|
+
* declares* on the host it is authored on (`CHANNEL.skew`, `'discrete'` and `data-kui-step`, from
|
|
120
103
|
* `SPATIAL_RING_PRIMITIVE`), not about which element in the subtree physically paints them — the
|
|
121
104
|
* compiler has no notion of "the primitive's own target moved the real work three nodes down," and
|
|
122
105
|
* does not need one here, because `requiresOwnSubtree` already keeps this preset off of anything
|
|
@@ -135,8 +118,15 @@ export declare const CAROUSEL_PRIMITIVES: Primitive[];
|
|
|
135
118
|
* over the `transform` shorthand (`CHANNEL.skew`) for the ring's whole lifetime, and there is no
|
|
136
119
|
* turn-taking to model. It replaces "nothing is known" with the actual, checked fact.
|
|
137
120
|
*
|
|
138
|
-
* None of
|
|
121
|
+
* None of them declares `transitions`: nothing here renders through a CSS `transition:` on the
|
|
139
122
|
* host box for `phaseOf` to derive `state` from — the ring is `renderer: 'javascript'` throughout.
|
|
123
|
+
*
|
|
124
|
+
* All of them are `cloak: true`. Before the script runs, a ring is its cards laid out flat in the
|
|
125
|
+
* page's flow, and the first frame after start snaps them onto the ring: a flash of the wrong
|
|
126
|
+
* layout on every load (the showcase hero showed it). Under `<html data-kui-cloak>` the host and its
|
|
127
|
+
* cards stay hidden until the host's `data-kui-state` lands, which happens in the same task that
|
|
128
|
+
* places the cards (no frame or timer in between), and the CSS-only two-second release still shows
|
|
129
|
+
* them if the script never runs.
|
|
140
130
|
*/
|
|
141
131
|
export declare const CAROUSEL_PRESETS: Preset[];
|
|
142
132
|
/**
|