@meframe/core 0.0.2 → 0.0.4
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/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +6 -4
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +2 -2
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +4 -3
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/VideoL1Cache.d.ts +2 -2
- package/dist/cache/l1/VideoL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/VideoL1Cache.js +13 -8
- package/dist/cache/l1/VideoL1Cache.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +3 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/types.d.ts +6 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +7 -8
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +56 -76
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/controllers/types.d.ts +2 -3
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/event/events.d.ts +1 -4
- package/dist/event/events.d.ts.map +1 -1
- package/dist/event/events.js.map +1 -1
- package/dist/model/CompositionModel.d.ts +1 -0
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +2 -0
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/patch.d.ts +6 -2
- package/dist/model/patch.d.ts.map +1 -1
- package/dist/model/patch.js +76 -2
- package/dist/model/patch.js.map +1 -1
- package/dist/model/types.d.ts +1 -0
- package/dist/model/types.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts +8 -7
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +33 -56
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +0 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +40 -19
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +3 -5
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +66 -69
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/types.d.ts +2 -0
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts +6 -0
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +17 -1
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/compose/types.d.ts +2 -1
- package/dist/stages/compose/types.d.ts.map +1 -1
- package/dist/stages/demux/MP4Demuxer.d.ts +0 -1
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +22 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +71 -25
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +1 -1
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +3 -2
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/stages/load/types.d.ts +2 -0
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/utils/time-utils.d.ts +3 -2
- package/dist/utils/time-utils.d.ts.map +1 -1
- package/dist/utils/time-utils.js +2 -1
- package/dist/utils/time-utils.js.map +1 -1
- package/dist/vite-plugin.d.ts +5 -3
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +109 -52
- package/dist/vite-plugin.js.map +1 -1
- package/dist/worker/WorkerPool.d.ts +9 -0
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +32 -5
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/{stages/demux → workers}/MP4Demuxer.js +4 -13
- package/dist/workers/MP4Demuxer.js.map +1 -0
- package/dist/workers/WorkerChannel.js +486 -0
- package/dist/workers/WorkerChannel.js.map +1 -0
- package/dist/{assets/video-demux.worker-D019I7GQ.js → workers/mp4box.all.js} +4 -912
- package/dist/workers/mp4box.all.js.map +1 -0
- package/dist/{assets/audio-compose.worker-nGVvHD5Q.js → workers/stages/compose/audio-compose.worker.js} +7 -481
- package/dist/workers/stages/compose/audio-compose.worker.js.map +1 -0
- package/dist/{assets/video-compose.worker-DPzsC21d.js → workers/stages/compose/video-compose.worker.js} +120 -562
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -0
- package/dist/{assets/decode.worker-DpWHsc7R.js → workers/stages/decode/decode.worker.js} +7 -481
- package/dist/workers/stages/decode/decode.worker.js.map +1 -0
- package/dist/{stages → workers/stages}/demux/audio-demux.worker.js +184 -4
- package/dist/workers/stages/demux/audio-demux.worker.js.map +1 -0
- package/dist/{stages → workers/stages}/demux/video-demux.worker.js +2 -3
- package/dist/workers/stages/demux/video-demux.worker.js.map +1 -0
- package/dist/{stages → workers/stages}/encode/encode.worker.js +238 -4
- package/dist/workers/stages/encode/encode.worker.js.map +1 -0
- package/dist/{stages/mux/MP4Muxer.js → workers/stages/mux/mux.worker.js} +244 -5
- package/dist/workers/stages/mux/mux.worker.js.map +1 -0
- package/package.json +21 -21
- package/dist/assets/audio-compose.worker-nGVvHD5Q.js.map +0 -1
- package/dist/assets/audio-demux.worker-xwWBtbAe.js +0 -8299
- package/dist/assets/audio-demux.worker-xwWBtbAe.js.map +0 -1
- package/dist/assets/decode.worker-DpWHsc7R.js.map +0 -1
- package/dist/assets/encode.worker-nfOb3kw6.js +0 -1026
- package/dist/assets/encode.worker-nfOb3kw6.js.map +0 -1
- package/dist/assets/mux.worker-uEMQY066.js +0 -8019
- package/dist/assets/mux.worker-uEMQY066.js.map +0 -1
- package/dist/assets/video-compose.worker-DPzsC21d.js.map +0 -1
- package/dist/assets/video-demux.worker-D019I7GQ.js.map +0 -1
- package/dist/controllers/PreviewHandle.d.ts +0 -25
- package/dist/controllers/PreviewHandle.d.ts.map +0 -1
- package/dist/controllers/PreviewHandle.js +0 -45
- package/dist/controllers/PreviewHandle.js.map +0 -1
- package/dist/model/dirty-range.js +0 -220
- package/dist/model/dirty-range.js.map +0 -1
- package/dist/model/types.js +0 -5
- package/dist/model/types.js.map +0 -1
- package/dist/plugins/BackpressureMonitor.js +0 -62
- package/dist/plugins/BackpressureMonitor.js.map +0 -1
- package/dist/stages/compose/AudioDucker.js +0 -161
- package/dist/stages/compose/AudioDucker.js.map +0 -1
- package/dist/stages/compose/AudioMixer.js +0 -373
- package/dist/stages/compose/AudioMixer.js.map +0 -1
- package/dist/stages/compose/FilterProcessor.js +0 -226
- package/dist/stages/compose/FilterProcessor.js.map +0 -1
- package/dist/stages/compose/LayerRenderer.js +0 -215
- package/dist/stages/compose/LayerRenderer.js.map +0 -1
- package/dist/stages/compose/TransitionProcessor.js +0 -189
- package/dist/stages/compose/TransitionProcessor.js.map +0 -1
- package/dist/stages/compose/VideoComposer.js +0 -186
- package/dist/stages/compose/VideoComposer.js.map +0 -1
- package/dist/stages/compose/audio-compose.worker.d.ts +0 -79
- package/dist/stages/compose/audio-compose.worker.d.ts.map +0 -1
- package/dist/stages/compose/audio-compose.worker.js +0 -540
- package/dist/stages/compose/audio-compose.worker.js.map +0 -1
- package/dist/stages/compose/audio-compose.worker2.js +0 -5
- package/dist/stages/compose/audio-compose.worker2.js.map +0 -1
- package/dist/stages/compose/video-compose.worker.d.ts +0 -60
- package/dist/stages/compose/video-compose.worker.d.ts.map +0 -1
- package/dist/stages/compose/video-compose.worker.js +0 -379
- package/dist/stages/compose/video-compose.worker.js.map +0 -1
- package/dist/stages/compose/video-compose.worker2.js +0 -5
- package/dist/stages/compose/video-compose.worker2.js.map +0 -1
- package/dist/stages/decode/AudioChunkDecoder.js +0 -82
- package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
- package/dist/stages/decode/BaseDecoder.js +0 -130
- package/dist/stages/decode/BaseDecoder.js.map +0 -1
- package/dist/stages/decode/VideoChunkDecoder.js +0 -199
- package/dist/stages/decode/VideoChunkDecoder.js.map +0 -1
- package/dist/stages/decode/decode.worker.d.ts +0 -70
- package/dist/stages/decode/decode.worker.d.ts.map +0 -1
- package/dist/stages/decode/decode.worker.js +0 -423
- package/dist/stages/decode/decode.worker.js.map +0 -1
- package/dist/stages/decode/decode.worker2.js +0 -5
- package/dist/stages/decode/decode.worker2.js.map +0 -1
- package/dist/stages/demux/MP3FrameParser.js +0 -186
- package/dist/stages/demux/MP3FrameParser.js.map +0 -1
- package/dist/stages/demux/MP4Demuxer.js.map +0 -1
- package/dist/stages/demux/audio-demux.worker.d.ts +0 -51
- package/dist/stages/demux/audio-demux.worker.d.ts.map +0 -1
- package/dist/stages/demux/audio-demux.worker.js.map +0 -1
- package/dist/stages/demux/audio-demux.worker2.js +0 -5
- package/dist/stages/demux/audio-demux.worker2.js.map +0 -1
- package/dist/stages/demux/video-demux.worker.d.ts +0 -51
- package/dist/stages/demux/video-demux.worker.d.ts.map +0 -1
- package/dist/stages/demux/video-demux.worker.js.map +0 -1
- package/dist/stages/demux/video-demux.worker2.js +0 -5
- package/dist/stages/demux/video-demux.worker2.js.map +0 -1
- package/dist/stages/encode/AudioChunkEncoder.js +0 -37
- package/dist/stages/encode/AudioChunkEncoder.js.map +0 -1
- package/dist/stages/encode/BaseEncoder.js +0 -164
- package/dist/stages/encode/BaseEncoder.js.map +0 -1
- package/dist/stages/encode/VideoChunkEncoder.js +0 -50
- package/dist/stages/encode/VideoChunkEncoder.js.map +0 -1
- package/dist/stages/encode/encode.worker.d.ts +0 -3
- package/dist/stages/encode/encode.worker.d.ts.map +0 -1
- package/dist/stages/encode/encode.worker.js.map +0 -1
- package/dist/stages/encode/encode.worker2.js +0 -5
- package/dist/stages/encode/encode.worker2.js.map +0 -1
- package/dist/stages/mux/MP4Muxer.js.map +0 -1
- package/dist/stages/mux/mux.worker.d.ts +0 -65
- package/dist/stages/mux/mux.worker.d.ts.map +0 -1
- package/dist/stages/mux/mux.worker.js +0 -219
- package/dist/stages/mux/mux.worker.js.map +0 -1
- package/dist/stages/mux/mux.worker2.js +0 -5
- package/dist/stages/mux/mux.worker2.js.map +0 -1
- package/dist/stages/mux/utils.js +0 -34
- package/dist/stages/mux/utils.js.map +0 -1
- package/dist/worker/worker-registry.d.ts +0 -12
- package/dist/worker/worker-registry.d.ts.map +0 -1
- package/dist/worker/worker-registry.js +0 -20
- package/dist/worker/worker-registry.js.map +0 -1
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
class LayerRenderer {
|
|
2
|
-
ctx;
|
|
3
|
-
width;
|
|
4
|
-
height;
|
|
5
|
-
constructor(ctx, width, height) {
|
|
6
|
-
this.ctx = ctx;
|
|
7
|
-
this.width = width;
|
|
8
|
-
this.height = height;
|
|
9
|
-
this.ensureHighQualityRendering();
|
|
10
|
-
}
|
|
11
|
-
ensureHighQualityRendering() {
|
|
12
|
-
this.ctx.imageSmoothingEnabled = true;
|
|
13
|
-
this.ctx.imageSmoothingQuality = "high";
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Render a single layer with all its properties
|
|
17
|
-
*/
|
|
18
|
-
async renderLayer(layer) {
|
|
19
|
-
if (!layer.visible || layer.opacity <= 0) return;
|
|
20
|
-
this.ctx.save();
|
|
21
|
-
try {
|
|
22
|
-
this.ensureHighQualityRendering();
|
|
23
|
-
this.ctx.globalAlpha = layer.opacity;
|
|
24
|
-
if (layer.blendMode) {
|
|
25
|
-
this.ctx.globalCompositeOperation = layer.blendMode;
|
|
26
|
-
}
|
|
27
|
-
if (layer.transform) {
|
|
28
|
-
this.applyTransform(layer.transform);
|
|
29
|
-
}
|
|
30
|
-
switch (layer.type) {
|
|
31
|
-
case "video":
|
|
32
|
-
await this.renderVideoLayer(layer);
|
|
33
|
-
break;
|
|
34
|
-
case "image":
|
|
35
|
-
await this.renderImageLayer(layer);
|
|
36
|
-
break;
|
|
37
|
-
case "text":
|
|
38
|
-
await this.renderTextLayer(layer);
|
|
39
|
-
break;
|
|
40
|
-
}
|
|
41
|
-
if (layer.mask) {
|
|
42
|
-
this.applyMask(layer.mask);
|
|
43
|
-
}
|
|
44
|
-
} finally {
|
|
45
|
-
this.ctx.restore();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
applyTransform(transform) {
|
|
49
|
-
const centerX = this.width * (transform.anchorX ?? 0.5);
|
|
50
|
-
const centerY = this.height * (transform.anchorY ?? 0.5);
|
|
51
|
-
this.ctx.translate(transform.x + centerX, transform.y + centerY);
|
|
52
|
-
if (transform.rotation) {
|
|
53
|
-
this.ctx.rotate(transform.rotation);
|
|
54
|
-
}
|
|
55
|
-
this.ctx.scale(transform.scaleX, transform.scaleY);
|
|
56
|
-
if (transform.skewX || transform.skewY) {
|
|
57
|
-
this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);
|
|
58
|
-
}
|
|
59
|
-
this.ctx.translate(-centerX, -centerY);
|
|
60
|
-
}
|
|
61
|
-
async renderVideoLayer(layer) {
|
|
62
|
-
const { videoFrame, crop } = layer;
|
|
63
|
-
const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;
|
|
64
|
-
const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;
|
|
65
|
-
const scaleX = this.width / videoWidth;
|
|
66
|
-
const scaleY = this.height / videoHeight;
|
|
67
|
-
const scale = Math.min(scaleX, scaleY);
|
|
68
|
-
const renderWidth = Math.round(videoWidth * scale);
|
|
69
|
-
const renderHeight = Math.round(videoHeight * scale);
|
|
70
|
-
const renderX = Math.round((this.width - renderWidth) / 2);
|
|
71
|
-
const renderY = Math.round((this.height - renderHeight) / 2);
|
|
72
|
-
if (crop) {
|
|
73
|
-
this.ctx.drawImage(
|
|
74
|
-
videoFrame,
|
|
75
|
-
crop.x,
|
|
76
|
-
crop.y,
|
|
77
|
-
crop.width,
|
|
78
|
-
crop.height,
|
|
79
|
-
renderX,
|
|
80
|
-
renderY,
|
|
81
|
-
renderWidth,
|
|
82
|
-
renderHeight
|
|
83
|
-
);
|
|
84
|
-
} else {
|
|
85
|
-
this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
async renderImageLayer(layer) {
|
|
89
|
-
const { source, crop } = layer;
|
|
90
|
-
if (source instanceof ImageData) {
|
|
91
|
-
if (crop) {
|
|
92
|
-
const tempCanvas = new OffscreenCanvas(crop.width, crop.height);
|
|
93
|
-
const tempCtx = tempCanvas.getContext("2d");
|
|
94
|
-
tempCtx.putImageData(source, -crop.x, -crop.y);
|
|
95
|
-
this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);
|
|
96
|
-
} else {
|
|
97
|
-
this.ctx.putImageData(source, 0, 0);
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
if (!source) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (crop) {
|
|
104
|
-
this.ctx.drawImage(
|
|
105
|
-
source,
|
|
106
|
-
crop.x,
|
|
107
|
-
crop.y,
|
|
108
|
-
crop.width,
|
|
109
|
-
crop.height,
|
|
110
|
-
0,
|
|
111
|
-
0,
|
|
112
|
-
this.width,
|
|
113
|
-
this.height
|
|
114
|
-
);
|
|
115
|
-
} else {
|
|
116
|
-
this.ctx.drawImage(source, 0, 0, this.width, this.height);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
async renderTextLayer(layer) {
|
|
121
|
-
const fontSize = layer.fontSize ?? 16;
|
|
122
|
-
const fontFamily = layer.fontFamily ?? "sans-serif";
|
|
123
|
-
const fontWeight = layer.fontWeight ?? "normal";
|
|
124
|
-
const fontStyle = layer.fontStyle ?? "normal";
|
|
125
|
-
this.ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
126
|
-
this.ctx.fillStyle = layer.color ?? "#000000";
|
|
127
|
-
this.ctx.textAlign = layer.textAlign ?? "left";
|
|
128
|
-
this.ctx.textBaseline = layer.verticalAlign ?? "top";
|
|
129
|
-
if (layer.letterSpacing && typeof this.ctx.letterSpacing !== "undefined") {
|
|
130
|
-
this.ctx.letterSpacing = `${layer.letterSpacing}px`;
|
|
131
|
-
}
|
|
132
|
-
this.ensureHighQualityRendering();
|
|
133
|
-
const baseX = this.calculateTextX(layer.textAlign);
|
|
134
|
-
const baseY = this.calculateTextY(layer.verticalAlign, fontSize);
|
|
135
|
-
const x = Math.round(baseX) + 0.5;
|
|
136
|
-
const y = Math.round(baseY) + 0.5;
|
|
137
|
-
if (layer.shadow) {
|
|
138
|
-
this.ctx.shadowColor = layer.shadow.color;
|
|
139
|
-
this.ctx.shadowOffsetX = layer.shadow.offsetX;
|
|
140
|
-
this.ctx.shadowOffsetY = layer.shadow.offsetY;
|
|
141
|
-
this.ctx.shadowBlur = layer.shadow.blur;
|
|
142
|
-
}
|
|
143
|
-
if (layer.strokeColor && layer.strokeWidth && layer.strokeWidth > 0) {
|
|
144
|
-
this.drawEnhancedStroke(layer.text, x, y, layer.strokeColor, layer.strokeWidth);
|
|
145
|
-
}
|
|
146
|
-
this.ctx.fillText(layer.text, x, y);
|
|
147
|
-
if (layer.shadow) {
|
|
148
|
-
this.ctx.shadowColor = "transparent";
|
|
149
|
-
this.ctx.shadowOffsetX = 0;
|
|
150
|
-
this.ctx.shadowOffsetY = 0;
|
|
151
|
-
this.ctx.shadowBlur = 0;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Draw enhanced multi-layer stroke for better text visibility
|
|
156
|
-
*/
|
|
157
|
-
drawEnhancedStroke(text, x, y, strokeColor, strokeWidth) {
|
|
158
|
-
this.ctx.save();
|
|
159
|
-
this.ctx.strokeStyle = strokeColor;
|
|
160
|
-
this.ctx.lineJoin = "round";
|
|
161
|
-
this.ctx.lineCap = "round";
|
|
162
|
-
this.ctx.miterLimit = 2;
|
|
163
|
-
const layers = [1.1, 1];
|
|
164
|
-
layers.forEach((multiplier) => {
|
|
165
|
-
this.ctx.lineWidth = strokeWidth * multiplier;
|
|
166
|
-
this.ctx.strokeText(text, x, y);
|
|
167
|
-
});
|
|
168
|
-
this.ctx.restore();
|
|
169
|
-
}
|
|
170
|
-
calculateTextX(align) {
|
|
171
|
-
switch (align) {
|
|
172
|
-
case "center":
|
|
173
|
-
return this.width / 2;
|
|
174
|
-
case "right":
|
|
175
|
-
return this.width;
|
|
176
|
-
default:
|
|
177
|
-
return 0;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
calculateTextY(align, fontSize = 16) {
|
|
181
|
-
switch (align) {
|
|
182
|
-
case "middle":
|
|
183
|
-
return this.height / 2;
|
|
184
|
-
case "bottom":
|
|
185
|
-
return this.height * 0.85;
|
|
186
|
-
default:
|
|
187
|
-
return fontSize;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
applyMask(mask) {
|
|
191
|
-
this.ctx.globalCompositeOperation = mask.invert ? "source-out" : "destination-in";
|
|
192
|
-
if (mask.source) {
|
|
193
|
-
this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);
|
|
194
|
-
} else if (mask.shape === "circle") {
|
|
195
|
-
this.ctx.beginPath();
|
|
196
|
-
this.ctx.arc(
|
|
197
|
-
this.width / 2,
|
|
198
|
-
this.height / 2,
|
|
199
|
-
Math.min(this.width, this.height) / 2,
|
|
200
|
-
0,
|
|
201
|
-
Math.PI * 2
|
|
202
|
-
);
|
|
203
|
-
this.ctx.fill();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
updateDimensions(width, height) {
|
|
207
|
-
this.width = width;
|
|
208
|
-
this.height = height;
|
|
209
|
-
this.ensureHighQualityRendering();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
export {
|
|
213
|
-
LayerRenderer
|
|
214
|
-
};
|
|
215
|
-
//# sourceMappingURL=LayerRenderer.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"LayerRenderer.js","sources":["../../../src/stages/compose/LayerRenderer.ts"],"sourcesContent":["import type { Layer, VideoLayer, ImageLayer, TextLayer, Transform2D, MaskConfig } from './types';\n\n/**\n * LayerRenderer - Handles rendering of individual layers\n * Single responsibility: Draw a single layer to the canvas context\n */\nexport class LayerRenderer {\n private ctx: OffscreenCanvasRenderingContext2D;\n private width: number;\n private height: number;\n\n constructor(ctx: OffscreenCanvasRenderingContext2D, width: number, height: number) {\n this.ctx = ctx;\n this.width = width;\n this.height = height;\n this.ensureHighQualityRendering();\n }\n\n private ensureHighQualityRendering(): void {\n this.ctx.imageSmoothingEnabled = true;\n this.ctx.imageSmoothingQuality = 'high';\n }\n\n /**\n * Render a single layer with all its properties\n */\n async renderLayer(layer: Layer): Promise<void> {\n if (!layer.visible || layer.opacity <= 0) return;\n\n this.ctx.save();\n\n try {\n this.ensureHighQualityRendering();\n\n // Apply layer properties\n this.ctx.globalAlpha = layer.opacity;\n\n if (layer.blendMode) {\n this.ctx.globalCompositeOperation = layer.blendMode;\n }\n\n if (layer.transform) {\n this.applyTransform(layer.transform);\n }\n\n // Render based on layer type\n switch (layer.type) {\n case 'video':\n await this.renderVideoLayer(layer as VideoLayer);\n break;\n case 'image':\n await this.renderImageLayer(layer as ImageLayer);\n break;\n case 'text':\n await this.renderTextLayer(layer as TextLayer);\n break;\n }\n\n // Apply mask if present\n if (layer.mask) {\n this.applyMask(layer.mask);\n }\n } finally {\n this.ctx.restore();\n }\n }\n\n private applyTransform(transform: Transform2D): void {\n const centerX = this.width * (transform.anchorX ?? 0.5);\n const centerY = this.height * (transform.anchorY ?? 0.5);\n\n this.ctx.translate(transform.x + centerX, transform.y + centerY);\n\n if (transform.rotation) {\n this.ctx.rotate(transform.rotation);\n }\n\n this.ctx.scale(transform.scaleX, transform.scaleY);\n\n if (transform.skewX || transform.skewY) {\n this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);\n }\n\n this.ctx.translate(-centerX, -centerY);\n }\n\n private async renderVideoLayer(layer: VideoLayer): Promise<void> {\n const { videoFrame, crop } = layer;\n\n // Get video dimensions\n const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;\n const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;\n\n // Calculate scaling to fit (contain mode - preserve aspect ratio)\n const scaleX = this.width / videoWidth;\n const scaleY = this.height / videoHeight;\n\n // Use the smaller scale to ensure entire video fits\n const scale = Math.min(scaleX, scaleY);\n\n // Calculate final render dimensions\n const renderWidth = Math.round(videoWidth * scale);\n const renderHeight = Math.round(videoHeight * scale);\n\n // Center the video\n const renderX = Math.round((this.width - renderWidth) / 2);\n const renderY = Math.round((this.height - renderHeight) / 2);\n\n if (crop) {\n this.ctx.drawImage(\n videoFrame,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n renderX,\n renderY,\n renderWidth,\n renderHeight\n );\n } else {\n this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);\n }\n }\n\n private async renderImageLayer(layer: ImageLayer): Promise<void> {\n const { source, crop } = layer;\n\n // Handle ImageData by putting it on canvas first\n if (source instanceof ImageData) {\n if (crop) {\n // For ImageData with crop, we need to extract the cropped region\n const tempCanvas = new OffscreenCanvas(crop.width, crop.height);\n const tempCtx = tempCanvas.getContext('2d')!;\n tempCtx.putImageData(source, -crop.x, -crop.y);\n this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);\n } else {\n // Put ImageData directly\n this.ctx.putImageData(source, 0, 0);\n }\n } else {\n // ImageBitmap can be drawn directly\n if (!source) {\n return;\n }\n if (crop) {\n this.ctx.drawImage(\n source,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n 0,\n 0,\n this.width,\n this.height\n );\n } else {\n this.ctx.drawImage(source, 0, 0, this.width, this.height);\n }\n }\n }\n\n private async renderTextLayer(layer: TextLayer): Promise<void> {\n const fontSize = layer.fontSize ?? 16;\n const fontFamily = layer.fontFamily ?? 'sans-serif';\n const fontWeight = layer.fontWeight ?? 'normal';\n const fontStyle = layer.fontStyle ?? 'normal';\n\n this.ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;\n this.ctx.fillStyle = layer.color ?? '#000000';\n this.ctx.textAlign = layer.textAlign ?? 'left';\n this.ctx.textBaseline = layer.verticalAlign ?? 'top';\n\n // Apply letter spacing if supported\n if (layer.letterSpacing && typeof (this.ctx as any).letterSpacing !== 'undefined') {\n (this.ctx as any).letterSpacing = `${layer.letterSpacing}px`;\n }\n\n // Ensure high-quality text rendering\n this.ensureHighQualityRendering();\n\n // Calculate position with sub-pixel adjustment for sharper rendering\n const baseX = this.calculateTextX(layer.textAlign);\n const baseY = this.calculateTextY(layer.verticalAlign, fontSize);\n const x = Math.round(baseX) + 0.5;\n const y = Math.round(baseY) + 0.5;\n\n // Apply shadow before rendering text (if needed)\n if (layer.shadow) {\n this.ctx.shadowColor = layer.shadow.color;\n this.ctx.shadowOffsetX = layer.shadow.offsetX;\n this.ctx.shadowOffsetY = layer.shadow.offsetY;\n this.ctx.shadowBlur = layer.shadow.blur;\n }\n\n // Render stroke with enhanced quality (multi-layer approach)\n if (layer.strokeColor && layer.strokeWidth && layer.strokeWidth > 0) {\n this.drawEnhancedStroke(layer.text, x, y, layer.strokeColor, layer.strokeWidth);\n }\n\n // Render fill text\n this.ctx.fillText(layer.text, x, y);\n\n // Reset shadow\n if (layer.shadow) {\n this.ctx.shadowColor = 'transparent';\n this.ctx.shadowOffsetX = 0;\n this.ctx.shadowOffsetY = 0;\n this.ctx.shadowBlur = 0;\n }\n }\n\n /**\n * Draw enhanced multi-layer stroke for better text visibility\n */\n private drawEnhancedStroke(\n text: string,\n x: number,\n y: number,\n strokeColor: string,\n strokeWidth: number\n ): void {\n this.ctx.save();\n\n this.ctx.strokeStyle = strokeColor;\n this.ctx.lineJoin = 'round';\n this.ctx.lineCap = 'round';\n this.ctx.miterLimit = 2;\n\n // Multi-layer stroke for enhanced visibility without excessive thickness\n const layers = [1.1, 1.0];\n layers.forEach((multiplier) => {\n this.ctx.lineWidth = strokeWidth * multiplier;\n this.ctx.strokeText(text, x, y);\n });\n\n this.ctx.restore();\n }\n\n private calculateTextX(align?: 'left' | 'center' | 'right'): number {\n switch (align) {\n case 'center':\n return this.width / 2;\n case 'right':\n return this.width;\n default:\n return 0;\n }\n }\n\n private calculateTextY(align?: 'top' | 'middle' | 'bottom', fontSize: number = 16): number {\n switch (align) {\n case 'middle':\n return this.height / 2;\n case 'bottom':\n // Place text at 85% height to avoid bottom edge, similar to SubtitleComposer's 70% position\n return this.height * 0.85;\n default:\n return fontSize;\n }\n }\n\n private applyMask(mask: MaskConfig): void {\n this.ctx.globalCompositeOperation = mask.invert ? 'source-out' : 'destination-in';\n\n if (mask.source) {\n this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);\n } else if (mask.shape === 'circle') {\n this.ctx.beginPath();\n this.ctx.arc(\n this.width / 2,\n this.height / 2,\n Math.min(this.width, this.height) / 2,\n 0,\n Math.PI * 2\n );\n this.ctx.fill();\n }\n }\n\n updateDimensions(width: number, height: number): void {\n this.width = width;\n this.height = height;\n this.ensureHighQualityRendering();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
class TransitionProcessor {
|
|
2
|
-
width;
|
|
3
|
-
height;
|
|
4
|
-
constructor(width, height) {
|
|
5
|
-
this.width = width;
|
|
6
|
-
this.height = height;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Apply transition effect to the canvas context
|
|
10
|
-
* Returns true if transition was applied, false if not needed
|
|
11
|
-
*/
|
|
12
|
-
applyTransition(ctx, transition) {
|
|
13
|
-
if (!transition || transition.progress <= 0) return false;
|
|
14
|
-
const progress = this.calculateEasedProgress(transition.progress, transition.easing);
|
|
15
|
-
switch (transition.type) {
|
|
16
|
-
case "fade":
|
|
17
|
-
return this.applyFade(ctx, progress);
|
|
18
|
-
case "slide":
|
|
19
|
-
return this.applySlide(ctx, progress, transition.direction);
|
|
20
|
-
case "wipe":
|
|
21
|
-
return this.applyWipe(ctx, progress, transition.direction);
|
|
22
|
-
case "zoom":
|
|
23
|
-
return this.applyZoom(ctx, progress, transition.direction);
|
|
24
|
-
case "rotate":
|
|
25
|
-
return this.applyRotate(ctx, progress);
|
|
26
|
-
case "dissolve":
|
|
27
|
-
return this.applyDissolve(ctx, progress);
|
|
28
|
-
default:
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
calculateEasedProgress(progress, easing) {
|
|
33
|
-
switch (easing) {
|
|
34
|
-
case "ease-in":
|
|
35
|
-
return progress * progress;
|
|
36
|
-
case "ease-out":
|
|
37
|
-
return 1 - (1 - progress) * (1 - progress);
|
|
38
|
-
case "ease-in-out":
|
|
39
|
-
return progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
|
40
|
-
default:
|
|
41
|
-
return progress;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
applyFade(ctx, progress) {
|
|
45
|
-
ctx.globalAlpha = progress;
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
applySlide(ctx, progress, direction) {
|
|
49
|
-
const distance = 1 - progress;
|
|
50
|
-
switch (direction) {
|
|
51
|
-
case "left":
|
|
52
|
-
ctx.translate(-this.width * distance, 0);
|
|
53
|
-
break;
|
|
54
|
-
case "right":
|
|
55
|
-
ctx.translate(this.width * distance, 0);
|
|
56
|
-
break;
|
|
57
|
-
case "up":
|
|
58
|
-
ctx.translate(0, -this.height * distance);
|
|
59
|
-
break;
|
|
60
|
-
case "down":
|
|
61
|
-
ctx.translate(0, this.height * distance);
|
|
62
|
-
break;
|
|
63
|
-
default:
|
|
64
|
-
ctx.translate(-this.width * distance, 0);
|
|
65
|
-
}
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
applyWipe(ctx, progress, direction) {
|
|
69
|
-
ctx.save();
|
|
70
|
-
ctx.beginPath();
|
|
71
|
-
switch (direction) {
|
|
72
|
-
case "left":
|
|
73
|
-
ctx.rect(0, 0, this.width * progress, this.height);
|
|
74
|
-
break;
|
|
75
|
-
case "right":
|
|
76
|
-
ctx.rect(this.width * (1 - progress), 0, this.width * progress, this.height);
|
|
77
|
-
break;
|
|
78
|
-
case "up":
|
|
79
|
-
ctx.rect(0, 0, this.width, this.height * progress);
|
|
80
|
-
break;
|
|
81
|
-
case "down":
|
|
82
|
-
ctx.rect(0, this.height * (1 - progress), this.width, this.height * progress);
|
|
83
|
-
break;
|
|
84
|
-
default:
|
|
85
|
-
ctx.rect(0, 0, this.width * progress, this.height);
|
|
86
|
-
}
|
|
87
|
-
ctx.clip();
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
applyZoom(ctx, progress, direction) {
|
|
91
|
-
const scale = direction === "out" ? 1 + (1 - progress) : progress;
|
|
92
|
-
const centerX = this.width / 2;
|
|
93
|
-
const centerY = this.height / 2;
|
|
94
|
-
ctx.translate(centerX, centerY);
|
|
95
|
-
ctx.scale(scale, scale);
|
|
96
|
-
ctx.translate(-centerX, -centerY);
|
|
97
|
-
if (direction === "out") {
|
|
98
|
-
ctx.globalAlpha = progress;
|
|
99
|
-
}
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
applyRotate(ctx, progress) {
|
|
103
|
-
const rotation = (1 - progress) * Math.PI * 2;
|
|
104
|
-
const centerX = this.width / 2;
|
|
105
|
-
const centerY = this.height / 2;
|
|
106
|
-
ctx.translate(centerX, centerY);
|
|
107
|
-
ctx.rotate(rotation);
|
|
108
|
-
ctx.translate(-centerX, -centerY);
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
applyDissolve(ctx, progress) {
|
|
112
|
-
ctx.globalAlpha = progress;
|
|
113
|
-
ctx.globalCompositeOperation = "multiply";
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Create a transition mask for advanced effects
|
|
118
|
-
*/
|
|
119
|
-
createTransitionMask(transition, canvas) {
|
|
120
|
-
const ctx = canvas.getContext("2d");
|
|
121
|
-
if (!ctx) return null;
|
|
122
|
-
const imageData = ctx.createImageData(this.width, this.height);
|
|
123
|
-
const data = imageData.data;
|
|
124
|
-
const progress = this.calculateEasedProgress(transition.progress, transition.easing);
|
|
125
|
-
for (let y = 0; y < this.height; y++) {
|
|
126
|
-
for (let x = 0; x < this.width; x++) {
|
|
127
|
-
const index = (y * this.width + x) * 4;
|
|
128
|
-
let alpha = 255;
|
|
129
|
-
switch (transition.type) {
|
|
130
|
-
case "wipe":
|
|
131
|
-
alpha = this.calculateWipeAlpha(x, y, progress, transition.direction);
|
|
132
|
-
break;
|
|
133
|
-
case "dissolve":
|
|
134
|
-
alpha = Math.random() < progress ? 255 : 0;
|
|
135
|
-
break;
|
|
136
|
-
default:
|
|
137
|
-
alpha = Math.floor(255 * progress);
|
|
138
|
-
}
|
|
139
|
-
data[index] = 255;
|
|
140
|
-
data[index + 1] = 255;
|
|
141
|
-
data[index + 2] = 255;
|
|
142
|
-
data[index + 3] = alpha;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return imageData;
|
|
146
|
-
}
|
|
147
|
-
calculateWipeAlpha(x, y, progress, direction) {
|
|
148
|
-
let position = 0;
|
|
149
|
-
switch (direction) {
|
|
150
|
-
case "left":
|
|
151
|
-
position = x / this.width;
|
|
152
|
-
break;
|
|
153
|
-
case "right":
|
|
154
|
-
position = 1 - x / this.width;
|
|
155
|
-
break;
|
|
156
|
-
case "up":
|
|
157
|
-
position = y / this.height;
|
|
158
|
-
break;
|
|
159
|
-
case "down":
|
|
160
|
-
position = 1 - y / this.height;
|
|
161
|
-
break;
|
|
162
|
-
case "in": {
|
|
163
|
-
const cx = x - this.width / 2;
|
|
164
|
-
const cy = y - this.height / 2;
|
|
165
|
-
const maxDist = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
|
|
166
|
-
position = Math.sqrt(cx * cx + cy * cy) / maxDist;
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
case "out": {
|
|
170
|
-
const cx2 = x - this.width / 2;
|
|
171
|
-
const cy2 = y - this.height / 2;
|
|
172
|
-
const maxDist2 = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
|
|
173
|
-
position = 1 - Math.sqrt(cx2 * cx2 + cy2 * cy2) / maxDist2;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
default:
|
|
177
|
-
position = x / this.width;
|
|
178
|
-
}
|
|
179
|
-
return position < progress ? 255 : 0;
|
|
180
|
-
}
|
|
181
|
-
updateDimensions(width, height) {
|
|
182
|
-
this.width = width;
|
|
183
|
-
this.height = height;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
export {
|
|
187
|
-
TransitionProcessor
|
|
188
|
-
};
|
|
189
|
-
//# sourceMappingURL=TransitionProcessor.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TransitionProcessor.js","sources":["../../../src/stages/compose/TransitionProcessor.ts"],"sourcesContent":["import type { TransitionEffect } from './types';\n\n/**\n * TransitionProcessor - Handles transition effects between frames/scenes\n * Single responsibility: Apply transition transformations to canvas context\n */\nexport class TransitionProcessor {\n private width: number;\n private height: number;\n\n constructor(width: number, height: number) {\n this.width = width;\n this.height = height;\n }\n\n /**\n * Apply transition effect to the canvas context\n * Returns true if transition was applied, false if not needed\n */\n applyTransition(ctx: OffscreenCanvasRenderingContext2D, transition: TransitionEffect): boolean {\n if (!transition || transition.progress <= 0) return false;\n\n const progress = this.calculateEasedProgress(transition.progress, transition.easing);\n\n switch (transition.type) {\n case 'fade':\n return this.applyFade(ctx, progress);\n case 'slide':\n return this.applySlide(ctx, progress, transition.direction);\n case 'wipe':\n return this.applyWipe(ctx, progress, transition.direction);\n case 'zoom':\n return this.applyZoom(ctx, progress, transition.direction);\n case 'rotate':\n return this.applyRotate(ctx, progress);\n case 'dissolve':\n return this.applyDissolve(ctx, progress);\n default:\n return false;\n }\n }\n\n private calculateEasedProgress(\n progress: number,\n easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'\n ): number {\n switch (easing) {\n case 'ease-in':\n return progress * progress;\n case 'ease-out':\n return 1 - (1 - progress) * (1 - progress);\n case 'ease-in-out':\n return progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;\n default:\n return progress;\n }\n }\n\n private applyFade(ctx: OffscreenCanvasRenderingContext2D, progress: number): boolean {\n ctx.globalAlpha = progress;\n return true;\n }\n\n private applySlide(\n ctx: OffscreenCanvasRenderingContext2D,\n progress: number,\n direction?: 'left' | 'right' | 'up' | 'down' | 'in' | 'out'\n ): boolean {\n const distance = 1 - progress;\n\n switch (direction) {\n case 'left':\n ctx.translate(-this.width * distance, 0);\n break;\n case 'right':\n ctx.translate(this.width * distance, 0);\n break;\n case 'up':\n ctx.translate(0, -this.height * distance);\n break;\n case 'down':\n ctx.translate(0, this.height * distance);\n break;\n default:\n ctx.translate(-this.width * distance, 0);\n }\n\n return true;\n }\n\n private applyWipe(\n ctx: OffscreenCanvasRenderingContext2D,\n progress: number,\n direction?: 'left' | 'right' | 'up' | 'down' | 'in' | 'out'\n ): boolean {\n ctx.save();\n ctx.beginPath();\n\n switch (direction) {\n case 'left':\n ctx.rect(0, 0, this.width * progress, this.height);\n break;\n case 'right':\n ctx.rect(this.width * (1 - progress), 0, this.width * progress, this.height);\n break;\n case 'up':\n ctx.rect(0, 0, this.width, this.height * progress);\n break;\n case 'down':\n ctx.rect(0, this.height * (1 - progress), this.width, this.height * progress);\n break;\n default:\n ctx.rect(0, 0, this.width * progress, this.height);\n }\n\n ctx.clip();\n return true;\n }\n\n private applyZoom(\n ctx: OffscreenCanvasRenderingContext2D,\n progress: number,\n direction?: 'left' | 'right' | 'up' | 'down' | 'in' | 'out'\n ): boolean {\n const scale = direction === 'out' ? 1 + (1 - progress) : progress;\n const centerX = this.width / 2;\n const centerY = this.height / 2;\n\n ctx.translate(centerX, centerY);\n ctx.scale(scale, scale);\n ctx.translate(-centerX, -centerY);\n\n if (direction === 'out') {\n ctx.globalAlpha = progress;\n }\n\n return true;\n }\n\n private applyRotate(ctx: OffscreenCanvasRenderingContext2D, progress: number): boolean {\n const rotation = (1 - progress) * Math.PI * 2;\n const centerX = this.width / 2;\n const centerY = this.height / 2;\n\n ctx.translate(centerX, centerY);\n ctx.rotate(rotation);\n ctx.translate(-centerX, -centerY);\n\n return true;\n }\n\n private applyDissolve(ctx: OffscreenCanvasRenderingContext2D, progress: number): boolean {\n // Simple dissolve using alpha\n ctx.globalAlpha = progress;\n ctx.globalCompositeOperation = 'multiply';\n return true;\n }\n\n /**\n * Create a transition mask for advanced effects\n */\n createTransitionMask(transition: TransitionEffect, canvas: OffscreenCanvas): ImageData | null {\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n\n const imageData = ctx.createImageData(this.width, this.height);\n const data = imageData.data;\n const progress = this.calculateEasedProgress(transition.progress, transition.easing);\n\n // Generate gradient mask based on transition type\n for (let y = 0; y < this.height; y++) {\n for (let x = 0; x < this.width; x++) {\n const index = (y * this.width + x) * 4;\n let alpha = 255;\n\n switch (transition.type) {\n case 'wipe':\n alpha = this.calculateWipeAlpha(x, y, progress, transition.direction);\n break;\n case 'dissolve':\n // Random noise dissolve\n alpha = Math.random() < progress ? 255 : 0;\n break;\n default:\n alpha = Math.floor(255 * progress);\n }\n\n data[index] = 255; // R\n data[index + 1] = 255; // G\n data[index + 2] = 255; // B\n data[index + 3] = alpha; // A\n }\n }\n\n return imageData;\n }\n\n private calculateWipeAlpha(\n x: number,\n y: number,\n progress: number,\n direction?: 'left' | 'right' | 'up' | 'down' | 'in' | 'out'\n ): number {\n let position = 0;\n\n switch (direction) {\n case 'left':\n position = x / this.width;\n break;\n case 'right':\n position = 1 - x / this.width;\n break;\n case 'up':\n position = y / this.height;\n break;\n case 'down':\n position = 1 - y / this.height;\n break;\n case 'in': {\n // Radial wipe from center\n const cx = x - this.width / 2;\n const cy = y - this.height / 2;\n const maxDist = Math.sqrt(this.width * this.width + this.height * this.height) / 2;\n position = Math.sqrt(cx * cx + cy * cy) / maxDist;\n break;\n }\n case 'out': {\n // Radial wipe to center\n const cx2 = x - this.width / 2;\n const cy2 = y - this.height / 2;\n const maxDist2 = Math.sqrt(this.width * this.width + this.height * this.height) / 2;\n position = 1 - Math.sqrt(cx2 * cx2 + cy2 * cy2) / maxDist2;\n break;\n }\n default:\n position = x / this.width;\n }\n\n return position < progress ? 255 : 0;\n }\n\n updateDimensions(width: number, height: number): void {\n this.width = width;\n this.height = height;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { frameDurationFromFps } from "../../utils/time-utils.js";
|
|
2
|
-
import { LayerRenderer } from "./LayerRenderer.js";
|
|
3
|
-
import { TransitionProcessor } from "./TransitionProcessor.js";
|
|
4
|
-
import { FilterProcessor } from "./FilterProcessor.js";
|
|
5
|
-
class VideoComposer {
|
|
6
|
-
config;
|
|
7
|
-
canvas;
|
|
8
|
-
ctx;
|
|
9
|
-
layerRenderer;
|
|
10
|
-
transitionProcessor;
|
|
11
|
-
filterProcessor;
|
|
12
|
-
timelineContext;
|
|
13
|
-
constructor(config) {
|
|
14
|
-
this.config = this.applyDefaults(config);
|
|
15
|
-
this.canvas = new OffscreenCanvas(this.config.width, this.config.height);
|
|
16
|
-
const ctx = this.canvas.getContext("2d", {
|
|
17
|
-
alpha: true,
|
|
18
|
-
desynchronized: true,
|
|
19
|
-
willReadFrequently: false,
|
|
20
|
-
colorSpace: "srgb"
|
|
21
|
-
});
|
|
22
|
-
if (!ctx) {
|
|
23
|
-
throw new Error("Failed to create 2D rendering context");
|
|
24
|
-
}
|
|
25
|
-
this.ctx = ctx;
|
|
26
|
-
this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
|
|
27
|
-
this.ctx.imageSmoothingQuality = "high";
|
|
28
|
-
this.layerRenderer = new LayerRenderer(ctx, this.config.width, this.config.height);
|
|
29
|
-
this.transitionProcessor = new TransitionProcessor(this.config.width, this.config.height);
|
|
30
|
-
this.filterProcessor = new FilterProcessor();
|
|
31
|
-
this.timelineContext = this.config.timeline;
|
|
32
|
-
}
|
|
33
|
-
applyDefaults(config) {
|
|
34
|
-
return {
|
|
35
|
-
width: config.width || 720,
|
|
36
|
-
height: config.height || 1280,
|
|
37
|
-
fps: config.fps || 30,
|
|
38
|
-
backgroundColor: config.backgroundColor ?? "#000",
|
|
39
|
-
renderer: config.renderer ?? "canvas2d",
|
|
40
|
-
enableSmoothing: config.enableSmoothing ?? true,
|
|
41
|
-
enableHardwareAcceleration: config.enableHardwareAcceleration ?? true,
|
|
42
|
-
revision: config.revision ?? 0,
|
|
43
|
-
inputHighWaterMark: config.inputHighWaterMark ?? 3,
|
|
44
|
-
outputHighWaterMark: config.outputHighWaterMark ?? 1,
|
|
45
|
-
maxLayers: config.maxLayers ?? 100,
|
|
46
|
-
timeline: config.timeline ?? {
|
|
47
|
-
clipId: "default",
|
|
48
|
-
trackId: "main",
|
|
49
|
-
clipStartUs: 0,
|
|
50
|
-
clipDurationUs: Infinity,
|
|
51
|
-
compositionFps: 30
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
createStreams(_instruction) {
|
|
56
|
-
if (_instruction?.baseConfig.timeline) {
|
|
57
|
-
this.timelineContext = _instruction.baseConfig.timeline;
|
|
58
|
-
}
|
|
59
|
-
const stream = new TransformStream(
|
|
60
|
-
{
|
|
61
|
-
transform: async (request, controller) => {
|
|
62
|
-
const result = await this.composeFrame(request);
|
|
63
|
-
controller.enqueue({
|
|
64
|
-
frame: result.frame,
|
|
65
|
-
metadata: result.metadata
|
|
66
|
-
});
|
|
67
|
-
},
|
|
68
|
-
flush: async () => {
|
|
69
|
-
this.filterProcessor.clearCache();
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
highWaterMark: this.config.inputHighWaterMark
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
highWaterMark: this.config.outputHighWaterMark
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
return {
|
|
80
|
-
composeStream: stream.writable,
|
|
81
|
-
cacheStream: stream.readable
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
async composeFrame(request) {
|
|
85
|
-
if (request.layers.length > this.config.maxLayers) {
|
|
86
|
-
throw new Error(`Too many layers: ${request.layers.length} > ${this.config.maxLayers}`);
|
|
87
|
-
}
|
|
88
|
-
this.clearCanvas();
|
|
89
|
-
if (request.transition) {
|
|
90
|
-
this.ctx.save();
|
|
91
|
-
this.transitionProcessor.applyTransition(this.ctx, request.transition);
|
|
92
|
-
}
|
|
93
|
-
for (const layer of request.layers) {
|
|
94
|
-
if (!layer.visible || layer.opacity <= 0) {
|
|
95
|
-
if (layer.type === "video") {
|
|
96
|
-
const vf = layer.videoFrame;
|
|
97
|
-
vf?.close?.();
|
|
98
|
-
}
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
if (layer.filters && layer.filters.length > 0) {
|
|
103
|
-
this.ctx.save();
|
|
104
|
-
this.filterProcessor.applyFilters(this.ctx, layer.filters);
|
|
105
|
-
}
|
|
106
|
-
await this.layerRenderer.renderLayer(layer);
|
|
107
|
-
if (layer.filters && layer.filters.length > 0) {
|
|
108
|
-
this.ctx.restore();
|
|
109
|
-
}
|
|
110
|
-
} finally {
|
|
111
|
-
if (layer.type === "video") {
|
|
112
|
-
const vf = layer.videoFrame;
|
|
113
|
-
vf?.close?.();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (request.transition) {
|
|
118
|
-
this.ctx.restore();
|
|
119
|
-
}
|
|
120
|
-
const frame = await this.createOutputFrame(request.timeUs);
|
|
121
|
-
const gopSerial = request.gopSerial;
|
|
122
|
-
const isKeyframe = request.isKeyframe;
|
|
123
|
-
return {
|
|
124
|
-
frame,
|
|
125
|
-
timeUs: request.timeUs,
|
|
126
|
-
metadata: {
|
|
127
|
-
layerCount: request.layers.length,
|
|
128
|
-
renderTime: 0,
|
|
129
|
-
gpuAccelerated: this.config.enableHardwareAcceleration && this.config.renderer !== "canvas2d",
|
|
130
|
-
...typeof gopSerial === "number" && { gopSerial },
|
|
131
|
-
...typeof isKeyframe === "boolean" && { isKeyframe }
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
async composeTransition(fromRequest, toRequest, transition) {
|
|
136
|
-
await this.composeFrame(fromRequest);
|
|
137
|
-
const toFrameRequest = {
|
|
138
|
-
...toRequest,
|
|
139
|
-
transition
|
|
140
|
-
};
|
|
141
|
-
return this.composeFrame(toFrameRequest);
|
|
142
|
-
}
|
|
143
|
-
clearCanvas() {
|
|
144
|
-
if (this.config.backgroundColor) {
|
|
145
|
-
this.ctx.fillStyle = this.config.backgroundColor;
|
|
146
|
-
this.ctx.fillRect(0, 0, this.config.width, this.config.height);
|
|
147
|
-
} else {
|
|
148
|
-
this.ctx.clearRect(0, 0, this.config.width, this.config.height);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
// private sortLayers(layers: Layer[]): Layer[] {
|
|
152
|
-
// return [...layers].sort((a, b) => a.zIndex - b.zIndex);
|
|
153
|
-
// }
|
|
154
|
-
async createOutputFrame(timeUs) {
|
|
155
|
-
const duration = frameDurationFromFps(this.timelineContext.compositionFps);
|
|
156
|
-
const frame = new VideoFrame(this.canvas, {
|
|
157
|
-
timestamp: timeUs,
|
|
158
|
-
duration,
|
|
159
|
-
alpha: "discard",
|
|
160
|
-
visibleRect: { x: 0, y: 0, width: this.canvas.width, height: this.canvas.height }
|
|
161
|
-
});
|
|
162
|
-
return frame;
|
|
163
|
-
}
|
|
164
|
-
updateConfig(config) {
|
|
165
|
-
Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));
|
|
166
|
-
if (config.width || config.height) {
|
|
167
|
-
this.canvas.width = this.config.width;
|
|
168
|
-
this.canvas.height = this.config.height;
|
|
169
|
-
this.layerRenderer.updateDimensions(this.config.width, this.config.height);
|
|
170
|
-
this.transitionProcessor.updateDimensions(this.config.width, this.config.height);
|
|
171
|
-
}
|
|
172
|
-
if (config.enableSmoothing !== void 0) {
|
|
173
|
-
this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
|
|
174
|
-
}
|
|
175
|
-
if (config.timeline) {
|
|
176
|
-
this.timelineContext = config.timeline;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
dispose() {
|
|
180
|
-
this.filterProcessor.clearCache();
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
export {
|
|
184
|
-
VideoComposer
|
|
185
|
-
};
|
|
186
|
-
//# sourceMappingURL=VideoComposer.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"VideoComposer.js","sources":["../../../src/stages/compose/VideoComposer.ts"],"sourcesContent":["import type {\n VideoComposeConfig,\n ComposeRequest,\n ComposeResult,\n TransitionEffect,\n VideoLayer,\n ComposeTimelineContext,\n} from './types';\nimport { frameDurationFromFps } from '../../utils/time-utils';\nimport { LayerRenderer } from './LayerRenderer';\nimport { TransitionProcessor } from './TransitionProcessor';\nimport { FilterProcessor } from './FilterProcessor';\nimport { ClipInstructionSet } from './instructions';\n\ninterface ComposeStreams {\n composeStream: WritableStream<ComposeRequest>;\n cacheStream: ReadableStream<VideoFrame>;\n}\n\n/**\n * VideoComposer - Main visual composition orchestrator\n */\nexport class VideoComposer {\n readonly config: Required<VideoComposeConfig>;\n readonly canvas: OffscreenCanvas;\n\n private ctx: OffscreenCanvasRenderingContext2D;\n private layerRenderer: LayerRenderer;\n private transitionProcessor: TransitionProcessor;\n private filterProcessor: FilterProcessor;\n private timelineContext: ComposeTimelineContext;\n\n constructor(config: VideoComposeConfig) {\n this.config = this.applyDefaults(config);\n this.canvas = new OffscreenCanvas(this.config.width, this.config.height);\n\n const ctx = this.canvas.getContext('2d', {\n alpha: true,\n desynchronized: true,\n willReadFrequently: false,\n colorSpace: 'srgb',\n });\n\n if (!ctx) {\n throw new Error('Failed to create 2D rendering context');\n }\n\n this.ctx = ctx;\n this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;\n this.ctx.imageSmoothingQuality = 'high';\n\n this.layerRenderer = new LayerRenderer(ctx, this.config.width, this.config.height);\n this.transitionProcessor = new TransitionProcessor(this.config.width, this.config.height);\n this.filterProcessor = new FilterProcessor();\n this.timelineContext = this.config.timeline;\n }\n\n private applyDefaults(config: VideoComposeConfig): Required<VideoComposeConfig> {\n return {\n width: config.width || 720,\n height: config.height || 1280,\n fps: config.fps || 30,\n backgroundColor: config.backgroundColor ?? '#000',\n renderer: config.renderer ?? 'canvas2d',\n enableSmoothing: config.enableSmoothing ?? true,\n enableHardwareAcceleration: config.enableHardwareAcceleration ?? true,\n revision: config.revision ?? 0,\n inputHighWaterMark: config.inputHighWaterMark ?? 3,\n outputHighWaterMark: config.outputHighWaterMark ?? 1,\n maxLayers: config.maxLayers ?? 100,\n timeline: config.timeline ?? {\n clipId: 'default',\n trackId: 'main',\n clipStartUs: 0,\n clipDurationUs: Infinity,\n compositionFps: 30,\n },\n };\n }\n\n createStreams(_instruction?: ClipInstructionSet): ComposeStreams {\n if (_instruction?.baseConfig.timeline) {\n this.timelineContext = _instruction.baseConfig.timeline;\n }\n\n // Always create new streams for each clip\n // ReadableStreams can only be consumed once\n const stream = new TransformStream<ComposeRequest, any>(\n {\n transform: async (request, controller) => {\n // console.log('[VideoComposer] transform', request, controller.desiredSize);\n const result = await this.composeFrame(request);\n controller.enqueue({\n frame: result.frame,\n metadata: result.metadata,\n });\n },\n\n flush: async () => {\n this.filterProcessor.clearCache();\n },\n },\n {\n highWaterMark: this.config.inputHighWaterMark,\n },\n {\n highWaterMark: this.config.outputHighWaterMark,\n }\n );\n // const [encodeStream, cacheStream] = stream.readable.tee();\n return {\n composeStream: stream.writable,\n cacheStream: stream.readable,\n };\n }\n\n async composeFrame(request: ComposeRequest): Promise<ComposeResult> {\n if (request.layers.length > this.config.maxLayers) {\n throw new Error(`Too many layers: ${request.layers.length} > ${this.config.maxLayers}`);\n }\n\n this.clearCanvas();\n // const sortedLayers = this.sortLayers(request.layers);\n\n if (request.transition) {\n this.ctx.save();\n this.transitionProcessor.applyTransition(this.ctx, request.transition);\n }\n\n for (const layer of request.layers) {\n if (!layer.visible || layer.opacity <= 0) {\n // Close video frame for invisible layers\n if ((layer as any).type === 'video') {\n const vf = (layer as VideoLayer).videoFrame;\n vf?.close?.();\n }\n continue;\n }\n\n try {\n if (layer.filters && layer.filters.length > 0) {\n this.ctx.save();\n this.filterProcessor.applyFilters(this.ctx, layer.filters);\n }\n\n await this.layerRenderer.renderLayer(layer);\n\n if (layer.filters && layer.filters.length > 0) {\n this.ctx.restore();\n }\n } finally {\n // Always close video frame after rendering (or on error)\n if ((layer as any).type === 'video') {\n const vf = (layer as VideoLayer).videoFrame;\n vf?.close?.();\n }\n }\n }\n\n if (request.transition) {\n this.ctx.restore();\n }\n\n const frame = await this.createOutputFrame(request.timeUs);\n\n const gopSerial = (request as any).gopSerial;\n const isKeyframe = (request as any).isKeyframe;\n\n return {\n frame,\n timeUs: request.timeUs,\n metadata: {\n layerCount: request.layers.length,\n renderTime: 0,\n gpuAccelerated:\n this.config.enableHardwareAcceleration && this.config.renderer !== 'canvas2d',\n ...(typeof gopSerial === 'number' && { gopSerial }),\n ...(typeof isKeyframe === 'boolean' && { isKeyframe }),\n },\n };\n }\n\n async composeTransition(\n fromRequest: ComposeRequest,\n toRequest: ComposeRequest,\n transition: TransitionEffect\n ): Promise<ComposeResult> {\n await this.composeFrame(fromRequest);\n\n const toFrameRequest = {\n ...toRequest,\n transition,\n };\n\n return this.composeFrame(toFrameRequest);\n }\n\n private clearCanvas(): void {\n if (this.config.backgroundColor) {\n this.ctx.fillStyle = this.config.backgroundColor;\n this.ctx.fillRect(0, 0, this.config.width, this.config.height);\n } else {\n this.ctx.clearRect(0, 0, this.config.width, this.config.height);\n }\n }\n\n // private sortLayers(layers: Layer[]): Layer[] {\n // return [...layers].sort((a, b) => a.zIndex - b.zIndex);\n // }\n\n private async createOutputFrame(timeUs: number): Promise<VideoFrame> {\n const duration = frameDurationFromFps(this.timelineContext.compositionFps);\n const frame = new VideoFrame(this.canvas, {\n timestamp: timeUs,\n duration,\n alpha: 'discard',\n visibleRect: { x: 0, y: 0, width: this.canvas.width, height: this.canvas.height },\n });\n return frame;\n }\n\n updateConfig(config: Partial<VideoComposeConfig>): void {\n Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));\n\n if (config.width || config.height) {\n this.canvas.width = this.config.width;\n this.canvas.height = this.config.height;\n this.layerRenderer.updateDimensions(this.config.width, this.config.height);\n this.transitionProcessor.updateDimensions(this.config.width, this.config.height);\n }\n\n if (config.enableSmoothing !== undefined) {\n this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;\n }\n\n if (config.timeline) {\n this.timelineContext = config.timeline;\n }\n }\n\n dispose(): void {\n this.filterProcessor.clearCache();\n }\n}\n"],"names":[],"mappings":"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"}
|