visualfries 0.1.10120 → 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 (133) hide show
  1. package/README.md +97 -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.js +73 -6
  83. package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
  84. package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
  85. package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
  86. package/dist/components/hooks/SubtitlesHook.js +48 -24
  87. package/dist/components/hooks/VerifyMediaHook.js +2 -2
  88. package/dist/directors/ComponentDirector.d.ts +1 -0
  89. package/dist/directors/ComponentDirector.js +41 -8
  90. package/dist/fonts/fontDiscovery.js +12 -1
  91. package/dist/layers/Layer.svelte.d.ts +54 -0
  92. package/dist/layers/Layer.svelte.js +2 -25
  93. package/dist/managers/AppManager.svelte.js +8 -5
  94. package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
  95. package/dist/managers/ComponentsManager.svelte.js +29 -15
  96. package/dist/managers/DeterministicMediaManager.js +38 -3
  97. package/dist/managers/EventManager.js +3 -1
  98. package/dist/managers/LayersManager.svelte.d.ts +54 -0
  99. package/dist/managers/RenderManager.d.ts +5 -0
  100. package/dist/managers/RenderManager.js +67 -21
  101. package/dist/managers/StateManager.svelte.d.ts +56 -0
  102. package/dist/managers/StateManager.svelte.js +60 -58
  103. package/dist/managers/SubtitlesManager.svelte.js +1 -3
  104. package/dist/managers/TimeManager.svelte.d.ts +4 -0
  105. package/dist/managers/TimeManager.svelte.js +23 -7
  106. package/dist/schemas/runtime/deterministic.js +5 -1
  107. package/dist/schemas/runtime/types.d.ts +36 -15
  108. package/dist/schemas/scene/components.d.ts +126 -0
  109. package/dist/schemas/scene/components.js +11 -0
  110. package/dist/schemas/scene/core.d.ts +109 -10
  111. package/dist/schemas/scene/core.js +1 -1
  112. package/dist/utils/mediaWindow.d.ts +13 -0
  113. package/dist/utils/mediaWindow.js +25 -0
  114. package/dist/utils/svgGenerator.js +1 -1
  115. package/dist/utils/timelineWindow.d.ts +2 -0
  116. package/dist/utils/timelineWindow.js +7 -0
  117. package/dist/utils/utils.js +2 -4
  118. package/docs/AGENT_PATTERNS.md +243 -0
  119. package/docs/AGENT_WORKFLOW.md +364 -0
  120. package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
  121. package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
  122. package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
  123. package/docs/DETERMINISTIC_RENDERER.md +83 -0
  124. package/docs/NEXT_RELEASE.md +28 -0
  125. package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
  126. package/docs/examples/agent-cues.json +49 -0
  127. package/docs/examples/agent-transcript.json +23 -0
  128. package/docs/examples/agent-transcript.srt +7 -0
  129. package/docs/examples/agent-transcript.vtt +7 -0
  130. package/docs/examples/image-animation.json +87 -0
  131. package/docs/examples/native-text-metric-badge.json +109 -0
  132. package/package.json +133 -100
  133. 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) {
@@ -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;
@@ -208,6 +209,7 @@ export class PixiSplitScreenDisplayObjectHook {
208
209
  mainSprite.height = appearance.height;
209
210
  mainSprite.x = appearance.x;
210
211
  mainSprite.y = appearance.y;
212
+ this.#mainSprite = mainSprite;
211
213
  this.#displayObject.addChild(mainSprite);
212
214
  }
213
215
  #initDisplayObject() {
@@ -250,18 +252,83 @@ export class PixiSplitScreenDisplayObjectHook {
250
252
  walk(this.#displayObject);
251
253
  this.#pixiTexture = nextTexture;
252
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
+ }
253
297
  async #handleUpdate() {
254
298
  const isActive = this.#context.isActive;
255
299
  if (this.#displayObject) {
256
- // Texture swaps are frequent in deterministic mode; update sprite textures
257
- // in-place instead of rebuilding split/blur geometry each frame.
258
300
  const currentTexture = this.#context.getResource('pixiTexture');
259
- 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.
260
319
  this.#swapDisplayTexture(currentTexture);
261
320
  }
262
321
  if (isActive && this.#bgCanvas) {
263
322
  this.#drawBlurredBackground(this.#blurStrength);
264
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
+ }
265
332
  // Always re-assert the resource in case the context was cleared or updated
266
333
  this.#context.setResource('pixiRenderObject', this.#displayObject);
267
334
  if (this.#displayObject.visible != isActive) {
@@ -290,7 +357,7 @@ export class PixiSplitScreenDisplayObjectHook {
290
357
  await this.#handleDestroy();
291
358
  this.#pixiTexture = currentTexture;
292
359
  if (this.#displayObject) {
293
- this.#displayObject.removeChildren();
360
+ this.#destroyChildren();
294
361
  this.#initDisplayObject();
295
362
  }
296
363
  }
@@ -298,7 +365,7 @@ export class PixiSplitScreenDisplayObjectHook {
298
365
  // Same texture - just update sprite properties (position, size, etc.)
299
366
  // For split screen, we may need to rebuild if effects changed
300
367
  // For now, trigger a full rebuild on refresh
301
- this.#displayObject.removeChildren();
368
+ this.#destroyChildren();
302
369
  if (currentTexture) {
303
370
  this.#pixiTexture = currentTexture;
304
371
  this.#initDisplayObject();
@@ -307,7 +374,7 @@ export class PixiSplitScreenDisplayObjectHook {
307
374
  // If no texture yet, #handleUpdate will handle initial creation
308
375
  }
309
376
  async #handleDestroy() {
310
- // remove event listeners from video
377
+ this.#mainSprite = undefined;
311
378
  this.#bgCanvas = undefined;
312
379
  this.#bgSprite = undefined;
313
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
+ }