@glissade/player 0.7.0-pre.0 → 0.8.0-pre.0
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/dist/index.d.ts +66 -2
- package/dist/index.js +147 -22
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,20 @@ interface ScrollDriverOptions {
|
|
|
39
39
|
/** User-controlled scalar: scroll progress 0..1 → t. Writes the playhead directly. */
|
|
40
40
|
declare function scrollDriver(opts: ScrollDriverOptions): Driver;
|
|
41
41
|
//#endregion
|
|
42
|
+
//#region src/reducedMotion.d.ts
|
|
43
|
+
type ReducedMotionMode = 'respect' | 'ignore' | ((doc: Timeline) => Timeline);
|
|
44
|
+
interface ReducedMotionPlan {
|
|
45
|
+
/** Whether to autoplay after applying the plan. */
|
|
46
|
+
autoplay: boolean;
|
|
47
|
+
/** Seek the playhead here before first paint (the poster frame). */
|
|
48
|
+
seekTo?: number;
|
|
49
|
+
/** Replace the bound timeline with this calmer alternative (the fn form). */
|
|
50
|
+
swapTo?: Timeline;
|
|
51
|
+
}
|
|
52
|
+
declare function planReducedMotion(mode: ReducedMotionMode | undefined, prefersReduced: boolean, doc: Timeline, duration: number, autoplayRequested: boolean): ReducedMotionPlan;
|
|
53
|
+
/** Live `prefers-reduced-motion: reduce` reading; false off-DOM. */
|
|
54
|
+
declare function mediaPrefersReducedMotion(): boolean;
|
|
55
|
+
//#endregion
|
|
42
56
|
//#region src/player.d.ts
|
|
43
57
|
type LoopMode = boolean | {
|
|
44
58
|
mode: 'restart' | 'alternate';
|
|
@@ -48,6 +62,20 @@ interface PlayerOptions {
|
|
|
48
62
|
loop?: LoopMode;
|
|
49
63
|
rate?: number;
|
|
50
64
|
autoplay?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* What playback does while the tab is hidden (§4.1, via the driver's
|
|
67
|
+
* `visibility`). `'pause'` (default) freezes and resumes where it left off —
|
|
68
|
+
* no wall-clock jump on return; `'run'` advances by the hidden duration.
|
|
69
|
+
*/
|
|
70
|
+
background?: 'pause' | 'run';
|
|
71
|
+
/**
|
|
72
|
+
* prefers-reduced-motion policy (applied by mount(), §4.2). `'respect'`
|
|
73
|
+
* (default) holds the poster frame and suppresses autoplay; `'ignore'` opts
|
|
74
|
+
* out; a function returns a calmer alternative timeline.
|
|
75
|
+
*/
|
|
76
|
+
reducedMotion?: ReducedMotionMode;
|
|
77
|
+
/** Override reduced-motion detection (defaults to the media query). */
|
|
78
|
+
prefersReducedMotion?: () => boolean;
|
|
51
79
|
}
|
|
52
80
|
interface PlayHandle {
|
|
53
81
|
/** true = completed naturally, false = interrupted. Completion signal ONLY (§2). */
|
|
@@ -86,8 +114,21 @@ interface Player {
|
|
|
86
114
|
pause(): void;
|
|
87
115
|
/** Direct playhead write: pure, never fires marker callbacks (§4.2). */
|
|
88
116
|
seek(t: number): void;
|
|
117
|
+
/**
|
|
118
|
+
* Rebind to a recompiled timeline (HMR, §4.3): swap duration/markers/targets
|
|
119
|
+
* in place and **preserve the current playhead** (clamped to the new
|
|
120
|
+
* duration) — no replay-to-frame. Playing state and registered marker/cue
|
|
121
|
+
* callbacks survive; range resets to the new full span.
|
|
122
|
+
*/
|
|
123
|
+
swap(next: {
|
|
124
|
+
duration: number;
|
|
125
|
+
markers?: Marker[];
|
|
126
|
+
targets?: Iterable<string>;
|
|
127
|
+
}): void;
|
|
89
128
|
/** Register a marker callback; fired only when continuous playback crosses it. */
|
|
90
129
|
onMarker(name: string, cb: (marker: Marker) => void): () => void;
|
|
130
|
+
/** Like onMarker but matches a cue's `data.kind` (e.g. 'ad-break') across all names. */
|
|
131
|
+
onCue(kind: string, cb: (marker: Marker) => void): () => void;
|
|
91
132
|
/**
|
|
92
133
|
* Wire a machine to the host clock (v2 §A.5): step(now) on every tick, in
|
|
93
134
|
* attach order, before evaluation — even while linear playback is paused.
|
|
@@ -104,8 +145,31 @@ interface Mounted {
|
|
|
104
145
|
backend: Canvas2DBackend;
|
|
105
146
|
/** Force a synchronous render of the current playhead time. */
|
|
106
147
|
render(): void;
|
|
148
|
+
/**
|
|
149
|
+
* Hot-swap the bound scene and/or timeline (HMR, §4.3) while preserving the
|
|
150
|
+
* playhead: recompiles, rebinds the player (duration/markers/targets), and
|
|
151
|
+
* re-renders at the current time. A scene node a track targeted but the new
|
|
152
|
+
* scene no longer has simply stops being written (stale binding) — it keeps
|
|
153
|
+
* its last value rather than erroring.
|
|
154
|
+
*/
|
|
155
|
+
swap(next: {
|
|
156
|
+
scene?: Scene;
|
|
157
|
+
timeline: Timeline;
|
|
158
|
+
}): void;
|
|
107
159
|
dispose(): void;
|
|
108
160
|
}
|
|
109
|
-
declare function mount(
|
|
161
|
+
declare function mount(initialScene: Scene, initialDoc: Timeline, canvas: HTMLCanvasElement | OffscreenCanvas, opts?: PlayerOptions): Mounted;
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/hmr.d.ts
|
|
164
|
+
interface SceneModuleShape {
|
|
165
|
+
scene?: Scene;
|
|
166
|
+
timeline: Timeline;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Build the `import.meta.hot.accept` callback that hot-swaps `mounted` from a
|
|
170
|
+
* re-evaluated scene module. `rerun` maps the updated module namespace to the
|
|
171
|
+
* fresh `{ scene?, timeline }`.
|
|
172
|
+
*/
|
|
173
|
+
declare function swapOnHmr(mounted: Mounted, initialTimeline: Timeline, rerun: (mod: Record<string, unknown>) => SceneModuleShape): (mod: Record<string, unknown> | undefined) => void;
|
|
110
174
|
//#endregion
|
|
111
|
-
export { type AttachedMachine, type Driver, type DriverContext, type InputDriver, type LoopMode, type Mounted, type PlayHandle, type Player, type PlayerInit, type PlayerOptions, type ScrollDriverOptions, TargetOverlapError, clockDriver, createPlayer, mount, scrollDriver };
|
|
175
|
+
export { type AttachedMachine, type Driver, type DriverContext, type InputDriver, type LoopMode, type Mounted, type PlayHandle, type Player, type PlayerInit, type PlayerOptions, type ReducedMotionMode, type ReducedMotionPlan, type SceneModuleShape, type ScrollDriverOptions, TargetOverlapError, clockDriver, createPlayer, mediaPrefersReducedMotion, mount, planReducedMotion, scrollDriver, swapOnHmr };
|
package/dist/index.js
CHANGED
|
@@ -69,26 +69,35 @@ var TargetOverlapError = class extends Error {
|
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
function createPlayer(init, opts = {}) {
|
|
72
|
-
const { playhead
|
|
73
|
-
|
|
72
|
+
const { playhead } = init;
|
|
73
|
+
let liveDuration = init.duration;
|
|
74
|
+
let markers = init.markers ?? [];
|
|
74
75
|
const driver = init.driver ?? clockDriver();
|
|
75
76
|
const loop = opts.loop ?? false;
|
|
77
|
+
const background = opts.background ?? "pause";
|
|
78
|
+
const visibility = init.visibility ?? (() => "visible");
|
|
76
79
|
const callbacks = /* @__PURE__ */ new Map();
|
|
80
|
+
const cueCallbacks = /* @__PURE__ */ new Map();
|
|
77
81
|
let rate = opts.rate ?? 1;
|
|
78
82
|
let playing = false;
|
|
79
83
|
let driverRunning = false;
|
|
80
|
-
|
|
84
|
+
let ownTargets = new Set(init.targets ?? []);
|
|
81
85
|
const machines = [];
|
|
82
86
|
let base = 0;
|
|
83
87
|
let elapsedOrigin = null;
|
|
84
88
|
let lastElapsed = 0;
|
|
85
|
-
let range = [0,
|
|
89
|
+
let range = [0, liveDuration];
|
|
86
90
|
let loopsDone = 0;
|
|
87
91
|
let resolveFinished = null;
|
|
88
92
|
function fireMarkers(from, to) {
|
|
89
|
-
if (from === to || callbacks.size === 0) return;
|
|
93
|
+
if (from === to || callbacks.size === 0 && cueCallbacks.size === 0) return;
|
|
90
94
|
const forward = to > from;
|
|
91
|
-
for (const m of markers)
|
|
95
|
+
for (const m of markers) {
|
|
96
|
+
if (!(forward ? m.t > from && m.t <= to : m.t >= to && m.t < from)) continue;
|
|
97
|
+
for (const cb of callbacks.get(m.name) ?? []) cb(m);
|
|
98
|
+
const kind = m.data?.kind;
|
|
99
|
+
if (typeof kind === "string") for (const cb of cueCallbacks.get(kind) ?? []) cb(m);
|
|
100
|
+
}
|
|
92
101
|
}
|
|
93
102
|
function settle(completed) {
|
|
94
103
|
playing = false;
|
|
@@ -101,6 +110,11 @@ function createPlayer(init, opts = {}) {
|
|
|
101
110
|
lastElapsed = elapsed;
|
|
102
111
|
for (const m of machines) m.step(elapsed);
|
|
103
112
|
if (!playing) return;
|
|
113
|
+
if (background === "pause" && visibility() === "hidden") {
|
|
114
|
+
base = playhead.peek();
|
|
115
|
+
elapsedOrigin = null;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
104
118
|
elapsedOrigin ??= elapsed;
|
|
105
119
|
const prev = playhead.peek();
|
|
106
120
|
let t = base + (elapsed - elapsedOrigin) * rate;
|
|
@@ -137,13 +151,15 @@ function createPlayer(init, opts = {}) {
|
|
|
137
151
|
if (driverRunning) return;
|
|
138
152
|
driverRunning = true;
|
|
139
153
|
driver.start(onElapsed, {
|
|
140
|
-
duration,
|
|
141
|
-
visibility
|
|
154
|
+
duration: liveDuration,
|
|
155
|
+
visibility
|
|
142
156
|
});
|
|
143
157
|
}
|
|
144
158
|
return {
|
|
145
159
|
playhead,
|
|
146
|
-
duration
|
|
160
|
+
get duration() {
|
|
161
|
+
return liveDuration;
|
|
162
|
+
},
|
|
147
163
|
get playing() {
|
|
148
164
|
return playing;
|
|
149
165
|
},
|
|
@@ -157,7 +173,7 @@ function createPlayer(init, opts = {}) {
|
|
|
157
173
|
},
|
|
158
174
|
play(playOpts) {
|
|
159
175
|
if (resolveFinished) settle(false);
|
|
160
|
-
range = playOpts?.range ?? [0,
|
|
176
|
+
range = playOpts?.range ?? [0, liveDuration];
|
|
161
177
|
const t = playhead.peek();
|
|
162
178
|
const [lo, hi] = range;
|
|
163
179
|
base = rate >= 0 ? t >= hi || t < lo ? lo : t : t <= lo || t > hi ? hi : t;
|
|
@@ -176,10 +192,21 @@ function createPlayer(init, opts = {}) {
|
|
|
176
192
|
settle(false);
|
|
177
193
|
},
|
|
178
194
|
seek(t) {
|
|
179
|
-
base = Math.min(Math.max(t, 0),
|
|
195
|
+
base = Math.min(Math.max(t, 0), liveDuration);
|
|
180
196
|
elapsedOrigin = playing ? lastElapsed : null;
|
|
181
197
|
playhead.set(base);
|
|
182
198
|
},
|
|
199
|
+
swap(next) {
|
|
200
|
+
liveDuration = next.duration;
|
|
201
|
+
markers = next.markers ?? [];
|
|
202
|
+
ownTargets = new Set(next.targets ?? []);
|
|
203
|
+
const t = Math.min(playhead.peek(), liveDuration);
|
|
204
|
+
range = [0, liveDuration];
|
|
205
|
+
base = t;
|
|
206
|
+
elapsedOrigin = playing ? lastElapsed : null;
|
|
207
|
+
loopsDone = 0;
|
|
208
|
+
playhead.set(t);
|
|
209
|
+
},
|
|
183
210
|
attach(machine) {
|
|
184
211
|
if (machine.targets) {
|
|
185
212
|
const overlaps = [];
|
|
@@ -203,6 +230,15 @@ function createPlayer(init, opts = {}) {
|
|
|
203
230
|
set.add(cb);
|
|
204
231
|
return () => set.delete(cb);
|
|
205
232
|
},
|
|
233
|
+
onCue(kind, cb) {
|
|
234
|
+
let set = cueCallbacks.get(kind);
|
|
235
|
+
if (!set) {
|
|
236
|
+
set = /* @__PURE__ */ new Set();
|
|
237
|
+
cueCallbacks.set(kind, set);
|
|
238
|
+
}
|
|
239
|
+
set.add(cb);
|
|
240
|
+
return () => set.delete(cb);
|
|
241
|
+
},
|
|
206
242
|
dispose() {
|
|
207
243
|
if (playing) settle(false);
|
|
208
244
|
machines.length = 0;
|
|
@@ -212,23 +248,44 @@ function createPlayer(init, opts = {}) {
|
|
|
212
248
|
};
|
|
213
249
|
}
|
|
214
250
|
//#endregion
|
|
251
|
+
//#region src/reducedMotion.ts
|
|
252
|
+
function planReducedMotion(mode, prefersReduced, doc, duration, autoplayRequested) {
|
|
253
|
+
const m = mode ?? "respect";
|
|
254
|
+
if (m === "ignore" || !prefersReduced) return { autoplay: autoplayRequested };
|
|
255
|
+
if (typeof m === "function") return {
|
|
256
|
+
autoplay: autoplayRequested,
|
|
257
|
+
swapTo: m(doc)
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
autoplay: false,
|
|
261
|
+
seekTo: doc.posterTime ?? duration
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Live `prefers-reduced-motion: reduce` reading; false off-DOM. */
|
|
265
|
+
function mediaPrefersReducedMotion() {
|
|
266
|
+
return typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
215
269
|
//#region src/mount.ts
|
|
216
270
|
/**
|
|
217
271
|
* mount() — the vanilla embedding primitive (DESIGN.md §4.3): wire a scene +
|
|
218
272
|
* timeline + canvas into a rendering Player. Rendering is pulled: a playhead
|
|
219
273
|
* invalidation schedules one rAF-coalesced render.
|
|
220
274
|
*/
|
|
221
|
-
function mount(
|
|
275
|
+
function mount(initialScene, initialDoc, canvas, opts = {}) {
|
|
276
|
+
let scene = initialScene;
|
|
277
|
+
let doc = initialDoc;
|
|
222
278
|
const compiled = compileTimeline(doc);
|
|
223
279
|
const backend = new Canvas2DBackend(canvas);
|
|
224
280
|
scene.setTextMeasurer(backend);
|
|
281
|
+
const playhead = scene.playhead;
|
|
225
282
|
const player = createPlayer({
|
|
226
|
-
playhead
|
|
283
|
+
playhead,
|
|
227
284
|
duration: compiled.duration,
|
|
228
285
|
markers: compiled.markers,
|
|
229
286
|
targets: compiled.tracks.keys()
|
|
230
287
|
}, opts);
|
|
231
|
-
const renderNow = () => backend.render(evaluate(scene, doc,
|
|
288
|
+
const renderNow = () => backend.render(evaluate(scene, doc, playhead.peek()));
|
|
232
289
|
let scheduled = false;
|
|
233
290
|
const schedule = () => {
|
|
234
291
|
if (scheduled) return;
|
|
@@ -238,19 +295,51 @@ function mount(scene, doc, canvas, opts = {}) {
|
|
|
238
295
|
renderNow();
|
|
239
296
|
});
|
|
240
297
|
};
|
|
241
|
-
const unsubscribe =
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
298
|
+
const unsubscribe = playhead.subscribe(schedule);
|
|
299
|
+
const loadFonts = (forDoc) => {
|
|
300
|
+
if (typeof FontFace === "undefined") return;
|
|
301
|
+
for (const [family, ref] of Object.entries(forDoc.assets ?? {})) {
|
|
302
|
+
if (ref.kind !== "font") continue;
|
|
303
|
+
const face = new FontFace(family, `url(${ref.url})`);
|
|
304
|
+
document.fonts.add(face);
|
|
305
|
+
face.load().then(() => renderNow(), () => void 0);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
loadFonts(doc);
|
|
309
|
+
const prefersReduced = (opts.prefersReducedMotion ?? mediaPrefersReducedMotion)();
|
|
310
|
+
const plan = planReducedMotion(opts.reducedMotion, prefersReduced, doc, compiled.duration, !!opts.autoplay);
|
|
311
|
+
if (plan.swapTo) {
|
|
312
|
+
doc = plan.swapTo;
|
|
313
|
+
const recompiled = compileTimeline(doc);
|
|
314
|
+
player.swap({
|
|
315
|
+
duration: recompiled.duration,
|
|
316
|
+
markers: recompiled.markers,
|
|
317
|
+
targets: recompiled.tracks.keys()
|
|
318
|
+
});
|
|
319
|
+
loadFonts(doc);
|
|
247
320
|
}
|
|
321
|
+
if (plan.seekTo !== void 0) player.seek(plan.seekTo);
|
|
248
322
|
renderNow();
|
|
249
|
-
if (
|
|
323
|
+
if (plan.autoplay) player.play();
|
|
250
324
|
return {
|
|
251
325
|
player,
|
|
252
326
|
backend,
|
|
253
327
|
render: renderNow,
|
|
328
|
+
swap(next) {
|
|
329
|
+
if (next.scene) {
|
|
330
|
+
scene = next.scene;
|
|
331
|
+
scene.setTextMeasurer(backend);
|
|
332
|
+
}
|
|
333
|
+
doc = next.timeline;
|
|
334
|
+
const recompiled = compileTimeline(doc);
|
|
335
|
+
player.swap({
|
|
336
|
+
duration: recompiled.duration,
|
|
337
|
+
markers: recompiled.markers,
|
|
338
|
+
targets: recompiled.tracks.keys()
|
|
339
|
+
});
|
|
340
|
+
loadFonts(doc);
|
|
341
|
+
renderNow();
|
|
342
|
+
},
|
|
254
343
|
dispose() {
|
|
255
344
|
unsubscribe();
|
|
256
345
|
player.dispose();
|
|
@@ -259,4 +348,40 @@ function mount(scene, doc, canvas, opts = {}) {
|
|
|
259
348
|
};
|
|
260
349
|
}
|
|
261
350
|
//#endregion
|
|
262
|
-
|
|
351
|
+
//#region src/hmr.ts
|
|
352
|
+
/**
|
|
353
|
+
* Vite HMR glue for a mounted embed (DESIGN.md §4.3). When the scene module is
|
|
354
|
+
* edited, the dev server hands us the re-evaluated module; we hot-swap the live
|
|
355
|
+
* Mounted without losing the playhead (see Mounted.swap / Player.swap). The
|
|
356
|
+
* consumer owns the dependency path (only they know it), so this returns the
|
|
357
|
+
* accept callback rather than calling `import.meta.hot.accept` itself:
|
|
358
|
+
*
|
|
359
|
+
* import * as sceneMod from './hero.scene';
|
|
360
|
+
* const mounted = mount(sceneMod.createScene(), sceneMod.timeline, canvas);
|
|
361
|
+
* import.meta.hot?.accept(
|
|
362
|
+
* './hero.scene',
|
|
363
|
+
* swapOnHmr(mounted, sceneMod.timeline, (m) => ({ scene: m.createScene(), timeline: m.timeline })),
|
|
364
|
+
* );
|
|
365
|
+
*
|
|
366
|
+
* It also warns when an edit removes a label the previous timeline declared —
|
|
367
|
+
* code that seeks to that label (`play({ range: [labels.x, …] })`) would break.
|
|
368
|
+
*/
|
|
369
|
+
/**
|
|
370
|
+
* Build the `import.meta.hot.accept` callback that hot-swaps `mounted` from a
|
|
371
|
+
* re-evaluated scene module. `rerun` maps the updated module namespace to the
|
|
372
|
+
* fresh `{ scene?, timeline }`.
|
|
373
|
+
*/
|
|
374
|
+
function swapOnHmr(mounted, initialTimeline, rerun) {
|
|
375
|
+
let prevLabels = Object.keys(compileTimeline(initialTimeline).labels);
|
|
376
|
+
return (mod) => {
|
|
377
|
+
if (mod == null) return;
|
|
378
|
+
const next = rerun(mod);
|
|
379
|
+
const nextLabels = Object.keys(compileTimeline(next.timeline).labels);
|
|
380
|
+
const dropped = prevLabels.filter((l) => !nextLabels.includes(l));
|
|
381
|
+
if (dropped.length > 0) console.warn(`[glissade] HMR: this edit removed label(s) ${dropped.map((l) => `'${l}'`).join(", ")} — anything seeking to them will no longer resolve`);
|
|
382
|
+
prevLabels = nextLabels;
|
|
383
|
+
mounted.swap(next);
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
export { TargetOverlapError, clockDriver, createPlayer, mediaPrefersReducedMotion, mount, planReducedMotion, scrollDriver, swapOnHmr };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-pre.0",
|
|
4
4
|
"description": "glissade embed runtime: Player, Drivers (clock, scroll), mount().",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@glissade/backend-canvas2d": "0.
|
|
19
|
-
"@glissade/core": "0.
|
|
20
|
-
"@glissade/scene": "0.
|
|
18
|
+
"@glissade/backend-canvas2d": "0.8.0-pre.0",
|
|
19
|
+
"@glissade/core": "0.8.0-pre.0",
|
|
20
|
+
"@glissade/scene": "0.8.0-pre.0"
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|