visualfries 0.1.10115 → 0.3.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.
Files changed (139) hide show
  1. package/README.md +122 -0
  2. package/bin/visualfries.js +1570 -0
  3. package/dist/DIContainer.d.ts +2 -1
  4. package/dist/DIContainer.js +16 -5
  5. package/dist/SceneBuilder.svelte.d.ts +70 -1
  6. package/dist/SceneBuilder.svelte.js +96 -29
  7. package/dist/agent/audioMixer.d.ts +34 -0
  8. package/dist/agent/audioMixer.js +308 -0
  9. package/dist/agent/broll.d.ts +35 -0
  10. package/dist/agent/broll.js +226 -0
  11. package/dist/agent/capabilities.d.ts +34 -0
  12. package/dist/agent/capabilities.js +246 -0
  13. package/dist/agent/captionScene.d.ts +23 -0
  14. package/dist/agent/captionScene.js +204 -0
  15. package/dist/agent/catalog.d.ts +24 -0
  16. package/dist/agent/catalog.js +67 -0
  17. package/dist/agent/cueFiles.d.ts +54 -0
  18. package/dist/agent/cueFiles.js +219 -0
  19. package/dist/agent/cuePresets.d.ts +7 -0
  20. package/dist/agent/cuePresets.js +50 -0
  21. package/dist/agent/deterministicMedia.d.ts +61 -0
  22. package/dist/agent/deterministicMedia.js +303 -0
  23. package/dist/agent/index.d.ts +18 -0
  24. package/dist/agent/index.js +18 -0
  25. package/dist/agent/inspectScene.d.ts +36 -0
  26. package/dist/agent/inspectScene.js +139 -0
  27. package/dist/agent/localRender.d.ts +82 -0
  28. package/dist/agent/localRender.js +702 -0
  29. package/dist/agent/overlays.d.ts +40 -0
  30. package/dist/agent/overlays.js +325 -0
  31. package/dist/agent/productionPlan.d.ts +131 -0
  32. package/dist/agent/productionPlan.js +322 -0
  33. package/dist/agent/renderPlan.d.ts +27 -0
  34. package/dist/agent/renderPlan.js +68 -0
  35. package/dist/agent/renderRanges.d.ts +24 -0
  36. package/dist/agent/renderRanges.js +73 -0
  37. package/dist/agent/runtimeSupport.d.ts +19 -0
  38. package/dist/agent/runtimeSupport.js +192 -0
  39. package/dist/agent/streamingEncoder.d.ts +31 -0
  40. package/dist/agent/streamingEncoder.js +208 -0
  41. package/dist/agent/transcripts.d.ts +29 -0
  42. package/dist/agent/transcripts.js +146 -0
  43. package/dist/agent/transitions.d.ts +20 -0
  44. package/dist/agent/transitions.js +169 -0
  45. package/dist/animations/PixiAnimationTarget.d.ts +29 -0
  46. package/dist/animations/PixiAnimationTarget.js +63 -0
  47. package/dist/animations/builders/LineHighlighterAnimationBuilder.js +20 -3
  48. package/dist/animations/builders/WordHighlighterAnimationBuilder.js +15 -2
  49. package/dist/browser/browserExport.d.ts +73 -0
  50. package/dist/browser/browserExport.js +180 -0
  51. package/dist/browser/glTransitions.d.ts +37 -0
  52. package/dist/browser/glTransitions.js +261 -0
  53. package/dist/browser/index.d.ts +3 -0
  54. package/dist/browser/index.js +3 -0
  55. package/dist/browser/mediaDecode.d.ts +31 -0
  56. package/dist/browser/mediaDecode.js +152 -0
  57. package/dist/builders/PixiComponentBuilder.d.ts +6 -0
  58. package/dist/builders/PixiComponentBuilder.js +12 -0
  59. package/dist/builders/_ComponentState.svelte.d.ts +7 -5
  60. package/dist/builders/_ComponentState.svelte.js +29 -15
  61. package/dist/builders/html/StyleBuilder.js +9 -8
  62. package/dist/builders/html/TextComponentHtmlBuilder.js +4 -2
  63. package/dist/builders/html/processors/AppearanceStyleProcessor.js +1 -1
  64. package/dist/builders/html/processors/TextAppearanceStyleProcessor.js +7 -0
  65. package/dist/builders/html/processors/TextEffectsStyleProcessor.js +32 -13
  66. package/dist/commands/ReplaceSourceOnTimeCommand.js +9 -1
  67. package/dist/commands/SeekCommand.js +19 -6
  68. package/dist/components/AnimatedGIF.d.ts +3 -0
  69. package/dist/components/AnimatedGIF.js +28 -1
  70. package/dist/components/Component.svelte.d.ts +5 -5
  71. package/dist/components/Component.svelte.js +29 -13
  72. package/dist/components/ComponentContext.svelte.js +68 -23
  73. package/dist/components/hooks/AnimationHook.js +1 -1
  74. package/dist/components/hooks/CanvasFillHook.d.ts +12 -0
  75. package/dist/components/hooks/CanvasFillHook.js +75 -0
  76. package/dist/components/hooks/DeterministicMediaFrameHook.js +3 -0
  77. package/dist/components/hooks/HtmlTextHook.js +11 -3
  78. package/dist/components/hooks/HtmlToCanvasHook.js +8 -25
  79. package/dist/components/hooks/MediaHook.js +172 -47
  80. package/dist/components/hooks/MediaSeekingHook.js +138 -88
  81. package/dist/components/hooks/PixiGifHook.js +4 -4
  82. package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.d.ts +3 -0
  83. package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +85 -8
  84. package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
  85. package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
  86. package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
  87. package/dist/components/hooks/SubtitlesHook.js +48 -24
  88. package/dist/components/hooks/VerifyMediaHook.js +2 -2
  89. package/dist/directors/ComponentDirector.d.ts +1 -0
  90. package/dist/directors/ComponentDirector.js +41 -8
  91. package/dist/factories/SceneBuilderFactory.d.ts +3 -0
  92. package/dist/factories/SceneBuilderFactory.js +6 -0
  93. package/dist/fonts/fontDiscovery.js +12 -1
  94. package/dist/layers/Layer.svelte.d.ts +54 -0
  95. package/dist/layers/Layer.svelte.js +2 -25
  96. package/dist/managers/AppManager.svelte.d.ts +12 -0
  97. package/dist/managers/AppManager.svelte.js +180 -8
  98. package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
  99. package/dist/managers/ComponentsManager.svelte.js +29 -15
  100. package/dist/managers/DeterministicMediaManager.d.ts +6 -0
  101. package/dist/managers/DeterministicMediaManager.js +57 -3
  102. package/dist/managers/EventManager.js +3 -1
  103. package/dist/managers/LayersManager.svelte.d.ts +54 -0
  104. package/dist/managers/RenderManager.d.ts +5 -0
  105. package/dist/managers/RenderManager.js +67 -21
  106. package/dist/managers/StateManager.svelte.d.ts +56 -0
  107. package/dist/managers/StateManager.svelte.js +60 -58
  108. package/dist/managers/SubtitlesManager.svelte.js +1 -3
  109. package/dist/managers/TimeManager.svelte.d.ts +4 -0
  110. package/dist/managers/TimeManager.svelte.js +23 -7
  111. package/dist/schemas/runtime/deterministic.d.ts +3 -0
  112. package/dist/schemas/runtime/deterministic.js +5 -1
  113. package/dist/schemas/runtime/types.d.ts +36 -15
  114. package/dist/schemas/scene/components.d.ts +126 -0
  115. package/dist/schemas/scene/components.js +11 -0
  116. package/dist/schemas/scene/core.d.ts +109 -10
  117. package/dist/schemas/scene/core.js +1 -1
  118. package/dist/utils/mediaWindow.d.ts +13 -0
  119. package/dist/utils/mediaWindow.js +25 -0
  120. package/dist/utils/svgGenerator.js +1 -1
  121. package/dist/utils/timelineWindow.d.ts +2 -0
  122. package/dist/utils/timelineWindow.js +7 -0
  123. package/dist/utils/utils.js +2 -4
  124. package/docs/AGENT_PATTERNS.md +243 -0
  125. package/docs/AGENT_WORKFLOW.md +364 -0
  126. package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
  127. package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
  128. package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
  129. package/docs/DETERMINISTIC_RENDERER.md +83 -0
  130. package/docs/NEXT_RELEASE.md +28 -0
  131. package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
  132. package/docs/examples/agent-cues.json +49 -0
  133. package/docs/examples/agent-transcript.json +23 -0
  134. package/docs/examples/agent-transcript.srt +7 -0
  135. package/docs/examples/agent-transcript.vtt +7 -0
  136. package/docs/examples/image-animation.json +87 -0
  137. package/docs/examples/native-text-metric-badge.json +109 -0
  138. package/package.json +133 -100
  139. package/skills/visualfries/SKILL.md +328 -0
