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.
- package/README.md +97 -0
- package/bin/visualfries.js +1570 -0
- package/dist/DIContainer.d.ts +2 -1
- package/dist/DIContainer.js +16 -5
- package/dist/SceneBuilder.svelte.d.ts +70 -1
- package/dist/SceneBuilder.svelte.js +96 -29
- package/dist/agent/audioMixer.d.ts +34 -0
- package/dist/agent/audioMixer.js +308 -0
- package/dist/agent/broll.d.ts +35 -0
- package/dist/agent/broll.js +226 -0
- package/dist/agent/capabilities.d.ts +34 -0
- package/dist/agent/capabilities.js +246 -0
- package/dist/agent/captionScene.d.ts +23 -0
- package/dist/agent/captionScene.js +204 -0
- package/dist/agent/catalog.d.ts +24 -0
- package/dist/agent/catalog.js +67 -0
- package/dist/agent/cueFiles.d.ts +54 -0
- package/dist/agent/cueFiles.js +219 -0
- package/dist/agent/cuePresets.d.ts +7 -0
- package/dist/agent/cuePresets.js +50 -0
- package/dist/agent/deterministicMedia.d.ts +61 -0
- package/dist/agent/deterministicMedia.js +303 -0
- package/dist/agent/index.d.ts +18 -0
- package/dist/agent/index.js +18 -0
- package/dist/agent/inspectScene.d.ts +36 -0
- package/dist/agent/inspectScene.js +139 -0
- package/dist/agent/localRender.d.ts +82 -0
- package/dist/agent/localRender.js +702 -0
- package/dist/agent/overlays.d.ts +40 -0
- package/dist/agent/overlays.js +325 -0
- package/dist/agent/productionPlan.d.ts +131 -0
- package/dist/agent/productionPlan.js +322 -0
- package/dist/agent/renderPlan.d.ts +27 -0
- package/dist/agent/renderPlan.js +68 -0
- package/dist/agent/renderRanges.d.ts +24 -0
- package/dist/agent/renderRanges.js +73 -0
- package/dist/agent/runtimeSupport.d.ts +19 -0
- package/dist/agent/runtimeSupport.js +192 -0
- package/dist/agent/streamingEncoder.d.ts +31 -0
- package/dist/agent/streamingEncoder.js +208 -0
- package/dist/agent/transcripts.d.ts +29 -0
- package/dist/agent/transcripts.js +146 -0
- package/dist/agent/transitions.d.ts +20 -0
- package/dist/agent/transitions.js +169 -0
- package/dist/animations/PixiAnimationTarget.d.ts +29 -0
- package/dist/animations/PixiAnimationTarget.js +63 -0
- package/dist/animations/builders/LineHighlighterAnimationBuilder.js +20 -3
- package/dist/animations/builders/WordHighlighterAnimationBuilder.js +15 -2
- package/dist/browser/browserExport.d.ts +73 -0
- package/dist/browser/browserExport.js +180 -0
- package/dist/browser/glTransitions.d.ts +37 -0
- package/dist/browser/glTransitions.js +261 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/mediaDecode.d.ts +31 -0
- package/dist/browser/mediaDecode.js +152 -0
- package/dist/builders/PixiComponentBuilder.d.ts +6 -0
- package/dist/builders/PixiComponentBuilder.js +12 -0
- package/dist/builders/_ComponentState.svelte.d.ts +7 -5
- package/dist/builders/_ComponentState.svelte.js +29 -15
- package/dist/builders/html/StyleBuilder.js +9 -8
- package/dist/builders/html/TextComponentHtmlBuilder.js +4 -2
- package/dist/builders/html/processors/AppearanceStyleProcessor.js +1 -1
- package/dist/builders/html/processors/TextAppearanceStyleProcessor.js +7 -0
- package/dist/builders/html/processors/TextEffectsStyleProcessor.js +32 -13
- package/dist/commands/ReplaceSourceOnTimeCommand.js +9 -1
- package/dist/commands/SeekCommand.js +19 -6
- package/dist/components/AnimatedGIF.d.ts +3 -0
- package/dist/components/AnimatedGIF.js +28 -1
- package/dist/components/Component.svelte.d.ts +5 -5
- package/dist/components/Component.svelte.js +29 -13
- package/dist/components/ComponentContext.svelte.js +68 -23
- package/dist/components/hooks/AnimationHook.js +1 -1
- package/dist/components/hooks/CanvasFillHook.d.ts +12 -0
- package/dist/components/hooks/CanvasFillHook.js +75 -0
- package/dist/components/hooks/DeterministicMediaFrameHook.js +3 -0
- package/dist/components/hooks/HtmlTextHook.js +11 -3
- package/dist/components/hooks/HtmlToCanvasHook.js +8 -25
- package/dist/components/hooks/MediaHook.js +172 -47
- package/dist/components/hooks/MediaSeekingHook.js +138 -88
- package/dist/components/hooks/PixiGifHook.js +4 -4
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +73 -6
- package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
- package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
- package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
- package/dist/components/hooks/SubtitlesHook.js +48 -24
- package/dist/components/hooks/VerifyMediaHook.js +2 -2
- package/dist/directors/ComponentDirector.d.ts +1 -0
- package/dist/directors/ComponentDirector.js +41 -8
- package/dist/fonts/fontDiscovery.js +12 -1
- package/dist/layers/Layer.svelte.d.ts +54 -0
- package/dist/layers/Layer.svelte.js +2 -25
- package/dist/managers/AppManager.svelte.js +8 -5
- package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
- package/dist/managers/ComponentsManager.svelte.js +29 -15
- package/dist/managers/DeterministicMediaManager.js +38 -3
- package/dist/managers/EventManager.js +3 -1
- package/dist/managers/LayersManager.svelte.d.ts +54 -0
- package/dist/managers/RenderManager.d.ts +5 -0
- package/dist/managers/RenderManager.js +67 -21
- package/dist/managers/StateManager.svelte.d.ts +56 -0
- package/dist/managers/StateManager.svelte.js +60 -58
- package/dist/managers/SubtitlesManager.svelte.js +1 -3
- package/dist/managers/TimeManager.svelte.d.ts +4 -0
- package/dist/managers/TimeManager.svelte.js +23 -7
- package/dist/schemas/runtime/deterministic.js +5 -1
- package/dist/schemas/runtime/types.d.ts +36 -15
- package/dist/schemas/scene/components.d.ts +126 -0
- package/dist/schemas/scene/components.js +11 -0
- package/dist/schemas/scene/core.d.ts +109 -10
- package/dist/schemas/scene/core.js +1 -1
- package/dist/utils/mediaWindow.d.ts +13 -0
- package/dist/utils/mediaWindow.js +25 -0
- package/dist/utils/svgGenerator.js +1 -1
- package/dist/utils/timelineWindow.d.ts +2 -0
- package/dist/utils/timelineWindow.js +7 -0
- package/dist/utils/utils.js +2 -4
- package/docs/AGENT_PATTERNS.md +243 -0
- package/docs/AGENT_WORKFLOW.md +364 -0
- package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
- package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
- package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
- package/docs/DETERMINISTIC_RENDERER.md +83 -0
- package/docs/NEXT_RELEASE.md +28 -0
- package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
- package/docs/examples/agent-cues.json +49 -0
- package/docs/examples/agent-transcript.json +23 -0
- package/docs/examples/agent-transcript.srt +7 -0
- package/docs/examples/agent-transcript.vtt +7 -0
- package/docs/examples/image-animation.json +87 -0
- package/docs/examples/native-text-metric-badge.json +109 -0
- package/package.json +133 -100
- 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
|
|
25
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
65
|
-
|
|
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
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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
|
+
}
|