@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.
Files changed (50) hide show
  1. package/LICENSE +338 -0
  2. package/README.md +893 -0
  3. package/RUDIMENTS-API.md +394 -0
  4. package/docs/README.md +73 -0
  5. package/docs/api/adapters.md +400 -0
  6. package/docs/api/downloads.md +158 -0
  7. package/docs/api/effects.md +463 -0
  8. package/docs/api/javascript.md +500 -0
  9. package/docs/api/paper-planes.md +167 -0
  10. package/docs/api/styling.md +292 -0
  11. package/docs/api/surface.json +446 -0
  12. package/docs/api/wordpress.md +365 -0
  13. package/docs/clay-transform-validation.md +84 -0
  14. package/docs/development.md +254 -0
  15. package/docs/guide.md +288 -0
  16. package/docs/javascript-api.md +292 -0
  17. package/docs/javascript-server.md +150 -0
  18. package/docs/react-next.md +185 -0
  19. package/docs/releasing-shared.md +162 -0
  20. package/docs/rudiments-source-integrity.md +80 -0
  21. package/docs/shared-feature-coverage.md +34 -0
  22. package/docs/shared-runtime-migration.md +111 -0
  23. package/docs/source-audit.md +120 -0
  24. package/docs/validation-setup.md +102 -0
  25. package/docs/wordpress-examples.md +206 -0
  26. package/docs/wordpress-shared-runtime.md +133 -0
  27. package/lib/admin.min.js +1 -0
  28. package/lib/assets/DanceRudiments-LICENSE +21 -0
  29. package/lib/assets/DanceRudiments-build-manifest.json +21 -0
  30. package/lib/assets/paper-dreams-plane-atlas.png +0 -0
  31. package/lib/build-manifest.json +117 -0
  32. package/lib/dancemoves.css +9 -0
  33. package/lib/dancemoves.min.js +5 -0
  34. package/lib/index.cjs +4802 -0
  35. package/lib/index.d.ts +71 -0
  36. package/lib/index.mjs +4780 -0
  37. package/lib/react.cjs +154 -0
  38. package/lib/react.d.ts +12 -0
  39. package/lib/react.mjs +135 -0
  40. package/lib/rudiments.d.ts +124 -0
  41. package/lib/server.cjs +411 -0
  42. package/lib/server.d.ts +11 -0
  43. package/lib/server.mjs +367 -0
  44. package/lib/styles/clay-stars-effects.css +1 -0
  45. package/lib/styles/clay-stars-rudiments.css +1 -0
  46. package/lib/styles/dance-moves-core.css +1 -0
  47. package/lib/styles/ks-epk-device-orientation.css +1 -0
  48. package/lib/styles/paper-dreams-flight.css +1 -0
  49. package/lib/wordpress.js +5 -0
  50. 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.