@@ -9,10 +9,18 @@ export class AppManager {
9
9
  state;
10
10
  dom;
11
11
  forceCanvas; // ForceCanvas
12
+ serverRendererMode;
13
+ preferWebGL2;
14
+ powerPreference;
15
+ deterministicMediaManager;
12
16
  constructor(cradle) {
13
17
  this.state = cradle.stateManager;
14
18
  this.dom = cradle.domManager;
15
19
  this.forceCanvas = cradle.forceCanvas;
20
+ this.serverRendererMode = cradle.serverRendererMode ?? 'canvas';
21
+ this.preferWebGL2 = cradle.preferWebGL2 ?? true;
22
+ this.powerPreference = cradle.powerPreference ?? 'high-performance';
23
+ this.deterministicMediaManager = cradle.deterministicMediaManager;
16
24
  }
17
25
  get app() {
18
26
  return this.#app;
@@ -30,29 +38,193 @@ export class AppManager {
30
38
  const { width, height, environment, scale } = this.state;
31
39
  this.#destroyed = false;
32
40
  const canvas = this.dom.canvas;
33
- if (environment === 'server') {
34
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
41
+ let rendererSelection = this.#resolveRendererSelection(environment);
42
+ if (environment === 'server' && rendererSelection.forceCanvas) {
43
+ canvas.getContext('2d', { willReadFrequently: true });
35
44
  }
36
45
  const { PixiPlugin } = await registerGsapPlugins();
37
46
  // give the plugin a reference to the PIXI object
38
- PixiPlugin.registerPIXI(this.app);
39
- const options = {
47
+ PixiPlugin.registerPIXI(PIXI);
48
+ const sceneBackground = this.state.data.settings.backgroundColor;
49
+ const solidBackground = typeof sceneBackground === 'string' ? sceneBackground : 'transparent';
50
+ const baseOptions = {
40
51
  ...PIXI_DEFAULTS,
41
52
  preserveDrawingBuffer: environment === 'server',
42
53
  // clearBeforeRender: environment === 'server' ? false : true,
43
54
  width,
44
55
  height,
45
56
  view: canvas,
46
- forceCanvas: environment === 'server' || this.forceCanvas,
47
- backgroundColor: environment === 'server' ? 'transparent' : '#ffffff',
48
- backgroundAlpha: environment === 'server' ? 0 : 1
57
+ backgroundColor: solidBackground,
58
+ backgroundAlpha: solidBackground === 'transparent' ? 0 : 1
49
59
  };
50
- this.#app = new PIXI.Application(options);
60
+ let options = this.#buildRendererOptions(baseOptions, environment, rendererSelection);
61
+ try {
62
+ this.#app = new PIXI.Application(options);
63
+ }
64
+ catch (error) {
65
+ if (!rendererSelection.forceCanvas) {
66
+ const fallbackReason = this.#buildInitFallbackReason(error);
67
+ this.#warnRendererFallback(fallbackReason);
68
+ rendererSelection = {
69
+ requestedMode: rendererSelection.requestedMode,
70
+ selectedMode: 'canvas',
71
+ forceCanvas: true,
72
+ fallbackOccurred: true,
73
+ fallbackReason
74
+ };
75
+ canvas.getContext('2d', { willReadFrequently: true });
76
+ options = this.#buildRendererOptions(baseOptions, environment, rendererSelection);
77
+ this.#app = new PIXI.Application(options);
78
+ }
79
+ else {
80
+ throw error;
81
+ }
82
+ }
83
+ if (!rendererSelection.forceCanvas) {
84
+ const actualRenderer = this.#detectRendererType();
85
+ if (actualRenderer !== 'webgl') {
86
+ const fallbackReason = 'Renderer initialized as canvas despite serverRendererMode="webgl"';
87
+ this.#warnRendererFallback(fallbackReason);
88
+ rendererSelection = {
89
+ requestedMode: rendererSelection.requestedMode,
90
+ selectedMode: 'canvas',
91
+ forceCanvas: true,
92
+ fallbackOccurred: true,
93
+ fallbackReason
94
+ };
95
+ }
96
+ else {
97
+ rendererSelection = { ...rendererSelection, selectedMode: actualRenderer };
98
+ }
99
+ }
51
100
  if (scale !== 1) {
52
101
  this.scale(scale);
53
102
  }
54
103
  // Stop the default ticker as we'll use GSAP's ticker
55
104
  this.#app.ticker.stop();
105
+ this.deterministicMediaManager?.recordRendererSelection({
106
+ rendererType: rendererSelection.selectedMode,
107
+ fallbackOccurred: rendererSelection.fallbackOccurred,
108
+ fallbackReason: rendererSelection.fallbackReason
109
+ });
110
+ }
111
+ #buildRendererOptions(baseOptions, environment, selection) {
112
+ const options = {
113
+ ...baseOptions,
114
+ forceCanvas: selection.forceCanvas
115
+ };
116
+ if (environment === 'server' && selection.selectedMode === 'webgl' && !selection.forceCanvas) {
117
+ this.#configureWebGLEnvironmentPreference();
118
+ options.preference = 'webgl';
119
+ options.powerPreference = this.powerPreference;
120
+ }
121
+ return options;
122
+ }
123
+ #resolveRendererSelection(environment) {
124
+ if (environment !== 'server') {
125
+ return {
126
+ requestedMode: 'canvas',
127
+ selectedMode: this.forceCanvas ? 'canvas' : 'webgl',
128
+ forceCanvas: this.forceCanvas,
129
+ fallbackOccurred: false
130
+ };
131
+ }
132
+ const requestedMode = this.serverRendererMode;
133
+ if (this.forceCanvas) {
134
+ if (requestedMode === 'webgl') {
135
+ const fallbackReason = 'serverRendererMode="webgl" requested but forceCanvas=true override is enabled';
136
+ this.#warnRendererFallback(fallbackReason);
137
+ return {
138
+ requestedMode,
139
+ selectedMode: 'canvas',
140
+ forceCanvas: true,
141
+ fallbackOccurred: true,
142
+ fallbackReason
143
+ };
144
+ }
145
+ return {
146
+ requestedMode,
147
+ selectedMode: 'canvas',
148
+ forceCanvas: true,
149
+ fallbackOccurred: false
150
+ };
151
+ }
152
+ if (requestedMode === 'canvas') {
153
+ return {
154
+ requestedMode,
155
+ selectedMode: 'canvas',
156
+ forceCanvas: true,
157
+ fallbackOccurred: false
158
+ };
159
+ }
160
+ const support = this.#checkWebGLSupport();
161
+ if (!support.supported) {
162
+ this.#warnRendererFallback(support.reason);
163
+ return {
164
+ requestedMode,
165
+ selectedMode: 'canvas',
166
+ forceCanvas: true,
167
+ fallbackOccurred: true,
168
+ fallbackReason: support.reason
169
+ };
170
+ }
171
+ return {
172
+ requestedMode,
173
+ selectedMode: 'webgl',
174
+ forceCanvas: false,
175
+ fallbackOccurred: false
176
+ };
177
+ }
178
+ #checkWebGLSupport() {
179
+ try {
180
+ const probe = this.dom?.canvas ??
181
+ (typeof document !== 'undefined' ? document.createElement('canvas') : null);
182
+ if (!probe || typeof probe.getContext !== 'function') {
183
+ return { supported: false, reason: 'WebGL unavailable: no canvas available for probing' };
184
+ }
185
+ if (this.preferWebGL2 && probe.getContext('webgl2')) {
186
+ return { supported: true, reason: '' };
187
+ }
188
+ if (probe.getContext('webgl') || probe.getContext('experimental-webgl')) {
189
+ return { supported: true, reason: '' };
190
+ }
191
+ return { supported: false, reason: 'WebGL unavailable: context creation failed' };
192
+ }
193
+ catch {
194
+ return { supported: false, reason: 'WebGL unavailable: context probe failed' };
195
+ }
196
+ }
197
+ #configureWebGLEnvironmentPreference() {
198
+ const pixiAny = PIXI;
199
+ if (!pixiAny.settings || !pixiAny.ENV) {
200
+ return;
201
+ }
202
+ if (this.preferWebGL2 && pixiAny.ENV.WEBGL2 !== undefined) {
203
+ pixiAny.settings.PREFER_ENV = pixiAny.ENV.WEBGL2;
204
+ return;
205
+ }
206
+ if (pixiAny.ENV.WEBGL !== undefined) {
207
+ pixiAny.settings.PREFER_ENV = pixiAny.ENV.WEBGL;
208
+ }
209
+ }
210
+ #detectRendererType() {
211
+ if (!this.#app) {
212
+ return 'canvas';
213
+ }
214
+ const renderer = this.#app.renderer;
215
+ if (renderer?.gl || renderer?.context?.gl || renderer?.context?.webGLVersion) {
216
+ return 'webgl';
217
+ }
218
+ return 'canvas';
219
+ }
220
+ #buildInitFallbackReason(error) {
221
+ if (error instanceof Error && error.message) {
222
+ return `WebGL initialization failed: ${error.message}`;
223
+ }
224
+ return 'WebGL initialization failed with unknown error';
225
+ }
226
+ #warnRendererFallback(reason) {
227
+ console.warn(`[AppManager] Falling back to canvas renderer. ${reason}`);
56
228
  }
