@glissade/player 0.1.0 → 0.3.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/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @glissade/player
2
+
3
+ The embed runtime: `Player` (play/pause/seek/loop/rate — time-based, never frame-counted), the **Driver** seam (`InputDriver<T>`: clock, scroll, or anything that writes a value), and `mount()` — scene + timeline + canvas in one call. v2 machines attach here: `player.attach(machine)` steps them on every host tick, even while linear playback is paused, with hard target-disjointness validation.
4
+
5
+ ```sh
6
+ npm i @glissade/player
7
+ ```
8
+
9
+ ```ts
10
+ import { mount } from '@glissade/player';
11
+
12
+ const { player } = mount(scene, doc, canvas, { loop: true, autoplay: true });
13
+ player.seek(1.5); // pure: identical to having played there
14
+ ```
15
+
16
+ ## Part of glissade
17
+
18
+ *(glide & slide)* — programmatic motion graphics for TypeScript: realtime-first in any web page, deterministic headless video export from the same code, a visual studio over the same document. No generator functions.
19
+
20
+ - [Repository & full README](https://github.com/tyevco/glissade)
21
+ - [Getting started](https://github.com/tyevco/glissade/blob/main/docs/getting-started.md) · [Concepts](https://github.com/tyevco/glissade/blob/main/docs/concepts.md) · [Interactivity](https://github.com/tyevco/glissade/blob/main/docs/interactivity.md)
22
+
23
+ Apache-2.0.
package/dist/index.d.ts CHANGED
@@ -10,14 +10,19 @@ import { Canvas2DBackend } from "@glissade/backend-canvas2d";
10
10
  * clock is the default Driver, not a privileged one — this seam is the v2
11
11
  * interactivity path (§2.9).
12
12
  */
13
- interface Driver {
14
- /** Begin writing. Call write(seconds) whenever the driven value changes. */
15
- start(write: (t: number) => void, ctx: DriverContext): void;
13
+ interface InputDriver<T = number> {
14
+ /** Begin writing. Call write(value) whenever the driven value changes. */
15
+ start(write: (value: T) => void, ctx: DriverContext): void;
16
16
  stop(): void;
17
17
  }
18
+ /** v1 alias (v2 §C.1): the playhead was always just the T = number case. */
19
+ type Driver = InputDriver<number>;
18
20
  interface DriverContext {
19
- /** Timeline duration, for normalization. */
20
- duration: number;
21
+ /**
22
+ * Timeline duration — present when driving a playhead; ABSENT in input mode
23
+ * (machines have no duration, v2 §C.1).
24
+ */
25
+ duration?: number;
21
26
  visibility: () => 'visible' | 'hidden';
22
27
  }
23
28
  /**
@@ -55,6 +60,20 @@ interface PlayerInit {
55
60
  /** Injected clock; defaults to the rAF clockDriver. Tests pass a manual driver. */
56
61
  driver?: Driver;
57
62
  visibility?: () => 'visible' | 'hidden';
63
+ /** Track targets of the bound timeline — the v2 §A.1 disjointness set for attach(). */
64
+ targets?: Iterable<string>;
65
+ }
66
+ /**
67
+ * The structural shape of an attached machine (v2 addendum §A.5). Defined
68
+ * structurally so @glissade/player never imports @glissade/interact (§7.1).
69
+ */
70
+ interface AttachedMachine {
71
+ step(now: number): void;
72
+ readonly targets?: ReadonlySet<string>;
73
+ }
74
+ /** §A.1: machine and Player track-target sets must be statically disjoint. */
75
+ declare class TargetOverlapError extends Error {
76
+ constructor(overlaps: string[]);
58
77
  }
59
78
  interface Player {
60
79
  readonly playhead: Playhead;
@@ -69,6 +88,12 @@ interface Player {
69
88
  seek(t: number): void;
70
89
  /** Register a marker callback; fired only when continuous playback crosses it. */
71
90
  onMarker(name: string, cb: (marker: Marker) => void): () => void;
91
+ /**
92
+ * Wire a machine to the host clock (v2 §A.5): step(now) on every tick, in
93
+ * attach order, before evaluation — even while linear playback is paused.
94
+ * Returns detach. Throws TargetOverlapError on a non-disjoint target set.
95
+ */
96
+ attach(machine: AttachedMachine): () => void;
72
97
  dispose(): void;
73
98
  }
74
99
  declare function createPlayer(init: PlayerInit, opts?: PlayerOptions): Player;
@@ -83,4 +108,4 @@ interface Mounted {
83
108
  }
84
109
  declare function mount(scene: Scene, doc: Timeline, canvas: HTMLCanvasElement | OffscreenCanvas, opts?: PlayerOptions): Mounted;
85
110
  //#endregion
86
- export { type Driver, type DriverContext, type LoopMode, type Mounted, type PlayHandle, type Player, type PlayerInit, type PlayerOptions, type ScrollDriverOptions, clockDriver, createPlayer, mount, scrollDriver };
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 };
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ function scrollDriver(opts) {
34
34
  const el = opts.source;
35
35
  return {
36
36
  start(write, ctx) {
37
- const range = opts.range ?? [0, ctx.duration];
37
+ const range = opts.range ?? [0, ctx.duration ?? 1];
38
38
  const progress = () => {
39
39
  if (el instanceof Element) {
40
40
  const max = axis === "y" ? el.scrollHeight - el.clientHeight : el.scrollWidth - el.clientWidth;
@@ -61,6 +61,13 @@ function scrollDriver(opts) {
61
61
  }
62
62
  //#endregion
63
63
  //#region src/player.ts
64
+ /** §A.1: machine and Player track-target sets must be statically disjoint. */
65
+ var TargetOverlapError = class extends Error {
66
+ constructor(overlaps) {
67
+ super(`machine targets overlap an already-bound target set: ${overlaps.join(", ")} — concurrent writers to one property is the silent last-writer-wins §2.2 exists to kill`);
68
+ this.name = "TargetOverlapError";
69
+ }
70
+ };
64
71
  function createPlayer(init, opts = {}) {
65
72
  const { playhead, duration } = init;
66
73
  const markers = init.markers ?? [];
@@ -70,6 +77,8 @@ function createPlayer(init, opts = {}) {
70
77
  let rate = opts.rate ?? 1;
71
78
  let playing = false;
72
79
  let driverRunning = false;
80
+ const ownTargets = new Set(init.targets ?? []);
81
+ const machines = [];
73
82
  let base = 0;
74
83
  let elapsedOrigin = null;
75
84
  let lastElapsed = 0;
@@ -90,6 +99,7 @@ function createPlayer(init, opts = {}) {
90
99
  }
91
100
  function onElapsed(elapsed) {
92
101
  lastElapsed = elapsed;
102
+ for (const m of machines) m.step(elapsed);
93
103
  if (!playing) return;
94
104
  elapsedOrigin ??= elapsed;
95
105
  const prev = playhead.peek();
@@ -170,6 +180,20 @@ function createPlayer(init, opts = {}) {
170
180
  elapsedOrigin = playing ? lastElapsed : null;
171
181
  playhead.set(base);
172
182
  },
183
+ attach(machine) {
184
+ if (machine.targets) {
185
+ const overlaps = [];
186
+ for (const t of machine.targets) if (ownTargets.has(t)) overlaps.push(`'${t}' (player timeline)`);
187
+ else if (machines.some((m) => m.targets?.has(t))) overlaps.push(`'${t}' (another machine)`);
188
+ if (overlaps.length > 0) throw new TargetOverlapError(overlaps);
189
+ }
190
+ machines.push(machine);
191
+ ensureDriver();
192
+ return () => {
193
+ const i = machines.indexOf(machine);
194
+ if (i >= 0) machines.splice(i, 1);
195
+ };
196
+ },
173
197
  onMarker(name, cb) {
174
198
  let set = callbacks.get(name);
175
199
  if (!set) {
@@ -181,6 +205,7 @@ function createPlayer(init, opts = {}) {
181
205
  },
182
206
  dispose() {
183
207
  if (playing) settle(false);
208
+ machines.length = 0;
184
209
  if (driverRunning) driver.stop();
185
210
  driverRunning = false;
186
211
  }
@@ -200,7 +225,8 @@ function mount(scene, doc, canvas, opts = {}) {
200
225
  const player = createPlayer({
201
226
  playhead: scene.playhead,
202
227
  duration: compiled.duration,
203
- markers: compiled.markers
228
+ markers: compiled.markers,
229
+ targets: compiled.tracks.keys()
204
230
  }, opts);
205
231
  const renderNow = () => backend.render(evaluate(scene, doc, scene.playhead.peek()));
206
232
  let scheduled = false;
@@ -233,4 +259,4 @@ function mount(scene, doc, canvas, opts = {}) {
233
259
  };
234
260
  }
235
261
  //#endregion
236
- export { clockDriver, createPlayer, mount, scrollDriver };
262
+ export { TargetOverlapError, clockDriver, createPlayer, mount, scrollDriver };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/player",
3
- "version": "0.1.0",
3
+ "version": "0.3.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.1.0",
19
- "@glissade/core": "0.1.0",
20
- "@glissade/scene": "0.1.0"
18
+ "@glissade/core": "0.3.0",
19
+ "@glissade/backend-canvas2d": "0.3.0",
20
+ "@glissade/scene": "0.3.0"
21
21
  },
22
22
  "repository": {
23
23
  "type": "git",