@velumo/runtime 0.1.0-beta.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/action-dispatcher.d.ts +27 -0
- package/dist/action-dispatcher.d.ts.map +1 -0
- package/dist/action-dispatcher.js +34 -0
- package/dist/action-dispatcher.js.map +1 -0
- package/dist/camera.d.ts +51 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +327 -0
- package/dist/camera.js.map +1 -0
- package/dist/controller.d.ts +33 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +109 -0
- package/dist/controller.js.map +1 -0
- package/dist/cue-dispatcher.d.ts +76 -0
- package/dist/cue-dispatcher.d.ts.map +1 -0
- package/dist/cue-dispatcher.js +164 -0
- package/dist/cue-dispatcher.js.map +1 -0
- package/dist/hash.d.ts +8 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +27 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/notes.d.ts +32 -0
- package/dist/notes.d.ts.map +1 -0
- package/dist/notes.js +132 -0
- package/dist/notes.js.map +1 -0
- package/dist/registry.d.ts +32 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +76 -0
- package/dist/registry.js.map +1 -0
- package/dist/scene-player.d.ts +40 -0
- package/dist/scene-player.d.ts.map +1 -0
- package/dist/scene-player.js +177 -0
- package/dist/scene-player.js.map +1 -0
- package/dist/spatial.d.ts +11 -0
- package/dist/spatial.d.ts.map +1 -0
- package/dist/spatial.js +28 -0
- package/dist/spatial.js.map +1 -0
- package/dist/theme-bundle.d.ts +32 -0
- package/dist/theme-bundle.d.ts.map +1 -0
- package/dist/theme-bundle.js +8 -0
- package/dist/theme-bundle.js.map +1 -0
- package/dist/timeline.d.ts +46 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +217 -0
- package/dist/timeline.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +19 -0
- package/src/action-dispatcher.ts +81 -0
- package/src/camera.ts +513 -0
- package/src/controller.ts +171 -0
- package/src/cue-dispatcher.ts +299 -0
- package/src/hash.ts +39 -0
- package/src/index.ts +12 -0
- package/src/notes.ts +194 -0
- package/src/registry.ts +194 -0
- package/src/scene-player.ts +244 -0
- package/src/spatial.ts +40 -0
- package/src/theme-bundle.ts +35 -0
- package/src/timeline.ts +323 -0
- package/tsconfig.json +10 -0
package/src/registry.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { Manifest } from "@velumo/core";
|
|
2
|
+
|
|
3
|
+
export type LayerRenderer<TContext = unknown, TResult = unknown> = (
|
|
4
|
+
context: TContext,
|
|
5
|
+
) => TResult;
|
|
6
|
+
|
|
7
|
+
export type ActionHandler<TContext = unknown> = (context: TContext) => void;
|
|
8
|
+
|
|
9
|
+
export type ManifestTransform = (manifest: Manifest) => Manifest;
|
|
10
|
+
|
|
11
|
+
export interface PluginSetupContext<
|
|
12
|
+
TLayerContext = unknown,
|
|
13
|
+
TLayerResult = unknown,
|
|
14
|
+
TActionContext = unknown,
|
|
15
|
+
> {
|
|
16
|
+
readonly registry: RuntimeRegistry<
|
|
17
|
+
TLayerContext,
|
|
18
|
+
TLayerResult,
|
|
19
|
+
TActionContext
|
|
20
|
+
>;
|
|
21
|
+
readonly manifest: Manifest;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VelumoPlugin<
|
|
25
|
+
TLayerContext = unknown,
|
|
26
|
+
TLayerResult = unknown,
|
|
27
|
+
TActionContext = unknown,
|
|
28
|
+
> {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
setup?(
|
|
31
|
+
context: PluginSetupContext<TLayerContext, TLayerResult, TActionContext>,
|
|
32
|
+
): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface InstallPluginsOptions<
|
|
36
|
+
TLayerContext = unknown,
|
|
37
|
+
TLayerResult = unknown,
|
|
38
|
+
TActionContext = unknown,
|
|
39
|
+
> {
|
|
40
|
+
readonly registry: RuntimeRegistry<
|
|
41
|
+
TLayerContext,
|
|
42
|
+
TLayerResult,
|
|
43
|
+
TActionContext
|
|
44
|
+
>;
|
|
45
|
+
readonly manifest: Manifest;
|
|
46
|
+
readonly plugins: readonly VelumoPlugin<
|
|
47
|
+
TLayerContext,
|
|
48
|
+
TLayerResult,
|
|
49
|
+
TActionContext
|
|
50
|
+
>[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class RuntimeRegistry<
|
|
54
|
+
TLayerContext = unknown,
|
|
55
|
+
TLayerResult = unknown,
|
|
56
|
+
TActionContext = unknown,
|
|
57
|
+
> {
|
|
58
|
+
readonly #layerRenderers = new Map<
|
|
59
|
+
string,
|
|
60
|
+
LayerRenderer<TLayerContext, TLayerResult>
|
|
61
|
+
>();
|
|
62
|
+
readonly #actionHandlers = new Map<string, ActionHandler<TActionContext>>();
|
|
63
|
+
readonly #manifestTransforms: ManifestTransform[] = [];
|
|
64
|
+
readonly #installedPluginIds = new Set<string>();
|
|
65
|
+
|
|
66
|
+
registerLayerRenderer(
|
|
67
|
+
type: string,
|
|
68
|
+
renderer: LayerRenderer<TLayerContext, TLayerResult>,
|
|
69
|
+
): void {
|
|
70
|
+
if (this.#layerRenderers.has(type)) {
|
|
71
|
+
throw new Error(`Layer renderer already registered: ${type}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.#layerRenderers.set(type, renderer);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
hasLayerRenderer(type: string): boolean {
|
|
78
|
+
return this.#layerRenderers.has(type);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getLayerRenderer(
|
|
82
|
+
type: string,
|
|
83
|
+
): LayerRenderer<TLayerContext, TLayerResult> | undefined {
|
|
84
|
+
return this.#layerRenderers.get(type);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
registerActionHandler(
|
|
88
|
+
type: string,
|
|
89
|
+
handler: ActionHandler<TActionContext>,
|
|
90
|
+
): void {
|
|
91
|
+
if (this.#actionHandlers.has(type)) {
|
|
92
|
+
throw new Error(`Action handler already registered: ${type}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.#actionHandlers.set(type, handler);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
hasActionHandler(type: string): boolean {
|
|
99
|
+
return this.#actionHandlers.has(type);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getActionHandler(type: string): ActionHandler<TActionContext> | undefined {
|
|
103
|
+
return this.#actionHandlers.get(type);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
registerManifestTransform(transform: ManifestTransform): void {
|
|
107
|
+
this.#manifestTransforms.push(transform);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
applyManifestTransforms(manifest: Manifest): Manifest {
|
|
111
|
+
return this.#manifestTransforms.reduce(
|
|
112
|
+
(current, transform) => transform(current),
|
|
113
|
+
cloneManifest(manifest),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
installPlugins(
|
|
118
|
+
manifest: Manifest,
|
|
119
|
+
plugins: readonly VelumoPlugin<
|
|
120
|
+
TLayerContext,
|
|
121
|
+
TLayerResult,
|
|
122
|
+
TActionContext
|
|
123
|
+
>[],
|
|
124
|
+
): void {
|
|
125
|
+
this.#assertPluginsCanInstall(plugins);
|
|
126
|
+
|
|
127
|
+
for (const plugin of plugins) {
|
|
128
|
+
plugin.setup?.({ registry: this, manifest });
|
|
129
|
+
this.#installedPluginIds.add(plugin.id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#assertPluginsCanInstall(
|
|
134
|
+
plugins: readonly VelumoPlugin<
|
|
135
|
+
TLayerContext,
|
|
136
|
+
TLayerResult,
|
|
137
|
+
TActionContext
|
|
138
|
+
>[],
|
|
139
|
+
): void {
|
|
140
|
+
const nextPluginIds = new Set<string>();
|
|
141
|
+
|
|
142
|
+
for (const plugin of plugins) {
|
|
143
|
+
if (
|
|
144
|
+
this.#installedPluginIds.has(plugin.id) ||
|
|
145
|
+
nextPluginIds.has(plugin.id)
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(`Plugin already installed: ${plugin.id}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
nextPluginIds.add(plugin.id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createRuntimeRegistry<
|
|
156
|
+
TLayerContext = unknown,
|
|
157
|
+
TLayerResult = unknown,
|
|
158
|
+
TActionContext = unknown,
|
|
159
|
+
>(): RuntimeRegistry<TLayerContext, TLayerResult, TActionContext> {
|
|
160
|
+
return new RuntimeRegistry<TLayerContext, TLayerResult, TActionContext>();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function installPlugins<
|
|
164
|
+
TLayerContext = unknown,
|
|
165
|
+
TLayerResult = unknown,
|
|
166
|
+
TActionContext = unknown,
|
|
167
|
+
>(
|
|
168
|
+
options: InstallPluginsOptions<TLayerContext, TLayerResult, TActionContext>,
|
|
169
|
+
): void {
|
|
170
|
+
options.registry.installPlugins(options.manifest, options.plugins);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function cloneManifest(manifest: Manifest): Manifest {
|
|
174
|
+
const slides =
|
|
175
|
+
manifest.slides === undefined ? {} : { slides: [...manifest.slides] };
|
|
176
|
+
const assets =
|
|
177
|
+
manifest.assets === undefined ? {} : { assets: [...manifest.assets] };
|
|
178
|
+
const plugins =
|
|
179
|
+
manifest.plugins === undefined ? {} : { plugins: [...manifest.plugins] };
|
|
180
|
+
const theme =
|
|
181
|
+
manifest.theme === undefined ? {} : { theme: { ...manifest.theme } };
|
|
182
|
+
const exportOptions =
|
|
183
|
+
manifest.export === undefined ? {} : { export: { ...manifest.export } };
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...manifest,
|
|
187
|
+
deck: { ...manifest.deck },
|
|
188
|
+
...slides,
|
|
189
|
+
...assets,
|
|
190
|
+
...plugins,
|
|
191
|
+
...theme,
|
|
192
|
+
...exportOptions,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { CameraEasing, CameraStop, CameraView, Cue } from "@velumo/core";
|
|
2
|
+
import { type ResolvedView, resolveView } from "./spatial.js";
|
|
3
|
+
import { TimelineScheduler } from "./timeline.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DURATION_MS = 800;
|
|
6
|
+
const DEFAULT_EASING: CameraEasing = "ease-in-out";
|
|
7
|
+
const FINALIZE_SEEK_MS = Number.MAX_SAFE_INTEGER;
|
|
8
|
+
|
|
9
|
+
export type ScenePlayerDirection = "forward" | "backward";
|
|
10
|
+
export type ScenePlayerMove = "moved" | "exhausted";
|
|
11
|
+
|
|
12
|
+
export type ScenePlayerEventName = "camera:move" | "cue:fired" | "stop:enter";
|
|
13
|
+
|
|
14
|
+
export interface ScenePlayerCameraMoveEvent {
|
|
15
|
+
readonly type: "camera:move";
|
|
16
|
+
readonly view: ResolvedView;
|
|
17
|
+
readonly durationMs: number;
|
|
18
|
+
readonly easing: CameraEasing;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ScenePlayerCueFiredEvent {
|
|
22
|
+
readonly type: "cue:fired";
|
|
23
|
+
readonly cue: Cue;
|
|
24
|
+
readonly stopIndex: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ScenePlayerStopEnterEvent {
|
|
28
|
+
readonly type: "stop:enter";
|
|
29
|
+
readonly stopIndex: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ScenePlayerEvent =
|
|
33
|
+
| ScenePlayerCameraMoveEvent
|
|
34
|
+
| ScenePlayerCueFiredEvent
|
|
35
|
+
| ScenePlayerStopEnterEvent;
|
|
36
|
+
|
|
37
|
+
export type ScenePlayerEventHandler = (event: ScenePlayerEvent) => void;
|
|
38
|
+
|
|
39
|
+
export interface ScenePlayerOptions {
|
|
40
|
+
readonly stops: readonly CameraStop[];
|
|
41
|
+
readonly reducedMotion?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isCameraView(value: unknown): value is CameraView {
|
|
45
|
+
if (typeof value !== "object" || value === null) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const v = value as { x?: unknown; y?: unknown; zoom?: unknown };
|
|
49
|
+
if (!Number.isFinite(v.x) || !Number.isFinite(v.y)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (v.zoom !== undefined && !Number.isFinite(v.zoom)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class ScenePlayer {
|
|
59
|
+
readonly #stops: readonly CameraStop[];
|
|
60
|
+
readonly #reducedMotion: boolean;
|
|
61
|
+
readonly #handlers = new Map<
|
|
62
|
+
ScenePlayerEventName,
|
|
63
|
+
Set<ScenePlayerEventHandler>
|
|
64
|
+
>();
|
|
65
|
+
#stopIndex = -1;
|
|
66
|
+
#view: ResolvedView = { x: 0, y: 0, zoom: 1 };
|
|
67
|
+
#timeline: TimelineScheduler | undefined;
|
|
68
|
+
#timelineCueCount = 0;
|
|
69
|
+
#elapsedSinceLand = 0;
|
|
70
|
+
|
|
71
|
+
constructor(options: ScenePlayerOptions) {
|
|
72
|
+
this.#stops = options.stops;
|
|
73
|
+
this.#reducedMotion = options.reducedMotion ?? false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
enter(direction: ScenePlayerDirection): void {
|
|
77
|
+
const index = direction === "forward" ? 0 : this.#stops.length - 1;
|
|
78
|
+
this.#land(index);
|
|
79
|
+
if (direction === "backward") {
|
|
80
|
+
this.#finalizeTimeline();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
advance(): ScenePlayerMove {
|
|
85
|
+
if (this.#stopIndex >= this.#stops.length - 1) {
|
|
86
|
+
return "exhausted";
|
|
87
|
+
}
|
|
88
|
+
this.#finalizeTimeline();
|
|
89
|
+
this.#land(this.#stopIndex + 1);
|
|
90
|
+
return "moved";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
back(): ScenePlayerMove {
|
|
94
|
+
if (this.#stopIndex <= 0) {
|
|
95
|
+
return "exhausted";
|
|
96
|
+
}
|
|
97
|
+
this.#land(this.#stopIndex - 1);
|
|
98
|
+
this.#finalizeTimeline();
|
|
99
|
+
return "moved";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
tick(deltaMs: number): void {
|
|
103
|
+
if (this.#timeline !== undefined && deltaMs > 0) {
|
|
104
|
+
this.#timeline.advance(deltaMs);
|
|
105
|
+
}
|
|
106
|
+
this.#elapsedSinceLand += deltaMs;
|
|
107
|
+
const stop = this.#stops[this.#stopIndex];
|
|
108
|
+
if (
|
|
109
|
+
stop !== undefined &&
|
|
110
|
+
stop.hold !== undefined &&
|
|
111
|
+
this.#elapsedSinceLand >= stop.hold &&
|
|
112
|
+
this.#stopIndex < this.#stops.length - 1
|
|
113
|
+
) {
|
|
114
|
+
this.advance();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getView(): ResolvedView {
|
|
119
|
+
return this.#view;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getStopIndex(): number {
|
|
123
|
+
return this.#stopIndex;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
on(
|
|
127
|
+
eventName: ScenePlayerEventName,
|
|
128
|
+
handler: ScenePlayerEventHandler,
|
|
129
|
+
): () => void {
|
|
130
|
+
let handlers = this.#handlers.get(eventName);
|
|
131
|
+
if (handlers === undefined) {
|
|
132
|
+
handlers = new Set();
|
|
133
|
+
this.#handlers.set(eventName, handlers);
|
|
134
|
+
}
|
|
135
|
+
handlers.add(handler);
|
|
136
|
+
return () => {
|
|
137
|
+
handlers.delete(handler);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#land(index: number): void {
|
|
142
|
+
const stop = this.#stops[index];
|
|
143
|
+
if (stop === undefined) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.#stopIndex = index;
|
|
147
|
+
this.#elapsedSinceLand = 0;
|
|
148
|
+
this.#view = resolveView(stop.camera);
|
|
149
|
+
this.#emitCameraMove(stop.transition?.duration, stop.transition?.easing);
|
|
150
|
+
this.#emit({ type: "stop:enter", stopIndex: index });
|
|
151
|
+
this.#startTimeline(stop);
|
|
152
|
+
if (this.#reducedMotion) {
|
|
153
|
+
this.#finalizeTimeline();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
isIdle(): boolean {
|
|
158
|
+
if (this.#stopIndex === -1) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
// Unfired cues remain: timeline exists and has not yet fired all cues.
|
|
162
|
+
if (this.#timeline !== undefined) {
|
|
163
|
+
const fired = this.#timeline.getState().firedCueIndices.length;
|
|
164
|
+
if (fired < this.#timelineCueCount) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Hold pending: not on the last stop and elapsed has not reached the hold.
|
|
169
|
+
const stop = this.#stops[this.#stopIndex];
|
|
170
|
+
if (
|
|
171
|
+
stop !== undefined &&
|
|
172
|
+
stop.hold !== undefined &&
|
|
173
|
+
this.#stopIndex < this.#stops.length - 1 &&
|
|
174
|
+
this.#elapsedSinceLand < stop.hold
|
|
175
|
+
) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#startTimeline(stop: CameraStop): void {
|
|
182
|
+
const cues = stop.cues ?? [];
|
|
183
|
+
if (cues.length === 0) {
|
|
184
|
+
this.#timeline = undefined;
|
|
185
|
+
this.#timelineCueCount = 0;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const timeline = new TimelineScheduler({ cues, initialPlaying: true });
|
|
189
|
+
timeline.on("cue:fired", (event) => {
|
|
190
|
+
if (event.type !== "cue:fired") {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.#handleCue(event.cue);
|
|
194
|
+
});
|
|
195
|
+
this.#timeline = timeline;
|
|
196
|
+
this.#timelineCueCount = cues.length;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#finalizeTimeline(): void {
|
|
200
|
+
if (this.#timeline === undefined) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Seek far beyond any cue time: every not-yet-fired cue fires now,
|
|
204
|
+
// applying its end-state, so a press mid-sequence leaves state consistent.
|
|
205
|
+
this.#timeline.advance(FINALIZE_SEEK_MS);
|
|
206
|
+
this.#timeline = undefined;
|
|
207
|
+
this.#timelineCueCount = 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#handleCue(cue: Cue): void {
|
|
211
|
+
if (cue.action.type === "camera" && isCameraView(cue.action.payload)) {
|
|
212
|
+
this.#view = resolveView(cue.action.payload);
|
|
213
|
+
this.#emitCameraMove(undefined, undefined);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.#emit({ type: "cue:fired", cue, stopIndex: this.#stopIndex });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#emitCameraMove(
|
|
220
|
+
duration: number | undefined,
|
|
221
|
+
easing: CameraEasing | undefined,
|
|
222
|
+
): void {
|
|
223
|
+
this.#emit({
|
|
224
|
+
type: "camera:move",
|
|
225
|
+
view: this.#view,
|
|
226
|
+
durationMs: this.#reducedMotion ? 0 : (duration ?? DEFAULT_DURATION_MS),
|
|
227
|
+
easing: easing ?? DEFAULT_EASING,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#emit(event: ScenePlayerEvent): void {
|
|
232
|
+
const handlers = this.#handlers.get(event.type);
|
|
233
|
+
if (handlers === undefined) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
for (const handler of [...handlers]) {
|
|
237
|
+
handler(event);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function createScenePlayer(options: ScenePlayerOptions): ScenePlayer {
|
|
243
|
+
return new ScenePlayer(options);
|
|
244
|
+
}
|
package/src/spatial.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CameraEasing, CameraView } from "@velumo/core";
|
|
2
|
+
import { type EasingFunction, easeInOutCubic } from "./timeline.js";
|
|
3
|
+
|
|
4
|
+
export const CAMERA_EASINGS: readonly CameraEasing[] = [
|
|
5
|
+
"linear",
|
|
6
|
+
"ease",
|
|
7
|
+
"ease-in",
|
|
8
|
+
"ease-out",
|
|
9
|
+
"ease-in-out",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function easeIn(t: number): number {
|
|
13
|
+
return t * t;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function easeOut(t: number): number {
|
|
17
|
+
return 1 - (1 - t) * (1 - t);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const EASING_MAP: Record<CameraEasing, EasingFunction> = {
|
|
21
|
+
linear: (t) => t,
|
|
22
|
+
ease: easeInOutCubic,
|
|
23
|
+
"ease-in": easeIn,
|
|
24
|
+
"ease-out": easeOut,
|
|
25
|
+
"ease-in-out": easeInOutCubic,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function resolveEasing(name: CameraEasing | undefined): EasingFunction {
|
|
29
|
+
return name === undefined ? easeInOutCubic : EASING_MAP[name];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ResolvedView {
|
|
33
|
+
readonly x: number;
|
|
34
|
+
readonly y: number;
|
|
35
|
+
readonly zoom: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveView(view: CameraView): ResolvedView {
|
|
39
|
+
return { x: view.x, y: view.y, zoom: view.zoom ?? 1 };
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Theme } from "@velumo/core";
|
|
2
|
+
import type { VelumoPlugin } from "./registry.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A theme bundle is a heterogeneous container: it carries plugin factories
|
|
6
|
+
* whose internal renderer/action context types it deliberately does not track.
|
|
7
|
+
* Plugins are loosely coupled at registration — the registry stores renderers
|
|
8
|
+
* keyed by layer type and enforces context-correctness there — so the bundle
|
|
9
|
+
* uses `any` for the plugin generics to stay ergonomic for any host.
|
|
10
|
+
*/
|
|
11
|
+
type AnyVelumoPlugin = VelumoPlugin<any, any, any>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A host-side convenience that pairs a theme with everything it needs to run:
|
|
15
|
+
* its theme tokens, its CSS, and a factory for the plugins it depends on. A
|
|
16
|
+
* theme package can export one `ThemeBundle` so an author wires a single import
|
|
17
|
+
* instead of three (theme + css + plugin).
|
|
18
|
+
*
|
|
19
|
+
* Plugins stay explicit and host-controlled: the bundle only *carries* a plugin
|
|
20
|
+
* factory — the host still calls it and registers the result. The runtime never
|
|
21
|
+
* auto-loads plugins from a manifest or config reference.
|
|
22
|
+
*/
|
|
23
|
+
export interface ThemeBundle {
|
|
24
|
+
readonly theme: Theme;
|
|
25
|
+
readonly css?: string;
|
|
26
|
+
readonly createPlugins?: () => readonly AnyVelumoPlugin[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a bundle's plugins. Returns a fresh array (empty when the bundle
|
|
31
|
+
* brings no plugins) so the result is safe for the host to register directly.
|
|
32
|
+
*/
|
|
33
|
+
export function themePlugins(bundle: ThemeBundle): readonly AnyVelumoPlugin[] {
|
|
34
|
+
return bundle.createPlugins?.() ?? [];
|
|
35
|
+
}
|