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
@@ -1,107 +1,147 @@
1
- import { z } from 'zod';
2
1
  import { StateManager } from '../../managers/StateManager.svelte.js';
3
2
  export class MediaSeekingHook {
4
- types = ['setup', 'destroy', 'refresh', 'update'];
3
+ types = ['setup', 'destroy', 'refresh:content', 'update'];
5
4
  priority = 1;
6
5
  #context;
7
6
  #mediaElement;
7
+ #detachMediaEventHandlers;
8
8
  state;
9
9
  constructor(cradle) {
10
10
  this.state = cradle.stateManager;
11
11
  }
12
+ #attachMediaEventHandlers(media) {
13
+ const seekStatus = {
14
+ start: null,
15
+ end: null,
16
+ isSeeking: false
17
+ };
18
+ let canPlayTime = null;
19
+ let fullyReady = false;
20
+ let needCheckState = false;
21
+ let detached = false;
22
+ const checkReadyState = async () => {
23
+ if (detached || !needCheckState) {
24
+ return;
25
+ }
26
+ if (media.readyState < 3) {
27
+ await new Promise((resolve) => setTimeout(resolve, 500));
28
+ if (detached) {
29
+ return;
30
+ }
31
+ needCheckState = true;
32
+ return checkReadyState();
33
+ }
34
+ this.state.removeLoadingComponent(this.#context.contextData.id);
35
+ needCheckState = false;
36
+ if (media.readyState === 4) {
37
+ fullyReady = true;
38
+ }
39
+ };
40
+ const onseeking = () => {
41
+ if (detached)
42
+ return;
43
+ const mediaTime = parseFloat(media.currentTime.toFixed(3));
44
+ if (!seekStatus.isSeeking && seekStatus.start != mediaTime) {
45
+ seekStatus.start = mediaTime;
46
+ seekStatus.end = null;
47
+ seekStatus.isSeeking = true;
48
+ this.state.addLoadingComponent(this.#context.contextData.id, 'seeking');
49
+ needCheckState = true;
50
+ checkReadyState();
51
+ }
52
+ };
53
+ const onseeked = () => {
54
+ if (detached)
55
+ return;
56
+ const mediaTime = parseFloat(media.currentTime.toFixed(3));
57
+ if (seekStatus.isSeeking) {
58
+ seekStatus.end = mediaTime;
59
+ seekStatus.isSeeking = false;
60
+ if (media.readyState === 4) {
61
+ fullyReady = true;
62
+ }
63
+ needCheckState = false;
64
+ }
65
+ };
66
+ const onwaiting = () => {
67
+ if (detached)
68
+ return;
69
+ if (!fullyReady) {
70
+ this.state.addLoadingComponent(this.#context.contextData.id, 'waiting');
71
+ needCheckState = true;
72
+ checkReadyState();
73
+ }
74
+ };
75
+ const oncanplay = () => {
76
+ if (detached)
77
+ return;
78
+ const mediaTime = parseFloat(media.currentTime.toFixed(3));
79
+ if (canPlayTime != mediaTime) {
80
+ this.state.removeLoadingComponent(this.#context.contextData.id);
81
+ canPlayTime = mediaTime;
82
+ needCheckState = false;
83
+ }
84
+ if (media.readyState === 4) {
85
+ fullyReady = true;
86
+ }
87
+ };
88
+ const onerror = () => {
89
+ if (detached)
90
+ return;
91
+ if (media.error && media.error.code !== 4) {
92
+ console.error('Media error:', media.src, media.error);
93
+ }
94
+ };
95
+ media.onseeking = onseeking;
96
+ media.onseeked = onseeked;
97
+ media.onwaiting = onwaiting;
98
+ media.oncanplay = oncanplay;
99
+ media.onerror = onerror;
100
+ return () => {
101
+ detached = true;
102
+ needCheckState = false;
103
+ if (media.onseeking === onseeking) {
104
+ media.onseeking = null;
105
+ }
106
+ if (media.onseeked === onseeked) {
107
+ media.onseeked = null;
108
+ }
109
+ if (media.onwaiting === onwaiting) {
110
+ media.onwaiting = null;
111
+ }
112
+ if (media.oncanplay === oncanplay) {
113
+ media.oncanplay = null;
114
+ }
115
+ if (media.onerror === onerror) {
116
+ media.onerror = null;
117
+ }
118
+ };
119
+ }
120
+ #clearMediaEventHandlers() {
121
+ this.#detachMediaEventHandlers?.();
122
+ this.#detachMediaEventHandlers = undefined;
123
+ this.state.removeLoadingComponent(this.#context.contextData.id);
124
+ }
12
125
  async #handleSetup() {
13
- if (this.#mediaElement) {
14
- return;
15
- }
16
126
  if (this.#context.contextData.type !== 'VIDEO' && this.#context.contextData.type !== 'AUDIO')
17
127
  return;
18
128
  const mediaType = this.#context.type === 'VIDEO' ? 'video' : 'audio';
19
129
  const media = this.#context.getResource(mediaType === 'video' ? 'videoElement' : 'audioElement');
20
130
  if (!media) {
21
- console.error('MediaSeekingHook: No media element found');
22
131
  return;
23
132
  }
24
- this.#mediaElement = media;
25
- if (media && this.#context.isActive) {
26
- const seekStatus = {
27
- start: null,
28
- end: null,
29
- isSeeking: false
30
- };
31
- let canPlayTime = null;
32
- let fullyReady = false;
33
- let needCheckState = false;
34
- const checkReadyState = async () => {
35
- if (!needCheckState) {
36
- return;
37
- }
38
- if (media.readyState < 3) {
39
- await new Promise((resolve) => setTimeout(resolve, 500));
40
- needCheckState = true;
41
- return checkReadyState();
42
- }
43
- this.state.removeLoadingComponent(this.#context.contextData.id);
44
- needCheckState = false;
45
- if (media.readyState === 4) {
46
- fullyReady = true;
47
- }
48
- };
49
- media.onseeking = () => {
50
- const mediaTime = parseFloat(media.currentTime.toFixed(1));
51
- if (!seekStatus.isSeeking && seekStatus.start != mediaTime) {
52
- seekStatus.start = mediaTime;
53
- seekStatus.end = null;
54
- seekStatus.isSeeking = true;
55
- this.state.addLoadingComponent(this.#context.contextData.id, 'seeking');
56
- needCheckState = true;
57
- checkReadyState();
58
- }
59
- };
60
- media.onseeked = () => {
61
- const mediaTime = parseFloat(media.currentTime.toFixed(1));
62
- if (seekStatus.isSeeking) {
63
- seekStatus.end = mediaTime;
64
- seekStatus.isSeeking = false;
65
- if (media.readyState === 4) {
66
- fullyReady = true;
67
- }
68
- needCheckState = false;
69
- }
70
- };
71
- // Loading states
72
- media.onwaiting = () => {
73
- if (!fullyReady) {
74
- this.state.addLoadingComponent(this.#context.contextData.id, 'waiting');
75
- needCheckState = true;
76
- checkReadyState();
77
- }
78
- };
79
- media.oncanplay = () => {
80
- const mediaTime = parseFloat(media.currentTime.toFixed(1));
81
- if (canPlayTime != mediaTime) {
82
- this.state.removeLoadingComponent(this.#context.contextData.id);
83
- canPlayTime = mediaTime;
84
- needCheckState = false;
85
- }
86
- if (media.readyState === 4) {
87
- fullyReady = true;
88
- }
89
- // Don't try to play here - let MediaHook handle play/pause logic
90
- // This prevents conflicts and race conditions, especially on Safari
91
- };
92
- // Add error event handling
93
- media.onerror = () => {
94
- if (media.error && media.error.code !== 4) {
95
- console.error('Media error:', media.src, media.error);
96
- }
97
- };
133
+ if (this.#mediaElement === media) {
134
+ return;
98
135
  }
136
+ this.#clearMediaEventHandlers();
137
+ this.#mediaElement = media;
99
138
  }
100
139
  async #handleRefresh() {
101
140
  await this.#handleDestroy();
102
141
  await this.#handleSetup();
103
142
  }
104
143
  async #handleDestroy() {
144
+ this.#clearMediaEventHandlers();
105
145
  // Clear media element reference - MediaHook will handle releaseMediaElement
106
146
  this.#mediaElement = undefined;
107
147
  }
@@ -109,15 +149,25 @@ export class MediaSeekingHook {
109
149
  // Only handle video components as requested
110
150
  if (this.#context.contextData.type !== 'VIDEO')
111
151
  return;
112
- // Get the HTMLVideoElement from resources or cached reference
113
- let media = this.#mediaElement;
114
- if (!media) {
115
- const res = this.#context.getResource('videoElement');
116
- media = res;
117
- this.#mediaElement = media; // cache for later
152
+ const resource = this.#context.getResource('videoElement');
153
+ if (!resource) {
154
+ this.#clearMediaEventHandlers();
155
+ this.#mediaElement = undefined;
156
+ return;
157
+ }
158
+ if (resource !== this.#mediaElement) {
159
+ await this.#handleSetup();
118
160
  }
161
+ const media = this.#mediaElement;
119
162
  if (!media)
120
163
  return;
164
+ if (!this.#context.isActive) {
165
+ this.#clearMediaEventHandlers();
166
+ return;
167
+ }
168
+ if (!this.#detachMediaEventHandlers) {
169
+ this.#detachMediaEventHandlers = this.#attachMediaEventHandlers(media);
170
+ }
121
171
  const fps = this.state.data.settings.fps || 30;
122
172
  const targetTime = this.#context.currentComponentTime;
123
173
  const targetFrame = Math.round(targetTime * fps);
@@ -140,7 +190,7 @@ export class MediaSeekingHook {
140
190
  else {
141
191
  media.currentTime = seekTo;
142
192
  }
143
- // Await seek completion using a robust multi-attempt strategy similar to Remotion
193
+ // Await seek completion using a robust multi-attempt strategy.
144
194
  await new Promise((resolve) => {
145
195
  const anyMedia = media;
146
196
  let attempts = 0;
@@ -197,7 +247,7 @@ export class MediaSeekingHook {
197
247
  else if (type === 'destroy') {
198
248
  return await this.#handleDestroy();
199
249
  }
200
- else if (type === 'refresh') {
250
+ else if (type === 'refresh:content') {
201
251
  return await this.#handleRefresh();
202
252
  }
203
253
  else if (type === 'update') {
@@ -33,6 +33,8 @@ export class PixiGifHook {
33
33
  }
34
34
  if (this.#imageElement) {
35
35
  const { appearance } = this.componentElement;
36
+ this.#imageElement.loop = this.componentElement.playback?.loop ?? true;
37
+ this.#imageElement.animationSpeed = this.componentElement.playback?.speed ?? 1;
36
38
  setPlacementAndOpacity(this.#imageElement, appearance);
37
39
  this.#displayObject.addChild(this.#imageElement);
38
40
  this.#context.setResource('pixiRenderObject', this.#displayObject);
@@ -61,10 +63,8 @@ export class PixiGifHook {
61
63
  }
62
64
  else {
63
65
  this.#imageElement.stop();
64
- const gifFrame = this.#imageElement.totalFrames > 0
65
- ? this.state.currentFrame % this.#imageElement.totalFrames
66
- : 0;
67
- this.#imageElement.currentFrame = gifFrame;
66
+ const speed = this.componentElement.playback?.speed ?? 1;
67
+ this.#imageElement.seek(Math.max(0, this.#context.currentComponentTime) * 1000 * speed);
68
68
  }
69
69
  const isActive = this.#context.isActive;
70
70
  if (this.#displayObject) {
@@ -3,6 +3,7 @@ import { ImageComponentShape, VideoComponentShape } from '../..';
3
3
  import { z } from 'zod';
4
4
  import type { StateManager } from '../../managers/StateManager.svelte.ts';
5
5
  import type { DeterministicMediaManager } from '../../managers/DeterministicMediaManager.ts';
6
+ import type { AppManager } from '../../managers/AppManager.svelte.ts';
6
7
  export declare class PixiSplitScreenDisplayObjectHook implements IComponentHook {
7
8
  #private;
8
9
  types: HookType[];
@@ -10,9 +11,11 @@ export declare class PixiSplitScreenDisplayObjectHook implements IComponentHook
10
11
  componentElement: z.infer<typeof VideoComponentShape> | z.infer<typeof ImageComponentShape>;
11
12
  private sceneState;
12
13
  private deterministicMediaManager?;
14
+ private appManager?;
13
15
  constructor(cradle: {
14
16
  stateManager: StateManager;
15
17
  deterministicMediaManager?: DeterministicMediaManager;
18
+ appManager?: AppManager;
16
19
  });
17
20
  get sceneWidth(): number;
18
21
  get sceneHeight(): number;
@@ -15,6 +15,7 @@ export class PixiSplitScreenDisplayObjectHook {
15
15
  #context;
16
16
  #pixiTexture;
17
17
  #displayObject;
18
+ #mainSprite = undefined;
18
19
  #bgCanvas = undefined;
19
20
  #bgSprite = undefined;
20
21
  #blurStrength = 50;
@@ -25,9 +26,11 @@ export class PixiSplitScreenDisplayObjectHook {
25
26
  componentElement;
26
27
  sceneState;
27
28
  deterministicMediaManager;
29
+ appManager;
28
30
  constructor(cradle) {
29
31
  this.sceneState = cradle.stateManager;
30
32
  this.deterministicMediaManager = cradle.deterministicMediaManager;
33
+ this.appManager = cradle.appManager;
31
34
  }
32
35
  get sceneWidth() {
33
36
  return this.#context.sceneState.width;
@@ -70,7 +73,7 @@ export class PixiSplitScreenDisplayObjectHook {
70
73
  // Center the background sprite
71
74
  backgroundSprite.x = (this.sceneWidth - backgroundSprite.width) / 2;
72
75
  backgroundSprite.y = (this.sceneHeight - backgroundSprite.height) / 2;
73
- if (this.sceneState.environment === 'server') {
76
+ if (this.sceneState.environment === 'server' && !this.#isWebGLRendererActive()) {
74
77
  // Create a temporary canvas for blur effect
75
78
  const bgCanvas = document.createElement('canvas');
76
79
  bgCanvas.width = Math.max(1, Math.round(backgroundSprite.width * this.#blurDownscale));
@@ -81,11 +84,19 @@ export class PixiSplitScreenDisplayObjectHook {
81
84
  backgroundSprite.texture = blurredTexture;
82
85
  }
83
86
  else {
84
- // Create new PIXI texture from blurred canvas
87
+ // Client mode and server webgl mode both use PIXI blur filter.
88
+ this.#bgCanvas = undefined;
85
89
  const blurFilter = new PIXI.BlurFilter(sanitizedStrength, 50, 1, 7);
86
90
  backgroundSprite.filters = [blurFilter];
87
91
  }
88
92
  }
93
+ #isWebGLRendererActive() {
94
+ const renderer = this.appManager?.app?.renderer;
95
+ if (renderer?.gl || renderer?.context?.gl || renderer?.context?.webGLVersion) {
96
+ return true;
97
+ }
98
+ return this.deterministicMediaManager?.getSelectedRendererType?.() === 'webgl';
99
+ }
89
100
  #drawBlurredBackground(strength = 50, force = false) {
90
101
  if (!this.#bgCanvas) {
91
102
  return false;
@@ -198,6 +209,7 @@ export class PixiSplitScreenDisplayObjectHook {
198
209
  mainSprite.height = appearance.height;
199
210
  mainSprite.x = appearance.x;
200
211
  mainSprite.y = appearance.y;
212
+ this.#mainSprite = mainSprite;
201
213
  this.#displayObject.addChild(mainSprite);
202
214
  }
203
215
  #initDisplayObject() {
@@ -240,18 +252,83 @@ export class PixiSplitScreenDisplayObjectHook {
240
252
  walk(this.#displayObject);
241
253
  this.#pixiTexture = nextTexture;
242
254
  }
255
+ #isTextureValid(texture) {
256
+ if (!texture)
257
+ return false;
258
+ // PIXI textures expose valid/baseTexture after destroy
259
+ const tex = texture;
260
+ if (tex.valid === false)
261
+ return false;
262
+ if (tex.baseTexture?.valid === false || tex.baseTexture?.destroyed === true)
263
+ return false;
264
+ return true;
265
+ }
266
+ #needsRebuild() {
267
+ // No display object yet — needs initial build (handled by creation path)
268
+ if (!this.#displayObject)
269
+ return false;
270
+ // Tracked texture was destroyed or became invalid
271
+ if (!this.#isTextureValid(this.#pixiTexture))
272
+ return true;
273
+ // Display object lost its children (shouldn't happen, but defensive)
274
+ if (this.#displayObject.children.length === 0)
275
+ return true;
276
+ return false;
277
+ }
278
+ #destroyChildren() {
279
+ const removed = this.#displayObject.removeChildren();
280
+ if (Array.isArray(removed)) {
281
+ for (const child of removed) {
282
+ if (typeof child.destroy === 'function') {
283
+ child.destroy({ children: true });
284
+ }
285
+ }
286
+ }
287
+ }
288
+ #rebuild(texture) {
289
+ this.#mainSprite = undefined;
290
+ this.#bgCanvas = undefined;
291
+ this.#bgSprite = undefined;
292
+ this.#lastBlurFrameKey = '';
293
+ this.#pixiTexture = texture;
294
+ this.#destroyChildren();
295
+ this.#initDisplayObject();
296
+ }
243
297
  async #handleUpdate() {
244
298
  const isActive = this.#context.isActive;
245
299
  if (this.#displayObject) {
246
- // Texture swaps are frequent in deterministic mode; update sprite textures
247
- // in-place instead of rebuilding split/blur geometry each frame.
248
300
  const currentTexture = this.#context.getResource('pixiTexture');
249
- if (currentTexture && currentTexture !== this.#pixiTexture) {
301
+ // Auto-heal: if the tracked texture is destroyed or children are missing,
302
+ // rebuild the display object from the current valid texture.
303
+ if (this.#needsRebuild()) {
304
+ if (currentTexture && this.#isTextureValid(currentTexture)) {
305
+ this.#rebuild(currentTexture);
306
+ }
307
+ else {
308
+ // No valid texture available — hide and wait for next tick
309
+ this.#displayObject.visible = false;
310
+ this.#context.setResource('pixiRenderObject', this.#displayObject);
311
+ return;
312
+ }
313
+ }
314
+ else if (currentTexture &&
315
+ currentTexture !== this.#pixiTexture &&
316
+ this.#isTextureValid(currentTexture)) {
317
+ // Texture swaps are frequent in deterministic mode; update sprite textures
318
+ // in-place instead of rebuilding split/blur geometry each frame.
250
319
  this.#swapDisplayTexture(currentTexture);
251
320
  }
252
321
  if (isActive && this.#bgCanvas) {
253
322
  this.#drawBlurredBackground(this.#blurStrength);
254
323
  }
324
+ // Update main sprite position/size from appearance data
325
+ if (this.#mainSprite) {
326
+ const appearance = this.#context.data.appearance;
327
+ this.#mainSprite.x = appearance.x;
328
+ this.#mainSprite.y = appearance.y;
329
+ this.#mainSprite.width = appearance.width;
330
+ this.#mainSprite.height = appearance.height;
331
+ }
255
332
  // Always re-assert the resource in case the context was cleared or updated
256
333
  this.#context.setResource('pixiRenderObject', this.#displayObject);
257
334
  if (this.#displayObject.visible != isActive) {
@@ -280,7 +357,7 @@ export class PixiSplitScreenDisplayObjectHook {
280
357
  await this.#handleDestroy();
281
358
  this.#pixiTexture = currentTexture;
282
359
  if (this.#displayObject) {
283
- this.#displayObject.removeChildren();
360
+ this.#destroyChildren();
284
361
  this.#initDisplayObject();
285
362
  }
286
363
  }
@@ -288,7 +365,7 @@ export class PixiSplitScreenDisplayObjectHook {
288
365
  // Same texture - just update sprite properties (position, size, etc.)
289
366
  // For split screen, we may need to rebuild if effects changed
290
367
  // For now, trigger a full rebuild on refresh
291
- this.#displayObject.removeChildren();
368
+ this.#destroyChildren();
292
369
  if (currentTexture) {
293
370
  this.#pixiTexture = currentTexture;
294
371
  this.#initDisplayObject();
@@ -297,7 +374,7 @@ export class PixiSplitScreenDisplayObjectHook {
297
374
  // If no texture yet, #handleUpdate will handle initial creation
298
375
  }
299
376
  async #handleDestroy() {
300
- // remove event listeners from video
377
+ this.#mainSprite = undefined;
301
378
  this.#bgCanvas = undefined;
302
379
  this.#bgSprite = undefined;
303
380
  this.#lastBlurFrameKey = '';
@@ -32,6 +32,12 @@ export class PixiVideoTextureHook {
32
32
  return;
33
33
  }
34
34
  const media = this.#context.getResource('videoElement');
35
+ if (!media) {
36
+ if (this.#videoTexture || this.#videoElement) {
37
+ await this.#handleDestroy();
38
+ }
39
+ return;
40
+ }
35
41
  // If element changed or texture is missing, recreate it
36
42
  if (media && (media !== this.#videoElement || !this.#videoTexture)) {
37
43
  await this.#handleDestroy();
@@ -53,11 +59,11 @@ export class PixiVideoTextureHook {
53
59
  if (this.#videoTexture) {
54
60
  // Destroy the texture and its base texture to free GPU memory
55
61
  this.#videoTexture.destroy(true);
56
- this.#videoTexture = undefined;
57
- this.#videoElement = undefined;
58
- this.#pixiResource = undefined;
59
- this.#context.removeResource('pixiTexture');
60
62
  }
63
+ this.#videoTexture = undefined;
64
+ this.#videoElement = undefined;
65
+ this.#pixiResource = undefined;
66
+ this.#context.removeResource('pixiTexture');
61
67
  }
62
68
  async #handleRefreshContent() {
63
69
  // Only recreate texture when video source changes
@@ -0,0 +1,16 @@
1
+ import type { IComponentContext, IComponentHook, HookType } from '../..';
2
+ import type { StateManager } from '../../managers/StateManager.svelte.js';
3
+ /**
4
+ * Publishes one stable outer Pixi container as the visual animation owner.
5
+ * Renderer hooks remain responsible for their inner geometry; animation x/y
6
+ * are component-relative translation offsets around the fixed center pivot.
7
+ */
8
+ export declare class PixiVisualTransformHook implements IComponentHook {
9
+ #private;
10
+ types: HookType[];
11
+ priority: number;
12
+ constructor(cradle: {
13
+ stateManager: StateManager;
14
+ });
15
+ handle(type: HookType, context: IComponentContext): Promise<void>;
16
+ }
@@ -0,0 +1,102 @@
1
+ import * as PIXI from 'pixi.js-legacy';
2
+ import { createPixiAnimationTarget } from '../../animations/PixiAnimationTarget.js';
3
+ /**
4
+ * Publishes one stable outer Pixi container as the visual animation owner.
5
+ * Renderer hooks remain responsible for their inner geometry; animation x/y
6
+ * are component-relative translation offsets around the fixed center pivot.
7
+ */
8
+ export class PixiVisualTransformHook {
9
+ types = ['setup', 'update', 'refresh', 'refresh:content', 'destroy'];
10
+ priority = 1;
11
+ #context;
12
+ #outer;
13
+ #content;
14
+ #target;
15
+ #origin;
16
+ #state;
17
+ #handlers = {
18
+ setup: this.#publish.bind(this),
19
+ update: this.#publish.bind(this),
20
+ refresh: this.#publish.bind(this),
21
+ 'refresh:content': this.#publish.bind(this),
22
+ destroy: this.#destroy.bind(this)
23
+ };
24
+ constructor(cradle) {
25
+ this.#state = cradle.stateManager;
26
+ }
27
+ #ensureOuter() {
28
+ if (this.#outer)
29
+ return;
30
+ const appearance = this.#context.data.appearance;
31
+ const centerX = appearance.x + appearance.width / 2;
32
+ const centerY = appearance.y + appearance.height / 2;
33
+ this.#outer = new PIXI.Container();
34
+ this.#outer.pivot.set(centerX, centerY);
35
+ this.#outer.position.set(centerX, centerY);
36
+ this.#origin = { x: centerX, y: centerY };
37
+ this.#target = createPixiAnimationTarget(this.#outer, () => this.#state.markDirty(), this.#origin);
38
+ }
39
+ #syncPlacement() {
40
+ if (!this.#outer || !this.#target || !this.#origin)
41
+ return;
42
+ const appearance = this.#context.data.appearance;
43
+ const centerX = appearance.x + appearance.width / 2;
44
+ const centerY = appearance.y + appearance.height / 2;
45
+ if (centerX === this.#origin.x && centerY === this.#origin.y)
46
+ return;
47
+ const offsetX = this.#target.x;
48
+ const offsetY = this.#target.y;
49
+ this.#origin.x = centerX;
50
+ this.#origin.y = centerY;
51
+ this.#outer.pivot.set(centerX, centerY);
52
+ this.#outer.position.set(centerX + offsetX, centerY + offsetY);
53
+ this.#state.markDirty();
54
+ }
55
+ async #publish() {
56
+ const renderObject = this.#context.getResource('pixiRenderObject');
57
+ if (!renderObject && !this.#outer)
58
+ return;
59
+ this.#ensureOuter();
60
+ if (!this.#outer || !this.#target)
61
+ return;
62
+ this.#syncPlacement();
63
+ if (renderObject && renderObject !== this.#outer && renderObject !== this.#content) {
64
+ if (this.#content?.parent === this.#outer)
65
+ this.#outer.removeChild(this.#content);
66
+ if (renderObject.parent)
67
+ renderObject.parent.removeChild(renderObject);
68
+ this.#outer.addChild(renderObject);
69
+ this.#content = renderObject;
70
+ this.#state.markDirty();
71
+ }
72
+ const visible = this.#context.isActive && this.#context.data.visible !== false;
73
+ if (this.#outer.visible !== visible) {
74
+ this.#outer.visible = visible;
75
+ this.#state.markDirty();
76
+ }
77
+ this.#context.setResource('pixiRenderObject', this.#outer);
78
+ this.#context.setResource('animationTarget', this.#target);
79
+ }
80
+ async #destroy() {
81
+ this.#context.removeResource('animationTarget');
82
+ if (this.#outer) {
83
+ if (this.#outer.parent)
84
+ this.#outer.parent.removeChild(this.#outer);
85
+ this.#outer.removeChildren();
86
+ this.#outer.destroy({ children: false });
87
+ if (this.#context.getResource('pixiRenderObject') === this.#outer) {
88
+ this.#context.removeResource('pixiRenderObject');
89
+ }
90
+ }
91
+ this.#content = undefined;
92
+ this.#target = undefined;
93
+ this.#origin = undefined;
94
+ this.#outer = undefined;
95
+ }
96
+ async handle(type, context) {
97
+ this.#context = context;
98
+ const handler = this.#handlers[type];
99
+ if (handler)
100
+ await handler();
101
+ }
102
+ }