@kieransimkin/dancemoves 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +338 -0
- package/README.md +893 -0
- package/RUDIMENTS-API.md +394 -0
- package/docs/README.md +73 -0
- package/docs/api/adapters.md +400 -0
- package/docs/api/downloads.md +158 -0
- package/docs/api/effects.md +463 -0
- package/docs/api/javascript.md +500 -0
- package/docs/api/paper-planes.md +167 -0
- package/docs/api/styling.md +292 -0
- package/docs/api/surface.json +446 -0
- package/docs/api/wordpress.md +365 -0
- package/docs/clay-transform-validation.md +84 -0
- package/docs/development.md +254 -0
- package/docs/guide.md +288 -0
- package/docs/javascript-api.md +292 -0
- package/docs/javascript-server.md +150 -0
- package/docs/react-next.md +185 -0
- package/docs/releasing-shared.md +162 -0
- package/docs/rudiments-source-integrity.md +80 -0
- package/docs/shared-feature-coverage.md +34 -0
- package/docs/shared-runtime-migration.md +111 -0
- package/docs/source-audit.md +120 -0
- package/docs/validation-setup.md +102 -0
- package/docs/wordpress-examples.md +206 -0
- package/docs/wordpress-shared-runtime.md +133 -0
- package/lib/admin.min.js +1 -0
- package/lib/assets/DanceRudiments-LICENSE +21 -0
- package/lib/assets/DanceRudiments-build-manifest.json +21 -0
- package/lib/assets/paper-dreams-plane-atlas.png +0 -0
- package/lib/build-manifest.json +117 -0
- package/lib/dancemoves.css +9 -0
- package/lib/dancemoves.min.js +5 -0
- package/lib/index.cjs +4802 -0
- package/lib/index.d.ts +71 -0
- package/lib/index.mjs +4780 -0
- package/lib/react.cjs +154 -0
- package/lib/react.d.ts +12 -0
- package/lib/react.mjs +135 -0
- package/lib/rudiments.d.ts +124 -0
- package/lib/server.cjs +411 -0
- package/lib/server.d.ts +11 -0
- package/lib/server.mjs +367 -0
- package/lib/styles/clay-stars-effects.css +1 -0
- package/lib/styles/clay-stars-rudiments.css +1 -0
- package/lib/styles/dance-moves-core.css +1 -0
- package/lib/styles/ks-epk-device-orientation.css +1 -0
- package/lib/styles/paper-dreams-flight.css +1 -0
- package/lib/wordpress.js +5 -0
- package/package.json +101 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# JavaScript API reference
|
|
2
|
+
|
|
3
|
+
[Documentation index](../README.md) · [Adapter APIs](adapters.md) ·
|
|
4
|
+
[CSS/HTML contract](styling.md) · [WordPress configuration](wordpress.md)
|
|
5
|
+
|
|
6
|
+
Source: [assets/dance-moves-core.js](../../assets/dance-moves-core.js).
|
|
7
|
+
Baseline: **2.8.0 / `4cb6a71f60459b5579be87d7e55ac8b1426c578e`**.
|
|
8
|
+
|
|
9
|
+
## Loading, globals and types
|
|
10
|
+
|
|
11
|
+
`window.DanceMoves` and `window.KieranEpkMotion` reference the **same object**.
|
|
12
|
+
Load page code after the WordPress `dance-moves-core` script handle. The object is
|
|
13
|
+
created synchronously during script evaluation; DOM setup and file fetching are
|
|
14
|
+
separate. There is no module package, constructor, public `init()`, `ready()`
|
|
15
|
+
promise, `destroy()` or general configuration setter in the core.
|
|
16
|
+
|
|
17
|
+
In the signatures below, `Cancel` means `() => void`, and `Element` means a real
|
|
18
|
+
DOM element. Times in parsed entries are **seconds**; duration helper results are
|
|
19
|
+
**milliseconds**; `boundaryTick` and `currentTick()` are **ticks**. Supply finite
|
|
20
|
+
numbers and functions. Most numeric helpers coerce with `Number()` rather than
|
|
21
|
+
throwing on malformed values, so nonfinite arguments can produce nonfinite
|
|
22
|
+
results; this is not a runtime-validated TypeScript interface.
|
|
23
|
+
|
|
24
|
+
### Core properties
|
|
25
|
+
|
|
26
|
+
| Property | Type | Meaning |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `version` | string | Boot `version`, or `""` when absent. |
|
|
29
|
+
| `bpm` | number | Boot BPM if finite and within 20–400, otherwise 120. |
|
|
30
|
+
| `bpmSource` | `"explicit"` or `"fallback"` | Explicit only when the boot source string is exactly `"explicit"`. |
|
|
31
|
+
| `ticksPerBeat` | number | Always 16. |
|
|
32
|
+
|
|
33
|
+
Treat these as read-only integration data. Changing `DanceMoves.bpm` does not
|
|
34
|
+
change the BPM captured by the core's internal clock or rewrite its configuration.
|
|
35
|
+
|
|
36
|
+
### Boot object: `window.danceMovesConfig`
|
|
37
|
+
|
|
38
|
+
WordPress emits this before the core script. A standalone harness must do the same.
|
|
39
|
+
|
|
40
|
+
| Key | Type / normal WordPress value | Core behaviour |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `pageId` | number | Event/diagnostic page identity; numerically coerced, fallback 0. |
|
|
43
|
+
| `version` | string, `"2.8.0"` | Exposed on the API and HTML state. |
|
|
44
|
+
| `bpm` | number, 20–400 or fallback 120 | Captured at script evaluation. |
|
|
45
|
+
| `bpmSource` | `"explicit"` / `"fallback"` | Provenance marker. |
|
|
46
|
+
| `fallbackBpm` | number, 120 | Informational: the core's fallback is hard-coded to 120. |
|
|
47
|
+
| `lyricTimingUrl` | string, `""` when unset | Fetched whether or not pop-ups are enabled. |
|
|
48
|
+
| `lyricPopupsEnabled` | boolean, false | Gates the built-in renderer, not lyric parsing/subscriptions. Use a real boolean; the core tests truthiness. |
|
|
49
|
+
| `cueTimingUrl` | string, `""` when unset | Optional cue-file fetch. |
|
|
50
|
+
| `masterDurationMilliseconds` | number, 0 when unknown | Positive value fixes the reference duration; otherwise audio discovery derives it. |
|
|
51
|
+
| `tickDefinition` | `"sixteenth-of-beat"` | Informational. |
|
|
52
|
+
| `ticksPerBeat` | number, 16 | Informational for the core, which fixes its own value at 16. |
|
|
53
|
+
| `longDurationQuantumTicks` | number, 16 | Informational for the core, which fixes its own quantum at 16. |
|
|
54
|
+
| `sharedControlTicks` | number, 8 on selected pages, otherwise 0 | Positive value enables shared control timing, quantised as a visual duration. |
|
|
55
|
+
| `lyricDisclosureTicks` | number, normally 5, 6 or 7 | Defaults to 6 when falsy; quantised as a visual duration. |
|
|
56
|
+
| `effect` | `""` or `"paper-planes"` | Emitted by WordPress for ambient-effect loading; the core clock itself does not interpret it. |
|
|
57
|
+
| `diagnostics` | optional boolean | Only literal `true` permits a diagnostics sink. **Not emitted by ordinary WordPress configuration.** |
|
|
58
|
+
|
|
59
|
+
A boot change requires a fresh runtime/page load. There is no public method to
|
|
60
|
+
replace the internally loaded cue or lyric list. Parsing text with the public
|
|
61
|
+
parsers returns data but does not install it in the playback engine.
|
|
62
|
+
|
|
63
|
+
## Duration and clock methods
|
|
64
|
+
|
|
65
|
+
### `quantizeTicks(value) -> number`
|
|
66
|
+
|
|
67
|
+
First rounds half upward with `floor(Number(value) + 0.5)`, with a minimum of 1.
|
|
68
|
+
If that integer exceeds 16, rounds it to a multiple of 16, also half upward.
|
|
69
|
+
This rule is for **visual durations**, not interval spacing.
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
DanceMoves.quantizeTicks(0); // 1
|
|
73
|
+
DanceMoves.quantizeTicks(1.5); // 2
|
|
74
|
+
DanceMoves.quantizeTicks(17); // 16
|
|
75
|
+
DanceMoves.quantizeTicks(24); // 32
|
|
76
|
+
DanceMoves.quantizeTicks(432); // 432
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `durationMilliseconds(ticks, bpmOverride?) -> number`
|
|
80
|
+
|
|
81
|
+
Returns `(3750 / effectiveBpm) * quantizeTicks(ticks)`. A truthy override replaces
|
|
82
|
+
the configured BPM; an invalid/out-of-range truthy override resolves to 120. An
|
|
83
|
+
omitted/falsy override uses the configured BPM. At 120 BPM, 16 ticks is 500 ms.
|
|
84
|
+
Do not use this method to represent a negative delay: calculate the positive
|
|
85
|
+
duration and negate the result.
|
|
86
|
+
|
|
87
|
+
### `currentTick(options?) -> number`
|
|
88
|
+
|
|
89
|
+
Options: `clock?: "page" | "audio"`, `audio?: HTMLAudioElement`, `bpm?: number`.
|
|
90
|
+
Returns a potentially fractional tick, not an integer beat counter.
|
|
91
|
+
|
|
92
|
+
An explicit `audio` takes precedence even without `clock: "audio"`. Otherwise
|
|
93
|
+
`clock: "audio"` selects the runtime's active matching player. When that element
|
|
94
|
+
is playing, not ended, and has a finite `currentTime`, the result is
|
|
95
|
+
`currentTime * effectiveBpm * 16 / 60`. In **every other case** it returns elapsed
|
|
96
|
+
page-clock time at the effective BPM. It is therefore not a frozen-song-time API.
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
const audio = document.querySelector('#release-audio');
|
|
100
|
+
if (audio && !audio.paused && !audio.ended) {
|
|
101
|
+
const phase = DanceMoves.currentTick({ audio }) % 64;
|
|
102
|
+
// Use phase in a page-owned canvas/WebGL effect.
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `nextIntervalTick(intervalTicks, fromTick, strictlyFuture?) -> number`
|
|
107
|
+
|
|
108
|
+
Rounds the interval half upward to an integer of at least 1; it does **not** apply
|
|
109
|
+
the duration quantum above 16. Coerces/clamps `fromTick` to a nonnegative value.
|
|
110
|
+
Returns the next interval boundary. A tick within 0.001 of a boundary is treated
|
|
111
|
+
as on that boundary: the current tick is returned unless `strictlyFuture` is
|
|
112
|
+
truthy, in which case one interval is added.
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
DanceMoves.nextIntervalTick(16, 17); // 32
|
|
116
|
+
DanceMoves.nextIntervalTick(16, 32); // 32
|
|
117
|
+
DanceMoves.nextIntervalTick(16, 32, true); // 48
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Scheduling and interval subscriptions
|
|
121
|
+
|
|
122
|
+
All methods in this section return a `Cancel`. Cancellation does not undo work
|
|
123
|
+
already performed. Retain removers and invoke them when disposing page code.
|
|
124
|
+
|
|
125
|
+
### Boundary detail
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
{
|
|
129
|
+
intervalTicks: number,
|
|
130
|
+
boundaryTick: number,
|
|
131
|
+
audio: HTMLAudioElement | null,
|
|
132
|
+
clock: "audio" | "page"
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Callbacks are ordinary synchronous JavaScript callbacks. These APIs schedule
|
|
137
|
+
visual work near a boundary; they do not implement sample-accurate audio
|
|
138
|
+
scheduling, an AudioContext clock, or background-tab execution guarantees.
|
|
139
|
+
|
|
140
|
+
### Diagnostic identity
|
|
141
|
+
|
|
142
|
+
The metadata accepted by subscriptions is a string ID, or an object with `id` or
|
|
143
|
+
`handlerId`. IDs are trimmed and limited to 160 characters. A missing ID becomes
|
|
144
|
+
an `unattributed:...` label; this does not disable the callback, but the release
|
|
145
|
+
workflow requires attributable IDs for pre-live evidence. IDs label diagnostics;
|
|
146
|
+
they are not lookup keys for cancelling a subscription.
|
|
147
|
+
|
|
148
|
+
### `scheduleAtInterval(intervalTicks, callback, options?) -> Cancel`
|
|
149
|
+
|
|
150
|
+
Throws `TypeError` when `callback` is not a function. Interval ticks are rounded
|
|
151
|
+
to an integer of at least 1, without long-duration quantisation.
|
|
152
|
+
|
|
153
|
+
| Option | Meaning |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `clock` | Defaults to the page clock; `"audio"` waits for playing active audio. |
|
|
156
|
+
| `audio` | Explicit audio element; selects the audio scheduling branch independently of `clock`. It is not required to be master-duration-bound by this low-level method. |
|
|
157
|
+
| `strictlyFuture` | Used when choosing the initial boundary for audio scheduling. The page-clock branch does not honour it. |
|
|
158
|
+
| `id`, `handlerId` | Diagnostic identifier, with `id` preferred. |
|
|
159
|
+
|
|
160
|
+
Page scheduling uses a timeout to the next page boundary and may call the callback
|
|
161
|
+
**synchronously** when the computed delay is at most 1 ms. Do not reference an
|
|
162
|
+
as-yet-unassigned return value from inside that callback. Audio scheduling polls
|
|
163
|
+
with `requestAnimationFrame`, waits through absent/paused audio, and fires with a
|
|
164
|
+
0.02-tick tolerance. It cancels when a previously followed source ends. A large
|
|
165
|
+
forward jump reselects a boundary rather than replaying all missed intervals.
|
|
166
|
+
|
|
167
|
+
The scheduled callback is not wrapped in the subscription error logger: an
|
|
168
|
+
exception propagates from the synchronous callback/timer/frame invocation, though
|
|
169
|
+
an enabled diagnostic span records it. Audio seeks are not a fully specified
|
|
170
|
+
interval rebaselining API; cancel and re-register around seeks when that matters.
|
|
171
|
+
|
|
172
|
+
### `deferStart(target, intervalTicks, options?) -> Cancel`
|
|
173
|
+
|
|
174
|
+
Requires an element with `nodeType === 1`, otherwise throws `TypeError`. Options
|
|
175
|
+
are those above plus `start?: (detail) => void`.
|
|
176
|
+
|
|
177
|
+
Immediately sets `data-dance-moves-waiting="true"` and removes the started marker.
|
|
178
|
+
At the boundary it sets `data-dance-moves-started="true"`, removes the waiting
|
|
179
|
+
marker, resets/plays animations returned by `target.getAnimations({subtree:true})`,
|
|
180
|
+
then calls `start(detail)` and dispatches bubbling `dance-moves-start` from the
|
|
181
|
+
target. This animation reset is not filtered through registered ownership scopes.
|
|
182
|
+
If `start` throws, the subsequent start event is not dispatched.
|
|
183
|
+
|
|
184
|
+
Cancellation only cancels the schedule; it does **not** clear waiting markers or
|
|
185
|
+
restore animation styles. A programmatic target is not automatically paused by
|
|
186
|
+
setting the waiting marker: the provided CSS pause selector specifically uses
|
|
187
|
+
`data-dance-moves-start-interval`. Supply that attribute or your own waiting style.
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
const target = document.querySelector('#release-intro');
|
|
191
|
+
if (target) {
|
|
192
|
+
target.setAttribute('data-dance-moves-start-interval', '16');
|
|
193
|
+
const cancel = DanceMoves.deferStart(target, 16, {
|
|
194
|
+
clock: 'audio',
|
|
195
|
+
id: 'release:intro',
|
|
196
|
+
start(detail) { target.dataset.startTick = String(detail.boundaryTick); }
|
|
197
|
+
});
|
|
198
|
+
// Keep cancel for disposal. Remove/restore waiting styles yourself on abort.
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `onNextInterval(func, interval, metadata?) -> Cancel`
|
|
203
|
+
|
|
204
|
+
**Callback first, interval second.** Equivalent to audio-clock
|
|
205
|
+
`scheduleAtInterval(interval, func, {strictlyFuture:true, ...identity})`, using the
|
|
206
|
+
active matching player. Fires once and cancels itself. `metadata` is diagnostic
|
|
207
|
+
identity only; it cannot select an explicit audio element.
|
|
208
|
+
|
|
209
|
+
### `onEveryInterval(func, interval, metadata?) -> Cancel`
|
|
210
|
+
|
|
211
|
+
**Callback first, interval second.** Throws `TypeError` for a nonfunction. Repeats
|
|
212
|
+
on the active matching player's integer interval boundaries, beginning with a
|
|
213
|
+
future boundary. Waits while no player is active, pauses with playback and removes
|
|
214
|
+
itself after its followed source ends. It is not automatically reinstalled when
|
|
215
|
+
a finished track is replayed.
|
|
216
|
+
|
|
217
|
+
A switch to another active player selects a new future boundary. A forward jump
|
|
218
|
+
of more than an interval skips the backlog; smaller delays can result in catch-up
|
|
219
|
+
callbacks. Backward seeks do not automatically reset this method's saved boundary.
|
|
220
|
+
Cancel/re-register on seek for deterministic post-seek intervals. Callback errors
|
|
221
|
+
are logged as `DanceMoves interval handler failed` and do not remove the loop.
|
|
222
|
+
|
|
223
|
+
### Beat and bar aliases
|
|
224
|
+
|
|
225
|
+
| Method | Equivalent |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `onNextBeat(func, metadata?) -> Cancel` | `onNextInterval(func, 16, metadata)` |
|
|
228
|
+
| `onEveryBeat(func, metadata?) -> Cancel` | `onEveryInterval(func, 16, metadata)` |
|
|
229
|
+
| `onNextBar(func, metadata?) -> Cancel` | `onNextInterval(func, 64, metadata)` |
|
|
230
|
+
| `onEveryBar(func, metadata?) -> Cancel` | `onEveryInterval(func, 64, metadata)` |
|
|
231
|
+
|
|
232
|
+
Bars mean **four beats**. A three-beat phrase should use interval 48 explicitly.
|
|
233
|
+
|
|
234
|
+
## Timing parsers
|
|
235
|
+
|
|
236
|
+
### `normaliseCueName(value) -> string`
|
|
237
|
+
|
|
238
|
+
Coerces a falsy value to an empty string, trims, uppercases, replaces each run of
|
|
239
|
+
characters outside ASCII `A–Z` and `0–9` with a space, and trims again. It is not
|
|
240
|
+
Unicode-aware transliteration; retain original text for display.
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
DanceMoves.normaliseCueName(' chorus-1! '); // 'CHORUS 1'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### `parseTimingFile(text) -> CueEntry[]`
|
|
247
|
+
|
|
248
|
+
Returns `[]` for nonstrings or strings containing a null or replacement character.
|
|
249
|
+
Normalises line endings; reads bracketed timestamps with one-to-three minute
|
|
250
|
+
digits, two second digits, and optional one-to-three decimal fraction digits
|
|
251
|
+
introduced by `.` or `:`. Removes timestamps from the label. Empty labels and
|
|
252
|
+
untimestamped lines are ignored. Multiple timestamps create multiple entries.
|
|
253
|
+
A label such as `[SECTION: CHORUS 1]` becomes type `SECTION`, name `CHORUS 1`;
|
|
254
|
+
otherwise type defaults to `CUE` and outer square brackets are removed from name.
|
|
255
|
+
Entries are filtered to finite nonnegative time and sorted by time.
|
|
256
|
+
|
|
257
|
+
```text
|
|
258
|
+
CueEntry = {
|
|
259
|
+
time: number, // seconds
|
|
260
|
+
type: string, name: string, label: string,
|
|
261
|
+
normalisedType: string, normalisedName: string, normalisedLabel: string
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The parser does not fetch, install, validate file size, implement LRC offsets,
|
|
266
|
+
require physically ordered timestamps, or execute labels. Server attachment
|
|
267
|
+
validation and its error codes are documented separately.
|
|
268
|
+
|
|
269
|
+
### `parseLyricTimingFile(text) -> LyricEntry[]`
|
|
270
|
+
|
|
271
|
+
Same input rejection and timestamp grammar, but **keeps blank timed lines** and
|
|
272
|
+
treats the remaining text as the lyric, without cue-type parsing. Returns entries
|
|
273
|
+
sorted by time. The final entry at an equal timestamp is the one selected by the
|
|
274
|
+
playback engine's at-or-before lookup.
|
|
275
|
+
|
|
276
|
+
```text
|
|
277
|
+
LyricEntry = { time: number, text: string, normalisedText: string }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Cue and lyric subscriptions
|
|
281
|
+
|
|
282
|
+
### `onCue(name, handler, metadata?) -> Cancel`
|
|
283
|
+
|
|
284
|
+
Throws `TypeError` for a nonfunction. `name === "*"` receives all cues; other
|
|
285
|
+
names are normalised. A cue calls the group for its normalised name, then its
|
|
286
|
+
normalised full label when different, then the wildcard group. It does **not**
|
|
287
|
+
independently dispatch a group for the cue type. To observe all `SECTION` cues,
|
|
288
|
+
subscribe to `"*"` and filter `detail.normalisedType`.
|
|
289
|
+
|
|
290
|
+
For the same normalised key and same function object, registering again replaces
|
|
291
|
+
metadata rather than adding another entry. Removers delete that function from
|
|
292
|
+
the group, so old removers can remove a later re-registration of the same pair.
|
|
293
|
+
Separate registrations for name, label and wildcard can invoke the same function
|
|
294
|
+
more than once for one cue. Callback exceptions are logged and later handlers
|
|
295
|
+
continue.
|
|
296
|
+
|
|
297
|
+
### Cue detail and dispatch order
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
CueDetail = {
|
|
301
|
+
pageId: number,
|
|
302
|
+
time: number, // cue time, seconds
|
|
303
|
+
type: string, name: string, label: string,
|
|
304
|
+
normalisedType: string, normalisedName: string,
|
|
305
|
+
audio: HTMLAudioElement | null,
|
|
306
|
+
resetAnimationCount: number
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`normalisedLabel` is present on parsed entries but is **not** included in the
|
|
311
|
+
published detail. Dispatch first resets running owned animations, then invokes
|
|
312
|
+
registered handlers, then dispatches `dance-moves-cue` and the legacy
|
|
313
|
+
`kieran-epk-cue` on `document`. These bubbling custom events share the same detail
|
|
314
|
+
object; do not mutate it. Listening to both event names duplicates your work.
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
const removeCue = DanceMoves.onCue('CHORUS 1', detail => {
|
|
318
|
+
console.log(detail.time, detail.resetAnimationCount);
|
|
319
|
+
}, { id: 'release:chorus' });
|
|
320
|
+
|
|
321
|
+
function inspectCue(event) { console.log(event.detail.name); }
|
|
322
|
+
document.addEventListener('dance-moves-cue', inspectCue);
|
|
323
|
+
// Cleanup:
|
|
324
|
+
removeCue();
|
|
325
|
+
document.removeEventListener('dance-moves-cue', inspectCue);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### `fireCue(input) -> CueDetail`
|
|
329
|
+
|
|
330
|
+
Pass `{name?, type?, label?, time?}`. Requires a nonempty name or fallback label;
|
|
331
|
+
otherwise throws `TypeError`. Default type is `CUE`; default label is the name.
|
|
332
|
+
Invalid/negative time becomes zero. Normalisation, owned resets, handlers and DOM
|
|
333
|
+
events follow the ordinary cue path, but `audio` is `null`. Useful for a local
|
|
334
|
+
harness; it does not seek audio or alter the loaded file/cue cursor. Calling it is
|
|
335
|
+
not gated by the diagnostics flag and it can reset real page animations.
|
|
336
|
+
|
|
337
|
+
<a id="onlyric"></a>
|
|
338
|
+
|
|
339
|
+
### `onLyric(handler, metadata?) -> Cancel`
|
|
340
|
+
|
|
341
|
+
Throws `TypeError` for a nonfunction. Subscribes by function identity; registering
|
|
342
|
+
the same function again replaces its metadata. Runs when a bound playing audio
|
|
343
|
+
changes lyric index and on playback restart/reindex, including blank entries and
|
|
344
|
+
the before-first-entry state. There is no name filter and no immediate replay on
|
|
345
|
+
registration. Callback errors are logged and do not stop subsequent handlers.
|
|
346
|
+
|
|
347
|
+
```text
|
|
348
|
+
LyricDetail = {
|
|
349
|
+
pageId: number,
|
|
350
|
+
time: number, // entry seconds; 0 if no entry
|
|
351
|
+
text: string, normalisedText: string,
|
|
352
|
+
index: number, // zero-based; -1 before the first entry
|
|
353
|
+
nextTime: number | null,
|
|
354
|
+
nextText: string, nextNormalisedText: string, nextIndex: number,
|
|
355
|
+
nextVisibleTime: number | null,
|
|
356
|
+
nextVisibleText: string, nextVisibleNormalisedText: string,
|
|
357
|
+
nextVisibleIndex: number,
|
|
358
|
+
audio: HTMLAudioElement | null
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`next*` describes the **immediately following LRC entry**, including a blank
|
|
363
|
+
clear. `nextVisible*` scans forward to the first nonblank trimmed lyric, skipping
|
|
364
|
+
all blank clears. Missing next entries use `null` time, `""` text/normalised text
|
|
365
|
+
and `-1` index. Before the first entry, look-ahead starts at entry zero. Repeated
|
|
366
|
+
nonblank lines still count as distinct entries; normalisation is the same ASCII
|
|
367
|
+
cue-name normalisation, not a translation or Unicode-preserving identifier.
|
|
368
|
+
|
|
369
|
+
For `[00:01]A`, `[00:02]`, `[00:03]B`, the A event has `nextTime: 2`,
|
|
370
|
+
`nextText: ""`, `nextVisibleTime: 3`, and `nextVisibleText: "B"`. A preview should
|
|
371
|
+
use those timestamps with `detail.audio.currentTime`; do not parse the LRC twice
|
|
372
|
+
or assume the next visible line starts at the blank clear. Fields were added by
|
|
373
|
+
the 2.4.1/2.5.1 changes and are included in 2.8.0.
|
|
374
|
+
|
|
375
|
+
After callbacks, the core dispatches bubbling `dance-moves-lyric` on `document`
|
|
376
|
+
with the same detail. It then updates the built-in visual layer when enabled.
|
|
377
|
+
No compatibility lyric-event alias exists. Pause/end hide the built-in renderer
|
|
378
|
+
without invoking subscribers or dispatching a blank lyric. A custom renderer
|
|
379
|
+
must implement its own pause/end handling with the native audio events.
|
|
380
|
+
|
|
381
|
+
### Exact cue landings
|
|
382
|
+
|
|
383
|
+
The core tests a **50 ms** neighbourhood at initial audio binding, after `seeked`,
|
|
384
|
+
and after end when re-arming time zero. It selects the earliest cue in that
|
|
385
|
+
neighbourhood, not necessarily the nearest. A paused landing is held until
|
|
386
|
+
`play`/`playing`; a playing seek invokes it during restart. The pending marker is
|
|
387
|
+
cleared so the ordinary `play` followed by `playing` sequence does not duplicate
|
|
388
|
+
that pending dispatch. Seeking clears the previous pending marker.
|
|
389
|
+
|
|
390
|
+
This is separate from skipped-cue suppression and from the interval schedulers.
|
|
391
|
+
Do not assume an arbitrary current section is reconstructed by `onCue()`.
|
|
392
|
+
If a pending cue is slightly **ahead** of a landing, the cursor can later cross
|
|
393
|
+
that same cue normally, so the pending-marker rule is not universal cue-ID
|
|
394
|
+
deduplication. Use [cueTimeline](effects.md#cuetimelineoptions) when you need
|
|
395
|
+
explicit traversal deduplication, interval state, or optional seek-landings.
|
|
396
|
+
|
|
397
|
+
## Animation ownership and audio discovery
|
|
398
|
+
|
|
399
|
+
### `registerAnimationScope(root, selectors) -> Cancel`
|
|
400
|
+
|
|
401
|
+
Registers a root exposing `getAnimations()`. A missing/incompatible root produces
|
|
402
|
+
a no-op remover. `selectors` must be an array; falsy entries are removed. **An
|
|
403
|
+
empty list owns no animations.** An animation must target an element inside the
|
|
404
|
+
root and match one selector itself or through `closest()`. Invalid selectors are
|
|
405
|
+
ignored. Use narrow release-owned selectors, or `["*"]` only when the entire
|
|
406
|
+
subtree is deliberately owned. Multiple scopes deduplicate animation objects.
|
|
407
|
+
|
|
408
|
+
Returns a remover for that one scope. It does not stop animations or undo styling.
|
|
409
|
+
|
|
410
|
+
### `resetRunningAnimations(audio?) -> number`
|
|
411
|
+
|
|
412
|
+
Finds owned animations in `running` or `pending` state. For each, sets
|
|
413
|
+
`currentTime = 0`, sets a positive supplied `audio.playbackRate` (otherwise 1),
|
|
414
|
+
and calls `play()`. Returns the count of successfully reset animations. Paused,
|
|
415
|
+
finished and unrelated animations are not restarted. Per-animation failures are
|
|
416
|
+
ignored. Passing `audio` does not perform a duration-match check here.
|
|
417
|
+
|
|
418
|
+
<a id="audio-discovery-and-playback"></a>
|
|
419
|
+
|
|
420
|
+
### `discoverAudio() -> void`
|
|
421
|
+
|
|
422
|
+
Scans current `<audio>` elements, establishes a reference duration if absent, and
|
|
423
|
+
binds players whose positive finite duration differs from the reference by at
|
|
424
|
+
most `max(0.25, audio.duration / 100000)` seconds. Unknown durations get a one-shot
|
|
425
|
+
`loadedmetadata` listener. Bound elements are retained in a WeakSet and marked
|
|
426
|
+
`data-dance-moves-timing="master-length"`; later calls do not double-bind them.
|
|
427
|
+
|
|
428
|
+
Reference selection uses the first element in document order matching
|
|
429
|
+
`audio[data-dance-moves-master], #ks-clay-stars-audio`, otherwise the first audio. The reference is fixed once known. Replacing the
|
|
430
|
+
source on an already bound element does not remove its existing binding or
|
|
431
|
+
revalidate the new source. For dynamic player replacement, create a new element
|
|
432
|
+
and call discovery; use a fresh page load when changing the master programme.
|
|
433
|
+
|
|
434
|
+
The playback engine binds `play`, `playing`, `pause`, `ended`, `seeking`, `seeked`,
|
|
435
|
+
`ratechange` and `timeupdate`. It uses animation frames while playing. There is no
|
|
436
|
+
public unbind method or automatic discovery of later DOM insertions in the core.
|
|
437
|
+
The separate [effects API](effects.md) supplies a cue-timeline controller; its
|
|
438
|
+
`start()`/`stop()` control effect processing, not the underlying media transport.
|
|
439
|
+
|
|
440
|
+
## Optional catalogue additions
|
|
441
|
+
|
|
442
|
+
When an EPK root is found by the separately loaded catalogue script, these
|
|
443
|
+
functions are added to the same `DanceMoves` / `KieranEpkMotion` object:
|
|
444
|
+
|
|
445
|
+
| Function | Meaning |
|
|
446
|
+
| --- | --- |
|
|
447
|
+
| `applyCatalogueTiming() -> CatalogueSnapshot` | Re-run CSS timing adoption. |
|
|
448
|
+
| `catalogueTimingSnapshot() -> CatalogueSnapshot` | Read bounded conversion evidence. |
|
|
449
|
+
| `removeCatalogueAnimationScope() -> void` | Unregister the catalogue's wildcard cue-reset scope only. |
|
|
450
|
+
|
|
451
|
+
`DanceMovesCatalogueTiming.apply()` and `.snapshot()` alias the first two.
|
|
452
|
+
These additions are absent if the adopter cannot find a root. See the complete
|
|
453
|
+
[adopter contract](adapters.md#catalogue-timing), including what removal does not
|
|
454
|
+
undo.
|
|
455
|
+
|
|
456
|
+
## Diagnostics
|
|
457
|
+
|
|
458
|
+
### `setDiagnosticsSink(sink) -> boolean`
|
|
459
|
+
|
|
460
|
+
Returns `false` without enabling anything unless boot configuration contained
|
|
461
|
+
literal `diagnostics: true`. When permitted, `null`/`undefined` removes the sink,
|
|
462
|
+
a function installs/replaces it, and any other value throws `TypeError`.
|
|
463
|
+
Successful changes return `true`. Only one sink is retained.
|
|
464
|
+
|
|
465
|
+
### `diagnosticsEnabled() -> boolean`
|
|
466
|
+
|
|
467
|
+
True only when diagnostics were allowed at boot and a sink is currently attached.
|
|
468
|
+
Enabling the flag alone does not retain or publish records.
|
|
469
|
+
|
|
470
|
+
Every record contains `type`, `startTime`, `endTime`, `duration` (milliseconds) and
|
|
471
|
+
`pageId`. `duration` measures synchronous work around a span, not the lifetime of
|
|
472
|
+
a returned Promise or the full browser rendering pipeline. Optional `error` is a
|
|
473
|
+
message limited to 500 characters. Sink exceptions are swallowed.
|
|
474
|
+
|
|
475
|
+
| Record `type` | Additional fields |
|
|
476
|
+
| --- | --- |
|
|
477
|
+
| `interval-handler` | `handlerId`, `intervalTicks`, `boundaryTick`, `clock`, `repeating` |
|
|
478
|
+
| `cue-handler` | `handlerId`, `registration`, `cueName`, `cueType`; DOM event dispatches use `custom-event:<event-name>` IDs |
|
|
479
|
+
| `lyric-handler` | `handlerId`, `lyricIndex`, `lyricTime` |
|
|
480
|
+
| `animation-reset` | `resetCount`, `playbackRate` |
|
|
481
|
+
| `cue-dispatch-total` | `cueName`, `cueType`, `resetAnimationCount` |
|
|
482
|
+
| `cue-reindex` | `time`, `nextIndex`, `cueCount` |
|
|
483
|
+
| `cue-detect` | `currentTime`, `firedCount`, `nextIndex` |
|
|
484
|
+
|
|
485
|
+
A bounded development receiver:
|
|
486
|
+
|
|
487
|
+
```js
|
|
488
|
+
const records = [];
|
|
489
|
+
const allowed = DanceMoves.setDiagnosticsSink(record => {
|
|
490
|
+
if (records.length === 200) records.shift();
|
|
491
|
+
records.push(record);
|
|
492
|
+
});
|
|
493
|
+
// `allowed` is false on ordinary WordPress pages.
|
|
494
|
+
// Dispose after collecting the local evidence:
|
|
495
|
+
DanceMoves.setDiagnosticsSink(null);
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
The core neither stores nor transmits diagnostic records. This does not describe
|
|
499
|
+
external sinks or the independent capture REST endpoint. Keep detailed diagnostics
|
|
500
|
+
and test recording interfaces out of public deployment.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Paper-plane effect API
|
|
2
|
+
|
|
3
|
+
[Documentation index](../README.md) · [Page metadata](wordpress.md#page-metadata) ·
|
|
4
|
+
[Shared primitives](effects.md) · [CSS/HTML](styling.md)
|
|
5
|
+
|
|
6
|
+
Sources: [JavaScript](../../assets/paper-dreams-flight.js),
|
|
7
|
+
[stylesheet](../../assets/paper-dreams-flight.css),
|
|
8
|
+
[PHP loading/configuration](../../kieran-epk-device-orientation.php).
|
|
9
|
+
Plugin baseline: **2.8.0 / `4cb6a71f60459b5579be87d7e55ac8b1426c578e`**.
|
|
10
|
+
The renderer's internal snapshot version remains **`2.6.3`**; that is not the
|
|
11
|
+
version of the installed DanceMoves plugin.
|
|
12
|
+
|
|
13
|
+
## Enable and supply the required DOM
|
|
14
|
+
|
|
15
|
+
Set the Page's **EPK Timing → Ambient effect → Paper planes**, which stores
|
|
16
|
+
`_dance_moves_effect = "paper-planes"`. The same field is revision-aware and
|
|
17
|
+
REST-exposed. `dance_moves_sanitize_effect()` allows only `"paper-planes"` or `""`;
|
|
18
|
+
blank, missing or invalid editor input clears it. Selection loads the
|
|
19
|
+
`dance-moves-paper-dreams` CSS/JS handle with `dance-moves-core` as a dependency.
|
|
20
|
+
The atlas is a bundled image, not a remote model or generated-on-demand asset.
|
|
21
|
+
|
|
22
|
+
The JS finds the first `.ks-epk` and its first `.epk-hero`; both must exist when
|
|
23
|
+
it executes. It returns without installation if either is absent or
|
|
24
|
+
`window.DanceMovesPaperDreams` already exists. It does not wait for later DOM
|
|
25
|
+
insertion, register a ready event, or create a hero/player.
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<main class="ks-epk" data-release="example-release">
|
|
29
|
+
<section class="epk-hero">
|
|
30
|
+
<div class="epk-heading"><h1>Example release</h1></div>
|
|
31
|
+
<div class="epk-cover-wrap">Existing artwork</div>
|
|
32
|
+
</section>
|
|
33
|
+
</main>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The renderer inserts an `aria-hidden="true"` `.paper-dreams-flight` stage as the
|
|
37
|
+
hero's first child. It is decorative and pointer-transparent. The JavaScript
|
|
38
|
+
activation is release-agnostic, but the shipped rule that establishes hero
|
|
39
|
+
`position:relative; isolation:isolate; overflow:hidden` is scoped to
|
|
40
|
+
`data-release="paper-dreams-from-the-sky"`. Another release must supply its own
|
|
41
|
+
hero containment; selecting the effect is not sufficient proof of correct bounds.
|
|
42
|
+
|
|
43
|
+
For the illustrative markup above, a page-owned containment rule is:
|
|
44
|
+
|
|
45
|
+
```css
|
|
46
|
+
.ks-epk[data-release="example-release"] .epk-hero {
|
|
47
|
+
position: relative;
|
|
48
|
+
isolation: isolate;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The stage uses absolute inset bounds and z-index 2. Following siblings are made
|
|
54
|
+
positioned; following `.epk-cover-wrap` uses z-index 1 and `.epk-heading` z-index 3.
|
|
55
|
+
Audit player/control stacking explicitly; the stylesheet does not guarantee that
|
|
56
|
+
every arbitrary hero child is above the planes. On screens up to 760 px, its
|
|
57
|
+
stage inset starts 72 px from the top. Existing hero dimensions determine bounds.
|
|
58
|
+
|
|
59
|
+
## Boot configuration: window.danceMovesPaperDreamsConfig
|
|
60
|
+
|
|
61
|
+
PHP emits this before the JS. These are trusted boot values, **not** individually
|
|
62
|
+
editable settings in the WordPress sidebar or a runtime setter API.
|
|
63
|
+
|
|
64
|
+
| Key | WordPress value | Actual consumer behaviour |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `effect` | `"paper-planes"` | Emitted for identity; the JS does not inspect it. PHP has already gated loading. |
|
|
67
|
+
| `atlasUrl` | Plugin `assets/paper-dreams-plane-atlas.png` URL | Written into the stage's `--paper-dreams-atlas` CSS URL; no fetch/error API. |
|
|
68
|
+
| `planeCount` | `12` | Loop bound uses `Number(config.planeCount || 12)`. Not clamped to a hard maximum in JS; keep the shipped cap. |
|
|
69
|
+
| `compactPlaneCount` | `5` | Limits stepped planes in compact mode. Shipped CSS independently hides child 6 onward; changing only this number does not change that CSS cap. |
|
|
70
|
+
| `bpm` | Page-resolved BPM, fallback 120 | Used in page-clock conversion and snapshot. Keep it consistent with `DanceMoves.bpm`. |
|
|
71
|
+
| `effectBounds` | `"hero"` | Informational only; code always finds `.epk-hero`. It does not select a viewport mode or arbitrary selector. |
|
|
72
|
+
|
|
73
|
+
Zero/falsy counts fall back rather than disabling the effect. Arbitrary nonfinite,
|
|
74
|
+
negative, fractional or excessive boot values are not a supported tuning surface;
|
|
75
|
+
there is no robust public schema validator here. Use the Page effect field to
|
|
76
|
+
disable loading. The `planeCount` constant is a configured default, not an
|
|
77
|
+
unbreakable safety clamp.
|
|
78
|
+
|
|
79
|
+
## Rendering and lifecycle
|
|
80
|
+
|
|
81
|
+
Twelve pooled span nodes use a **6-column × 4-row atlas** and three depth profiles.
|
|
82
|
+
Plane selection uses `(index * 5) % 24`; no per-frame DOM insertion is required.
|
|
83
|
+
States cycle through `glide`, `climb`, `mush`, `stall`, `drop`, `recovery`, `swoop`
|
|
84
|
+
and back to glide, with bounded recycling outside the hero. Transform updates
|
|
85
|
+
include translation, pitch/roll-derived rotation and a scale-X squeeze. Two
|
|
86
|
+
sprite-local pseudo-elements supply bounded shutter echoes; no moving blur or
|
|
87
|
+
blend-mode field is added.
|
|
88
|
+
|
|
89
|
+
Geometry is cached from the hero and ResizeObserver, then passed once to the
|
|
90
|
+
plane loop. Integration delta is clamped to 0.05 seconds per frame. The ambient
|
|
91
|
+
phase reads `DanceMoves.currentTick({clock:"page"})`, not audio time, with a frame
|
|
92
|
+
clock fallback when the core is absent. **This is not a beat-triggered choreography
|
|
93
|
+
or a seek-reconstructable audio simulation**: physical flight state advances from
|
|
94
|
+
frame deltas, and phase conversion does not make every manoeuvre an integer-tick
|
|
95
|
+
duration. Do not apply the core CSS-duration rule as a false description of the
|
|
96
|
+
physics constants in this adapter.
|
|
97
|
+
|
|
98
|
+
A compact match is `(max-width: 760px), (update: slow)`; the first five planes are
|
|
99
|
+
stepped and later nodes are hidden by CSS. The stage's `data-quality` values are
|
|
100
|
+
`full`, `compact`, or `static`. These are distinct from the shared `quality()`
|
|
101
|
+
primitive and the Clay/Stars measured performance tiers.
|
|
102
|
+
|
|
103
|
+
The loop pauses for `document.hidden`, reduced motion and, when IntersectionObserver
|
|
104
|
+
is available, a hero outside its 120 px root margin. Without that observer,
|
|
105
|
+
offscreen pausing is unavailable. Visibility, reduced-motion and compact changes
|
|
106
|
+
call reconciliation. Returning to visibility resets the frame delta accumulator
|
|
107
|
+
but preserves plane state; hidden elapsed time is not replayed as flight motion.
|
|
108
|
+
|
|
109
|
+
Reduced motion stops the JavaScript loop and sets `static`. Although non-media
|
|
110
|
+
static rules describe three decorative sprites, the final reduced-motion CSS
|
|
111
|
+
hides **all** plane nodes and echoes with `!important`. Do not promise visible
|
|
112
|
+
static planes under that preference. Forced colours hides the entire stage using
|
|
113
|
+
CSS; the JS does not observe forced colours, so that preference alone does **not**
|
|
114
|
+
stop the loop. Audit CPU cost separately from visual hiding.
|
|
115
|
+
|
|
116
|
+
## Window API and snapshot
|
|
117
|
+
|
|
118
|
+
`window.DanceMovesPaperDreams` is installed after initial reconciliation. There
|
|
119
|
+
is one instance and no constructor/factory exposed for arbitrary multiple roots.
|
|
120
|
+
The object is not frozen; treat it as a handle, not a mutable configuration bag.
|
|
121
|
+
|
|
122
|
+
| Member | Type / behaviour |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `root` | The generated `.paper-dreams-flight` stage element, **not** the enclosing `.ks-epk`. |
|
|
125
|
+
| `snapshot()` | Returns the object below; no historical frames retained. |
|
|
126
|
+
| `teardown()` | Returns `undefined`. Stops/cancels the loop, disconnects intersection/resize observers, removes media/visibility listeners and removes the generated stage. |
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
{
|
|
130
|
+
version: "2.6.3", // adapter-local implementation marker
|
|
131
|
+
planes: number, // allocated nodes, normally 12 even in compact mode
|
|
132
|
+
active: boolean, // frame loop running
|
|
133
|
+
quality: "full" | "compact" | "static",
|
|
134
|
+
bpm: number,
|
|
135
|
+
bounds: "hero",
|
|
136
|
+
states: string[] // one current state per allocated plane
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
There are no public `setParameters`, `setMotion`, `pause`, `resume`, `reset` or
|
|
141
|
+
`start` methods. Teardown does not delete `window.DanceMovesPaperDreams`; injecting
|
|
142
|
+
the script again encounters the existing-global guard. Prefer a fresh page load
|
|
143
|
+
for restart after teardown; do not claim disposal is a general remount facility.
|
|
144
|
+
There is no automatic teardown on root removal.
|
|
145
|
+
|
|
146
|
+
## Styling contract
|
|
147
|
+
|
|
148
|
+
These properties and attributes describe the generated DOM. They are useful for
|
|
149
|
+
release-owned CSS inspection, not promises of additional JavaScript setters.
|
|
150
|
+
|
|
151
|
+
| Surface | Meaning |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `.paper-dreams-flight` | Contained, clipped, non-interactive decorative stage. |
|
|
154
|
+
| `.paper-dreams-flight__plane` | Pooled sprite; animated via inline `transform`. |
|
|
155
|
+
| `--paper-dreams-atlas` | Atlas background URL inherited from stage. |
|
|
156
|
+
| `--sprite-x`, `--sprite-y` | Per-node atlas-cell percentage positions. |
|
|
157
|
+
| `--plane-size`, `--plane-size-compact` | Per-depth normal/compact widths. |
|
|
158
|
+
| `--plane-opacity` | Per-depth opacity. |
|
|
159
|
+
| `--static-transform` | Precomputed decorative transform, overridden by final reduced-motion hiding. |
|
|
160
|
+
| `data-depth` | `0`, `1`, `2` depth layer. |
|
|
161
|
+
| `data-flight-state` | Flight state above; drop/recovery/swoop strengthen local echoes. |
|
|
162
|
+
| Stage `data-quality` | `full`, `compact`, `static`. |
|
|
163
|
+
|
|
164
|
+
Use the [preview](../../tests/paper-dreams-flight-preview.html) and
|
|
165
|
+
[contract](../../tests/paper-dreams-flight.test.cjs) for local checks. They do not
|
|
166
|
+
replace signed-out viewport, overflow, readability, offscreen/visibility,
|
|
167
|
+
accessibility and measured device-performance checks on the complete adopting EPK.
|