@meframe/core 0.0.28 → 0.0.30-beta
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 +2 -13
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +6 -100
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +35 -19
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +223 -134
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/VideoL1Cache.d.ts +15 -2
- package/dist/cache/l1/VideoL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/VideoL1Cache.js +58 -38
- package/dist/cache/l1/VideoL1Cache.js.map +1 -1
- package/dist/cache/l2/L2Cache.d.ts.map +1 -1
- package/dist/cache/l2/L2Cache.js +5 -5
- package/dist/cache/l2/L2Cache.js.map +1 -1
- package/dist/cache/l2/L2OPFSStore.d.ts +37 -0
- package/dist/cache/l2/L2OPFSStore.d.ts.map +1 -0
- package/dist/cache/l2/L2OPFSStore.js +89 -0
- package/dist/cache/l2/L2OPFSStore.js.map +1 -0
- package/dist/cache/resource/AudioSampleCache.d.ts +52 -0
- package/dist/cache/resource/AudioSampleCache.d.ts.map +1 -0
- package/dist/cache/resource/AudioSampleCache.js +69 -0
- package/dist/cache/resource/AudioSampleCache.js.map +1 -0
- package/dist/cache/resource/ImageBitmapCache.d.ts +65 -0
- package/dist/cache/resource/ImageBitmapCache.d.ts.map +1 -0
- package/dist/cache/resource/ImageBitmapCache.js +101 -0
- package/dist/cache/resource/ImageBitmapCache.js.map +1 -0
- package/dist/cache/resource/MP4IndexCache.d.ts +48 -0
- package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -0
- package/dist/cache/resource/MP4IndexCache.js +104 -0
- package/dist/cache/resource/MP4IndexCache.js.map +1 -0
- package/dist/cache/resource/ResourceCache.d.ts +46 -0
- package/dist/cache/resource/ResourceCache.d.ts.map +1 -0
- package/dist/cache/resource/ResourceCache.js +92 -0
- package/dist/cache/resource/ResourceCache.js.map +1 -0
- package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts +75 -0
- package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts.map +1 -0
- package/dist/cache/{l2/IndexedDBStore.js → storage/indexeddb/ChunkRecordStore.js} +3 -3
- package/dist/cache/storage/indexeddb/ChunkRecordStore.js.map +1 -0
- package/dist/cache/storage/opfs/OPFSManager.d.ts +54 -0
- package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -0
- package/dist/cache/storage/opfs/OPFSManager.js +133 -0
- package/dist/cache/storage/opfs/OPFSManager.js.map +1 -0
- package/dist/cache/storage/opfs/types.d.ts +16 -0
- package/dist/cache/storage/opfs/types.d.ts.map +1 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +21 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/types.d.ts +28 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/controllers/ExportController.d.ts +16 -0
- package/dist/controllers/ExportController.d.ts.map +1 -0
- package/dist/controllers/ExportController.js +44 -0
- package/dist/controllers/ExportController.js.map +1 -0
- package/dist/controllers/PlaybackController.d.ts +28 -4
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +117 -52
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/controllers/index.d.ts +2 -3
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/types.d.ts +0 -28
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/event/events.d.ts +8 -0
- package/dist/event/events.d.ts.map +1 -1
- package/dist/event/events.js +1 -0
- package/dist/event/events.js.map +1 -1
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +11 -6
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/RcFrame.d.ts +2 -0
- package/dist/model/RcFrame.d.ts.map +1 -1
- package/dist/model/RcFrame.js +3 -0
- package/dist/model/RcFrame.js.map +1 -1
- package/dist/orchestrator/ExportScheduler.d.ts +35 -0
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -0
- package/dist/orchestrator/ExportScheduler.js +241 -0
- package/dist/orchestrator/ExportScheduler.js.map +1 -0
- package/dist/orchestrator/GlobalAudioSession.d.ts +21 -7
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +132 -140
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/OnDemandVideoSession.d.ts +73 -0
- package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -0
- package/dist/orchestrator/OnDemandVideoSession.js +281 -0
- package/dist/orchestrator/OnDemandVideoSession.js.map +1 -0
- package/dist/orchestrator/Orchestrator.d.ts +22 -17
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +234 -301
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +3 -15
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/index.d.ts +0 -1
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/types.d.ts +4 -4
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/compose/FilterProcessor.d.ts +1 -1
- package/dist/stages/compose/FilterProcessor.d.ts.map +1 -1
- package/dist/stages/compose/FilterProcessor.js +226 -0
- package/dist/stages/compose/FilterProcessor.js.map +1 -0
- package/dist/stages/compose/FrameRateConverter.d.ts +68 -0
- package/dist/stages/compose/FrameRateConverter.d.ts.map +1 -0
- package/dist/stages/compose/LayerRenderer.d.ts +1 -1
- package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
- package/dist/stages/compose/LayerRenderer.js +270 -0
- package/dist/stages/compose/LayerRenderer.js.map +1 -0
- package/dist/stages/compose/TransitionProcessor.d.ts +1 -1
- package/dist/stages/compose/TransitionProcessor.d.ts.map +1 -1
- package/dist/stages/compose/TransitionProcessor.js +189 -0
- package/dist/stages/compose/TransitionProcessor.js.map +1 -0
- package/dist/stages/compose/VideoComposer.d.ts +6 -4
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/compose/VideoComposer.js +229 -0
- package/dist/stages/compose/VideoComposer.js.map +1 -0
- package/dist/stages/compose/text-renderers/animation-utils.js +76 -0
- package/dist/stages/compose/text-renderers/animation-utils.js.map +1 -0
- package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +2 -2
- package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
- package/dist/stages/compose/text-renderers/basic-text-renderer.js +93 -0
- package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -0
- package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +1 -1
- package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -1
- package/dist/stages/compose/text-renderers/character-ktv-renderer.js +132 -0
- package/dist/stages/compose/text-renderers/character-ktv-renderer.js.map +1 -0
- package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +1 -1
- package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -1
- package/dist/stages/compose/text-renderers/word-by-word-renderer.js +128 -0
- package/dist/stages/compose/text-renderers/word-by-word-renderer.js.map +1 -0
- package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +1 -1
- package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -1
- package/dist/stages/compose/text-renderers/word-fancy-renderer.js +135 -0
- package/dist/stages/compose/text-renderers/word-fancy-renderer.js.map +1 -0
- package/dist/stages/compose/text-utils/locale-detector.js +16 -0
- package/dist/stages/compose/text-utils/locale-detector.js.map +1 -0
- package/dist/stages/compose/text-utils/text-metrics.js +21 -0
- package/dist/stages/compose/text-utils/text-metrics.js.map +1 -0
- package/dist/stages/compose/text-utils/text-wrapper.js +225 -0
- package/dist/stages/compose/text-utils/text-wrapper.js.map +1 -0
- package/dist/stages/compose/types.d.ts +2 -1
- package/dist/stages/compose/types.d.ts.map +1 -1
- package/dist/stages/decode/BaseDecoder.js +0 -3
- package/dist/stages/decode/BaseDecoder.js.map +1 -1
- package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
- package/dist/stages/demux/MP4Demuxer.js +281 -0
- package/dist/stages/demux/MP4Demuxer.js.map +1 -0
- package/dist/stages/demux/MP4IndexParser.d.ts +71 -0
- package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -0
- package/dist/stages/demux/MP4IndexParser.js +416 -0
- package/dist/stages/demux/MP4IndexParser.js.map +1 -0
- package/dist/stages/demux/types.d.ts +48 -0
- package/dist/stages/demux/types.d.ts.map +1 -1
- package/dist/stages/encode/index.d.ts +0 -1
- package/dist/stages/encode/index.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +44 -2
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +281 -37
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +6 -2
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +27 -4
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/stages/load/types.d.ts +7 -0
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.d.ts +2 -2
- package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.js +24 -13
- package/dist/stages/mux/MP4Muxer.js.map +1 -1
- package/dist/stages/mux/MuxManager.d.ts +10 -21
- package/dist/stages/mux/MuxManager.d.ts.map +1 -1
- package/dist/stages/mux/MuxManager.js +21 -162
- package/dist/stages/mux/MuxManager.js.map +1 -1
- package/dist/stages/mux/index.d.ts +0 -1
- package/dist/stages/mux/index.d.ts.map +1 -1
- package/dist/utils/binary-search.d.ts +12 -4
- package/dist/utils/binary-search.d.ts.map +1 -1
- package/dist/utils/binary-search.js +52 -6
- package/dist/utils/binary-search.js.map +1 -1
- package/dist/workers/{BaseDecoder.BWYu1W0B.js → BaseDecoder.CTW-vr29.js} +1 -4
- package/dist/workers/BaseDecoder.CTW-vr29.js.map +1 -0
- package/dist/workers/{MP4Demuxer.CFHDkPYc.js → MP4Demuxer.BEa6PLJm.js} +10 -3
- package/dist/workers/{MP4Demuxer.CFHDkPYc.js.map → MP4Demuxer.BEa6PLJm.js.map} +1 -1
- package/dist/workers/stages/compose/{video-compose.worker.M5uomNVr.js → video-compose.worker.DHQ8B105.js} +260 -83
- package/dist/workers/stages/compose/video-compose.worker.DHQ8B105.js.map +1 -0
- package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js → audio-decode.worker.CP8bXXa4.js} +2 -2
- package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js.map → audio-decode.worker.CP8bXXa4.js.map} +1 -1
- package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js → video-decode.worker.BIspTxgV.js} +2 -2
- package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js.map → video-decode.worker.BIspTxgV.js.map} +1 -1
- package/dist/workers/stages/demux/{audio-demux.worker.BTFPcY7P.js → audio-demux.worker._VRQdLdv.js} +2 -2
- package/dist/workers/stages/demux/{audio-demux.worker.BTFPcY7P.js.map → audio-demux.worker._VRQdLdv.js.map} +1 -1
- package/dist/workers/stages/demux/{video-demux.worker.D_WeHPkt.js → video-demux.worker.CSkxGtmx.js} +3 -19
- package/dist/workers/stages/demux/video-demux.worker.CSkxGtmx.js.map +1 -0
- package/dist/workers/worker-manifest.json +5 -5
- package/package.json +1 -1
- package/dist/cache/l2/IndexedDBStore.js.map +0 -1
- package/dist/cache/l2/OPFSStore.js +0 -131
- package/dist/cache/l2/OPFSStore.js.map +0 -1
- package/dist/controllers/PreRenderService.d.ts +0 -59
- package/dist/controllers/PreRenderService.d.ts.map +0 -1
- package/dist/controllers/PreRenderService.js +0 -185
- package/dist/controllers/PreRenderService.js.map +0 -1
- package/dist/controllers/PreRenderTaskQueue.d.ts +0 -21
- package/dist/controllers/PreRenderTaskQueue.d.ts.map +0 -1
- package/dist/orchestrator/ClipSessionManager.d.ts +0 -70
- package/dist/orchestrator/ClipSessionManager.d.ts.map +0 -1
- package/dist/orchestrator/ClipSessionManager.js +0 -158
- package/dist/orchestrator/ClipSessionManager.js.map +0 -1
- package/dist/stages/decode/AudioChunkDecoder.js +0 -169
- package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
- package/dist/stages/encode/ClipEncoderManager.d.ts +0 -64
- package/dist/stages/encode/ClipEncoderManager.d.ts.map +0 -1
- package/dist/stages/mux/OPFSWriter.d.ts +0 -46
- package/dist/stages/mux/OPFSWriter.d.ts.map +0 -1
- package/dist/utils/BackpressureAdapter.d.ts +0 -26
- package/dist/utils/BackpressureAdapter.d.ts.map +0 -1
- package/dist/utils/time-utils.js +0 -45
- package/dist/utils/time-utils.js.map +0 -1
- package/dist/workers/BaseDecoder.BWYu1W0B.js.map +0 -1
- package/dist/workers/stages/compose/video-compose.worker.M5uomNVr.js.map +0 -1
- package/dist/workers/stages/demux/video-demux.worker.D_WeHPkt.js.map +0 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
class FilterProcessor {
|
|
2
|
+
filterCache = /* @__PURE__ */ new Map();
|
|
3
|
+
/**
|
|
4
|
+
* Apply filters to canvas context
|
|
5
|
+
* Combines multiple filters into a single CSS filter string for performance
|
|
6
|
+
*/
|
|
7
|
+
applyFilters(ctx, filters) {
|
|
8
|
+
if (!filters || filters.length === 0) {
|
|
9
|
+
ctx.filter = "none";
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const cacheKey = this.generateCacheKey(filters);
|
|
13
|
+
let filterString = this.filterCache.get(cacheKey);
|
|
14
|
+
if (!filterString) {
|
|
15
|
+
filterString = this.buildFilterString(filters);
|
|
16
|
+
this.filterCache.set(cacheKey, filterString);
|
|
17
|
+
}
|
|
18
|
+
ctx.filter = filterString;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build CSS filter string from filter array
|
|
22
|
+
*/
|
|
23
|
+
buildFilterString(filters) {
|
|
24
|
+
const filterStrings = [];
|
|
25
|
+
for (const filter of filters) {
|
|
26
|
+
const filterStr = this.buildSingleFilter(filter);
|
|
27
|
+
if (filterStr) {
|
|
28
|
+
filterStrings.push(filterStr);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return filterStrings.length > 0 ? filterStrings.join(" ") : "none";
|
|
32
|
+
}
|
|
33
|
+
buildSingleFilter(filter) {
|
|
34
|
+
switch (filter.type) {
|
|
35
|
+
case "blur":
|
|
36
|
+
return `blur(${filter.value ?? 0}px)`;
|
|
37
|
+
case "brightness":
|
|
38
|
+
return `brightness(${filter.value ?? 1})`;
|
|
39
|
+
case "contrast":
|
|
40
|
+
return `contrast(${filter.value ?? 1})`;
|
|
41
|
+
case "grayscale":
|
|
42
|
+
return `grayscale(${filter.value ?? 0})`;
|
|
43
|
+
case "hue-rotate":
|
|
44
|
+
return `hue-rotate(${filter.value ?? 0}deg)`;
|
|
45
|
+
case "saturate":
|
|
46
|
+
return `saturate(${filter.value ?? 1})`;
|
|
47
|
+
case "sepia":
|
|
48
|
+
return `sepia(${filter.value ?? 0})`;
|
|
49
|
+
case "custom":
|
|
50
|
+
return this.buildCustomFilter(filter);
|
|
51
|
+
default:
|
|
52
|
+
console.warn(`Unknown filter type: ${filter.type}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build custom filter from params
|
|
58
|
+
*/
|
|
59
|
+
buildCustomFilter(filter) {
|
|
60
|
+
if (!filter.params) return null;
|
|
61
|
+
const { type, ...params } = filter.params;
|
|
62
|
+
switch (type) {
|
|
63
|
+
case "drop-shadow":
|
|
64
|
+
return `drop-shadow(${params.offsetX}px ${params.offsetY}px ${params.blur}px ${params.color})`;
|
|
65
|
+
case "opacity":
|
|
66
|
+
return `opacity(${params.value})`;
|
|
67
|
+
case "invert":
|
|
68
|
+
return `invert(${params.value})`;
|
|
69
|
+
default:
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Apply color matrix transformation for advanced effects
|
|
75
|
+
* This allows for more complex color manipulations than CSS filters
|
|
76
|
+
*/
|
|
77
|
+
applyColorMatrix(imageData, matrix) {
|
|
78
|
+
if (matrix.length !== 20) {
|
|
79
|
+
throw new Error("Color matrix must have 20 values (4x5 matrix)");
|
|
80
|
+
}
|
|
81
|
+
const data = imageData.data;
|
|
82
|
+
const length = data.length;
|
|
83
|
+
for (let i = 0; i < length; i += 4) {
|
|
84
|
+
const r = data[i];
|
|
85
|
+
const g = data[i + 1];
|
|
86
|
+
const b = data[i + 2];
|
|
87
|
+
const a = data[i + 3];
|
|
88
|
+
const m = matrix;
|
|
89
|
+
data[i] = this.clamp(r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255);
|
|
90
|
+
data[i + 1] = this.clamp(r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255);
|
|
91
|
+
data[i + 2] = this.clamp(r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255);
|
|
92
|
+
data[i + 3] = this.clamp(r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255);
|
|
93
|
+
}
|
|
94
|
+
return imageData;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Predefined color matrices for common effects
|
|
98
|
+
*/
|
|
99
|
+
getPresetMatrix(preset) {
|
|
100
|
+
switch (preset) {
|
|
101
|
+
case "vintage":
|
|
102
|
+
return [
|
|
103
|
+
0.393,
|
|
104
|
+
0.769,
|
|
105
|
+
0.189,
|
|
106
|
+
0,
|
|
107
|
+
0,
|
|
108
|
+
0.349,
|
|
109
|
+
0.686,
|
|
110
|
+
0.168,
|
|
111
|
+
0,
|
|
112
|
+
0,
|
|
113
|
+
0.272,
|
|
114
|
+
0.534,
|
|
115
|
+
0.131,
|
|
116
|
+
0,
|
|
117
|
+
0,
|
|
118
|
+
0,
|
|
119
|
+
0,
|
|
120
|
+
0,
|
|
121
|
+
1,
|
|
122
|
+
0
|
|
123
|
+
];
|
|
124
|
+
case "noir":
|
|
125
|
+
return [
|
|
126
|
+
0.25,
|
|
127
|
+
0.25,
|
|
128
|
+
0.25,
|
|
129
|
+
0,
|
|
130
|
+
0,
|
|
131
|
+
0.25,
|
|
132
|
+
0.25,
|
|
133
|
+
0.25,
|
|
134
|
+
0,
|
|
135
|
+
0,
|
|
136
|
+
0.25,
|
|
137
|
+
0.25,
|
|
138
|
+
0.25,
|
|
139
|
+
0,
|
|
140
|
+
0,
|
|
141
|
+
0,
|
|
142
|
+
0,
|
|
143
|
+
0,
|
|
144
|
+
1,
|
|
145
|
+
0
|
|
146
|
+
];
|
|
147
|
+
case "cool":
|
|
148
|
+
return [0.8, 0, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1, 0];
|
|
149
|
+
case "warm":
|
|
150
|
+
return [1.2, 0, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 0, 0.8, 0, 0, 0, 0, 0, 1, 0];
|
|
151
|
+
default:
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Apply Gaussian blur manually (for cases where CSS filter is not enough)
|
|
157
|
+
*/
|
|
158
|
+
applyGaussianBlur(imageData, radius) {
|
|
159
|
+
const output = new ImageData(
|
|
160
|
+
new Uint8ClampedArray(imageData.data),
|
|
161
|
+
imageData.width,
|
|
162
|
+
imageData.height
|
|
163
|
+
);
|
|
164
|
+
const width = imageData.width;
|
|
165
|
+
const height = imageData.height;
|
|
166
|
+
const data = imageData.data;
|
|
167
|
+
const outData = output.data;
|
|
168
|
+
for (let y = 0; y < height; y++) {
|
|
169
|
+
for (let x = 0; x < width; x++) {
|
|
170
|
+
let r = 0, g = 0, b = 0, a = 0;
|
|
171
|
+
let count = 0;
|
|
172
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
173
|
+
const nx = Math.min(Math.max(x + dx, 0), width - 1);
|
|
174
|
+
const idx2 = (y * width + nx) * 4;
|
|
175
|
+
r += data[idx2];
|
|
176
|
+
g += data[idx2 + 1];
|
|
177
|
+
b += data[idx2 + 2];
|
|
178
|
+
a += data[idx2 + 3];
|
|
179
|
+
count++;
|
|
180
|
+
}
|
|
181
|
+
const idx = (y * width + x) * 4;
|
|
182
|
+
outData[idx] = r / count;
|
|
183
|
+
outData[idx + 1] = g / count;
|
|
184
|
+
outData[idx + 2] = b / count;
|
|
185
|
+
outData[idx + 3] = a / count;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (let x = 0; x < width; x++) {
|
|
189
|
+
for (let y = 0; y < height; y++) {
|
|
190
|
+
let r = 0, g = 0, b = 0, a = 0;
|
|
191
|
+
let count = 0;
|
|
192
|
+
for (let dy = -radius; dy <= radius; dy++) {
|
|
193
|
+
const ny = Math.min(Math.max(y + dy, 0), height - 1);
|
|
194
|
+
const idx2 = (ny * width + x) * 4;
|
|
195
|
+
r += outData[idx2];
|
|
196
|
+
g += outData[idx2 + 1];
|
|
197
|
+
b += outData[idx2 + 2];
|
|
198
|
+
a += outData[idx2 + 3];
|
|
199
|
+
count++;
|
|
200
|
+
}
|
|
201
|
+
const idx = (y * width + x) * 4;
|
|
202
|
+
data[idx] = r / count;
|
|
203
|
+
data[idx + 1] = g / count;
|
|
204
|
+
data[idx + 2] = b / count;
|
|
205
|
+
data[idx + 3] = a / count;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return imageData;
|
|
209
|
+
}
|
|
210
|
+
clamp(value) {
|
|
211
|
+
return Math.min(255, Math.max(0, Math.round(value)));
|
|
212
|
+
}
|
|
213
|
+
generateCacheKey(filters) {
|
|
214
|
+
return filters.map((f) => `${f.type}:${f.value ?? "default"}`).join("|");
|
|
215
|
+
}
|
|
216
|
+
clearCache() {
|
|
217
|
+
this.filterCache.clear();
|
|
218
|
+
}
|
|
219
|
+
getCacheSize() {
|
|
220
|
+
return this.filterCache.size;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
FilterProcessor
|
|
225
|
+
};
|
|
226
|
+
//# sourceMappingURL=FilterProcessor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilterProcessor.js","sources":["../../../src/stages/compose/FilterProcessor.ts"],"sourcesContent":["import type { VisualFilter } from './types';\n\n/**\n * FilterProcessor - Handles visual filters and effects\n * Single responsibility: Apply CSS filters and custom shader effects\n */\nexport class FilterProcessor {\n private filterCache = new Map<string, string>();\n\n /**\n * Apply filters to canvas context\n * Combines multiple filters into a single CSS filter string for performance\n */\n applyFilters(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n filters: VisualFilter[]\n ): void {\n if (!filters || filters.length === 0) {\n ctx.filter = 'none';\n return;\n }\n\n // Generate cache key\n const cacheKey = this.generateCacheKey(filters);\n\n // Check cache\n let filterString = this.filterCache.get(cacheKey);\n\n if (!filterString) {\n filterString = this.buildFilterString(filters);\n this.filterCache.set(cacheKey, filterString);\n }\n\n ctx.filter = filterString;\n }\n\n /**\n * Build CSS filter string from filter array\n */\n private buildFilterString(filters: VisualFilter[]): string {\n const filterStrings: string[] = [];\n\n for (const filter of filters) {\n const filterStr = this.buildSingleFilter(filter);\n if (filterStr) {\n filterStrings.push(filterStr);\n }\n }\n\n return filterStrings.length > 0 ? filterStrings.join(' ') : 'none';\n }\n\n private buildSingleFilter(filter: VisualFilter): string | null {\n switch (filter.type) {\n case 'blur':\n return `blur(${filter.value ?? 0}px)`;\n\n case 'brightness':\n return `brightness(${filter.value ?? 1})`;\n\n case 'contrast':\n return `contrast(${filter.value ?? 1})`;\n\n case 'grayscale':\n return `grayscale(${filter.value ?? 0})`;\n\n case 'hue-rotate':\n return `hue-rotate(${filter.value ?? 0}deg)`;\n\n case 'saturate':\n return `saturate(${filter.value ?? 1})`;\n\n case 'sepia':\n return `sepia(${filter.value ?? 0})`;\n\n case 'custom':\n return this.buildCustomFilter(filter);\n\n default:\n console.warn(`Unknown filter type: ${filter.type}`);\n return null;\n }\n }\n\n /**\n * Build custom filter from params\n */\n private buildCustomFilter(filter: VisualFilter): string | null {\n if (!filter.params) return null;\n\n const { type, ...params } = filter.params;\n\n switch (type) {\n case 'drop-shadow':\n return `drop-shadow(${params.offsetX}px ${params.offsetY}px ${params.blur}px ${params.color})`;\n\n case 'opacity':\n return `opacity(${params.value})`;\n\n case 'invert':\n return `invert(${params.value})`;\n\n default:\n return null;\n }\n }\n\n /**\n * Apply color matrix transformation for advanced effects\n * This allows for more complex color manipulations than CSS filters\n */\n applyColorMatrix(imageData: ImageData, matrix: number[]): ImageData {\n if (matrix.length !== 20) {\n throw new Error('Color matrix must have 20 values (4x5 matrix)');\n }\n\n const data = imageData.data;\n const length = data.length;\n\n for (let i = 0; i < length; i += 4) {\n const r = data[i]!;\n const g = data[i + 1]!;\n const b = data[i + 2]!;\n const a = data[i + 3]!;\n const m = matrix;\n\n // Apply matrix transformation\n data[i] = this.clamp(r * m[0]! + g * m[1]! + b * m[2]! + a * m[3]! + m[4]! * 255);\n data[i + 1] = this.clamp(r * m[5]! + g * m[6]! + b * m[7]! + a * m[8]! + m[9]! * 255);\n data[i + 2] = this.clamp(r * m[10]! + g * m[11]! + b * m[12]! + a * m[13]! + m[14]! * 255);\n data[i + 3] = this.clamp(r * m[15]! + g * m[16]! + b * m[17]! + a * m[18]! + m[19]! * 255);\n }\n\n return imageData;\n }\n\n /**\n * Predefined color matrices for common effects\n */\n getPresetMatrix(preset: string): number[] | null {\n switch (preset) {\n case 'vintage':\n return [\n 0.393, 0.769, 0.189, 0, 0, 0.349, 0.686, 0.168, 0, 0, 0.272, 0.534, 0.131, 0, 0, 0, 0, 0,\n 1, 0,\n ];\n\n case 'noir':\n return [\n 0.25, 0.25, 0.25, 0, 0, 0.25, 0.25, 0.25, 0, 0, 0.25, 0.25, 0.25, 0, 0, 0, 0, 0, 1, 0,\n ];\n\n case 'cool':\n return [0.8, 0, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1, 0];\n\n case 'warm':\n return [1.2, 0, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 0, 0.8, 0, 0, 0, 0, 0, 1, 0];\n\n default:\n return null;\n }\n }\n\n /**\n * Apply Gaussian blur manually (for cases where CSS filter is not enough)\n */\n applyGaussianBlur(imageData: ImageData, radius: number): ImageData {\n // Simplified box blur approximation of Gaussian blur\n const output = new ImageData(\n new Uint8ClampedArray(imageData.data),\n imageData.width,\n imageData.height\n );\n\n const width = imageData.width;\n const height = imageData.height;\n const data = imageData.data;\n const outData = output.data;\n\n // Horizontal pass\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n let r = 0,\n g = 0,\n b = 0,\n a = 0;\n let count = 0;\n\n for (let dx = -radius; dx <= radius; dx++) {\n const nx = Math.min(Math.max(x + dx, 0), width - 1);\n const idx = (y * width + nx) * 4;\n r += data[idx]!;\n g += data[idx + 1]!;\n b += data[idx + 2]!;\n a += data[idx + 3]!;\n count++;\n }\n\n const idx = (y * width + x) * 4;\n outData[idx] = r / count;\n outData[idx + 1] = g / count;\n outData[idx + 2] = b / count;\n outData[idx + 3] = a / count;\n }\n }\n\n // Vertical pass\n for (let x = 0; x < width; x++) {\n for (let y = 0; y < height; y++) {\n let r = 0,\n g = 0,\n b = 0,\n a = 0;\n let count = 0;\n\n for (let dy = -radius; dy <= radius; dy++) {\n const ny = Math.min(Math.max(y + dy, 0), height - 1);\n const idx = (ny * width + x) * 4;\n r += outData[idx]!;\n g += outData[idx + 1]!;\n b += outData[idx + 2]!;\n a += outData[idx + 3]!;\n count++;\n }\n\n const idx = (y * width + x) * 4;\n data[idx] = r / count;\n data[idx + 1] = g / count;\n data[idx + 2] = b / count;\n data[idx + 3] = a / count;\n }\n }\n\n return imageData;\n }\n\n private clamp(value: number): number {\n return Math.min(255, Math.max(0, Math.round(value)));\n }\n\n private generateCacheKey(filters: VisualFilter[]): string {\n return filters.map((f) => `${f.type}:${f.value ?? 'default'}`).join('|');\n }\n\n clearCache(): void {\n this.filterCache.clear();\n }\n\n getCacheSize(): number {\n return this.filterCache.size;\n }\n}\n"],"names":["idx"],"mappings":"AAMO,MAAM,gBAAgB;AAAA,EACnB,kCAAkB,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM1B,aACE,KACA,SACM;AACN,QAAI,CAAC,WAAW,QAAQ,WAAW,GAAG;AACpC,UAAI,SAAS;AACb;AAAA,IACF;AAGA,UAAM,WAAW,KAAK,iBAAiB,OAAO;AAG9C,QAAI,eAAe,KAAK,YAAY,IAAI,QAAQ;AAEhD,QAAI,CAAC,cAAc;AACjB,qBAAe,KAAK,kBAAkB,OAAO;AAC7C,WAAK,YAAY,IAAI,UAAU,YAAY;AAAA,IAC7C;AAEA,QAAI,SAAS;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKQ,kBAAkB,SAAiC;AACzD,UAAM,gBAA0B,CAAA;AAEhC,eAAW,UAAU,SAAS;AAC5B,YAAM,YAAY,KAAK,kBAAkB,MAAM;AAC/C,UAAI,WAAW;AACb,sBAAc,KAAK,SAAS;AAAA,MAC9B;AAAA,IACF;AAEA,WAAO,cAAc,SAAS,IAAI,cAAc,KAAK,GAAG,IAAI;AAAA,EAC9D;AAAA,EAEQ,kBAAkB,QAAqC;AAC7D,YAAQ,OAAO,MAAA;AAAA,MACb,KAAK;AACH,eAAO,QAAQ,OAAO,SAAS,CAAC;AAAA,MAElC,KAAK;AACH,eAAO,cAAc,OAAO,SAAS,CAAC;AAAA,MAExC,KAAK;AACH,eAAO,YAAY,OAAO,SAAS,CAAC;AAAA,MAEtC,KAAK;AACH,eAAO,aAAa,OAAO,SAAS,CAAC;AAAA,MAEvC,KAAK;AACH,eAAO,cAAc,OAAO,SAAS,CAAC;AAAA,MAExC,KAAK;AACH,eAAO,YAAY,OAAO,SAAS,CAAC;AAAA,MAEtC,KAAK;AACH,eAAO,SAAS,OAAO,SAAS,CAAC;AAAA,MAEnC,KAAK;AACH,eAAO,KAAK,kBAAkB,MAAM;AAAA,MAEtC;AACE,gBAAQ,KAAK,wBAAwB,OAAO,IAAI,EAAE;AAClD,eAAO;AAAA,IAAA;AAAA,EAEb;AAAA;AAAA;AAAA;AAAA,EAKQ,kBAAkB,QAAqC;AAC7D,QAAI,CAAC,OAAO,OAAQ,QAAO;AAE3B,UAAM,EAAE,MAAM,GAAG,OAAA,IAAW,OAAO;AAEnC,YAAQ,MAAA;AAAA,MACN,KAAK;AACH,eAAO,eAAe,OAAO,OAAO,MAAM,OAAO,OAAO,MAAM,OAAO,IAAI,MAAM,OAAO,KAAK;AAAA,MAE7F,KAAK;AACH,eAAO,WAAW,OAAO,KAAK;AAAA,MAEhC,KAAK;AACH,eAAO,UAAU,OAAO,KAAK;AAAA,MAE/B;AACE,eAAO;AAAA,IAAA;AAAA,EAEb;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAiB,WAAsB,QAA6B;AAClE,QAAI,OAAO,WAAW,IAAI;AACxB,YAAM,IAAI,MAAM,+CAA+C;AAAA,IACjE;AAEA,UAAM,OAAO,UAAU;AACvB,UAAM,SAAS,KAAK;AAEpB,aAAS,IAAI,GAAG,IAAI,QAAQ,KAAK,GAAG;AAClC,YAAM,IAAI,KAAK,CAAC;AAChB,YAAM,IAAI,KAAK,IAAI,CAAC;AACpB,YAAM,IAAI,KAAK,IAAI,CAAC;AACpB,YAAM,IAAI,KAAK,IAAI,CAAC;AACpB,YAAM,IAAI;AAGV,WAAK,CAAC,IAAI,KAAK,MAAM,IAAI,EAAE,CAAC,IAAK,IAAI,EAAE,CAAC,IAAK,IAAI,EAAE,CAAC,IAAK,IAAI,EAAE,CAAC,IAAK,EAAE,CAAC,IAAK,GAAG;AAChF,WAAK,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,EAAE,CAAC,IAAK,IAAI,EAAE,CAAC,IAAK,IAAI,EAAE,CAAC,IAAK,IAAI,EAAE,CAAC,IAAK,EAAE,CAAC,IAAK,GAAG;AACpF,WAAK,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,EAAE,EAAE,IAAK,IAAI,EAAE,EAAE,IAAK,IAAI,EAAE,EAAE,IAAK,IAAI,EAAE,EAAE,IAAK,EAAE,EAAE,IAAK,GAAG;AACzF,WAAK,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,EAAE,EAAE,IAAK,IAAI,EAAE,EAAE,IAAK,IAAI,EAAE,EAAE,IAAK,IAAI,EAAE,EAAE,IAAK,EAAE,EAAE,IAAK,GAAG;AAAA,IAC3F;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,QAAiC;AAC/C,YAAQ,QAAA;AAAA,MACN,KAAK;AACH,eAAO;AAAA,UACL;AAAA,UAAO;AAAA,UAAO;AAAA,UAAO;AAAA,UAAG;AAAA,UAAG;AAAA,UAAO;AAAA,UAAO;AAAA,UAAO;AAAA,UAAG;AAAA,UAAG;AAAA,UAAO;AAAA,UAAO;AAAA,UAAO;AAAA,UAAG;AAAA,UAAG;AAAA,UAAG;AAAA,UAAG;AAAA,UACvF;AAAA,UAAG;AAAA,QAAA;AAAA,MAGP,KAAK;AACH,eAAO;AAAA,UACL;AAAA,UAAM;AAAA,UAAM;AAAA,UAAM;AAAA,UAAG;AAAA,UAAG;AAAA,UAAM;AAAA,UAAM;AAAA,UAAM;AAAA,UAAG;AAAA,UAAG;AAAA,UAAM;AAAA,UAAM;AAAA,UAAM;AAAA,UAAG;AAAA,UAAG;AAAA,UAAG;AAAA,UAAG;AAAA,UAAG;AAAA,UAAG;AAAA,QAAA;AAAA,MAGxF,KAAK;AACH,eAAO,CAAC,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAAA,MAE1E,KAAK;AACH,eAAO,CAAC,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAAA,MAE1E;AACE,eAAO;AAAA,IAAA;AAAA,EAEb;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAkB,WAAsB,QAA2B;AAEjE,UAAM,SAAS,IAAI;AAAA,MACjB,IAAI,kBAAkB,UAAU,IAAI;AAAA,MACpC,UAAU;AAAA,MACV,UAAU;AAAA,IAAA;AAGZ,UAAM,QAAQ,UAAU;AACxB,UAAM,SAAS,UAAU;AACzB,UAAM,OAAO,UAAU;AACvB,UAAM,UAAU,OAAO;AAGvB,aAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,eAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAI,IAAI,GACN,IAAI,GACJ,IAAI,GACJ,IAAI;AACN,YAAI,QAAQ;AAEZ,iBAAS,KAAK,CAAC,QAAQ,MAAM,QAAQ,MAAM;AACzC,gBAAM,KAAK,KAAK,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,QAAQ,CAAC;AAClD,gBAAMA,QAAO,IAAI,QAAQ,MAAM;AAC/B,eAAK,KAAKA,IAAG;AACb,eAAK,KAAKA,OAAM,CAAC;AACjB,eAAK,KAAKA,OAAM,CAAC;AACjB,eAAK,KAAKA,OAAM,CAAC;AACjB;AAAA,QACF;AAEA,cAAM,OAAO,IAAI,QAAQ,KAAK;AAC9B,gBAAQ,GAAG,IAAI,IAAI;AACnB,gBAAQ,MAAM,CAAC,IAAI,IAAI;AACvB,gBAAQ,MAAM,CAAC,IAAI,IAAI;AACvB,gBAAQ,MAAM,CAAC,IAAI,IAAI;AAAA,MACzB;AAAA,IACF;AAGA,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,eAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,YAAI,IAAI,GACN,IAAI,GACJ,IAAI,GACJ,IAAI;AACN,YAAI,QAAQ;AAEZ,iBAAS,KAAK,CAAC,QAAQ,MAAM,QAAQ,MAAM;AACzC,gBAAM,KAAK,KAAK,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,SAAS,CAAC;AACnD,gBAAMA,QAAO,KAAK,QAAQ,KAAK;AAC/B,eAAK,QAAQA,IAAG;AAChB,eAAK,QAAQA,OAAM,CAAC;AACpB,eAAK,QAAQA,OAAM,CAAC;AACpB,eAAK,QAAQA,OAAM,CAAC;AACpB;AAAA,QACF;AAEA,cAAM,OAAO,IAAI,QAAQ,KAAK;AAC9B,aAAK,GAAG,IAAI,IAAI;AAChB,aAAK,MAAM,CAAC,IAAI,IAAI;AACpB,aAAK,MAAM,CAAC,IAAI,IAAI;AACpB,aAAK,MAAM,CAAC,IAAI,IAAI;AAAA,MACtB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,MAAM,OAAuB;AACnC,WAAO,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC,CAAC;AAAA,EACrD;AAAA,EAEQ,iBAAiB,SAAiC;AACxD,WAAO,QAAQ,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,IAAI,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,GAAG;AAAA,EACzE;AAAA,EAEA,aAAmB;AACjB,SAAK,YAAY,MAAA;AAAA,EACnB;AAAA,EAEA,eAAuB;AACrB,WAAO,KAAK,YAAY;AAAA,EAC1B;AACF;"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { TimeUs } from '../../model/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FrameRateConverter - Converts VFR (Variable Frame Rate) to CFR (Constant Frame Rate)
|
|
5
|
+
*
|
|
6
|
+
* Receives VideoFrames with irregular timestamps from decoder (VFR)
|
|
7
|
+
* and outputs VideoFrames with regular timestamps based on target fps (CFR).
|
|
8
|
+
*
|
|
9
|
+
* Algorithm:
|
|
10
|
+
* - Generate target frame sequence: t = 0, 33333, 66666, ... µs (for 30fps)
|
|
11
|
+
* - For each target time t, find the closest source frame
|
|
12
|
+
* - Output new frame with standardized timestamp t
|
|
13
|
+
* - Close source frames after use to prevent memory leaks
|
|
14
|
+
*
|
|
15
|
+
* Scenarios:
|
|
16
|
+
* - Downsampling (60fps→30fps): Skip every other frame
|
|
17
|
+
* - Upsampling (24fps→30fps): Duplicate some frames
|
|
18
|
+
* - Same rate (30fps→30fps): Normalize timestamps
|
|
19
|
+
*/
|
|
20
|
+
export declare class FrameRateConverter {
|
|
21
|
+
private readonly clipDurationUs;
|
|
22
|
+
private readonly frameDurationUs;
|
|
23
|
+
private targetFrameIndex;
|
|
24
|
+
private targetFrameTimeUs;
|
|
25
|
+
private sourceFrameBuffer;
|
|
26
|
+
constructor(targetFps: number, clipDurationUs: TimeUs);
|
|
27
|
+
/**
|
|
28
|
+
* Create a TransformStream that converts VFR frames to CFR frames
|
|
29
|
+
*/
|
|
30
|
+
createStream(): TransformStream<VideoFrame, VideoFrame>;
|
|
31
|
+
private sourceFrameCount;
|
|
32
|
+
private outputFrameCount;
|
|
33
|
+
/**
|
|
34
|
+
* Process incoming source frame and output target frames
|
|
35
|
+
*/
|
|
36
|
+
private processSourceFrame;
|
|
37
|
+
/**
|
|
38
|
+
* Flush remaining target frames at end of stream
|
|
39
|
+
*/
|
|
40
|
+
private flushRemainingFrames;
|
|
41
|
+
/**
|
|
42
|
+
* Find the source frame closest to target time
|
|
43
|
+
*/
|
|
44
|
+
private findClosestFrame;
|
|
45
|
+
/**
|
|
46
|
+
* Check if we should wait for next source frame before outputting
|
|
47
|
+
* Returns true if:
|
|
48
|
+
* - We only have 1 frame in buffer
|
|
49
|
+
* - The closest frame is before target time
|
|
50
|
+
* - We might get a better match from next frame
|
|
51
|
+
*/
|
|
52
|
+
private shouldWaitForNextFrame;
|
|
53
|
+
/**
|
|
54
|
+
* Clean up source frames that are no longer needed
|
|
55
|
+
* Keep frames that might be needed for future target frames
|
|
56
|
+
*/
|
|
57
|
+
private cleanupOldFrames;
|
|
58
|
+
/**
|
|
59
|
+
* Get current conversion state (for debugging)
|
|
60
|
+
*/
|
|
61
|
+
getState(): {
|
|
62
|
+
targetFrameIndex: number;
|
|
63
|
+
targetFrameTimeUs: TimeUs;
|
|
64
|
+
bufferSize: number;
|
|
65
|
+
frameDurationUs: number;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=FrameRateConverter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FrameRateConverter.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/FrameRateConverter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IAGzC,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,iBAAiB,CAAa;IACtC,OAAO,CAAC,iBAAiB,CAAoB;gBAEjC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;IAYrD;;OAEG;IACH,YAAY,IAAI,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC;IAqBvD,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,gBAAgB,CAAK;IAE7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAgD1B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA6C5B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAmBxB;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IAa9B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAyCxB;;OAEG;IACH,QAAQ,IAAI;QACV,gBAAgB,EAAE,MAAM,CAAC;QACzB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,UAAU,EAAE,MAAM,CAAC;QACnB,eAAe,EAAE,MAAM,CAAC;KACzB;CAQF"}
|
|
@@ -10,7 +10,7 @@ export declare class LayerRenderer {
|
|
|
10
10
|
private height;
|
|
11
11
|
private currentFrame;
|
|
12
12
|
private fps;
|
|
13
|
-
constructor(ctx: OffscreenCanvasRenderingContext2D, width: number, height: number, fps?: number);
|
|
13
|
+
constructor(ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D, width: number, height: number, fps?: number);
|
|
14
14
|
setCurrentFrame(frame: number): void;
|
|
15
15
|
private ensureHighQualityRendering;
|
|
16
16
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LayerRenderer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/LayerRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAA8D,MAAM,SAAS,CAAC;AAMjG;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"LayerRenderer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/LayerRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAA8D,MAAM,SAAS,CAAC;AAMjG;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAA+D;IAC1E,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,GAAG,CAAc;gBAGvB,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAW;IASlB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIpC,OAAO,CAAC,0BAA0B;IAKlC;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IA0C9C,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,kBAAkB;IA+C1B,OAAO,CAAC,cAAc;YA2BR,gBAAgB;YAwChB,gBAAgB;YAiFhB,eAAe;IAuC7B,OAAO,CAAC,SAAS;IAkBjB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;CAKtD"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { renderBasicText, renderTextWithEntrance } from "./text-renderers/basic-text-renderer.js";
|
|
2
|
+
import { renderWordByWord } from "./text-renderers/word-by-word-renderer.js";
|
|
3
|
+
import { renderCharacterKTV } from "./text-renderers/character-ktv-renderer.js";
|
|
4
|
+
import { renderWordByWordFancy } from "./text-renderers/word-fancy-renderer.js";
|
|
5
|
+
class LayerRenderer {
|
|
6
|
+
ctx;
|
|
7
|
+
width;
|
|
8
|
+
height;
|
|
9
|
+
currentFrame = 0;
|
|
10
|
+
fps = 30;
|
|
11
|
+
constructor(ctx, width, height, fps = 30) {
|
|
12
|
+
this.ctx = ctx;
|
|
13
|
+
this.width = width;
|
|
14
|
+
this.height = height;
|
|
15
|
+
this.fps = fps;
|
|
16
|
+
this.ensureHighQualityRendering();
|
|
17
|
+
}
|
|
18
|
+
setCurrentFrame(frame) {
|
|
19
|
+
this.currentFrame = frame;
|
|
20
|
+
}
|
|
21
|
+
ensureHighQualityRendering() {
|
|
22
|
+
this.ctx.imageSmoothingEnabled = true;
|
|
23
|
+
this.ctx.imageSmoothingQuality = "high";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Render a single layer with all its properties
|
|
27
|
+
*/
|
|
28
|
+
async renderLayer(layer) {
|
|
29
|
+
if (!layer.visible || layer.opacity <= 0) return;
|
|
30
|
+
this.ctx.save();
|
|
31
|
+
try {
|
|
32
|
+
this.ensureHighQualityRendering();
|
|
33
|
+
this.ctx.globalAlpha = layer.opacity;
|
|
34
|
+
if (layer.blendMode) {
|
|
35
|
+
this.ctx.globalCompositeOperation = layer.blendMode;
|
|
36
|
+
}
|
|
37
|
+
if (layer.transform) {
|
|
38
|
+
const layerDimensions = this.getLayerDimensions(layer);
|
|
39
|
+
this.applyTransform(layer.transform, layerDimensions);
|
|
40
|
+
}
|
|
41
|
+
switch (layer.type) {
|
|
42
|
+
case "video":
|
|
43
|
+
await this.renderVideoLayer(layer);
|
|
44
|
+
break;
|
|
45
|
+
case "image":
|
|
46
|
+
await this.renderImageLayer(layer);
|
|
47
|
+
break;
|
|
48
|
+
case "text":
|
|
49
|
+
await this.renderTextLayer(layer);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (layer.mask) {
|
|
53
|
+
this.applyMask(layer.mask);
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
this.ctx.restore();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
parseDimension(value, canvasSize) {
|
|
60
|
+
if (value === void 0) return void 0;
|
|
61
|
+
if (typeof value === "number") return value;
|
|
62
|
+
const strValue = value;
|
|
63
|
+
if (strValue.includes("%")) {
|
|
64
|
+
const numValue = parseFloat(strValue);
|
|
65
|
+
return isNaN(numValue) ? void 0 : numValue / 100 * canvasSize;
|
|
66
|
+
}
|
|
67
|
+
const parsed = parseFloat(strValue);
|
|
68
|
+
return isNaN(parsed) ? void 0 : parsed;
|
|
69
|
+
}
|
|
70
|
+
getLayerDimensions(layer) {
|
|
71
|
+
if (layer.type === "image") {
|
|
72
|
+
const imageLayer = layer;
|
|
73
|
+
if (imageLayer.source) {
|
|
74
|
+
const imgWidth = imageLayer.source.width;
|
|
75
|
+
const imgHeight = imageLayer.source.height;
|
|
76
|
+
const isAttachment = !!imageLayer.attachmentId;
|
|
77
|
+
if (isAttachment) {
|
|
78
|
+
const targetWidthRaw = imageLayer.targetWidth;
|
|
79
|
+
const targetHeightRaw = imageLayer.targetHeight;
|
|
80
|
+
const targetWidth = this.parseDimension(targetWidthRaw, this.width);
|
|
81
|
+
const targetHeight = this.parseDimension(targetHeightRaw, this.height);
|
|
82
|
+
if (targetWidth && targetHeight) {
|
|
83
|
+
return { width: targetWidth, height: targetHeight };
|
|
84
|
+
} else if (targetWidth) {
|
|
85
|
+
return {
|
|
86
|
+
width: targetWidth,
|
|
87
|
+
height: imgHeight / imgWidth * targetWidth
|
|
88
|
+
};
|
|
89
|
+
} else if (targetHeight) {
|
|
90
|
+
return {
|
|
91
|
+
width: imgWidth / imgHeight * targetHeight,
|
|
92
|
+
height: targetHeight
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { width: imgWidth, height: imgHeight };
|
|
97
|
+
}
|
|
98
|
+
} else if (layer.type === "video") {
|
|
99
|
+
const videoLayer = layer;
|
|
100
|
+
const videoFrame = videoLayer.videoFrame;
|
|
101
|
+
return {
|
|
102
|
+
width: videoFrame.displayWidth || videoFrame.codedWidth,
|
|
103
|
+
height: videoFrame.displayHeight || videoFrame.codedHeight
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { width: this.width, height: this.height };
|
|
107
|
+
}
|
|
108
|
+
applyTransform(transform, layerDimensions) {
|
|
109
|
+
const anchorX = transform.anchorX ?? 0.5;
|
|
110
|
+
const anchorY = transform.anchorY ?? 0.5;
|
|
111
|
+
const centerX = layerDimensions.width * anchorX;
|
|
112
|
+
const centerY = layerDimensions.height * anchorY;
|
|
113
|
+
this.ctx.translate(transform.x + centerX, transform.y + centerY);
|
|
114
|
+
if (transform.rotation) {
|
|
115
|
+
this.ctx.rotate(transform.rotation);
|
|
116
|
+
}
|
|
117
|
+
this.ctx.scale(transform.scaleX, transform.scaleY);
|
|
118
|
+
if (transform.skewX || transform.skewY) {
|
|
119
|
+
this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);
|
|
120
|
+
}
|
|
121
|
+
this.ctx.translate(-centerX, -centerY);
|
|
122
|
+
}
|
|
123
|
+
async renderVideoLayer(layer) {
|
|
124
|
+
const { videoFrame, crop } = layer;
|
|
125
|
+
const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;
|
|
126
|
+
const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;
|
|
127
|
+
const scaleX = this.width / videoWidth;
|
|
128
|
+
const scaleY = this.height / videoHeight;
|
|
129
|
+
const scale = Math.min(scaleX, scaleY);
|
|
130
|
+
const renderWidth = Math.round(videoWidth * scale);
|
|
131
|
+
const renderHeight = Math.round(videoHeight * scale);
|
|
132
|
+
const renderX = Math.round((this.width - renderWidth) / 2);
|
|
133
|
+
const renderY = Math.round((this.height - renderHeight) / 2);
|
|
134
|
+
if (crop) {
|
|
135
|
+
this.ctx.drawImage(
|
|
136
|
+
videoFrame,
|
|
137
|
+
crop.x,
|
|
138
|
+
crop.y,
|
|
139
|
+
crop.width,
|
|
140
|
+
crop.height,
|
|
141
|
+
renderX,
|
|
142
|
+
renderY,
|
|
143
|
+
renderWidth,
|
|
144
|
+
renderHeight
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);
|
|
148
|
+
}
|
|
149
|
+
videoFrame.close();
|
|
150
|
+
}
|
|
151
|
+
async renderImageLayer(layer) {
|
|
152
|
+
const { source, crop } = layer;
|
|
153
|
+
if (source instanceof ImageData) {
|
|
154
|
+
if (crop) {
|
|
155
|
+
const tempCanvas = new OffscreenCanvas(crop.width, crop.height);
|
|
156
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
157
|
+
tempCtx.putImageData(source, -crop.x, -crop.y);
|
|
158
|
+
this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);
|
|
159
|
+
} else {
|
|
160
|
+
this.ctx.putImageData(source, 0, 0);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
if (!source) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const isAttachment = !!layer.attachmentId;
|
|
167
|
+
const imgWidth = source.width;
|
|
168
|
+
const imgHeight = source.height;
|
|
169
|
+
let renderWidth;
|
|
170
|
+
let renderHeight;
|
|
171
|
+
if (isAttachment) {
|
|
172
|
+
const targetWidthRaw = layer.targetWidth;
|
|
173
|
+
const targetHeightRaw = layer.targetHeight;
|
|
174
|
+
const targetWidth = this.parseDimension(targetWidthRaw, this.width);
|
|
175
|
+
const targetHeight = this.parseDimension(targetHeightRaw, this.height);
|
|
176
|
+
if (targetWidth && targetHeight) {
|
|
177
|
+
renderWidth = targetWidth;
|
|
178
|
+
renderHeight = targetHeight;
|
|
179
|
+
} else if (targetWidth) {
|
|
180
|
+
renderWidth = targetWidth;
|
|
181
|
+
renderHeight = imgHeight / imgWidth * targetWidth;
|
|
182
|
+
} else if (targetHeight) {
|
|
183
|
+
renderHeight = targetHeight;
|
|
184
|
+
renderWidth = imgWidth / imgHeight * targetHeight;
|
|
185
|
+
} else {
|
|
186
|
+
renderWidth = imgWidth;
|
|
187
|
+
renderHeight = imgHeight;
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
renderWidth = this.width;
|
|
191
|
+
renderHeight = this.height;
|
|
192
|
+
}
|
|
193
|
+
if (crop) {
|
|
194
|
+
this.ctx.drawImage(
|
|
195
|
+
source,
|
|
196
|
+
crop.x,
|
|
197
|
+
crop.y,
|
|
198
|
+
crop.width,
|
|
199
|
+
crop.height,
|
|
200
|
+
0,
|
|
201
|
+
0,
|
|
202
|
+
renderWidth,
|
|
203
|
+
renderHeight
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
this.ctx.drawImage(source, 0, 0, renderWidth, renderHeight);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async renderTextLayer(layer) {
|
|
211
|
+
const animationType = layer.animation?.type;
|
|
212
|
+
const hasWordTimings = layer.wordTimings && layer.wordTimings.length > 0;
|
|
213
|
+
const needsWordTimings = ["wordByWord", "characterKTV", "wordByWordFancy"].includes(
|
|
214
|
+
animationType || ""
|
|
215
|
+
);
|
|
216
|
+
if (needsWordTimings && !hasWordTimings) {
|
|
217
|
+
renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
switch (animationType) {
|
|
221
|
+
case "wordByWord":
|
|
222
|
+
renderWordByWord(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);
|
|
223
|
+
break;
|
|
224
|
+
case "characterKTV":
|
|
225
|
+
renderCharacterKTV(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);
|
|
226
|
+
break;
|
|
227
|
+
case "wordByWordFancy":
|
|
228
|
+
renderWordByWordFancy(
|
|
229
|
+
this.ctx,
|
|
230
|
+
layer,
|
|
231
|
+
this.width,
|
|
232
|
+
this.height,
|
|
233
|
+
this.currentFrame,
|
|
234
|
+
this.fps
|
|
235
|
+
);
|
|
236
|
+
break;
|
|
237
|
+
case "fade":
|
|
238
|
+
renderTextWithEntrance(this.ctx, layer, this.width, this.height, this.currentFrame);
|
|
239
|
+
break;
|
|
240
|
+
default:
|
|
241
|
+
renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
applyMask(mask) {
|
|
246
|
+
this.ctx.globalCompositeOperation = mask.invert ? "source-out" : "destination-in";
|
|
247
|
+
if (mask.source) {
|
|
248
|
+
this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);
|
|
249
|
+
} else if (mask.shape === "circle") {
|
|
250
|
+
this.ctx.beginPath();
|
|
251
|
+
this.ctx.arc(
|
|
252
|
+
this.width / 2,
|
|
253
|
+
this.height / 2,
|
|
254
|
+
Math.min(this.width, this.height) / 2,
|
|
255
|
+
0,
|
|
256
|
+
Math.PI * 2
|
|
257
|
+
);
|
|
258
|
+
this.ctx.fill();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
updateDimensions(width, height) {
|
|
262
|
+
this.width = width;
|
|
263
|
+
this.height = height;
|
|
264
|
+
this.ensureHighQualityRendering();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
export {
|
|
268
|
+
LayerRenderer
|
|
269
|
+
};
|
|
270
|
+
//# sourceMappingURL=LayerRenderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LayerRenderer.js","sources":["../../../src/stages/compose/LayerRenderer.ts"],"sourcesContent":["import type { Layer, VideoLayer, ImageLayer, TextLayer, Transform2D, MaskConfig } from './types';\nimport { renderBasicText, renderTextWithEntrance } from './text-renderers/basic-text-renderer';\nimport { renderWordByWord } from './text-renderers/word-by-word-renderer';\nimport { renderCharacterKTV } from './text-renderers/character-ktv-renderer';\nimport { renderWordByWordFancy } from './text-renderers/word-fancy-renderer';\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 | CanvasRenderingContext2D;\n private width: number;\n private height: number;\n private currentFrame: number = 0;\n private fps: number = 30;\n\n constructor(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n width: number,\n height: number,\n fps: number = 30\n ) {\n this.ctx = ctx;\n this.width = width;\n this.height = height;\n this.fps = fps;\n this.ensureHighQualityRendering();\n }\n\n setCurrentFrame(frame: number): void {\n this.currentFrame = frame;\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 // Get layer dimensions for transform anchor calculation\n const layerDimensions = this.getLayerDimensions(layer);\n this.applyTransform(layer.transform, layerDimensions);\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 parseDimension(\n value: number | string | undefined,\n canvasSize: number\n ): number | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'number') return value;\n\n // value is string at this point\n const strValue = value as string;\n\n // Parse percentage string like \"5%\"\n if (strValue.includes('%')) {\n const numValue = parseFloat(strValue);\n return isNaN(numValue) ? undefined : (numValue / 100) * canvasSize;\n }\n\n // Parse as pixel value\n const parsed = parseFloat(strValue);\n return isNaN(parsed) ? undefined : parsed;\n }\n\n private getLayerDimensions(layer: Layer): { width: number; height: number } {\n if (layer.type === 'image') {\n const imageLayer = layer as ImageLayer;\n if (imageLayer.source) {\n const imgWidth = imageLayer.source.width;\n const imgHeight = imageLayer.source.height;\n\n // For attachment layers with target dimensions, calculate rendered size\n const isAttachment = !!imageLayer.attachmentId;\n if (isAttachment) {\n const targetWidthRaw = (imageLayer as any).targetWidth;\n const targetHeightRaw = (imageLayer as any).targetHeight;\n\n // Parse dimensions (supports both pixels and percentages)\n const targetWidth = this.parseDimension(targetWidthRaw, this.width);\n const targetHeight = this.parseDimension(targetHeightRaw, this.height);\n\n if (targetWidth && targetHeight) {\n return { width: targetWidth, height: targetHeight };\n } else if (targetWidth) {\n return {\n width: targetWidth,\n height: (imgHeight / imgWidth) * targetWidth,\n };\n } else if (targetHeight) {\n return {\n width: (imgWidth / imgHeight) * targetHeight,\n height: targetHeight,\n };\n }\n }\n\n // No scaling, return original dimensions\n return { width: imgWidth, height: imgHeight };\n }\n } else if (layer.type === 'video') {\n const videoLayer = layer as VideoLayer;\n const videoFrame = videoLayer.videoFrame;\n return {\n width: videoFrame.displayWidth || videoFrame.codedWidth,\n height: videoFrame.displayHeight || videoFrame.codedHeight,\n };\n }\n // Default to canvas dimensions\n return { width: this.width, height: this.height };\n }\n\n private applyTransform(\n transform: Transform2D,\n layerDimensions: { width: number; height: number }\n ): void {\n // Use layer dimensions (not canvas dimensions) for anchor calculation\n const anchorX = transform.anchorX ?? 0.5;\n const anchorY = transform.anchorY ?? 0.5;\n const centerX = layerDimensions.width * anchorX;\n const centerY = layerDimensions.height * anchorY;\n\n // Move to the layer position + anchor offset\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 // Move back by anchor offset\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 videoFrame.close();\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\n // Determine if this is an attachment layer (has attachmentId)\n // Attachment images use original size (or targetWidth/targetHeight if specified)\n // Main track images fill canvas\n const isAttachment = !!layer.attachmentId;\n const imgWidth = source.width;\n const imgHeight = source.height;\n\n let renderWidth: number;\n let renderHeight: number;\n\n if (isAttachment) {\n const targetWidthRaw = (layer as any).targetWidth;\n const targetHeightRaw = (layer as any).targetHeight;\n\n // Parse dimensions (supports both pixels and percentages)\n const targetWidth = this.parseDimension(targetWidthRaw, this.width);\n const targetHeight = this.parseDimension(targetHeightRaw, this.height);\n\n if (targetWidth && targetHeight) {\n // Both specified, use as-is\n renderWidth = targetWidth;\n renderHeight = targetHeight;\n } else if (targetWidth) {\n // Only width specified, maintain aspect ratio\n renderWidth = targetWidth;\n renderHeight = (imgHeight / imgWidth) * targetWidth;\n } else if (targetHeight) {\n // Only height specified, maintain aspect ratio\n renderHeight = targetHeight;\n renderWidth = (imgWidth / imgHeight) * targetHeight;\n } else {\n // No target size, use original\n renderWidth = imgWidth;\n renderHeight = imgHeight;\n }\n } else {\n // Main track images fill canvas\n renderWidth = this.width;\n renderHeight = this.height;\n }\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 renderWidth,\n renderHeight\n );\n } else {\n // Draw at appropriate size based on layer type\n this.ctx.drawImage(source, 0, 0, renderWidth, renderHeight);\n }\n }\n }\n\n private async renderTextLayer(layer: TextLayer): Promise<void> {\n const animationType = layer.animation?.type;\n const hasWordTimings = layer.wordTimings && layer.wordTimings.length > 0;\n\n const needsWordTimings = ['wordByWord', 'characterKTV', 'wordByWordFancy'].includes(\n animationType || ''\n );\n\n if (needsWordTimings && !hasWordTimings) {\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n return;\n }\n\n switch (animationType) {\n case 'wordByWord':\n renderWordByWord(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);\n break;\n case 'characterKTV':\n renderCharacterKTV(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);\n break;\n case 'wordByWordFancy':\n renderWordByWordFancy(\n this.ctx,\n layer,\n this.width,\n this.height,\n this.currentFrame,\n this.fps\n );\n break;\n case 'fade':\n renderTextWithEntrance(this.ctx, layer, this.width, this.height, this.currentFrame);\n break;\n default:\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n break;\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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"}
|
|
@@ -12,7 +12,7 @@ export declare class TransitionProcessor {
|
|
|
12
12
|
* Apply transition effect to the canvas context
|
|
13
13
|
* Returns true if transition was applied, false if not needed
|
|
14
14
|
*/
|
|
15
|
-
applyTransition(ctx: OffscreenCanvasRenderingContext2D, transition: TransitionEffect): boolean;
|
|
15
|
+
applyTransition(ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D, transition: TransitionEffect): boolean;
|
|
16
16
|
private calculateEasedProgress;
|
|
17
17
|
private applyFade;
|
|
18
18
|
private applySlide;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransitionProcessor.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/TransitionProcessor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD;;;GAGG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;gBAEX,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKzC;;;OAGG;IACH,eAAe,
|
|
1
|
+
{"version":3,"file":"TransitionProcessor.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/TransitionProcessor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD;;;GAGG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;gBAEX,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKzC;;;OAGG;IACH,eAAe,CACb,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,UAAU,EAAE,gBAAgB,GAC3B,OAAO;IAuBV,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,UAAU;IA2BlB,OAAO,CAAC,SAAS;IA6BjB,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,aAAa;IAUrB;;OAEG;IACH,oBAAoB,CAAC,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,eAAe,GAAG,SAAS,GAAG,IAAI;IAoC7F,OAAO,CAAC,kBAAkB;IA4C1B,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;CAItD"}
|