@glissade/react 0.10.1 → 0.11.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 +41 -4
- package/dist/index.js +112 -3
- package/package.json +13 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { CSSProperties } from "react";
|
|
2
|
+
import { ReadonlySignal, Signal, Timeline } from "@glissade/core";
|
|
3
|
+
import { LoopMode, Player } from "@glissade/player";
|
|
4
|
+
import { Scene } from "@glissade/scene";
|
|
3
5
|
|
|
4
|
-
//#region src/
|
|
6
|
+
//#region src/ScenePlayer.d.ts
|
|
5
7
|
|
|
8
|
+
interface ScenePlayerProps {
|
|
9
|
+
/** The scene graph to render. */
|
|
10
|
+
scene: Scene;
|
|
11
|
+
/** The timeline document driving it. */
|
|
12
|
+
timeline: Timeline;
|
|
13
|
+
/** Loop policy (forwarded to the Player). */
|
|
14
|
+
loop?: LoopMode;
|
|
15
|
+
/** Render the default controls bar (play/pause, scrubber, time readout). */
|
|
16
|
+
controls?: boolean;
|
|
17
|
+
/** Start playing on mount. */
|
|
18
|
+
autoplay?: boolean;
|
|
19
|
+
/** Fired when a play completes naturally (true) or is interrupted (false). */
|
|
20
|
+
onFinished?: (completed: boolean) => void;
|
|
21
|
+
/** Called once the underlying Player is mounted (for imperative control). */
|
|
22
|
+
onReady?: (player: Player) => void;
|
|
23
|
+
className?: string;
|
|
24
|
+
style?: CSSProperties;
|
|
25
|
+
}
|
|
26
|
+
declare function ScenePlayer({
|
|
27
|
+
scene,
|
|
28
|
+
timeline,
|
|
29
|
+
loop,
|
|
30
|
+
controls,
|
|
31
|
+
autoplay,
|
|
32
|
+
onFinished,
|
|
33
|
+
onReady,
|
|
34
|
+
className,
|
|
35
|
+
style
|
|
36
|
+
}: ScenePlayerProps): React.ReactElement;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/index.d.ts
|
|
6
39
|
/** Subscribe a component to a signal; re-renders only when the value actually changes. */
|
|
7
40
|
declare function useSignalValue<T>(sig: ReadonlySignal<T>): T;
|
|
41
|
+
/** DESIGN §4 sketch name — alias of {@link useSignalValue} (`useSignal(node.width)`). */
|
|
42
|
+
declare const useSignal: typeof useSignalValue;
|
|
8
43
|
/** The player's current time, live. */
|
|
9
44
|
declare function usePlayhead(player: Player): number;
|
|
10
45
|
/** Playing state, derived from playhead motion + player state polling on invalidation. */
|
|
@@ -25,5 +60,7 @@ declare function useMachineState(machine: {
|
|
|
25
60
|
declare function useInput<T extends boolean | number>(machine: {
|
|
26
61
|
input(name: string): Signal<T>;
|
|
27
62
|
}, name: string): [T, (value: T) => void];
|
|
63
|
+
/** The declarative <ScenePlayer> over mount() (DESIGN §4.3, the <gs-player> twin). */
|
|
64
|
+
|
|
28
65
|
//#endregion
|
|
29
|
-
export { useInput, useMachineState, usePlayerState, usePlayhead, useSignalValue };
|
|
66
|
+
export { ScenePlayer, type ScenePlayerProps, useInput, useMachineState, usePlayerState, usePlayhead, useSignal, useSignalValue };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,113 @@
|
|
|
1
|
-
import { useCallback, useSyncExternalStore } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import "@glissade/core";
|
|
3
|
-
import "@glissade/player";
|
|
3
|
+
import { mount } from "@glissade/player";
|
|
4
|
+
import "@glissade/scene";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/ScenePlayer.tsx
|
|
7
|
+
/**
|
|
8
|
+
* <ScenePlayer> (DESIGN.md §4.3 tier 3, the React twin of <gs-player>): a
|
|
9
|
+
* declarative component over mount(). Props are mount-native (`scene` +
|
|
10
|
+
* `timeline`); the controls inventory mirrors the element's — play/pause,
|
|
11
|
+
* scrubber, time readout — but as plain React with className-styled nodes
|
|
12
|
+
* rather than a shadow DOM + CSS parts.
|
|
13
|
+
*
|
|
14
|
+
* All mount() work happens in a useEffect keyed on [scene, timeline] (never
|
|
15
|
+
* during render — SSR renders the inert canvas only). The component owns
|
|
16
|
+
* playback so it can observe each play's `.finished` promise for onFinished
|
|
17
|
+
* (a per-play completion signal, §2 — NOT a polled state).
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The controls bar — split out so it can subscribe to live playhead state via
|
|
21
|
+
* usePlayerState (a hook, so it must live in a component that only renders once
|
|
22
|
+
* the player exists).
|
|
23
|
+
*/
|
|
24
|
+
function Controls({ player }) {
|
|
25
|
+
const { playing, time, duration } = usePlayerState(player);
|
|
26
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
27
|
+
className: "gs-controls",
|
|
28
|
+
children: [
|
|
29
|
+
/* @__PURE__ */ jsx("button", {
|
|
30
|
+
className: "gs-controls-button",
|
|
31
|
+
type: "button",
|
|
32
|
+
"aria-label": "Play or pause",
|
|
33
|
+
onClick: () => {
|
|
34
|
+
if (player.playing) player.pause();
|
|
35
|
+
else player.play();
|
|
36
|
+
},
|
|
37
|
+
children: playing ? "Pause" : "Play"
|
|
38
|
+
}),
|
|
39
|
+
/* @__PURE__ */ jsx("input", {
|
|
40
|
+
className: "gs-controls-scrubber",
|
|
41
|
+
type: "range",
|
|
42
|
+
min: 0,
|
|
43
|
+
max: 1,
|
|
44
|
+
step: 1e-4,
|
|
45
|
+
value: duration > 0 ? time / duration : 0,
|
|
46
|
+
"aria-label": "Seek",
|
|
47
|
+
onChange: (e) => {
|
|
48
|
+
player.pause();
|
|
49
|
+
player.seek(parseFloat(e.target.value) * player.duration);
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
/* @__PURE__ */ jsxs("span", {
|
|
53
|
+
className: "gs-controls-time",
|
|
54
|
+
children: [
|
|
55
|
+
time.toFixed(2),
|
|
56
|
+
" / ",
|
|
57
|
+
duration.toFixed(2),
|
|
58
|
+
"s"
|
|
59
|
+
]
|
|
60
|
+
})
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function ScenePlayer({ scene, timeline, loop, controls = false, autoplay = false, onFinished, onReady, className, style }) {
|
|
65
|
+
const canvasRef = useRef(null);
|
|
66
|
+
const [player, setPlayer] = useState(null);
|
|
67
|
+
const onFinishedRef = useRef(onFinished);
|
|
68
|
+
onFinishedRef.current = onFinished;
|
|
69
|
+
const onReadyRef = useRef(onReady);
|
|
70
|
+
onReadyRef.current = onReady;
|
|
71
|
+
const autoplayRef = useRef(autoplay);
|
|
72
|
+
autoplayRef.current = autoplay;
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const canvas = canvasRef.current;
|
|
75
|
+
if (!canvas) return;
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
const mounted = mount(scene, timeline, canvas, {
|
|
78
|
+
...loop !== void 0 ? { loop } : {},
|
|
79
|
+
autoplay: false
|
|
80
|
+
});
|
|
81
|
+
const p = mounted.player;
|
|
82
|
+
const rawPlay = p.play.bind(p);
|
|
83
|
+
const playAndTrack = (opts) => {
|
|
84
|
+
const handle = rawPlay(opts);
|
|
85
|
+
handle.finished.then((completed) => {
|
|
86
|
+
if (!cancelled) onFinishedRef.current?.(completed);
|
|
87
|
+
}, () => void 0);
|
|
88
|
+
return handle;
|
|
89
|
+
};
|
|
90
|
+
p.play = playAndTrack;
|
|
91
|
+
setPlayer(p);
|
|
92
|
+
onReadyRef.current?.(p);
|
|
93
|
+
if (autoplayRef.current) playAndTrack();
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
setPlayer(null);
|
|
97
|
+
mounted.dispose();
|
|
98
|
+
};
|
|
99
|
+
}, [scene, timeline]);
|
|
100
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
101
|
+
className,
|
|
102
|
+
style,
|
|
103
|
+
children: [/* @__PURE__ */ jsx("canvas", {
|
|
104
|
+
ref: canvasRef,
|
|
105
|
+
width: scene.size.w,
|
|
106
|
+
height: scene.size.h
|
|
107
|
+
}), controls && player ? /* @__PURE__ */ jsx(Controls, { player }) : null]
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
4
111
|
//#region src/index.ts
|
|
5
112
|
/**
|
|
6
113
|
* @glissade/react — thin adapters (DESIGN.md §4.3 tier 3): vanilla core,
|
|
@@ -13,6 +120,8 @@ function useSignalValue(sig) {
|
|
|
13
120
|
const get = useCallback(() => sig.peek(), [sig]);
|
|
14
121
|
return useSyncExternalStore(subscribe, get, get);
|
|
15
122
|
}
|
|
123
|
+
/** DESIGN §4 sketch name — alias of {@link useSignalValue} (`useSignal(node.width)`). */
|
|
124
|
+
const useSignal = useSignalValue;
|
|
16
125
|
/** The player's current time, live. */
|
|
17
126
|
function usePlayhead(player) {
|
|
18
127
|
return useSignalValue(player.playhead);
|
|
@@ -40,4 +149,4 @@ function useInput(machine, name) {
|
|
|
40
149
|
return [useSignalValue(sig), useCallback((v) => sig.set(v), [sig])];
|
|
41
150
|
}
|
|
42
151
|
//#endregion
|
|
43
|
-
export { useInput, useMachineState, usePlayerState, usePlayhead, useSignalValue };
|
|
152
|
+
export { ScenePlayer, useInput, useMachineState, usePlayerState, usePlayhead, useSignal, useSignalValue };
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-pre.0",
|
|
4
4
|
"description": "glissade React bindings: signals via useSyncExternalStore, player hooks.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.19"
|
|
8
|
+
},
|
|
6
9
|
"type": "module",
|
|
7
10
|
"sideEffects": false,
|
|
8
11
|
"exports": {
|
|
@@ -15,15 +18,21 @@
|
|
|
15
18
|
"dist"
|
|
16
19
|
],
|
|
17
20
|
"dependencies": {
|
|
18
|
-
"@glissade/core": "0.
|
|
19
|
-
"@glissade/player": "0.
|
|
21
|
+
"@glissade/core": "0.11.0-pre.0",
|
|
22
|
+
"@glissade/player": "0.11.0-pre.0",
|
|
23
|
+
"@glissade/scene": "0.11.0-pre.0"
|
|
20
24
|
},
|
|
21
25
|
"peerDependencies": {
|
|
22
26
|
"react": ">=18"
|
|
23
27
|
},
|
|
24
28
|
"devDependencies": {
|
|
29
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
30
|
+
"@testing-library/react": "^16.3.0",
|
|
25
31
|
"@types/react": "^19.1.0",
|
|
26
|
-
"react": "^19.1.0"
|
|
32
|
+
"@types/react-dom": "^19.1.0",
|
|
33
|
+
"jsdom": "^26.0.0",
|
|
34
|
+
"react": "^19.1.0",
|
|
35
|
+
"react-dom": "^19.1.0"
|
|
27
36
|
},
|
|
28
37
|
"repository": {
|
|
29
38
|
"type": "git",
|