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.
- package/README.md +122 -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.d.ts +3 -0
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +85 -8
- 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/factories/SceneBuilderFactory.d.ts +3 -0
- package/dist/factories/SceneBuilderFactory.js +6 -0
- 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.d.ts +12 -0
- package/dist/managers/AppManager.svelte.js +180 -8
- package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
- package/dist/managers/ComponentsManager.svelte.js +29 -15
- package/dist/managers/DeterministicMediaManager.d.ts +6 -0
- package/dist/managers/DeterministicMediaManager.js +57 -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.d.ts +3 -0
- 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) {
|
|
@@ -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
|
-
//
|
|
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
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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
|
+
}
|