@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,400 @@
|
|
|
1
|
+
# Catalogue, orientation and release-adapter APIs
|
|
2
|
+
|
|
3
|
+
[Documentation index](../README.md) · [Core JavaScript](javascript.md) ·
|
|
4
|
+
[CSS/HTML](styling.md)
|
|
5
|
+
|
|
6
|
+
Baseline: **2.8.0 / `4cb6a71f60459b5579be87d7e55ac8b1426c578e`**.
|
|
7
|
+
Conditional globals exist only when their asset and required root have loaded.
|
|
8
|
+
These APIs do not create a missing EPK or register a new WordPress Page mapping.
|
|
9
|
+
|
|
10
|
+
For the separate reusable factories see [shared effects](effects.md); for the
|
|
11
|
+
page-selected sprite renderer see [paper planes](paper-planes.md). Their lifecycle
|
|
12
|
+
and quality rules differ from this file's release-specific Clay/Stars API.
|
|
13
|
+
|
|
14
|
+
## Catalogue timing
|
|
15
|
+
|
|
16
|
+
Source: [dance-moves-catalogue-timing.js](../../assets/dance-moves-catalogue-timing.js).
|
|
17
|
+
|
|
18
|
+
Root discovery first selects `.ks-epk`. Only when that is absent does it use the
|
|
19
|
+
configured Page ID's fallback:
|
|
20
|
+
|
|
21
|
+
| Page ID | Fallback selector |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| 130 | `.dfad-motion-stage` |
|
|
24
|
+
| 137 | `.birth-stories-page` |
|
|
25
|
+
| 140 | `#light-will-win-top` |
|
|
26
|
+
| 150 | `.mog-epk` |
|
|
27
|
+
| 298 | `.dmt-epk` |
|
|
28
|
+
| 397 | `.sft-epk` |
|
|
29
|
+
| 399 | `.entry-content` |
|
|
30
|
+
|
|
31
|
+
Other IDs use the implementation sentinel `[data-dance-moves-no-root]`, not a
|
|
32
|
+
publicly configurable selector. With no core/root, the script returns without
|
|
33
|
+
installing an API. Root discovery happens once, not on every later DOM insertion.
|
|
34
|
+
|
|
35
|
+
### Methods and aliases
|
|
36
|
+
|
|
37
|
+
| Method | Return | Behaviour |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `DanceMovesCatalogueTiming.apply()` | `CatalogueSnapshot` | Re-runs the conversion pass synchronously and marks readiness. |
|
|
40
|
+
| `DanceMovesCatalogueTiming.snapshot()` | `CatalogueSnapshot` | Reads evidence without running conversion. |
|
|
41
|
+
| `DanceMoves.applyCatalogueTiming()` | `CatalogueSnapshot` | Same function as `.apply()`. |
|
|
42
|
+
| `DanceMoves.catalogueTimingSnapshot()` | `CatalogueSnapshot` | Same function as `.snapshot()`. |
|
|
43
|
+
| `DanceMoves.removeCatalogueAnimationScope()` | void | Removes the one wildcard cue-reset scope registered by the catalogue adopter. |
|
|
44
|
+
|
|
45
|
+
Scope removal **does not** undo stylesheet/inline changes, remove generated CSS,
|
|
46
|
+
disconnect the MutationObserver, remove other scopes or uninstall the API. There
|
|
47
|
+
is no full catalogue teardown/undo method.
|
|
48
|
+
|
|
49
|
+
### What gets converted
|
|
50
|
+
|
|
51
|
+
The pass scans accessible CSS rules and referenced timing custom properties,
|
|
52
|
+
then inline and computed declarations on the root and descendants, then computed
|
|
53
|
+
`::before`/`::after` timing. Properties are `animation-duration`, `animation-delay`,
|
|
54
|
+
`transition-duration` and `transition-delay`. Nonzero time tokens are converted
|
|
55
|
+
to signed, quantised tick durations. Negative delays preserve their sign. Values
|
|
56
|
+
already containing `--dance-moves-` are left alone.
|
|
57
|
+
|
|
58
|
+
Zero and magnitudes at most 0.02 ms are preserved. Animation timing associated
|
|
59
|
+
with a non-auto/non-none view timeline preserves magnitudes at most 1 ms; Page 248
|
|
60
|
+
also has the legacy 1 ms rule exception. Generated CSS adds a reduced-motion
|
|
61
|
+
clamp for the owned subtree/pseudo-elements: 0.01 ms duration, zero delay, one
|
|
62
|
+
iteration and automatic scrolling.
|
|
63
|
+
|
|
64
|
+
Rule ownership is determined from root tokens or selector matches within the
|
|
65
|
+
root; transient selector states/pseudo-elements are stripped for matching.
|
|
66
|
+
Referenced timing variables can be rewritten where they are defined, including
|
|
67
|
+
outside the owned selector. A mixed selector list or broad shared selector is not
|
|
68
|
+
split into owned/unowned rules. Isolate release CSS and timing variables rather
|
|
69
|
+
than assuming this heuristic is a strict CSS sandbox. Inaccessible cross-origin
|
|
70
|
+
CSSOM sheets are skipped, though computed styles within the root can still be
|
|
71
|
+
converted. The pass does not rewrite canvas, shaders, JS timers, easing functions,
|
|
72
|
+
keyframe geometry or animation names.
|
|
73
|
+
|
|
74
|
+
The adopter registers `["*"]` on the root for cue resets. A MutationObserver
|
|
75
|
+
watches child insertions and `style` attributes in that root and batches another
|
|
76
|
+
pass into one pending animation frame. Class changes, stylesheet changes outside
|
|
77
|
+
the root, dynamic root replacement and audio discovery are not general observer
|
|
78
|
+
contracts; call `.apply()` when appropriate, and use a new page load for a new root.
|
|
79
|
+
Repeated passes append generated rules; this is not a side-effect-free inspection
|
|
80
|
+
API or a promise of stylesheet compaction.
|
|
81
|
+
|
|
82
|
+
### `CatalogueSnapshot`
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
{
|
|
86
|
+
pageId: number,
|
|
87
|
+
bpm: number,
|
|
88
|
+
bpmSource: "explicit" | "fallback",
|
|
89
|
+
conversionCount: number,
|
|
90
|
+
convertedCustomProperties: string[],
|
|
91
|
+
records: [{source, property, before, after, ticks: number[]}]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Evidence is capped at **1000 records**. `conversionCount` is the retained record
|
|
96
|
+
count, not an unlimited total of converted tokens or elements. Custom property
|
|
97
|
+
names are sorted. `records` is an array copy, not a deep freeze; do not mutate
|
|
98
|
+
record objects. Evidence stays local to the runtime unless a consumer exports it.
|
|
99
|
+
|
|
100
|
+
## Orientation page map
|
|
101
|
+
|
|
102
|
+
Sources: [PHP map](../../kieran-epk-device-orientation.php),
|
|
103
|
+
[orientation wrapper](../../assets/ks-epk-device-orientation.js),
|
|
104
|
+
[orientation CSS](../../assets/ks-epk-device-orientation.css).
|
|
105
|
+
|
|
106
|
+
| Page | Adapter key | Required root / additional target | Release behaviour |
|
|
107
|
+
| --- | --- | --- | --- |
|
|
108
|
+
| 130 | `dying-for-a-diagnosis` | `.dfad-motion-stage`; optional `[data-tilt]`, `[data-parallax] img` | Cover shift/rotation and artwork translation; responsive artwork scale. |
|
|
109
|
+
| 140 | `light-will-win` | `#light-will-win-top` | Cover translation and tilt on release cards. |
|
|
110
|
+
| 243 | `presents-and-chocolate` | `.ks-epk[data-release="presents-and-chocolate"]` | Cover sensor shift/rotation. |
|
|
111
|
+
| 252 | `clay-stars` | `.ks-epk.ks-clay-stars-v2` and matching `DanceMovesClayStars.root` | Delegates to `setMotionTarget()` or `setMotion()`; no independent duplicate Clay runtime. |
|
|
112
|
+
| 268 | `fully-nocturnal` | `.ks-epk.fn-live[data-release="fully-nocturnal"]` | Scene/card movement; resets while `data-fnx-motion="off"`. |
|
|
113
|
+
| 270 | `amnesty-honestly` | `.ks-epk` | Cover tilt/shift and heading counter-movement. |
|
|
114
|
+
| 276 | `walk-with-me` | `.ks-epk[data-release="walk-with-me"]` and `.wwm-cover-stage` or `.epk-cover-wrap` | Cover tilt/shift and moving glow position. |
|
|
115
|
+
| 298 | `dmitri-my-talisman` | `.dmt-epk` | Light/parallax/tilt; resets with the `reduce-motion` class. |
|
|
116
|
+
| 839 | `california-screamin` | `#cs-epk.cs-epk` (release attribute optional) | Writes `--cs-x`/`--cs-y`; resets with `motion-paused`. |
|
|
117
|
+
|
|
118
|
+
The original seven adapters are joined by Clay/Stars and California Screamin'.
|
|
119
|
+
The wrapper can detect several roots when `config.adapter` is absent, but PHP
|
|
120
|
+
still controls which Pages receive its assets. There is no public
|
|
121
|
+
`registerAdapter()` or Page-ID filter. Adding a new production mapping requires
|
|
122
|
+
changes to the PHP map and wrapper factory table plus release tests.
|
|
123
|
+
|
|
124
|
+
### `window.ksEpkOrientationConfig`
|
|
125
|
+
|
|
126
|
+
WordPress emits `adapter`, `pageId`, `version`, `bpm`, `bpmSource`, `ticksPerBeat`
|
|
127
|
+
(16), and `transitionTargetTicks` (2). The wrapper uses BPM clamped to 20–400 and
|
|
128
|
+
computes `60000 / bpm / ticksPerBeat * transitionTargetTicks` milliseconds.
|
|
129
|
+
|
|
130
|
+
`harness: true` bypasses the mobile/support gate **only** on exact hostnames
|
|
131
|
+
`localhost` or `127.0.0.1`. It is not emitted in production. Merely loading the
|
|
132
|
+
core helper library does not start sensors.
|
|
133
|
+
|
|
134
|
+
### Input and permission lifecycle
|
|
135
|
+
|
|
136
|
+
The production gate requires `KSEpkOrientationCore`, mobile eligibility and sensor
|
|
137
|
+
support. The wrapper uses a 2000 ms rolling mapper, 1.5-degree minimum span, a
|
|
138
|
+
latest-sample animation-frame scheduler with 32 ms smoothing time constant, then
|
|
139
|
+
a target scheduler with a two-tick interval and 0.02 minimum normalised delta.
|
|
140
|
+
The first meaningful target is immediate; further targets retain only the latest
|
|
141
|
+
pending value. Release adapters apply a bounded signed square-root perceptual
|
|
142
|
+
curve before their own mappings.
|
|
143
|
+
|
|
144
|
+
Permission-requesting browsers get one **Use phone motion** button that requests
|
|
145
|
+
permission from its click handler. Granted permission starts listening; denial
|
|
146
|
+
shows **Phone motion blocked**. A 3500 ms availability timer can mark the button
|
|
147
|
+
unavailable if valid input never activates the adapter. This is an input
|
|
148
|
+
availability timeout, not a musical effect duration.
|
|
149
|
+
|
|
150
|
+
Visibility/pagehide stops listening and resets targets; page return can recreate
|
|
151
|
+
the permission control or resume nonpermission input. Screen rotation resets the
|
|
152
|
+
mapper. A preference change to reduced motion stops listening and removes the
|
|
153
|
+
control. At initial startup, the permission-control creation path is not itself
|
|
154
|
+
gated by the reduced-motion check, so the control can exist in the DOM in an
|
|
155
|
+
initially reduced-motion session. The bundled stylesheet hides it, and input
|
|
156
|
+
application remains gated.
|
|
157
|
+
Root removal triggers teardown through a MutationObserver.
|
|
158
|
+
|
|
159
|
+
### Internal lifecycle handle
|
|
160
|
+
|
|
161
|
+
`window.__ksEpkOrientationRuntime` is an internal diagnostic handle, installed
|
|
162
|
+
only after a valid adapter initialises. It exposes a frozen object:
|
|
163
|
+
|
|
164
|
+
| Method | Behaviour |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `reset() -> void` | Cancels pending work, resets mapper/target state, marks supported and calls adapter reset. Does not remove listeners or permanently stop the instance. |
|
|
167
|
+
| `snapshot() -> object` | `{adapter, active, listening, destroyed, latest:{x,y}, reducedMotion, documentHidden}`. |
|
|
168
|
+
| `teardown() -> void` | Idempotently removes listeners/control/observer, cancels both schedulers, resets adapter, and deletes the global if it still points to this instance. No public restart method. |
|
|
169
|
+
|
|
170
|
+
Do not treat the leading-underscore handle as the application-level configuration
|
|
171
|
+
API. For reusable math or a separate test harness, use the exported core below.
|
|
172
|
+
|
|
173
|
+
## `KSEpkOrientationCore`
|
|
174
|
+
|
|
175
|
+
Source: [ks-epk-device-orientation-core.js](../../assets/ks-epk-device-orientation-core.js).
|
|
176
|
+
Exposed as `window.KSEpkOrientationCore` in the browser or `module.exports` in
|
|
177
|
+
CommonJS. It is a pure/helper library: no permission prompts, DOM writes or sensor
|
|
178
|
+
listeners occur merely by importing it. Supply finite numeric inputs unless a
|
|
179
|
+
method explicitly checks them; not every helper validates its arguments.
|
|
180
|
+
|
|
181
|
+
### Scalar and environment helpers
|
|
182
|
+
|
|
183
|
+
| Signature | Result / defaults |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| `clamp(value, min, max) -> number` | `Math.min(max, Math.max(min, value))`; does not repair NaN. |
|
|
186
|
+
| `hasMotionData(event) -> boolean` | True only when `beta` and `gamma` are finite numbers; nulls/numeric strings are not valid readings. |
|
|
187
|
+
| `isMobileDevice(environment) -> boolean` | Combines UA/mobile client hint, touch iPad detection, or coarse pointer + touch + screen width at most 1024. Fallback width is `innerWidth`, then 9999. This is eligibility detection, not a measured performance classification. |
|
|
188
|
+
| `sensorSupported(environment) -> boolean` | `isSecureContext !== false` and `DeviceOrientationEvent` exists. This alone does not prove hardware availability or permission. |
|
|
189
|
+
| `permissionRequired(environment) -> boolean` | Support plus a `DeviceOrientationEvent.requestPermission` function. |
|
|
190
|
+
| `screenAngle(environment) -> number` | Finite `ksHarnessScreenAngle` override, then `screen.orientation.angle`, then legacy `orientation`, otherwise 0. |
|
|
191
|
+
| `screenAligned(event, angle) -> {x,y}` | Rotates absolute gamma/beta into screen coordinates in degrees: `x = gamma*cos + beta*sin`, `y = beta*cos - gamma*sin`. |
|
|
192
|
+
| `normalise(event, neutral, angle, maximumDegrees?) -> {x,y}` | Wraps beta/gamma deltas into -180–180, screen-rotates, divides by the positive caller-supplied maximum (default 20), clamps axes to -1–1. Separate from the production rolling mapper. |
|
|
193
|
+
| `mapPointToWindow(point, windowState, minimumSpanDegrees?) -> {x,y}` | Maps a point using `minX/maxX/minY/maxY`; a span below the default 1.5 degrees produces zero on that axis; otherwise clamps to -1–1. |
|
|
194
|
+
| `smooth(previous, next, amount?) -> {x,y}` | Linear interpolation; falsy amount defaults to 0.18, then clamps to 0.01–1. Passing 0 therefore uses 0.18. |
|
|
195
|
+
| `smoothTimeBased(previous, next, elapsedMilliseconds, timeConstantMilliseconds?) -> {x,y}` | Uses alpha `1 - exp(-elapsed/tau)`; elapsed is nonnegative, default tau 32 ms with minimum 1. Zero elapsed returns the previous position. |
|
|
196
|
+
|
|
197
|
+
### `createRollingMapper(windowMilliseconds?, minimumSpanDegrees?) -> Mapper`
|
|
198
|
+
|
|
199
|
+
Defaults are **10000 ms** and **1.5 degrees**. The production wrapper explicitly
|
|
200
|
+
uses 2000 ms, so do not confuse the factory default with that runtime setting.
|
|
201
|
+
The numeric defaults use truthy fallback rather than a strict positive-number
|
|
202
|
+
validator; pass valid positive values and monotonic timestamps in milliseconds.
|
|
203
|
+
|
|
204
|
+
| Mapper method | Result |
|
|
205
|
+
| --- | --- |
|
|
206
|
+
| `push({x,y}, timestamp)` | Adds one point, evicts batches older than `timestamp - windowMilliseconds`, returns a mapped result. |
|
|
207
|
+
| `pushBatch({count,sumX,sumY,minX,maxX,minY,maxY}, timestamp)` | Adds weighted aggregate data; `count` has a minimum of 1. Current position is this batch's mean. |
|
|
208
|
+
| `reset()` | Clears retained batches; no return value. |
|
|
209
|
+
| `size()` | Number of retained batches, not weighted sample count. |
|
|
210
|
+
|
|
211
|
+
Mapped result:
|
|
212
|
+
|
|
213
|
+
```text
|
|
214
|
+
{
|
|
215
|
+
x, y, // current point/last batch mean mapped to -1..1
|
|
216
|
+
currentX, currentY,
|
|
217
|
+
meanX, meanY, // weighted window means, in input units
|
|
218
|
+
minX, maxX, minY, maxY,
|
|
219
|
+
sampleCount, batchCount,
|
|
220
|
+
windowMilliseconds
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `createLatestSampleRafScheduler(options) -> Scheduler`
|
|
225
|
+
|
|
226
|
+
Requires callable `requestFrame(callback)` and `commit(x, y, detail)`; otherwise
|
|
227
|
+
throws `TypeError`. Options:
|
|
228
|
+
|
|
229
|
+
| Option | Default / use |
|
|
230
|
+
| --- | --- |
|
|
231
|
+
| `requestFrame` | Required frame requester, e.g. bound `requestAnimationFrame`. |
|
|
232
|
+
| `cancelFrame` | No-op; supply a real cancellation function in a browser. |
|
|
233
|
+
| `now` | Function returning 0 by default; use the same time base as frame timestamps. |
|
|
234
|
+
| `mapper` | `createRollingMapper(2000, 1.5)`; custom object needs `push()` and `reset()`. |
|
|
235
|
+
| `smoothingTimeConstantMilliseconds` | 32 via numeric truthy fallback. |
|
|
236
|
+
| `commit` | Required consumer of the smoothed latest position. |
|
|
237
|
+
| `reject` | Optional callback for invalid input received while running. |
|
|
238
|
+
|
|
239
|
+
The scheduler keeps one latest finite sensor sample and one pending frame. At
|
|
240
|
+
flush it screen-aligns/mapping-processes the retained point and uses time-based
|
|
241
|
+
smoothing. The first smoothing step uses eight time constants. Commit detail is:
|
|
242
|
+
`{sample:{x,y}, sampleTimestamp, frameTimestamp, sampleAge, commitCount}`.
|
|
243
|
+
`sampleAge` is nonnegative milliseconds.
|
|
244
|
+
|
|
245
|
+
| Scheduler method | Contract |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| `receive(event, angle, timestamp?) -> boolean` | Rejects invalid beta/gamma or a torn-down instance; otherwise retains the latest screen-aligned sample and schedules one frame. Missing/nonfinite timestamp uses `now()`. |
|
|
248
|
+
| `reset() -> void` | Cancels the frame, clears sample/smoothing/counts and resets the mapper; does not turn a torn-down instance back on. |
|
|
249
|
+
| `teardown() -> void` | Resets and permanently stops the instance. |
|
|
250
|
+
| `pending() -> boolean` | Whether a frame ID is pending. |
|
|
251
|
+
| `commitCount() -> number` | Commits since reset. |
|
|
252
|
+
|
|
253
|
+
### `createTransitionTargetScheduler(options) -> TargetScheduler`
|
|
254
|
+
|
|
255
|
+
Requires `commit(x, y, detail)`, otherwise throws `TypeError`. Defaults:
|
|
256
|
+
`now: () => 0`, `schedule: setTimeout` wrapper, `cancel: clearTimeout`,
|
|
257
|
+
`intervalMilliseconds: 0`, `minimumDelta: 0`. Interval/delta are coerced to
|
|
258
|
+
nonnegative numbers. Use an actual monotonic `now()` in a real integration.
|
|
259
|
+
|
|
260
|
+
`receive(x, y, detail?)` rejects nonfinite axes or a torn-down instance. Outside
|
|
261
|
+
the rate-limit window it immediately attempts a commit; inside it retains the
|
|
262
|
+
latest target and schedules a single trailing timeout. Material change is the
|
|
263
|
+
maximum axis difference being at least `minimumDelta`. With zero minimum delta,
|
|
264
|
+
identical targets can still commit. A queued receive can return true even if that
|
|
265
|
+
target is later superseded or suppressed as too small.
|
|
266
|
+
|
|
267
|
+
Commit detail copies the supplied detail and adds `targetTimestamp` and
|
|
268
|
+
`targetCommitCount`. Returned methods are `receive(x,y,detail?) -> boolean`,
|
|
269
|
+
`reset() -> void`, `teardown() -> void`, `pending() -> boolean` and
|
|
270
|
+
`commitCount() -> number`. Reset clears targets/timing/counts; teardown also
|
|
271
|
+
permanently stops the instance. Neither factory exposes a restart or flush method.
|
|
272
|
+
|
|
273
|
+
## `DanceMovesClayStars`
|
|
274
|
+
|
|
275
|
+
Source: [clay-stars-effects.js](../../assets/clay-stars-effects.js) and
|
|
276
|
+
[clay-stars-effects.css](../../assets/clay-stars-effects.css).
|
|
277
|
+
Created only with `.ks-epk.ks-clay-stars-v2` and a descendant `.epk-cover-wrap`.
|
|
278
|
+
A live instance for the same root prevents duplicate initialisation. PHP normally
|
|
279
|
+
loads it only on Page 252 without the legacy effects constant.
|
|
280
|
+
|
|
281
|
+
### Properties and parameters
|
|
282
|
+
|
|
283
|
+
`root` is the root DOM element. `defaults` is frozen; the exported API object is
|
|
284
|
+
also frozen. Modify runtime parameters through `setParameters()`, not `defaults`.
|
|
285
|
+
|
|
286
|
+
| Parameter | Default | Accepted range / coercion |
|
|
287
|
+
| --- | --- | --- |
|
|
288
|
+
| `enabled` | true | Boolean when the key is present. |
|
|
289
|
+
| `masterIntensity` | 2 | 0–2 |
|
|
290
|
+
| `coverTiltDegrees` | 1.15 | 0–4 |
|
|
291
|
+
| `coverTranslationPixels` | 4 | 0–16 |
|
|
292
|
+
| `bloomTravelPixels` | 8 | 0–32 |
|
|
293
|
+
| `flareTravelPixels` | 18 | 0–48 |
|
|
294
|
+
| `specularTravelPixels` | 24 | 0–64 |
|
|
295
|
+
| `particleReleaseTicks` | 32 | Rounded integer 1–128; its actual duration also passes through core duration quantisation. |
|
|
296
|
+
|
|
297
|
+
Numeric values are coerced and clamped; nonfinite values keep the previous
|
|
298
|
+
setting. `null` coerces to zero before clamping. Motion uses bounded axes times
|
|
299
|
+
`masterIntensity`; reduced motion or `enabled: false` sets active motion gain to
|
|
300
|
+
zero. The defaults therefore produce up to 2.3 degrees cover tilt and 8 px cover
|
|
301
|
+
translation per axis, not the single-gain parameter magnitudes alone.
|
|
302
|
+
|
|
303
|
+
### Methods
|
|
304
|
+
|
|
305
|
+
| Signature | Return / side effects |
|
|
306
|
+
| --- | --- |
|
|
307
|
+
| `setParameters(next?)` | Returns a shallow settings copy. Applies provided/clamped fields, writes intensity/release-duration CSS variables, reapplies the last motion input. Unspecified fields remain unchanged. |
|
|
308
|
+
| `setMotion({x,y}?)` | Boolean; clamps/coerces input to -1–1 and coalesces into one pending animation frame. False after teardown. |
|
|
309
|
+
| `setMotionTarget({x,y}?)` | Boolean; cancels pending motion frame and applies the bounded target immediately. Intended for already rate-limited orientation input. False after teardown. |
|
|
310
|
+
| `setCueState(detail?)` | Void; reads normalised name/type or name/type, lowercases and replaces whitespace with hyphens into root cue attributes. Empty input removes cue attributes. This is state styling, not core cue dispatch or animation reset. |
|
|
311
|
+
| `lifecycle(state)` | Void; applies lifecycle effects below and records the state attribute. |
|
|
312
|
+
| `reset()` | Void; restores default visual parameters, clears cue/particle/motion state and marks lifecycle `reset`. Does **not** reset performance profile, history or counters. |
|
|
313
|
+
| `snapshot()` | Returns the state shape below. |
|
|
314
|
+
| `teardown()` | Idempotent void disposal: stops performance monitor, particle/motion tasks, listeners and cue subscription; marks runtime stopped. Does not undo all CSS/attributes, unregister the separately registered core animation scope, or recreate a live instance. |
|
|
315
|
+
|
|
316
|
+
Lifecycle strings `seeking`, `hidden`, `pagehide`, `reduced-motion` clear cues,
|
|
317
|
+
particles and motion; the last three also stop performance sampling and clear
|
|
318
|
+
consecutive health counters. `seeked` sets a `seeked`/`state` cue state.
|
|
319
|
+
`visible`/`pageshow` clear motion and request a settled performance window.
|
|
320
|
+
Other strings merely update the recorded attribute. The method does not fake
|
|
321
|
+
`document.hidden` or browser media preferences.
|
|
322
|
+
|
|
323
|
+
Pointer movement is active for a fine hover-capable pointer with a cached cover
|
|
324
|
+
bounding rectangle. Pointer enter refreshes bounds; leave/rotation resets motion;
|
|
325
|
+
resize invalidates bounds. Hover/focus on controls samples the particle orbit once
|
|
326
|
+
on an animation frame, then pointer leave/blur can play a tick-timed release from
|
|
327
|
+
the sampled transform/opacity. There is no permanent particle-sampling loop.
|
|
328
|
+
|
|
329
|
+
### Snapshot shape
|
|
330
|
+
|
|
331
|
+
```text
|
|
332
|
+
{
|
|
333
|
+
destroyed: boolean,
|
|
334
|
+
settings: { all visual parameters above },
|
|
335
|
+
motion: {x, y, pendingFrame, commits},
|
|
336
|
+
particles: {controls, pendingFrames, pendingTimers, computedStyleReads, samples},
|
|
337
|
+
performance: {
|
|
338
|
+
enabled, profile, reason, monitoring, poorWindows, windows, reductions,
|
|
339
|
+
recoveryAttempts, healthyWindows, recoveryWaitWindows, probationFrom,
|
|
340
|
+
lastFps, observedCadenceFps, lastMedianFrameMilliseconds,
|
|
341
|
+
lastP95FrameMilliseconds
|
|
342
|
+
},
|
|
343
|
+
cue: string, cueType: string, lifecycle: string
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`profile` is `full`, `constrained` or `minimal`. Metrics/cadence/probation can be
|
|
348
|
+
`null` before measurement/no active probation. `recoveryAttempts` counts upward
|
|
349
|
+
profile changes, not only confirmed recoveries. `monitoring` indicates a scheduled
|
|
350
|
+
timer or frame, not a promise of a measured FPS. There is no public performance
|
|
351
|
+
profile setter.
|
|
352
|
+
|
|
353
|
+
## Recoverable Clay/Stars performance tiers
|
|
354
|
+
|
|
355
|
+
The monitor starts at `full`. It waits for document load completion plus a settling
|
|
356
|
+
period, samples visible frame intervals, and continues checking even in `minimal`.
|
|
357
|
+
Reduced motion/hidden state suspends it. Large frame gaps reset the current sample
|
|
358
|
+
window instead of counting as proof of incapacity.
|
|
359
|
+
|
|
360
|
+
`constrained` stops the ambient cloud, atmosphere-particle and gold-sparkle loops
|
|
361
|
+
while retaining a static treatment. `minimal` also removes blur/screen blending
|
|
362
|
+
from those ambient planes. Phone response, controls and musical cue infrastructure
|
|
363
|
+
remain separate. These tiers are implemented only for Clay/Stars, not every EPK.
|
|
364
|
+
|
|
365
|
+
The fastest stable observed cadence can rise but cannot fall. A window is eligible
|
|
366
|
+
for cadence learning at least at 28 FPS with p95 interval at most 1.25 times its
|
|
367
|
+
median; eligible observations below 35 FPS use a normalised 30 FPS reference. Without an established
|
|
368
|
+
reference, the monitor uses 60 FPS. A poor window is below 75% of the reference in
|
|
369
|
+
`full` or 80% in lower tiers. A healthy window requires at least 90% of the reference
|
|
370
|
+
and p95 no more than 1.5 reference intervals. These are observed frame statistics,
|
|
371
|
+
not authoritative physical display-refresh detection.
|
|
372
|
+
|
|
373
|
+
By default, **three** consecutive poor windows trigger one downgrade. After a
|
|
374
|
+
cooldown of initially three valid windows and three consecutive healthy windows,
|
|
375
|
+
a lower tier trials the next higher one. Two poor windows fail a trial and revert;
|
|
376
|
+
three healthy windows confirm it. Failed trials double the cooldown up to 24
|
|
377
|
+
windows; confirmed recovery restores the cooldown to three. This supersedes the
|
|
378
|
+
historical fixed-45/50-FPS, two-window, downward-only description.
|
|
379
|
+
|
|
380
|
+
### Pre-load configuration: `window.danceMovesClayPerformanceConfig`
|
|
381
|
+
|
|
382
|
+
Set before the Clay script. Settings are captured/frozen at initialisation and are
|
|
383
|
+
not changed by `setParameters()`.
|
|
384
|
+
|
|
385
|
+
| Key | Default | Range / meaning |
|
|
386
|
+
| --- | --- | --- |
|
|
387
|
+
| `enabled` | true | Only literal false disables monitoring. Independent of visual `settings.enabled`. |
|
|
388
|
+
| `windowMilliseconds` | 1600 | 500–5000 |
|
|
389
|
+
| `settleMilliseconds` | 5000 | 0–10000 |
|
|
390
|
+
| `retryMilliseconds` | 2000 | 0–10000; retry after poor measurement without tier change. |
|
|
391
|
+
| `recheckMilliseconds` | 10000 | 1000–60000; spaced checks, including minimal tier. |
|
|
392
|
+
| `minimumIntervals` | 6 | Rounded integer 4–240. |
|
|
393
|
+
| `maximumIntervalMilliseconds` | 250 | 100–1000; larger gaps reset a sample window. |
|
|
394
|
+
| `poorWindowsBeforeReduction` | 3 | Rounded integer 2–5. |
|
|
395
|
+
| `fullMinimumFps` | 45 | Parsed/clamped 20–60 for compatibility, but **unused by the current cadence-relative decision logic**. |
|
|
396
|
+
| `constrainedMinimumFps` | 50 | Likewise parsed/clamped but unused. |
|
|
397
|
+
|
|
398
|
+
These delays measure capability and must not be substituted for the musical
|
|
399
|
+
clock. Do not infer physical-device performance from a synthetic unit harness or
|
|
400
|
+
from the frequency of phone sensor events.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Signed MP3 download API
|
|
2
|
+
|
|
3
|
+
[Documentation index](../README.md) · [WordPress/PHP API](wordpress.md) ·
|
|
4
|
+
[User guide](../guide.md)
|
|
5
|
+
|
|
6
|
+
Source: [plugin entrypoint](../../kieran-epk-device-orientation.php), functions
|
|
7
|
+
`dance_moves_epk_download_*`. Baseline: **DanceMoves 2.8.0**, commit
|
|
8
|
+
`4cb6a71f60459b5579be87d7e55ac8b1426c578e`. Introduced in 2.5.0.
|
|
9
|
+
|
|
10
|
+
This feature gives explicit WordPress-upload MP3 downloads an attachment response
|
|
11
|
+
without changing the media URLs used by audio players. It is a **front-end query
|
|
12
|
+
endpoint**, not a REST route, shortcode, browser download API or media proxy.
|
|
13
|
+
|
|
14
|
+
## Authoring and automatic link transformation
|
|
15
|
+
|
|
16
|
+
In main Page content, use the original uploads URL and a `download` attribute:
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<a class="epk-download"
|
|
20
|
+
href="/wp-content/uploads/2026/09/example-track.mp3"
|
|
21
|
+
download>Download MP3</a>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
At `the_content` priority **30**, the plugin uses `WP_HTML_Tag_Processor` to inspect
|
|
25
|
+
anchors. A link is eligible when it has a `download` attribute and its URL resolves
|
|
26
|
+
to an MP3 path under the configured uploads base URL. Eligibility is not tied to
|
|
27
|
+
an EPK CSS class, release title or page ID: it applies to eligible anchors in the
|
|
28
|
+
main content of any singular Page.
|
|
29
|
+
|
|
30
|
+
The filter returns unchanged content in admin, off singular Pages, outside the
|
|
31
|
+
main loop/query, or if `WP_HTML_Tag_Processor` is unavailable. It does not create
|
|
32
|
+
anchors or add missing `download` attributes. It rewrites `href` only, preserving
|
|
33
|
+
other attributes. A custom value such as `download="custom-name.mp3"` remains in
|
|
34
|
+
the markup but the server's filename is derived from the actual stored basename.
|
|
35
|
+
|
|
36
|
+
Audio players and `<source>` elements, ordinary media links, external upload
|
|
37
|
+
hosts and non-MP3 downloads are not rewritten. Direct MP3 URLs remain ordinary
|
|
38
|
+
media URLs. Stored post content is unchanged: this is a render-time transform.
|
|
39
|
+
JavaScript-inserted anchors and template links that never pass through
|
|
40
|
+
`the_content` need a server-generated URL from the helpers below.
|
|
41
|
+
|
|
42
|
+
## URL and HTTP contract
|
|
43
|
+
|
|
44
|
+
The generated URL uses `home_url('/')` with two registered query variables:
|
|
45
|
+
|
|
46
|
+
| Query key / constant | Value |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `dance_moves_download` / `DANCE_MOVES_DOWNLOAD_QUERY_VAR` | Normalized path relative to the WordPress uploads directory, for example `2026/09/example-track.mp3`. |
|
|
49
|
+
| `dance_moves_download_signature` / `DANCE_MOVES_DOWNLOAD_SIGNATURE_QUERY_VAR` | Hex HMAC-SHA-256 over that normalized relative path using `wp_salt('auth')`. |
|
|
50
|
+
|
|
51
|
+
Generate the URL with WordPress rather than constructing a signature in browser
|
|
52
|
+
code. The URL is not a `wp-json` path; preserve subdirectory/site URL settings.
|
|
53
|
+
The handler runs at `template_redirect` priority **0**, independently of which
|
|
54
|
+
Page originally rendered the link.
|
|
55
|
+
|
|
56
|
+
### Successful GET and HEAD
|
|
57
|
+
|
|
58
|
+
A valid request returns status **200** with these headers:
|
|
59
|
+
|
|
60
|
+
| Header | Value / rule |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `Content-Description` | `File Transfer` |
|
|
63
|
+
| `Content-Type` | `audio/mpeg` |
|
|
64
|
+
| `Content-Disposition` | `attachment`; ASCII-compatible sanitized basename in `filename`, plus URL-encoded original basename in `filename*=UTF-8''...`. |
|
|
65
|
+
| `Content-Length` | Size of the local file in bytes. |
|
|
66
|
+
| `Cache-Control` | `private, no-store, max-age=0` |
|
|
67
|
+
| `X-Content-Type-Options` | `nosniff` |
|
|
68
|
+
| `X-Robots-Tag` | `noindex, nofollow` |
|
|
69
|
+
|
|
70
|
+
The handler clears active output buffers before sending a successful response.
|
|
71
|
+
GET streams the complete local file using `readfile()` and exits. HEAD sends the
|
|
72
|
+
same headers without the file body and exits. It does not implement Range/206,
|
|
73
|
+
conditional GET/304, ETag, remote fetching or an asynchronous job. Use the original
|
|
74
|
+
media URL in players that need normal seeking/range delivery, not this endpoint.
|
|
75
|
+
|
|
76
|
+
### Failure handling
|
|
77
|
+
|
|
78
|
+
| Condition | Behaviour |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| No download path, or path normalization returns empty | Handler returns without serving a file; ordinary WordPress routing continues. This branch is **not** a guaranteed 404 response. |
|
|
81
|
+
| Valid-shaped path but missing/incorrect signature | Status 404, WordPress no-cache headers, exit without a JSON response. |
|
|
82
|
+
| Unresolvable upload base/file, path outside the real upload base, unreadable/non-file or non-MP3 target | Same 404 path. |
|
|
83
|
+
| Other method after path/signature/file validation | Status 405, `Allow: GET, HEAD`, no-cache headers, exit. |
|
|
84
|
+
|
|
85
|
+
These are not `WP_Error` REST responses. The server checks real paths to stop a
|
|
86
|
+
symlink from resolving outside the uploads directory. Extension checking does
|
|
87
|
+
not decode or validate the audio format itself. Delivery still depends on a
|
|
88
|
+
readable local file; remote-only/offloaded media with no local copy cannot be
|
|
89
|
+
served by this implementation.
|
|
90
|
+
|
|
91
|
+
## PHP functions
|
|
92
|
+
|
|
93
|
+
All functions require a loaded WordPress environment. These eight functions are
|
|
94
|
+
part of the complete PHP inventory in [wordpress.md](wordpress.md).
|
|
95
|
+
|
|
96
|
+
| Signature | Return | Exact role |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `dance_moves_epk_download_normalize_relative_path($relative_path)` | string | Nonstring becomes `''`. Replaces literal backslashes with forward slashes, trims, then URL-decodes. Rejects empty/absolute paths, colons, control characters, non-`.mp3` suffixes and empty, `.` or `..` segments. Returns joined normalized segments. Case of the extension is not significant. |
|
|
99
|
+
| `dance_moves_epk_download_relative_path_from_url($url)` | string | Nonstring/blank or unusable upload-base data becomes `''`. HTML-decodes the URL, parses URL/base, requires paths, checks any supplied host/scheme/port against the uploads base, URL-decodes paths, requires the base-path prefix plus `/`, then passes the relative suffix through normalization. |
|
|
100
|
+
| `dance_moves_epk_download_signature($relative_path)` | string | Hex `hash_hmac('sha256', $relative_path, wp_salt('auth'))`. Does **not** normalize or validate its argument; use the URL helper for callers' input. |
|
|
101
|
+
| `dance_moves_epk_download_url($relative_path)` | string | Normalizes input, returns `''` if rejected, otherwise adds path/signature query args to `home_url('/')`. Does not verify file existence/readability at URL-generation time. |
|
|
102
|
+
| `dance_moves_epk_download_query_vars($query_vars)` | array | Adds both query-variable names and returns unique indexed values. Registered on `query_vars`. |
|
|
103
|
+
| `dance_moves_epk_download_filter_content($content)` | string | Eligible-anchor rewrite described above. Registered on `the_content` at 30. Returns original string if no change. |
|
|
104
|
+
| `dance_moves_epk_download_not_found()` | no return; exits | Sends 404 and no-cache headers, then terminates. Internal response helper, not a template renderer. |
|
|
105
|
+
| `dance_moves_epk_download_serve()` | void or exits | Reads registered query vars, verifies signature with `hash_equals`, checks real local path/method, emits headers and optionally body, then exits for a handled request. |
|
|
106
|
+
|
|
107
|
+
URL comparison is against the **configured upload base**, which may differ from
|
|
108
|
+
the site's display hostname. The helper checks supplied URL components, so a
|
|
109
|
+
root-relative upload URL can qualify. A different CDN scheme, hostname, port or
|
|
110
|
+
path can prevent rewriting. Query strings/fragments are not preserved in the
|
|
111
|
+
new download URL. Path decoding occurs in both the URL-to-path helper and the
|
|
112
|
+
normalizer; pass original URLs rather than repeatedly encoding/decoding them.
|
|
113
|
+
|
|
114
|
+
A template that does not use the content filter can generate an escaped link:
|
|
115
|
+
|
|
116
|
+
```php
|
|
117
|
+
<?php
|
|
118
|
+
// Execute within a WordPress template, with an actual uploads-relative path.
|
|
119
|
+
if (function_exists('dance_moves_epk_download_url')) {
|
|
120
|
+
$download = dance_moves_epk_download_url('2026/09/example-track.mp3');
|
|
121
|
+
if ($download !== '') {
|
|
122
|
+
printf('<a href="%s" download>Download MP3</a>', esc_url($download));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For an attachment, resolve its URL then convert it to an uploads-relative path
|
|
128
|
+
with `dance_moves_epk_download_relative_path_from_url()`. The feature has no
|
|
129
|
+
special attachment-ID download helper. Never emit `wp_salt('auth')` to clients or
|
|
130
|
+
reuse the independent motion-capture token to sign downloads.
|
|
131
|
+
|
|
132
|
+
## Security and operational boundaries
|
|
133
|
+
|
|
134
|
+
A signature proves that the server generated a URL for this path. It is **not** a
|
|
135
|
+
per-user access check: there is no login, capability, per-link expiry, attachment
|
|
136
|
+
privacy check, nonce or rate limit in this endpoint. Anyone holding a valid URL
|
|
137
|
+
can reuse/share it while the file and signing salt remain valid. The original
|
|
138
|
+
media URL is not made private. Use a separate reviewed access-control design for
|
|
139
|
+
restricted media; do not treat this as one.
|
|
140
|
+
|
|
141
|
+
Changing the signing salt invalidates earlier URLs. A long-lived HTML cache can
|
|
142
|
+
therefore serve stale links after a salt change. That is an operational detail,
|
|
143
|
+
not a reason to expose salts or weaken signature validation. Successful signed
|
|
144
|
+
responses themselves request no-store caching.
|
|
145
|
+
|
|
146
|
+
For troubleshooting, distinguish failure to **rewrite** (attribute, content
|
|
147
|
+
filter, tag processor or URL-base mismatch) from failure to **serve** (signature,
|
|
148
|
+
local realpath/readability or method). Confirm that the player still uses the
|
|
149
|
+
original media URL and that a real download returns the attachment header.
|
|
150
|
+
|
|
151
|
+
## Tests
|
|
152
|
+
|
|
153
|
+
[epk-download-contract.test.cjs](../../tests/epk-download-contract.test.cjs) checks
|
|
154
|
+
source contracts. [validate-epk-download-paths.php](../../tests/validate-epk-download-paths.php)
|
|
155
|
+
exercises normalization, upload URL restrictions and stable URL generation in a
|
|
156
|
+
stubbed WordPress environment. They do not replace a staging HTTP test of GET,
|
|
157
|
+
HEAD, 404/405, filenames, cache behaviour, main-loop link rewriting and player
|
|
158
|
+
seekability with real WordPress, server headers and local uploads.
|