57
229
  async extractBase64(target, format = 'png', quality = 1) {
58
230
  if (!this.#app)
@@ -24,9 +24,9 @@ export declare class ComponentsManager implements ResourceManager<IComponent, Co
24
24
  private isVisible;
25
25
  getAll(): IComponent[];
26
26
  getMediaComponents(): (Component & MediaComponent)[];
27
- update(componentId: string, data: Partial<AppearanceInput>, refreshType?: ComponentRefreshType): Promise<void>;
27
+ update(componentId: string, data: Partial<AppearanceInput>, refreshType?: ComponentRefreshType): void;
28
28
  get(componentId: string): IComponent | undefined;
29
- delete(componentId: string): void;
29
+ delete(componentId: string): Promise<void>;
30
30
  create(componentData: ComponentData): Promise<IComponent | null>;
31
31
  setOrder(id: string, order: number): void;
32
32
  moveUp(id: string): void;
@@ -39,11 +39,11 @@ export declare class ComponentsManager implements ResourceManager<IComponent, Co
39
39
  id: string;
40
40
  data: Partial<AppearanceInput>;
41
41
  }[]): void;
42
- bulkDelete(ids: string[]): void;
43
- hide(id: string): Promise<void>;
44
- show(id: string): Promise<void>;
45
- toggle(id: string): Promise<void>;
42
+ bulkDelete(ids: string[]): Promise<void>;
43
+ hide(id: string): void;
44
+ show(id: string): void;
45
+ toggle(id: string): void;
46
46
  filter(predicate: (component: IComponent) => boolean): IComponent[];
