@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,292 @@
|
|
|
1
|
+
# Shared JavaScript API
|
|
2
|
+
|
|
3
|
+
Package: `@kieransimkin/dancemoves`, source candidate 3.0.0.
|
|
4
|
+
This guide describes the public shared module; [WordPress](wordpress-shared-runtime.md)
|
|
5
|
+
loads the same runtime through its adapter.
|
|
6
|
+
|
|
7
|
+
## Installation and first mount
|
|
8
|
+
|
|
9
|
+
After publication:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @kieransimkin/dancemoves
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
From this repository before publication, run `npm install --ignore-scripts`,
|
|
16
|
+
commit the generated lockfile, and run `npm run build`. The included demos resolve
|
|
17
|
+
the built package through the repository's own package exports, not a registry
|
|
18
|
+
copy or a sibling checkout.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import {createDanceMoves} from '@kieransimkin/dancemoves';
|
|
22
|
+
import '@kieransimkin/dancemoves/styles.css';
|
|
23
|
+
|
|
24
|
+
const root = document.querySelector('#arcadians');
|
|
25
|
+
const runtime = createDanceMoves({
|
|
26
|
+
root,
|
|
27
|
+
bpm: 145,
|
|
28
|
+
lyricTimingUrl: '/media/canonical-lyric-timing.lrc',
|
|
29
|
+
cueTimingUrl: '/media/sections.cue',
|
|
30
|
+
masterDurationMilliseconds: 273604.558
|
|
31
|
+
});
|
|
32
|
+
await runtime.ready;
|
|
33
|
+
const stopBeat = runtime.onEveryBeat(({boundaryTick}) => {
|
|
34
|
+
root.dataset.lastBeat = String(boundaryTick / 16);
|
|
35
|
+
}, {id: 'arcadians:beat'});
|
|
36
|
+
|
|
37
|
+
// Route/component cleanup:
|
|
38
|
+
stopBeat();
|
|
39
|
+
runtime.destroy();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The root must exist in a browser document. It can contain normal HTML `<audio>`
|
|
43
|
+
elements. The library does not autoplay audio. The canonical discovery rules,
|
|
44
|
+
including explicit `data-dance-moves-master` and configured master-length
|
|
45
|
+
matching, are retained. Mount before starting playback; call `discoverAudio()`
|
|
46
|
+
after adding players dynamically when immediate discovery is required.
|
|
47
|
+
|
|
48
|
+
Imports are safe during SSR. Only `createDanceMoves` requires the browser.
|
|
49
|
+
CJS is available with `require('@kieransimkin/dancemoves')`. The independent
|
|
50
|
+
`/server` entry requires Node; `/react` requires React; importing the main module
|
|
51
|
+
does not import either dependency.
|
|
52
|
+
|
|
53
|
+
## Mount configuration
|
|
54
|
+
|
|
55
|
+
| Option | Default and contract |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `root` | Required Element. One mount per non-overlapping subtree. |
|
|
58
|
+
| `bpm` | Missing/null/empty selects 120 fallback. Otherwise a finite **number** from 20 to 400, rounded to three decimals. Changing tempo requires a new mount. |
|
|
59
|
+
| `pageId` | 0. Optional non-negative safe integer for diagnostics/legacy catalogue settings, not a requirement to have WordPress. |
|
|
60
|
+
| `bpmSource` | Computed; a supplied `fallback` preserves fallback provenance when converting host config. |
|
|
61
|
+
| `lyricTimingUrl`, `cueTimingUrl` | Empty. HTTP(S) or relative URLs; no embedded credentials or executable schemes. Served media must satisfy normal browser CORS rules. |
|
|
62
|
+
| `lyricPopupsEnabled` | false. Boolean, explicit opt-in to the canonical neutral renderer. Use `onLyric` or the React hook for custom treatments. |
|
|
63
|
+
| `masterDurationMilliseconds` | 0 (no length reference); finite number up to one day, rounded to three decimals. |
|
|
64
|
+
| `sharedControlTicks` | 0; enables the inherited control-duration treatment when positive. |
|
|
65
|
+
| `lyricDisclosureTicks` | 6; positive duration ticks. |
|
|
66
|
+
| `effect` | Empty or `paper-planes`. |
|
|
67
|
+
| `catalogue` | false; opt-in CSS timing adoption. |
|
|
68
|
+
| `clay` | false; initialize Clay and its native background adapter when the required Clay markup exists. |
|
|
69
|
+
| `clayPerformance` | Per-mount overrides for the existing Clay monitor. Fields retain their canonical validation and defaults. |
|
|
70
|
+
| `orientation` | Optional `{adapter, transitionTargetTicks, harness}`. Adapter is one of `ORIENTATION_ADAPTERS`. Harness remains loopback-only. |
|
|
71
|
+
| `paperPlanes` | `{atlasUrl, planeCount, compactPlaneCount}`. The atlas URL is required with `effect:'paper-planes'`; use the shipped asset, not a fabricated sprite. |
|
|
72
|
+
| `diagnostics` | false; explicitly enables diagnostic sinks. |
|
|
73
|
+
| `legacyGlobals` | false. WordPress compatibility mode; one per document, not for ordinary React roots. |
|
|
74
|
+
| `nonce` | Optional nonce attached to library-created style elements for an application's CSP. |
|
|
75
|
+
| `onError` | Optional callback for asynchronous readiness and cleanup errors. Handle individual native-controller `ready` failures as well. |
|
|
76
|
+
|
|
77
|
+
A bad root, overlapping mount or unknown adapter throws. Invalid config values
|
|
78
|
+
throw `TypeError`/`RangeError`; the public API does not silently turn bad BPM into
|
|
79
|
+
another tempo. `readWordPressConfig` is an internal wire adapter because
|
|
80
|
+
WordPress localization stringifies scalar values; generic JS should use typed
|
|
81
|
+
values directly.
|
|
82
|
+
|
|
83
|
+
`runtime.ready` completes canonical cue/lyric-file setup. Existing nonfatal file
|
|
84
|
+
fetch behaviour is preserved: an empty/unavailable timing file is not proof of a
|
|
85
|
+
working lyric track. Read native readiness separately with
|
|
86
|
+
`await runtime.rudiments.ready()`. `destroy()` is idempotent, aborts internal work
|
|
87
|
+
and removes resources; it does not stop or delete your audio element.
|
|
88
|
+
|
|
89
|
+
## Core methods: complete inherited surface
|
|
90
|
+
|
|
91
|
+
The core has **16 ticks per beat**. A tick lasts `3750 / bpm` milliseconds.
|
|
92
|
+
The native library uses **64 pips per beat**, so one core tick is four pips.
|
|
93
|
+
A bar in the convenience methods is four beats.
|
|
94
|
+
|
|
95
|
+
| Method | Meaning |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `quantizeTicks(value)` | Round half up, minimum one; durations above 16 ticks round to multiples of 16. |
|
|
98
|
+
| `durationMilliseconds(ticks, bpmOverride?)` | Quantized duration at configured/override BPM. Not a raw interval-spacing conversion. |
|
|
99
|
+
| `currentTick(options?)` | `{clock, audio, bpm}`. Uses a supplied/active playing audio clock; otherwise page elapsed time. A paused audio element does **not** give a paused clock through this legacy helper. |
|
|
100
|
+
| `nextIntervalTick(interval, fromTick, strictlyFuture?)` | Next integer-spaced interval boundary; interval spacing is not the long-duration quantizer. |
|
|
101
|
+
| `scheduleAtInterval(interval, callback, options?)` | Schedule a page/audio boundary and return a cancellation function. |
|
|
102
|
+
| `deferStart(element, interval, options?)` | Defer an element's visual start; returns cancellation. The canonical start attributes/options remain available. |
|
|
103
|
+
| `onNextInterval(callback, interval, metadata?)` | Next available audio boundary; returns unsubscribe. |
|
|
104
|
+
| `onEveryInterval(callback, interval, metadata?)` | Repeating audio interval handler; unsubscribe on cleanup. |
|
|
105
|
+
| `onNextBeat(callback, metadata?)` | `onNextInterval` at 16 ticks. |
|
|
106
|
+
| `onEveryBeat(callback, metadata?)` | Repeating 16-tick audio boundaries. |
|
|
107
|
+
| `onNextBar(callback, metadata?)` | Next 64-tick boundary. |
|
|
108
|
+
| `onEveryBar(callback, metadata?)` | Repeating 64-tick boundaries. |
|
|
109
|
+
| `normaliseCueName(value)` | Canonical uppercase normalized matching name. |
|
|
110
|
+
| `parseTimingFile(text)` | Parse bracketed timestamps and named/type cues, sorted by time. |
|
|
111
|
+
| `parseLyricTimingFile(text)` | Parse lyrics including empty clear entries, sorted by time. |
|
|
112
|
+
| `onCue(name, handler, metadata?)` | Subscribe to a name or `*`; returns unsubscribe. |
|
|
113
|
+
| `onLyric(handler, metadata?)` | Subscribe to canonical lyric details; returns unsubscribe. |
|
|
114
|
+
| `fireCue(cue)` | Dispatch through the same named-cue/diagnostic/event path. Supply `{name,type,time,audio?}`. |
|
|
115
|
+
| `registerAnimationScope(root, selectors)` | Explicit ownership of animations that may be reset/speed adjusted by cues. Returns removal function. |
|
|
116
|
+
| `resetRunningAnimations(audio?)` | Reset owned running/pending animations; returns reset count. Not a global animation reset. |
|
|
117
|
+
| `discoverAudio()` | Discover eligible audio in this mount. |
|
|
118
|
+
| `setDiagnosticsSink(callbackOrNull)` | Returns false unless diagnostics was enabled at mount. Sink exceptions are contained. |
|
|
119
|
+
| `diagnosticsEnabled()` | Current sink status. |
|
|
120
|
+
|
|
121
|
+
Interval callbacks receive `{intervalTicks,boundaryTick,audio,clock}`. Metadata
|
|
122
|
+
can be a handler ID string or `{id,handlerId}` for attribution. Repeating
|
|
123
|
+
intervals preserve the core's seek/deduplication behaviour; use `cueTimeline`
|
|
124
|
+
for deterministic state restoration instead of treating interval callbacks as
|
|
125
|
+
an event-history replay mechanism.
|
|
126
|
+
|
|
127
|
+
Additional mount properties/methods are `version`, `bpm`, `bpmSource`,
|
|
128
|
+
`ticksPerBeat`, `root`, `config`, `ready`, `destroyed`, `destroy()`, `resources()`
|
|
129
|
+
and `on(eventName, listener)`. `on` returns unsubscribe. Resources reports owned
|
|
130
|
+
listener/timer/frame/observer/generated-node counts, not the browser's entire
|
|
131
|
+
resource inventory.
|
|
132
|
+
|
|
133
|
+
## Timed files and lyrics
|
|
134
|
+
|
|
135
|
+
Both cue and lyric files use `[mm:ss.cc]`-style timestamps, optionally three-digit
|
|
136
|
+
fractions. Here `.cue` means the plugin's bracketed cue format, **not a CD
|
|
137
|
+
cuesheet**. Example:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
[01:25.00][BUILD: Build 1]
|
|
141
|
+
[01:44.01][DROP: Drop 1]
|
|
142
|
+
[03:07.54][DROP: Final Drop]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Lyric example with an explicit gap:
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
[00:02.89]No crown, no concrete
|
|
149
|
+
[00:07.76]
|
|
150
|
+
[00:07.81]Just mountain breath and moonlit stone
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The lyric callback contains `pageId`, `time`, `text`, `normalisedText`, `index`,
|
|
154
|
+
`nextTime`, `nextText`, `nextNormalisedText`, `nextIndex`, `nextVisibleTime`,
|
|
155
|
+
`nextVisibleText`, `nextVisibleNormalisedText`, `nextVisibleIndex`, and `audio`.
|
|
156
|
+
Immediate-next fields can describe a blank clear; next-visible fields skip
|
|
157
|
+
blanks. Absent times are null, text empty and indices -1.
|
|
158
|
+
|
|
159
|
+
Pause/end do not manufacture empty core lyric events. A custom renderer should
|
|
160
|
+
hide on audio pause/end; `useLyric` already clears its state for pause, end and
|
|
161
|
+
seeking. Render text with `textContent`/React text, not HTML injection. Styling the
|
|
162
|
+
neutral popup differs in scoped mode (inside the root) and WordPress legacy mode
|
|
163
|
+
(under body). The built-in neutral style is not song-specific art direction.
|
|
164
|
+
|
|
165
|
+
## Shared effect primitives
|
|
166
|
+
|
|
167
|
+
`runtime.effects` is the canonical registry for this mount. `get(id)` returns a
|
|
168
|
+
controller or null, `snapshot()` returns controller snapshots, `teardown(id)`
|
|
169
|
+
removes one, and `teardownAll()` removes all. Reusing an ID replaces that local
|
|
170
|
+
registry entry. Each factory returns `id`, `type`, `snapshot()` and `teardown()`.
|
|
171
|
+
Pass Elements or scoped selector strings for roots/targets.
|
|
172
|
+
|
|
173
|
+
| Factory | Configuration and extra methods |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `pointer(options)` | `id`, `root`, `bounds` and event `target`; `finePointer` defaults true, `render({x,y,root,bounds,reason})` optional. Publishes `--dance-moves-x/y`. Controller `set(x,y,reason?)`, `reset(reason?)`. |
|
|
176
|
+
| `playbackPulse(options)` | `id`, `root`, `audio`, `ticks` (16), `className` (`dance-moves-playing`), `propertyPrefix` (`--dance-moves-pulse`), optional render. Publishes duration/delay in seconds. Controller `sync()`. |
|
|
177
|
+
| `cueClass(options)` | `id`, `root`, `cue` (`*`), `className`, **`durationTicks`** (16), optional render. Class is applied to root, not a separate `target`. Controller `fire(detail)`, `clear(reason?)`. Its finite duration is wall-clock based. |
|
|
178
|
+
| `cueTimeline(options)` | `id`, `root`, `audio`, `cues:[{id,time,end?,...}]`, optional `render` and `onCue`. Controller `restore(reason?)`, `start(reason?)`, `stop(reason?)`. |
|
|
179
|
+
| `quality(options)` | `id`, `root`, `tiers`, `initial`, sample/window ratios and cooldowns, optional render. Controller `setTier(index,reason?)`. |
|
|
180
|
+
|
|
181
|
+
Pulse render receives `{root,audio,duration,delay,ticks}`. Cue-class render
|
|
182
|
+
receives `{root,detail,count,durationMilliseconds}`. Timeline transient crossings
|
|
183
|
+
are separate from restored active intervals: a seek reconstructs state rather
|
|
184
|
+
than firing every cue crossed. Backward seeks start another traversal. Pause,
|
|
185
|
+
visibility and accessibility changes rebuild/stop according to the canonical
|
|
186
|
+
transport implementation. Inspect the existing timeline demo's live snapshot.
|
|
187
|
+
|
|
188
|
+
Quality defaults are tiers `full/constrained/minimal`, initial 0, sample 1600ms
|
|
189
|
+
(minimum 800), poor windows 2, healthy windows 5, downgrade ratio .72, recovery
|
|
190
|
+
ratio .9, downgrade cooldown 5000ms and recovery cooldown 12000ms. It is not the
|
|
191
|
+
same monitor as Clay's adaptive performance system. Preserve reduced-motion and
|
|
192
|
+
forced-colour CSS; a quality tier is not permission to ignore accessibility.
|
|
193
|
+
|
|
194
|
+
## Native rudiments and custom renderers
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
await runtime.rudiments.ready();
|
|
198
|
+
const controller = runtime.rudiments.animate({
|
|
199
|
+
id: 'arcadians:drift', root: runtime.root,
|
|
200
|
+
target: runtime.root.querySelector('.drift'),
|
|
201
|
+
rudiment: 'clay_background', clock: 'audio', audio: 'audio',
|
|
202
|
+
rate: 0.5, amplitude: {x: 14, y: 10, z: 0}
|
|
203
|
+
});
|
|
204
|
+
await controller.ready;
|
|
205
|
+
// Later: controller.pause(), resume(), setEnabled(false), reset(), refresh(),
|
|
206
|
+
// snapshot(), destroy() / teardown().
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```css
|
|
210
|
+
.drift {
|
|
211
|
+
translate: var(--dance-moves-rudiment-x, 0px)
|
|
212
|
+
var(--dance-moves-rudiment-y, 0px);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The native service exposes `ready()`, `catalogue()`, `describe(name)`,
|
|
217
|
+
`sample(name, integerPip)`, `pipsFromTicks`, `pipsFromSeconds`, `animate`, `get`,
|
|
218
|
+
`snapshot` and `destroyAll`. All 15 pinned motions, negative wrapping, CSS output,
|
|
219
|
+
custom Canvas callbacks, page/audio/auto clocks, cue resets and accessibility
|
|
220
|
+
suspension remain available. Controllers share the native service's scheduler
|
|
221
|
+
within a mount. Do not add another timer to advance them.
|
|
222
|
+
|
|
223
|
+
The complete options, callback fields and limitations are in
|
|
224
|
+
[the native reference](../RUDIMENTS-API.md); `lib/rudiments.d.ts` ships with npm.
|
|
225
|
+
The four-beat Clay pattern at rate .5 preserves the old eight-beat background
|
|
226
|
+
cycle. Native offsets intentionally do not include opacity, rotation or scale;
|
|
227
|
+
the Clay adapter preserves those accents separately at the same phase.
|
|
228
|
+
|
|
229
|
+
## Catalogue and original adapters
|
|
230
|
+
|
|
231
|
+
With `catalogue:true`, `applyCatalogueTiming()`, `catalogueTimingSnapshot()` and
|
|
232
|
+
`removeCatalogueAnimationScope()` are attached to the runtime. The latter removes
|
|
233
|
+
animation ownership only; use runtime destruction to dispose the entire mount.
|
|
234
|
+
|
|
235
|
+
`getClay()` returns the existing Clay controller when initialized. Its methods
|
|
236
|
+
are `setParameters`, `setMotion`, `setMotionTarget`, `setCueState`, `lifecycle`,
|
|
237
|
+
`reset`, `snapshot`, `teardown`. Visual parameters remain `enabled`,
|
|
238
|
+
`masterIntensity`, `coverTiltDegrees`, `coverTranslationPixels`,
|
|
239
|
+
`bloomTravelPixels`, `flareTravelPixels`, `specularTravelPixels`, and
|
|
240
|
+
`particleReleaseTicks`. `getClayRudiment()` exposes its native background
|
|
241
|
+
controller snapshot/teardown. Required markup and all original mapping examples
|
|
242
|
+
remain in the 23-page WordPress-style gallery, now loading the shared package.
|
|
243
|
+
|
|
244
|
+
`getPaperPlanes()` returns the canonical paper-plane controller; its stage is
|
|
245
|
+
bounded by `.epk-hero`, with `.epk-cover-wrap` and `.epk-heading` for stacking.
|
|
246
|
+
Use `effect:'paper-planes'` and a valid atlas URL. Reduced motion, forced colours,
|
|
247
|
+
compact mode, visibility and resize behaviour remain in that implementation.
|
|
248
|
+
|
|
249
|
+
`orientation.adapter` accepts all nine names exported by `ORIENTATION_ADAPTERS`.
|
|
250
|
+
Their original DOM/CSS selectors and mobile/permission policy remain unchanged;
|
|
251
|
+
selecting another adapter does not fabricate its required DOM. `getOrientation()`
|
|
252
|
+
returns its existing snapshot/reset/teardown API. The unmodified orientation
|
|
253
|
+
helper surface is at `orientationCore` (14 exports, including both schedulers).
|
|
254
|
+
|
|
255
|
+
For an arbitrary application layout, prefer the new generic controller:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
const phone = runtime.createOrientation({
|
|
259
|
+
render: ({x, y}) => {
|
|
260
|
+
subject.style.transform = `translate(${x * 20}px, ${y * 16}px)`;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
button.addEventListener('click', () => phone.enable().catch(console.error));
|
|
264
|
+
// phone.disable(); phone.reset(); phone.snapshot(); phone.destroy();
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
This uses the existing rolling mapper and frame scheduler, with a 2000ms window,
|
|
268
|
+
1.5-degree minimum span and 32ms smoothing constant by default. Permission must
|
|
269
|
+
be requested from a real user gesture. The API does not bypass browser permission
|
|
270
|
+
or produce trusted sensor events from synthetic data.
|
|
271
|
+
|
|
272
|
+
## Captures, metadata and events
|
|
273
|
+
|
|
274
|
+
`createRecorder({durationMilliseconds:30000,maxSamples:500})` returns `start()`
|
|
275
|
+
(Promise of the completed capture), `stop(reason?)`, `snapshot()`, and
|
|
276
|
+
`upload(sameOriginUrl,{headers?,signal?})`. Capture is explicit, bounded and never
|
|
277
|
+
automatically uploaded. Its schema matches the WordPress capture schema but
|
|
278
|
+
physical parity is not claimed. See [Node endpoints](javascript-server.md).
|
|
279
|
+
|
|
280
|
+
`validatePageConfig`, `PAGE_META_KEYS`, `fromWordPressMeta(meta,resolver)` and
|
|
281
|
+
`createMemoryPageStore` are importable without a DOM. The attachment resolver is
|
|
282
|
+
required for nonzero WordPress attachment IDs. The memory store supports `read`,
|
|
283
|
+
`save`, `revisions`, `restore`, `delete`; `expectedRevision` detects concurrent
|
|
284
|
+
updates, and configured page/revision limits bound memory. It is not durable
|
|
285
|
+
storage and not an authorization mechanism.
|
|
286
|
+
|
|
287
|
+
The core's `dance-moves-ready`, `dance-moves-cue`, `kieran-epk-cue` and
|
|
288
|
+
`dance-moves-lyric` events, plus effects/native ready/error events, are emitted
|
|
289
|
+
on the scoped root. The WordPress adapter preserves document dispatch. Register
|
|
290
|
+
lifecycle subscriptions with `runtime.on`; consumers needing initial native
|
|
291
|
+
readiness should await the Promise instead of hoping to subscribe before an
|
|
292
|
+
initialization event already fired.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# JavaScript host services (Node)
|
|
2
|
+
|
|
3
|
+
Import from `@kieransimkin/dancemoves/server` only on the server. Node 22.14+
|
|
4
|
+
is the package baseline; Next route handlers must use `runtime = 'nodejs'`, not
|
|
5
|
+
Edge. The main browser entry contains no filesystem/signing/capture secret.
|
|
6
|
+
|
|
7
|
+
WordPress retains its existing PHP endpoints and native storage. These services
|
|
8
|
+
provide the corresponding operations to another host; there is no attempt to
|
|
9
|
+
turn a browser into WordPress or to reuse its public fixed capture token as an
|
|
10
|
+
application authentication scheme.
|
|
11
|
+
|
|
12
|
+
## Page properties and revisions
|
|
13
|
+
|
|
14
|
+
`validatePageConfig(input)` and `fromWordPressMeta(meta, attachmentResolver)` have
|
|
15
|
+
the same contracts as the main entry. Validation must run on the server before
|
|
16
|
+
persisting untrusted updates even when the React form already validated them.
|
|
17
|
+
Only persist fields the authenticated caller is allowed to change.
|
|
18
|
+
|
|
19
|
+
`createMemoryPageStore({maxPages:100,maxRevisions:20})` provides:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const record = pages.save('arcadians', {bpm:145}, {expectedRevision:0});
|
|
23
|
+
const current = pages.read('arcadians');
|
|
24
|
+
const history = pages.revisions('arcadians');
|
|
25
|
+
const restored = pages.restore('arcadians', record.revision,
|
|
26
|
+
{expectedRevision:current.revision});
|
|
27
|
+
pages.delete('arcadians');
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Records are `{revision,config,savedAt}`. Read operations return clones; restore
|
|
31
|
+
creates a new revision. A revision mismatch or exceeded page limit fails. This
|
|
32
|
+
bounded store demonstrates the contract, not a durable production database. Use
|
|
33
|
+
an application transaction/optimistic lock for durable multi-process storage.
|
|
34
|
+
|
|
35
|
+
## Timing upload validation
|
|
36
|
+
|
|
37
|
+
`validateTimingFile(textOrUint8Array, {kind:'cue',filename?,maxBytes:1048576})`
|
|
38
|
+
returns frozen `{text,kind,bytes,timestamps}`. It accepts lyric `.lrc` or cue
|
|
39
|
+
`.lrc`/`.cue` extensions, strict UTF-8, no null/replacement characters, at least
|
|
40
|
+
one bracketed timestamp and monotonically ordered timestamps. Equal times and
|
|
41
|
+
lyric clear lines are retained. File parsing into browser events is provided by
|
|
42
|
+
the canonical runtime's parser methods.
|
|
43
|
+
|
|
44
|
+
Failure uses `ValidationError` with `code`, `message` and `status`. Relevant codes
|
|
45
|
+
are `timing_extension`, `timing_size` (413), `timing_utf8`, `timing_timestamps` and
|
|
46
|
+
`timing_order` (400). Invalid option types throw `TypeError`.
|
|
47
|
+
|
|
48
|
+
Validate uploaded bytes and server-owned filenames. This API does not fetch
|
|
49
|
+
arbitrary remote timing URLs; adding such a feature to your host requires SSRF,
|
|
50
|
+
redirect, size and content-type controls. The `.cue` convention is bracketed
|
|
51
|
+
musical cues, not the unrelated CD INDEX syntax.
|
|
52
|
+
|
|
53
|
+
## Signed MP3 attachment delivery
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import {createDownloadService} from '@kieransimkin/dancemoves/server';
|
|
57
|
+
const downloads = createDownloadService({
|
|
58
|
+
rootDirectory: '/srv/my-app/media',
|
|
59
|
+
secret: process.env.DANCEMOVES_DOWNLOAD_SECRET,
|
|
60
|
+
origin: 'https://music.example',
|
|
61
|
+
route: '/api/download'
|
|
62
|
+
});
|
|
63
|
+
const attachmentUrl = downloads.sign('arcadians.mp3');
|
|
64
|
+
const response = await downloads.handle(request);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`rootDirectory` must be absolute. `secret` is a server-only string/Uint8Array of at
|
|
68
|
+
least 32 bytes. `origin` must be HTTP(S), and route a same-origin absolute path
|
|
69
|
+
without query/fragment. `sign` returns a URL with normalized relative MP3 `path`
|
|
70
|
+
and HMAC-SHA256 `signature` query fields. Unsafe input throws `ValidationError`.
|
|
71
|
+
|
|
72
|
+
`normalizeDownloadPath(value)` returns the safe decoded relative path or an
|
|
73
|
+
empty string. It rejects traversal, absolute/drive paths, controls, empty path
|
|
74
|
+
segments and non-MP3 suffixes. Signature comparison is timing-safe. The handler
|
|
75
|
+
resolves the real filesystem path and verifies containment below the allowed
|
|
76
|
+
root; symlink escapes are rejected. Do not give an untrusted writer control over
|
|
77
|
+
that directory while serving it.
|
|
78
|
+
|
|
79
|
+
`handle(Request)` returns a Web `Response`: 200 for valid GET/HEAD, 404 for an
|
|
80
|
+
invalid signature/path/missing file, and 405 (`Allow: GET, HEAD`) for other methods
|
|
81
|
+
with otherwise valid routing. It sends `Content-Type: audio/mpeg`, exact content
|
|
82
|
+
length, UTF-8 attachment filename, `nosniff`, `noindex` and private/no-store cache
|
|
83
|
+
headers. HEAD closes the file without a response body. GET streams the file and
|
|
84
|
+
closes its handle.
|
|
85
|
+
|
|
86
|
+
Playback and download are deliberately separate. Use the ordinary static media
|
|
87
|
+
URL for `<audio>` so the host can provide range requests. The signed attachment
|
|
88
|
+
handler does not implement playback ranges or rewrite audio/source elements.
|
|
89
|
+
Render the signed URL on an `<a download>` element. A valid signature demonstrates
|
|
90
|
+
a server-produced path, not that the visitor bought access; implement additional
|
|
91
|
+
authorization where needed. Rotating the secret invalidates old links.
|
|
92
|
+
|
|
93
|
+
## Private motion capture
|
|
94
|
+
|
|
95
|
+
`validateMotionCapture(payload)` validates and sanitizes the inherited
|
|
96
|
+
`ks-epk-motion-recording/v1` schema. It requires 1–500 samples; finite timestamp
|
|
97
|
+
0–120000ms, beta -180..180 and gamma -90..90; alpha may be null/0..360.
|
|
98
|
+
Optional sample/capture flags and bounded descriptive fields are preserved.
|
|
99
|
+
Invalid nullable optional numeric fields become null, following the existing
|
|
100
|
+
schema's permissive optional-field policy. It caps failure records/counters and
|
|
101
|
+
the sanitized JSON size. The new HTTP boundary also bounds the incoming body
|
|
102
|
+
before JSON parsing, including chunked requests without Content-Length.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import {createCaptureHandler} from '@kieransimkin/dancemoves/server';
|
|
106
|
+
const POST = createCaptureHandler({
|
|
107
|
+
allowedOrigin: 'https://music.example',
|
|
108
|
+
authorize: request => sessions.canCapture(request),
|
|
109
|
+
rateLimit: request => limits.consumeVerifiedUser(request),
|
|
110
|
+
store: async (capture, {visibility, request}) => {
|
|
111
|
+
const id = await database.storePrivateCapture(capture, request);
|
|
112
|
+
return {captureId:id, storedPrivately:true};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
All three callbacks are required. `authorize` and `rateLimit` return booleans or
|
|
118
|
+
Promises. `store` receives `{visibility:'private',request}` and must return a
|
|
119
|
+
truthy `captureId` and **`storedPrivately:true`**. The flag is a contract with the
|
|
120
|
+
host, not a substitute for real storage permissions. The library cannot verify
|
|
121
|
+
how a custom database adapter publishes its rows.
|
|
122
|
+
|
|
123
|
+
Responses are JSON, no-store:
|
|
124
|
+
|
|
125
|
+
| Condition | Status/code |
|
|
126
|
+
|---|---|
|
|
127
|
+
| Not POST | 405 `method_not_allowed`, Allow POST |
|
|
128
|
+
| Wrong or malformed supplied Origin | 403 `ks_motion_origin` |
|
|
129
|
+
| Authorization denied | 403 `ks_motion_forbidden` |
|
|
130
|
+
| Rate limit denied | 429 `ks_motion_rate`, Retry-After 600 |
|
|
131
|
+
| Not application/json | 415 `ks_motion_content_type` |
|
|
132
|
+
| Invalid schema/body/samples | 400 `ks_motion_schema`, `ks_motion_samples`, `ks_motion_invalid_samples` |
|
|
133
|
+
| Body/sanitized payload too large | 413 `ks_motion_size` |
|
|
134
|
+
| Storage/callback failed or did not confirm privacy | 500 `ks_motion_server`, with no internal error details |
|
|
135
|
+
| Success | 200 `{captureId,sampleCount,storedPrivately:true}` |
|
|
136
|
+
|
|
137
|
+
A missing Origin is not automatically authorization; the mandatory authorization
|
|
138
|
+
callback must still succeed. With cookie/session authentication, enforce your
|
|
139
|
+
host's CSRF policy. Do not authorize based on an untrusted payload flag or
|
|
140
|
+
forwarded IP header. Native/simulated/parity flags remain **client-reported**.
|
|
141
|
+
|
|
142
|
+
`createMemoryRateLimiter({limit:8,windowMilliseconds:600000,maxKeys:10000,now})`
|
|
143
|
+
returns `{consume(key)}`. It expires old keys, bounds key count and fails closed
|
|
144
|
+
when capacity is reached. This is for local demos/single-process applications;
|
|
145
|
+
replicas or serverless hosts need a shared atomic limiter.
|
|
146
|
+
|
|
147
|
+
The Next demo's private storage path is checked both lexically and through
|
|
148
|
+
realpath against `public/`. New files use exclusive creation and private POSIX
|
|
149
|
+
modes where supported. Production ACLs, backups, retention, user access and
|
|
150
|
+
data-protection policy remain the host's responsibility.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# React and Next.js
|
|
2
|
+
|
|
3
|
+
The React entry is optional and starts with `"use client"`. Neither the main
|
|
4
|
+
entry nor the hooks create a browser runtime during module import/server render.
|
|
5
|
+
The hooks call the same shared `createDanceMoves` factory and dispose it on
|
|
6
|
+
unmount. There is no React-only animation engine.
|
|
7
|
+
|
|
8
|
+
## A React component
|
|
9
|
+
|
|
10
|
+
```jsx
|
|
11
|
+
'use client';
|
|
12
|
+
import {useMemo, useRef} from 'react';
|
|
13
|
+
import {useDanceMoves, useRudiment, useLyric} from '@kieransimkin/dancemoves/react';
|
|
14
|
+
import '@kieransimkin/dancemoves/styles.css';
|
|
15
|
+
|
|
16
|
+
export default function ArcadiansPlayer() {
|
|
17
|
+
const root = useRef(null);
|
|
18
|
+
const target = useRef(null);
|
|
19
|
+
const config = useMemo(() => ({
|
|
20
|
+
bpm: 145,
|
|
21
|
+
lyricTimingUrl: '/media/canonical-lyric-timing.lrc',
|
|
22
|
+
cueTimingUrl: '/media/sections.cue',
|
|
23
|
+
masterDurationMilliseconds: 273604.558
|
|
24
|
+
}), []);
|
|
25
|
+
const motion = useMemo(() => ({
|
|
26
|
+
id: 'arcadians:sway', rudiment: 'sway', clock: 'audio', audio: 'audio',
|
|
27
|
+
amplitude: {x: 24, y: 12, z: 0}
|
|
28
|
+
}), []);
|
|
29
|
+
const {runtime, error} = useDanceMoves(root, config);
|
|
30
|
+
const native = useRudiment(runtime, target, motion);
|
|
31
|
+
const lyric = useLyric(runtime);
|
|
32
|
+
return <section ref={root} className="ks-epk">
|
|
33
|
+
<audio controls preload="metadata" src="/media/arcadians.mp3" />
|
|
34
|
+
<div ref={target} className="drifting-art">Arcadians</div>
|
|
35
|
+
<p>{lyric?.text || ''}</p>
|
|
36
|
+
{(error || native.error) && <p role="alert">{(error || native.error).message}</p>}
|
|
37
|
+
</section>;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
.drifting-art {
|
|
43
|
+
translate: var(--dance-moves-rudiment-x, 0px)
|
|
44
|
+
var(--dance-moves-rudiment-y, 0px);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Keep `config` and controller options stable with `useMemo`. New options deliberately
|
|
49
|
+
cause a destroy/remount so page clocks, media files and tempo are never partially
|
|
50
|
+
reconfigured. A ref must be attached when the effect runs. Remount the component
|
|
51
|
+
when its root changes; do not mount nested providers over the same subtree.
|
|
52
|
+
|
|
53
|
+
## Hook and component reference
|
|
54
|
+
|
|
55
|
+
| Export | Contract |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `useDanceMoves(rootRef, options?)` | `{runtime, error}`; runtime is initially null during SSR/first render. Automatically creates and destroys the shared runtime. |
|
|
58
|
+
| `DanceMovesProvider({options, children, className, ...divProps})` | Owns one `div.ks-epk` and exposes the hook result through context. |
|
|
59
|
+
| `useDanceMovesContext()` | Returns that context; throws outside a provider. |
|
|
60
|
+
| `useCue(runtime, name, callback)` | Subscribes/unsubscribes canonical cues; callback ref keeps the latest closure without recreating subscriptions. |
|
|
61
|
+
| `useLyric(runtime)` | Current lyric payload or null. Clears on player pause/end/seeking as well as canonical blank cues. |
|
|
62
|
+
| `useRudiment(runtime, targetRef, options)` | `{controller,error}`; owns one native animation and destroys it on dependency changes/unmount. |
|
|
63
|
+
| `Rudiment({runtime, options, children, ...divProps})` | Div wrapper using `useRudiment`. CSS consumes its scoped native output. |
|
|
64
|
+
| `PageMetadataEditor({value,onSave})` | Controlled host-neutral BPM, timing URLs, duration, popup and effect editor. Validates before calling `onSave`; persistence/auth are not supplied by a browser form. |
|
|
65
|
+
|
|
66
|
+
The provider does not inject audio, attach an automatic upload endpoint or
|
|
67
|
+
replace your application's router. Keep event subscriptions and custom renderer
|
|
68
|
+
writes inside effects with cleanup. Development Strict Mode is supported, not
|
|
69
|
+
disabled to hide duplicate initialization.
|
|
70
|
+
|
|
71
|
+
## Next.js App Router
|
|
72
|
+
|
|
73
|
+
Import global DanceMoves CSS in `app/layout.jsx`. Put the player/hooks inside a
|
|
74
|
+
Client Component. The shared `/server` entry belongs only in Server Components,
|
|
75
|
+
route handlers or other server-only modules. The download secret must never be
|
|
76
|
+
named `NEXT_PUBLIC_*` or passed to a Client Component.
|
|
77
|
+
|
|
78
|
+
The included `examples/next` app imports `examples/react/App.jsx`; there is one
|
|
79
|
+
React demonstration implementation, not a copy maintained in both apps. The
|
|
80
|
+
repository root supplies the local package exports and a single React install.
|
|
81
|
+
`tools/run-next.mjs` runs Next in that app directory. Do not `npm install` a
|
|
82
|
+
second copy of the library inside the demo app.
|
|
83
|
+
|
|
84
|
+
The production download/capture examples are Node route handlers:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
// app/api/download/route.js
|
|
88
|
+
import path from 'node:path';
|
|
89
|
+
import {createDownloadService} from '@kieransimkin/dancemoves/server';
|
|
90
|
+
export const runtime = 'nodejs';
|
|
91
|
+
const downloads = createDownloadService({
|
|
92
|
+
rootDirectory: path.resolve(process.cwd(), 'public/media'),
|
|
93
|
+
origin: process.env.DANCEMOVES_ORIGIN,
|
|
94
|
+
secret: process.env.DANCEMOVES_DOWNLOAD_SECRET
|
|
95
|
+
});
|
|
96
|
+
export const GET = request => downloads.handle(request);
|
|
97
|
+
export const HEAD = request => downloads.handle(request);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Sign the known, allowed filename in a Server Component and pass only the signed
|
|
101
|
+
URL to the player. Do not expose an unauthenticated public endpoint that signs
|
|
102
|
+
arbitrary filenames. The capture example requires a separate key and private
|
|
103
|
+
folder outside `public/`; its single-process limiter is for demonstration.
|
|
104
|
+
Replace it with application sessions, capabilities and a shared limiter for
|
|
105
|
+
production replicas. Filesystem persistence is unsuitable for read-only or
|
|
106
|
+
stateless hosting; provide a durable private database/object-store callback.
|
|
107
|
+
|
|
108
|
+
## Run the Arcadians demos
|
|
109
|
+
|
|
110
|
+
From the repository root after applying the patch:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
npm install --ignore-scripts
|
|
114
|
+
npm run build
|
|
115
|
+
npm run demo:prepare
|
|
116
|
+
npm run demo:react
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Open `http://127.0.0.1:4173/`. The media preparer downloads the hash-pinned Arcadians
|
|
120
|
+
MP3 and artwork from StemLab; it never generates a substitute. Existing canonical
|
|
121
|
+
LRC and section data are reused. A local StemLab checkout can be supplied through
|
|
122
|
+
`python tools/prepare-app-demos.py --stemlab-root ../stemlab`.
|
|
123
|
+
|
|
124
|
+
The React gallery has panels for all native motions, pointer mapping, playback
|
|
125
|
+
pulse, named cues, cue timeline, quality, generic phone orientation, Clay, planes,
|
|
126
|
+
metadata/revisions and private capture. Every panel uses the same Arcadians
|
|
127
|
+
player. Chapter buttons, playback rate, canonical lyrics, lifetime inspection,
|
|
128
|
+
and runtime unmount/remount controls are shared. Camera/sensor parity and
|
|
129
|
+
physical performance are not simulated as proof of real-device behaviour.
|
|
130
|
+
|
|
131
|
+
For Next.js:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
npm run demo:next
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Open `http://localhost:3000/`. Copy `examples/next/.env.example` to
|
|
138
|
+
`examples/next/.env.local` and set your own server-only secrets to enable signed
|
|
139
|
+
downloads/captures. Use at least 32 bytes of unpredictable secret material;
|
|
140
|
+
example test values in the automated tests are not production secrets.
|
|
141
|
+
Without configuration those server features are disabled/reject requests, not
|
|
142
|
+
silently backed by a public token. The song still plays via its ordinary media URL.
|
|
143
|
+
|
|
144
|
+
The existing 23-page WordPress-style gallery also imports `/lib/index.mjs` now:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
python tools/serve-wordpress-examples.py
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Open `http://127.0.0.1:8765/examples/wordpress/`. It retains all nine legacy
|
|
151
|
+
orientation mappings, all 23 core methods, the catalogue and diagnostic workbench,
|
|
152
|
+
all 15 native motions, HTTP simulations and page metadata inspectors. Those
|
|
153
|
+
simulated PHP/HTTP behaviours are labelled; the frontend module is real.
|
|
154
|
+
|
|
155
|
+
## Tests and production builds
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
npm test
|
|
159
|
+
npm run test:legacy
|
|
160
|
+
npx playwright-core install --with-deps chromium
|
|
161
|
+
npm run test:browser
|
|
162
|
+
npm run test:next
|
|
163
|
+
npm run test:next:smoke
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`npm test` includes real ESM/CJS imports and React server rendering after a build.
|
|
167
|
+
Browser tests use the real React bundle, actual WASM and canonical MP3, check
|
|
168
|
+
chapter seeking, feature mounts, cue effects, Strict Mode disposal/remount and
|
|
169
|
+
capture a reduced-motion screenshot. The Next smoke test starts the actual
|
|
170
|
+
production build and exercises server rendering, signed HEAD downloads,
|
|
171
|
+
unauthenticated rejection and private capture persistence. Missing build/media
|
|
172
|
+
or browser prerequisites fail; they are not reported as passing skipped tests.
|
|
173
|
+
|
|
174
|
+
Desktop Chromium automation does not certify iOS permission prompts, Safari
|
|
175
|
+
pseudo-element animation behaviour, screen orientation or real device cadence.
|
|
176
|
+
Keep attended mobile/browser checks before deployment.
|
|
177
|
+
|
|
178
|
+
## CSP and accessibility
|
|
179
|
+
|
|
180
|
+
Serve the atlas and media as same-origin public assets unless CORS is deliberately
|
|
181
|
+
configured. Dynamic style elements can use the `nonce` mount option. The native
|
|
182
|
+
backend needs browser WebAssembly permission under the application's CSP; do not
|
|
183
|
+
turn on arbitrary JavaScript eval. Test actual CSP headers and fallback behaviour.
|
|
184
|
+
Avoid full-frame flashes or brightness/tint modulation. Honour reduced motion
|
|
185
|
+
and forced colours in custom callbacks as well as in the shipped CSS.
|