47
47
  isComponentVisible(componentId: string): boolean;
48
- destroy(): void;
48
+ destroy(): Promise<void>;
49
49
  }
@@ -6,6 +6,7 @@ import { PixiComponentBuilder } from '../builders/PixiComponentBuilder.js';
6
6
  import { ComponentState } from '../builders/_ComponentState.svelte.js';
7
7
  import { asClass, asValue } from 'awilix/browser';
8
8
  import { debounce } from 'lodash-es';
9
+ import { isTimeWithinTimeline } from '../utils/timelineWindow.js';
9
10
  // import { builtTextComponentForSubtitle } from '../utils/subtitles.js';
10
11
  export class ComponentsManager {
11
12
  components = $state(new Map());
@@ -28,7 +29,7 @@ export class ComponentsManager {
28
29
  this.initializeEventListeners();
29
30
  }
30
31
  initializeEventListeners() {
31
- this.eventManager.on('subtitleschange', this.debouncedRefreshSubtitles.bind(this));
32
+ this.eventManager.on('subtitleschange', this.debouncedRefreshSubtitles);
32
33
  }
33
34
  #scheduleSubtitleRefresh() {
34
35
  // Schedule the refresh on the next animation frame to avoid blocking
@@ -43,7 +44,7 @@ export class ComponentsManager {
43
44
  const { currentTime, duration } = this.state;
44
45
  const startAt = component.props.timeline.startAt || 0;
45
46
  const endAt = component.props.timeline.endAt || duration;
46
- return currentTime >= startAt && currentTime <= endAt;
47
+ return isTimeWithinTimeline(currentTime, startAt, endAt);
47
48
  }
48
49
  getAll() {
49
50
  return Array.from(this.components.values());
@@ -57,10 +58,10 @@ export class ComponentsManager {
57
58
  component.refresh(refreshType);
58
59
  }
59
60
  }
60
- async update(componentId, data, refreshType = 'refresh') {
61
+ update(componentId, data, refreshType = 'refresh') {
61
62
  const component = this.get(componentId);
62
63
  if (component) {
63
- await component.props.updateAppearance(data);
64
+ component.props.updateAppearance(data);
64
65
  // Only manually refresh if auto-refresh is not enabled
65
66
  if (!component.autoRefresh) {
66
67
  this.#refreshComponent(component, refreshType);
@@ -70,7 +71,7 @@ export class ComponentsManager {
70
71
  get(componentId) {
71
72
  return this.components.get(componentId);
72
73
  }
73
- delete(componentId) {
74
+ async delete(componentId) {
74
75
  const component = this.components.get(componentId);
75
76
  if (component) {
76
77
  const layers = this.layersManager.getAll();
@@ -82,8 +83,8 @@ export class ComponentsManager {
82
83
  this.layersManager.delete(componentLayer.id);
83
84
  }
84
85
  }
85
- component.destroy();
86
86
  this.components.delete(componentId);
87
+ await component.destroy();
87
88
  }
88
89
  }
89
90
  async #buildComponent(componentData) {
@@ -236,25 +237,30 @@ export class ComponentsManager {
236
237
  bulkUpdate(updates) {
237
238
  updates.forEach(({ id, data }) => this.update(id, data));
238
239
  }
239
- bulkDelete(ids) {
240
- ids.forEach((id) => this.delete(id));
240
+ async bulkDelete(ids) {
241
+ const results = await Promise.allSettled(ids.map((id) => this.delete(id)));
242
+ const errors = results
243
+ .filter((result) => result.status === 'rejected')
244
+ .map((result) => result.reason);
245
+ if (errors.length > 0)
246
+ throw new AggregateError(errors, 'Component deletion failed.');
241
247
  }
242
- async hide(id) {
248
+ hide(id) {
243
249
  const component = this.get(id);
244
250
  if (component) {
245
- await component.props.setVisible(false);
251
+ component.props.setVisible(false);
246
252
  }
247
253
  }
248
- async show(id) {
254
+ show(id) {
249
255
  const component = this.get(id);
250
256
  if (component) {
251
- await component.props.setVisible(true);
257
+ component.props.setVisible(true);
252
258
  }
253
259
  }
254
- async toggle(id) {
260
+ toggle(id) {
255
261
  const component = this.get(id);
256
262
  if (component) {
257
- await component.props.setVisible(!component.props.visible);
263
+ component.props.setVisible(!component.props.visible);
258
264
  }
259
265
  }
260
266
  filter(predicate) {
@@ -267,8 +273,16 @@ export class ComponentsManager {
267
273
  }
268
274
  return this.isVisible(component);
269
275
  }
270
- destroy() {
276
+ async destroy() {
271
277
  this.eventManager.removeEventListener('subtitleschange', this.debouncedRefreshSubtitles);
272
278
  this.debouncedRefreshSubtitles.cancel(); // Cancel any pending debounced calls
279
+ const components = [...this.components.values()];
280
+ this.components.clear();
281
+ const results = await Promise.allSettled(components.map((component) => component.destroy()));
282
+ const errors = results
283
+ .filter((result) => result.status === 'rejected')
284
+ .map((result) => result.reason);
285
+ if (errors.length > 0)
286
+ throw new AggregateError(errors, 'Component teardown failed.');
273
287
  }
274
288
  }
@@ -16,6 +16,12 @@ export declare class DeterministicMediaManager {
16
16
  getFingerprint(): string;
17
17
  setOneTimeOverride(componentId: string, frameIndex: number, payload: DeterministicFramePayload): void;
18
18
  getDiagnosticsReport(): DeterministicDiagnosticsReport | null;
19
+ getSelectedRendererType(): 'canvas' | 'webgl';
20
+ recordRendererSelection(args: {
21
+ rendererType: 'canvas' | 'webgl';
22
+ fallbackOccurred?: boolean;
23
+ fallbackReason?: string;
24
+ }): void;
19
25
  recordReadyAttempt(sceneFrameIndex: number, count?: number): void;
20
26
  recordExtraRenderPass(sceneFrameIndex: number, count?: number): void;
21
27
  recordBlurRedraw(sceneFrameIndex: number, count?: number): void;
@@ -4,6 +4,9 @@ const createDefaultDiagnosticsState = () => ({
4
4
  providerHits: 0,
5
5
  providerMisses: 0,
6
6
  cacheHits: 0,
7
+ selectedRendererType: 'canvas',
8
+ rendererFallbackOccurred: false,
9
+ rendererFallbackReason: undefined,
7
10
  readyAttempts: 0,
8
11
  extraRenderPasses: 0,
9
12
  blurRedraws: 0,
@@ -13,6 +16,7 @@ const createDefaultDiagnosticsState = () => ({
13
16
  latencyMinMs: Number.POSITIVE_INFINITY,
14
17
  latencyMaxMs: 0
15
18
  });
19
+ const IMAGE_LOAD_TIMEOUT_MS = 5000;
16
20
  export class DeterministicMediaManager {
17
21
  config;
18
22
  #provider;
@@ -144,6 +148,9 @@ export class DeterministicMediaManager {
144
148
  providerMisses: this.#diagnostics.providerMisses,
145
149
  cacheHits: this.#diagnostics.cacheHits,
146
150
  cacheHitRatio,
151
+ selectedRendererType: this.#diagnostics.selectedRendererType,
152
+ rendererFallbackOccurred: this.#diagnostics.rendererFallbackOccurred,
153
+ rendererFallbackReason: this.#diagnostics.rendererFallbackReason,
147
154
  readyAttempts: this.#diagnostics.readyAttempts,
148
155
  extraRenderPasses: this.#diagnostics.extraRenderPasses,
149
156
  blurRedraws: this.#diagnostics.blurRedraws,
@@ -155,6 +162,19 @@ export class DeterministicMediaManager {
155
162
  }
156
163
  };
157
164
  }
165
+ getSelectedRendererType() {
166
+ return this.#diagnostics.selectedRendererType;
167
+ }
168
+ recordRendererSelection(args) {
169
+ try {
170
+ this.#diagnostics.selectedRendererType = args.rendererType;
171
+ this.#diagnostics.rendererFallbackOccurred = Boolean(args.fallbackOccurred);
172
+ this.#diagnostics.rendererFallbackReason = args.fallbackReason;
173
+ }
174
+ catch {
175
+ // Diagnostics are best-effort and never allowed to fail the render path.
176
+ }
177
+ }
158
178
  recordReadyAttempt(sceneFrameIndex, count = 1) {
159
179
  this.#recordRuntimeCounter(sceneFrameIndex, 'readyAttempts', count);
160
180
  }
@@ -300,10 +320,44 @@ export class DeterministicMediaManager {
300
320
  async #loadImage(url) {
301
321
  const img = new Image();
302
322
  img.crossOrigin = 'anonymous';
303
- img.src = url;
304
323
  await new Promise((resolve, reject) => {
305
- img.onload = () => resolve();
306
- img.onerror = () => reject(new Error(`Failed to load deterministic frame: ${url}`));
324
+ let settled = false;
325
+ const timeoutId = setTimeout(() => {
326
+ if (settled) {
327
+ return;
328
+ }
329
+ settled = true;
330
+ cleanup();
331
+ try {
332
+ img.src = '';
333
+ }
334
+ catch {
335
+ // Ignore cancellation failures.
336
+ }
337
+ reject(new Error(`Timed out loading deterministic frame after ${IMAGE_LOAD_TIMEOUT_MS}ms: ${url}`));
338
+ }, IMAGE_LOAD_TIMEOUT_MS);
339
+ const cleanup = () => {
340
+ clearTimeout(timeoutId);
341
+ img.onload = null;
342
+ img.onerror = null;
343
+ };
344
+ img.onload = () => {
345
+ if (settled) {
346
+ return;
347
+ }
348
+ settled = true;
349
+ cleanup();
350
+ resolve();
351
+ };
352
+ img.onerror = () => {
353
+ if (settled) {
354
+ return;
355
+ }
356
+ settled = true;
357
+ cleanup();
358
+ reject(new Error(`Failed to load deterministic frame: ${url}`));
359
+ };
360
+ img.src = url;
307
361
  });
308
362
  return img;
309
363
  }
@@ -17,6 +17,8 @@ export class EventManager extends EventTarget {
17
17
  }
18
18
  // "on" automatically returns the right type under event.details object
19
19
  on(event, callback, options) {
20
- this.addEventListener(event, ((e) => callback(e)), options);
20
+ // Register the original function identity so removeEventListener can reliably
21
+ // detach manager callbacks during scene teardown.
22
+ this.addEventListener(event, callback, options);
21
23
  }
22
24
  }
@@ -476,6 +476,12 @@ export declare class LayersManager implements ResourceManager<ILayer, SceneLayer
476
476
  unit: "px" | "em" | "rem" | "%";
477
477
  } | null | undefined;
478
478
  textTransform?: "none" | "uppercase" | "lowercase" | "capitalize" | undefined;
479
+ padding?: number | {
480
+ top: number;
481
+ right: number;
482
+ bottom: number;
483
+ left: number;
484
+ } | undefined;
479
485
  shadow?: {
480
486
  enabled?: boolean | undefined;
481
487
  preset?: string | undefined;
@@ -1095,6 +1101,12 @@ export declare class LayersManager implements ResourceManager<ILayer, SceneLayer
1095
1101
  unit: "px" | "em" | "rem" | "%";
1096
1102
  } | null | undefined;
1097
1103
  textTransform?: "none" | "uppercase" | "lowercase" | "capitalize" | undefined;
1104
+ padding?: number | {
1105
+ top: number;
1106
+ right: number;
1107
+ bottom: number;
1108
+ left: number;
1109
+ } | undefined;
1098
1110
  shadow?: {
1099
1111
  enabled?: boolean | undefined;
1100
1112
  preset?: string | undefined;
@@ -1688,6 +1700,12 @@ export declare class LayersManager implements ResourceManager<ILayer, SceneLayer
1688
1700
  unit: "px" | "em" | "rem" | "%";
1689
1701
  } | null | undefined;
1690
1702
  textTransform?: "none" | "uppercase" | "lowercase" | "capitalize" | undefined;
1703
+ padding?: number | {
1704
+ top: number;
1705
+ right: number;
1706
+ bottom: number;
1707
+ left: number;
1708
+ } | undefined;
1691
1709
  shadow?: {
1692
1710
  enabled?: boolean | undefined;
1693
1711
  preset?: string | undefined;
@@ -2279,6 +2297,12 @@ export declare class LayersManager implements ResourceManager<ILayer, SceneLayer
2279
2297
  unit: "px" | "em" | "rem" | "%";
2280
2298
  } | null | undefined;
2281
2299
  textTransform?: "none" | "uppercase" | "lowercase" | "capitalize" | undefined;
2300
+ padding?: number | {
2301
+ top: number;
2302
+ right: number;
2303
+ bottom: number;
2304
+ left: number;
2305
+ } | undefined;
2282
2306
  shadow?: {
2283
2307
  enabled?: boolean | undefined;
2284
2308
  preset?: string | undefined;
@@ -2904,6 +2928,12 @@ export declare class LayersManager implements ResourceManager<ILayer, SceneLayer
2904
2928
  unit: "px" | "em" | "rem" | "%";
2905
2929
  } | null | undefined;
2906
2930
  textTransform?: "none" | "uppercase" | "lowercase" | "capitalize" | undefined;
2931
+ padding?: number | {
2932
+ top: number;
2933
+ right: number;
2934
+ bottom: number;
2935
+ left: number;
2936
+ } | undefined;
2907
2937
  shadow?: {
2908
2938
  enabled?: boolean | undefined;
2909
2939
  preset?: string | undefined;
@@ -3499,6 +3529,12 @@ export declare class LayersManager implements ResourceManager<ILayer, SceneLayer
3499
3529
  unit: "px" | "em" | "rem" | "%";
3500
3530
  } | null | undefined;
3501
3531
  textTransform?: "none" | "uppercase" | "lowercase" | "capitalize" | undefined;
3532
+ padding?: number | {
3533
+ top: number;
3534
+ right: number;
3535
+ bottom: number;
3536
+ left: number;
3537
+ } | undefined;
3502
3538
  shadow?: {
3503
3539
  enabled?: boolean | undefined;
3504
3540
  preset?: string | undefined;
@@ -4045,6 +4081,12 @@ export declare class LayersManager implements ResourceManager<ILayer, SceneLayer
4045
4081
  unit: "px" | "em" | "rem" | "%";
4046
4082
  } | null | undefined;
4047
4083
  textTransform?: "none" | "uppercase" | "lowercase" | "capitalize" | undefined;
4084
+ padding?: number | {
4085
+ top: number;
4086
+ right: number;
4087
+ bottom: number;
4088
+ left: number;
4089
+ } | undefined;
4048
4090
  shadow?: {
4049
4091
  enabled?: boolean | undefined;
4050
4092
  preset?: string | undefined;
@@ -4596,6 +4638,12 @@ export declare class LayersManager implements ResourceManager<ILayer, SceneLayer
4596
4638
  unit: "px" | "em" | "rem" | "%";
4597
4639
  } | null | undefined;
4598
4640
  textTransform?: "none" | "uppercase" | "lowercase" | "capitalize" | undefined;
4641
+ padding?: number | {
4642
+ top: number;
4643
+ right: number;
4644
+ bottom: number;
4645
+ left: number;
4646
+ } | undefined;
4599
4647
  shadow?: {
4600
4648
  enabled?: boolean | undefined;
4601
4649
  preset?: string | undefined;
@@ -5140,6 +5188,12 @@ export declare class LayersManager implements ResourceManager<ILayer, SceneLayer
5140
5188
  unit: "px" | "em" | "rem" | "%";
5141
5189
  } | null | undefined;
5142
5190
  textTransform?: "none" | "uppercase" | "lowercase" | "capitalize" | undefined;
5191
+ padding?: number | {
5192
+ top: number;
5193
+ right: number;
5194
+ bottom: number;
5195
+ left: number;
5196
+ } | undefined;
5143
5197
  shadow?: {
5144
5198
  enabled?: boolean | undefined;
5145
5199
  preset?: string | undefined;
@@ -13,6 +13,11 @@ export declare class RenderManager {
13
13
  private layersManager;
14
14
  private lastActiveById;
15
15
  private lastRenderTime;
16
+ private renderInFlight;
17
+ private rerenderRequested;
18
+ private readonly boundHandleBeforeRender;
19
+ private readonly boundRender;
20
+ private readonly boundHandleChangeState;
16
21
  constructor(cradle: {
17
22
  stateManager: StateManager;
18
23
  componentsManager: ResourceManager<IComponent, ComponentData, AppearanceInput>;