@livekit/track-processors 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/index.js +615 -119
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +615 -119
- package/dist/index.mjs.map +1 -1
- package/dist/src/ProcessorWrapper.d.ts +26 -1
- package/dist/src/index.d.ts +20 -6
- package/dist/src/transformers/BackgroundTransformer.d.ts +8 -3
- package/dist/src/transformers/VideoTransformer.d.ts +2 -1
- package/dist/src/webgl/index.d.ts +9 -0
- package/package.json +9 -9
- package/src/ProcessorWrapper.ts +271 -18
- package/src/index.ts +82 -10
- package/src/transformers/BackgroundTransformer.ts +55 -104
- package/src/transformers/VideoTransformer.ts +14 -4
- package/src/utils.ts +1 -0
- package/src/webgl/index.ts +458 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setupWebGL } from '../webgl/index';
|
|
1
2
|
import { VideoTrackTransformer, VideoTransformerInitOptions } from './types';
|
|
2
3
|
|
|
3
4
|
export default abstract class VideoTransformer<Options extends Record<string, unknown>>
|
|
@@ -7,10 +8,12 @@ export default abstract class VideoTransformer<Options extends Record<string, un
|
|
|
7
8
|
|
|
8
9
|
canvas?: OffscreenCanvas;
|
|
9
10
|
|
|
10
|
-
ctx?: OffscreenCanvasRenderingContext2D;
|
|
11
|
+
// ctx?: OffscreenCanvasRenderingContext2D;
|
|
11
12
|
|
|
12
13
|
inputVideo?: HTMLVideoElement;
|
|
13
14
|
|
|
15
|
+
gl?: ReturnType<typeof setupWebGL>;
|
|
16
|
+
|
|
14
17
|
protected isDisabled?: Boolean = false;
|
|
15
18
|
|
|
16
19
|
async init({
|
|
@@ -26,7 +29,10 @@ export default abstract class VideoTransformer<Options extends Record<string, un
|
|
|
26
29
|
});
|
|
27
30
|
this.canvas = outputCanvas || null;
|
|
28
31
|
if (outputCanvas) {
|
|
29
|
-
this.ctx = this.canvas?.getContext('2d') || undefined;
|
|
32
|
+
// this.ctx = this.canvas?.getContext('2d') || undefined;
|
|
33
|
+
this.gl = setupWebGL(
|
|
34
|
+
this.canvas || new OffscreenCanvas(inputVideo.videoWidth, inputVideo.videoHeight),
|
|
35
|
+
);
|
|
30
36
|
}
|
|
31
37
|
this.inputVideo = inputVideo;
|
|
32
38
|
this.isDisabled = false;
|
|
@@ -34,7 +40,10 @@ export default abstract class VideoTransformer<Options extends Record<string, un
|
|
|
34
40
|
|
|
35
41
|
async restart({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {
|
|
36
42
|
this.canvas = outputCanvas || null;
|
|
37
|
-
this.
|
|
43
|
+
this.gl?.cleanup();
|
|
44
|
+
this.gl = setupWebGL(
|
|
45
|
+
this.canvas || new OffscreenCanvas(inputVideo.videoWidth, inputVideo.videoHeight),
|
|
46
|
+
);
|
|
38
47
|
|
|
39
48
|
this.inputVideo = inputVideo;
|
|
40
49
|
this.isDisabled = false;
|
|
@@ -43,7 +52,8 @@ export default abstract class VideoTransformer<Options extends Record<string, un
|
|
|
43
52
|
async destroy() {
|
|
44
53
|
this.isDisabled = true;
|
|
45
54
|
this.canvas = undefined;
|
|
46
|
-
this.
|
|
55
|
+
this.gl?.cleanup();
|
|
56
|
+
this.gl = undefined;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
abstract transform(
|
package/src/utils.ts
CHANGED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { MPMask } from '@mediapipe/tasks-vision';
|
|
2
|
+
|
|
3
|
+
// Define the blur fragment shader
|
|
4
|
+
const blurFragmentShader = `
|
|
5
|
+
precision highp float;
|
|
6
|
+
varying vec2 texCoords;
|
|
7
|
+
uniform sampler2D u_texture;
|
|
8
|
+
uniform vec2 u_texelSize;
|
|
9
|
+
uniform vec2 u_direction;
|
|
10
|
+
uniform float u_radius;
|
|
11
|
+
|
|
12
|
+
void main() {
|
|
13
|
+
float sigma = u_radius;
|
|
14
|
+
float twoSigmaSq = 2.0 * sigma * sigma;
|
|
15
|
+
float totalWeight = 0.0;
|
|
16
|
+
vec3 result = vec3(0.0);
|
|
17
|
+
const int MAX_SAMPLES = 16;
|
|
18
|
+
int radius = int(min(float(MAX_SAMPLES), ceil(u_radius)));
|
|
19
|
+
|
|
20
|
+
for (int i = -MAX_SAMPLES; i <= MAX_SAMPLES; ++i) {
|
|
21
|
+
float offset = float(i);
|
|
22
|
+
if (abs(offset) > float(radius)) continue;
|
|
23
|
+
float weight = exp(-(offset * offset) / twoSigmaSq);
|
|
24
|
+
vec2 sampleCoord = texCoords + u_direction * u_texelSize * offset;
|
|
25
|
+
result += texture2D(u_texture, sampleCoord).rgb * weight;
|
|
26
|
+
totalWeight += weight;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
gl_FragColor = vec4(result / totalWeight, 1.0);
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const createShaderProgram = (gl: WebGL2RenderingContext) => {
|
|
34
|
+
const vs = `
|
|
35
|
+
attribute vec2 position;
|
|
36
|
+
varying vec2 texCoords;
|
|
37
|
+
|
|
38
|
+
void main() {
|
|
39
|
+
texCoords = (position + 1.0) / 2.0;
|
|
40
|
+
texCoords.y = 1.0 - texCoords.y;
|
|
41
|
+
gl_Position = vec4(position, 0, 1.0);
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const cS = `
|
|
46
|
+
precision highp float;
|
|
47
|
+
varying vec2 texCoords;
|
|
48
|
+
uniform sampler2D background;
|
|
49
|
+
uniform sampler2D frame;
|
|
50
|
+
uniform sampler2D mask;
|
|
51
|
+
void main() {
|
|
52
|
+
vec4 maskTex = texture2D(mask, texCoords);
|
|
53
|
+
vec4 frameTex = texture2D(frame, texCoords);
|
|
54
|
+
vec4 bgTex = texture2D(background, texCoords);
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
float a = maskTex.r;
|
|
58
|
+
|
|
59
|
+
gl_FragColor = mix(bgTex, vec4(frameTex.rgb, 1.0), 1.0 - a);
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
65
|
+
if (!vertexShader) {
|
|
66
|
+
throw Error('can not create vertex shader');
|
|
67
|
+
}
|
|
68
|
+
gl.shaderSource(vertexShader, vs);
|
|
69
|
+
gl.compileShader(vertexShader);
|
|
70
|
+
|
|
71
|
+
// Create our fragment shader
|
|
72
|
+
const compositeShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
73
|
+
if (!compositeShader) {
|
|
74
|
+
throw Error('can not create fragment shader');
|
|
75
|
+
}
|
|
76
|
+
gl.shaderSource(compositeShader, cS);
|
|
77
|
+
gl.compileShader(compositeShader);
|
|
78
|
+
|
|
79
|
+
// Create the composite program
|
|
80
|
+
const compositeProgram = gl.createProgram();
|
|
81
|
+
if (!compositeProgram) {
|
|
82
|
+
throw Error('can not create composite program');
|
|
83
|
+
}
|
|
84
|
+
gl.attachShader(compositeProgram, vertexShader);
|
|
85
|
+
gl.attachShader(compositeProgram, compositeShader);
|
|
86
|
+
gl.linkProgram(compositeProgram);
|
|
87
|
+
|
|
88
|
+
let blurProgram = null;
|
|
89
|
+
let blurVertexShader = null;
|
|
90
|
+
let blurFrag = null;
|
|
91
|
+
let blurUniforms = null;
|
|
92
|
+
|
|
93
|
+
// Create blur shader if enabled
|
|
94
|
+
blurFrag = gl.createShader(gl.FRAGMENT_SHADER);
|
|
95
|
+
if (!blurFrag) {
|
|
96
|
+
throw Error('can not create blur shader');
|
|
97
|
+
}
|
|
98
|
+
gl.shaderSource(blurFrag, blurFragmentShader);
|
|
99
|
+
gl.compileShader(blurFrag);
|
|
100
|
+
|
|
101
|
+
// Get compile status and log errors if any
|
|
102
|
+
if (!gl.getShaderParameter(blurFrag, gl.COMPILE_STATUS)) {
|
|
103
|
+
const info = gl.getShaderInfoLog(blurFrag);
|
|
104
|
+
throw Error(`Failed to compile blur shader: ${info}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create blur program
|
|
108
|
+
blurVertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
109
|
+
if (!blurVertexShader) {
|
|
110
|
+
throw Error('can not create blur vertex shader');
|
|
111
|
+
}
|
|
112
|
+
gl.shaderSource(blurVertexShader, vs);
|
|
113
|
+
gl.compileShader(blurVertexShader);
|
|
114
|
+
|
|
115
|
+
blurProgram = gl.createProgram();
|
|
116
|
+
if (!blurProgram) {
|
|
117
|
+
throw Error('can not create blur program');
|
|
118
|
+
}
|
|
119
|
+
gl.attachShader(blurProgram, blurVertexShader);
|
|
120
|
+
gl.attachShader(blurProgram, blurFrag);
|
|
121
|
+
gl.linkProgram(blurProgram);
|
|
122
|
+
|
|
123
|
+
// Check blur program link status
|
|
124
|
+
if (!gl.getProgramParameter(blurProgram, gl.LINK_STATUS)) {
|
|
125
|
+
const info = gl.getProgramInfoLog(blurProgram);
|
|
126
|
+
throw Error(`Failed to link blur program: ${info}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
blurUniforms = {
|
|
130
|
+
position: gl.getAttribLocation(blurProgram, 'position'),
|
|
131
|
+
texture: gl.getUniformLocation(blurProgram, 'u_texture'),
|
|
132
|
+
texelSize: gl.getUniformLocation(blurProgram, 'u_texelSize'),
|
|
133
|
+
direction: gl.getUniformLocation(blurProgram, 'u_direction'),
|
|
134
|
+
radius: gl.getUniformLocation(blurProgram, 'u_radius'),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
vertexShader,
|
|
139
|
+
compositeShader,
|
|
140
|
+
blurShader: blurFrag,
|
|
141
|
+
compositeProgram,
|
|
142
|
+
blurProgram,
|
|
143
|
+
attribLocations: {
|
|
144
|
+
position: gl.getAttribLocation(compositeProgram, 'position'),
|
|
145
|
+
},
|
|
146
|
+
uniformLocations: {
|
|
147
|
+
mask: gl.getUniformLocation(compositeProgram, 'mask')!,
|
|
148
|
+
frame: gl.getUniformLocation(compositeProgram, 'frame')!,
|
|
149
|
+
background: gl.getUniformLocation(compositeProgram, 'background')!,
|
|
150
|
+
},
|
|
151
|
+
blurUniforms,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export function initTexture(gl: WebGL2RenderingContext, texIndex: number) {
|
|
156
|
+
const texRef = gl.TEXTURE0 + texIndex;
|
|
157
|
+
gl.activeTexture(texRef);
|
|
158
|
+
const texture = gl.createTexture();
|
|
159
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
160
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
161
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
162
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
163
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
164
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
165
|
+
|
|
166
|
+
return texture;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createFramebuffer(
|
|
170
|
+
gl: WebGL2RenderingContext,
|
|
171
|
+
texture: WebGLTexture,
|
|
172
|
+
width: number,
|
|
173
|
+
height: number,
|
|
174
|
+
) {
|
|
175
|
+
const framebuffer = gl.createFramebuffer();
|
|
176
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
177
|
+
|
|
178
|
+
// Set the texture as the color attachment
|
|
179
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
180
|
+
|
|
181
|
+
// Ensure texture dimensions match the provided width and height
|
|
182
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
183
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
184
|
+
|
|
185
|
+
// Check if framebuffer is complete
|
|
186
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
187
|
+
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
188
|
+
throw new Error('Framebuffer not complete');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
192
|
+
return framebuffer;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const createVertexBuffer = (gl: WebGL2RenderingContext) => {
|
|
196
|
+
if (!gl) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const vertexBuffer = gl.createBuffer();
|
|
200
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
201
|
+
gl.bufferData(
|
|
202
|
+
gl.ARRAY_BUFFER,
|
|
203
|
+
new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]),
|
|
204
|
+
gl.STATIC_DRAW,
|
|
205
|
+
);
|
|
206
|
+
return vertexBuffer;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const setupWebGL = (canvas: OffscreenCanvas) => {
|
|
210
|
+
const gl = canvas.getContext('webgl2', { premultipliedAlpha: false }) as WebGL2RenderingContext;
|
|
211
|
+
|
|
212
|
+
let blurRadius: number | null = null;
|
|
213
|
+
|
|
214
|
+
if (!gl) {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
gl.enable(gl.BLEND);
|
|
219
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
220
|
+
|
|
221
|
+
const {
|
|
222
|
+
compositeProgram,
|
|
223
|
+
blurProgram,
|
|
224
|
+
attribLocations: { position: positionLocation },
|
|
225
|
+
uniformLocations: {
|
|
226
|
+
mask: maskTextureLocation,
|
|
227
|
+
frame: frameTextureLocation,
|
|
228
|
+
background: bgTextureLocation,
|
|
229
|
+
},
|
|
230
|
+
blurUniforms,
|
|
231
|
+
} = createShaderProgram(gl);
|
|
232
|
+
|
|
233
|
+
const bgTexture = initTexture(gl, 0);
|
|
234
|
+
const frameTexture = initTexture(gl, 1);
|
|
235
|
+
const vertexBuffer = createVertexBuffer(gl);
|
|
236
|
+
|
|
237
|
+
// Create additional textures and framebuffers for processing
|
|
238
|
+
let processTextures: WebGLTexture[] = [];
|
|
239
|
+
let processFramebuffers: WebGLFramebuffer[] = [];
|
|
240
|
+
|
|
241
|
+
// Create textures for processing (blur)
|
|
242
|
+
processTextures.push(initTexture(gl, 3));
|
|
243
|
+
processTextures.push(initTexture(gl, 4));
|
|
244
|
+
|
|
245
|
+
// Create framebuffers for processing
|
|
246
|
+
processFramebuffers.push(createFramebuffer(gl, processTextures[0], canvas.width, canvas.height));
|
|
247
|
+
processFramebuffers.push(createFramebuffer(gl, processTextures[1], canvas.width, canvas.height));
|
|
248
|
+
|
|
249
|
+
// Set up uniforms for the composite shader
|
|
250
|
+
gl.useProgram(compositeProgram);
|
|
251
|
+
gl.uniform1i(bgTextureLocation, 0);
|
|
252
|
+
gl.uniform1i(frameTextureLocation, 1);
|
|
253
|
+
gl.uniform1i(maskTextureLocation, 2);
|
|
254
|
+
|
|
255
|
+
// Store custom background image
|
|
256
|
+
let customBackgroundImage: ImageBitmap | null = null;
|
|
257
|
+
|
|
258
|
+
function applyBlur(sourceTexture: WebGLTexture, width: number, height: number) {
|
|
259
|
+
if (!blurRadius || !blurProgram || !blurUniforms) return bgTexture;
|
|
260
|
+
|
|
261
|
+
gl.useProgram(blurProgram);
|
|
262
|
+
|
|
263
|
+
// Set common attributes
|
|
264
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
265
|
+
gl.vertexAttribPointer(blurUniforms.position, 2, gl.FLOAT, false, 0, 0);
|
|
266
|
+
gl.enableVertexAttribArray(blurUniforms.position);
|
|
267
|
+
|
|
268
|
+
const texelWidth = 1.0 / width;
|
|
269
|
+
const texelHeight = 1.0 / height;
|
|
270
|
+
|
|
271
|
+
// First pass - horizontal blur
|
|
272
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[0]);
|
|
273
|
+
gl.viewport(0, 0, width, height);
|
|
274
|
+
|
|
275
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
276
|
+
gl.bindTexture(gl.TEXTURE_2D, sourceTexture);
|
|
277
|
+
gl.uniform1i(blurUniforms.texture, 0);
|
|
278
|
+
gl.uniform2f(blurUniforms.texelSize, texelWidth, texelHeight);
|
|
279
|
+
gl.uniform2f(blurUniforms.direction, 1.0, 0.0); // Horizontal
|
|
280
|
+
gl.uniform1f(blurUniforms.radius, blurRadius);
|
|
281
|
+
|
|
282
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
283
|
+
|
|
284
|
+
// Second pass - vertical blur
|
|
285
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[1]);
|
|
286
|
+
gl.viewport(0, 0, width, height);
|
|
287
|
+
|
|
288
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
289
|
+
gl.bindTexture(gl.TEXTURE_2D, processTextures[0]);
|
|
290
|
+
gl.uniform1i(blurUniforms.texture, 0);
|
|
291
|
+
gl.uniform2f(blurUniforms.direction, 0.0, 1.0); // Vertical
|
|
292
|
+
|
|
293
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
294
|
+
|
|
295
|
+
// Reset framebuffer
|
|
296
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
297
|
+
|
|
298
|
+
return processTextures[1];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function render(frame: VideoFrame, mask: MPMask) {
|
|
302
|
+
if (frame.codedWidth === 0 || mask.width === 0) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const width = frame.displayWidth;
|
|
307
|
+
const height = frame.displayHeight;
|
|
308
|
+
|
|
309
|
+
// Prepare frame texture
|
|
310
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
311
|
+
gl.bindTexture(gl.TEXTURE_2D, frameTexture);
|
|
312
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame);
|
|
313
|
+
|
|
314
|
+
// Apply blur if enabled (and no custom background is set)
|
|
315
|
+
let backgroundTexture = bgTexture;
|
|
316
|
+
|
|
317
|
+
// If we have a custom background image, use that
|
|
318
|
+
if (customBackgroundImage) {
|
|
319
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
320
|
+
gl.bindTexture(gl.TEXTURE_2D, bgTexture);
|
|
321
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, customBackgroundImage);
|
|
322
|
+
backgroundTexture = bgTexture;
|
|
323
|
+
} else if (blurRadius) {
|
|
324
|
+
// Otherwise, if blur is enabled, apply blur effect to the frame
|
|
325
|
+
backgroundTexture = applyBlur(frameTexture, width, height);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Get the mask texture
|
|
329
|
+
const maskTexture = mask.getAsWebGLTexture();
|
|
330
|
+
|
|
331
|
+
// Render the final composite
|
|
332
|
+
gl.viewport(0, 0, width, height);
|
|
333
|
+
gl.clearColor(1.0, 1.0, 1.0, 1.0);
|
|
334
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
335
|
+
|
|
336
|
+
gl.useProgram(compositeProgram);
|
|
337
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
338
|
+
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
339
|
+
gl.enableVertexAttribArray(positionLocation);
|
|
340
|
+
|
|
341
|
+
// Set background texture (either original, blurred or custom)
|
|
342
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
343
|
+
gl.bindTexture(gl.TEXTURE_2D, backgroundTexture);
|
|
344
|
+
gl.uniform1i(bgTextureLocation, 0);
|
|
345
|
+
|
|
346
|
+
// Set frame texture
|
|
347
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
348
|
+
gl.bindTexture(gl.TEXTURE_2D, frameTexture);
|
|
349
|
+
gl.uniform1i(frameTextureLocation, 1);
|
|
350
|
+
|
|
351
|
+
// Set mask texture
|
|
352
|
+
gl.activeTexture(gl.TEXTURE2);
|
|
353
|
+
gl.bindTexture(gl.TEXTURE_2D, maskTexture);
|
|
354
|
+
gl.uniform1i(maskTextureLocation, 2);
|
|
355
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
356
|
+
|
|
357
|
+
mask.close();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Set or update the background image
|
|
362
|
+
* @param image The background image to use, or null to clear
|
|
363
|
+
*/
|
|
364
|
+
async function setBackgroundImage(image: ImageBitmap | null) {
|
|
365
|
+
// Clear existing background
|
|
366
|
+
customBackgroundImage = null;
|
|
367
|
+
|
|
368
|
+
if (image) {
|
|
369
|
+
try {
|
|
370
|
+
// Get current canvas dimensions
|
|
371
|
+
const canvasWidth = canvas.width;
|
|
372
|
+
const canvasHeight = canvas.height;
|
|
373
|
+
|
|
374
|
+
// Calculate dimensions and crop for "cover" mode
|
|
375
|
+
const imgAspect = image.width / image.height;
|
|
376
|
+
const canvasAspect = canvasWidth / canvasHeight;
|
|
377
|
+
|
|
378
|
+
let sx = 0;
|
|
379
|
+
let sy = 0;
|
|
380
|
+
let sWidth = image.width;
|
|
381
|
+
let sHeight = image.height;
|
|
382
|
+
|
|
383
|
+
// For cover mode, we need to crop some parts of the image
|
|
384
|
+
// to ensure it covers the canvas while maintaining aspect ratio
|
|
385
|
+
if (imgAspect > canvasAspect) {
|
|
386
|
+
// Image is wider than canvas - crop the sides
|
|
387
|
+
sWidth = Math.round(image.height * canvasAspect);
|
|
388
|
+
sx = Math.round((image.width - sWidth) / 2); // Center the crop horizontally
|
|
389
|
+
} else if (imgAspect < canvasAspect) {
|
|
390
|
+
// Image is taller than canvas - crop the top/bottom
|
|
391
|
+
sHeight = Math.round(image.width / canvasAspect);
|
|
392
|
+
sy = Math.round((image.height - sHeight) / 2); // Center the crop vertically
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Create a new ImageBitmap with the cropped portion
|
|
396
|
+
const croppedImage = await createImageBitmap(image, sx, sy, sWidth, sHeight, {
|
|
397
|
+
resizeWidth: canvasWidth,
|
|
398
|
+
resizeHeight: canvasHeight,
|
|
399
|
+
resizeQuality: 'medium',
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Store the cropped and resized image
|
|
403
|
+
customBackgroundImage = croppedImage;
|
|
404
|
+
|
|
405
|
+
// Load the image into the texture
|
|
406
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
407
|
+
gl.bindTexture(gl.TEXTURE_2D, bgTexture);
|
|
408
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, croppedImage);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error('Error processing background image:', error);
|
|
411
|
+
// Fallback to original image on error
|
|
412
|
+
customBackgroundImage = image;
|
|
413
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
414
|
+
gl.bindTexture(gl.TEXTURE_2D, bgTexture);
|
|
415
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
// set the background texture to an empty 2x2 image
|
|
419
|
+
const emptyImage = new ImageData(2, 2);
|
|
420
|
+
emptyImage.data[0] = 0;
|
|
421
|
+
emptyImage.data[1] = 0;
|
|
422
|
+
emptyImage.data[2] = 0;
|
|
423
|
+
emptyImage.data[3] = 0;
|
|
424
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
425
|
+
gl.bindTexture(gl.TEXTURE_2D, bgTexture);
|
|
426
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, emptyImage);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function setBlurRadius(radius: number | null) {
|
|
431
|
+
blurRadius = radius;
|
|
432
|
+
setBackgroundImage(null);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function cleanup() {
|
|
436
|
+
gl.deleteProgram(compositeProgram);
|
|
437
|
+
gl.deleteProgram(blurProgram);
|
|
438
|
+
gl.deleteTexture(bgTexture);
|
|
439
|
+
gl.deleteTexture(frameTexture);
|
|
440
|
+
for (const texture of processTextures) {
|
|
441
|
+
gl.deleteTexture(texture);
|
|
442
|
+
}
|
|
443
|
+
for (const framebuffer of processFramebuffers) {
|
|
444
|
+
gl.deleteFramebuffer(framebuffer);
|
|
445
|
+
}
|
|
446
|
+
gl.deleteBuffer(vertexBuffer);
|
|
447
|
+
|
|
448
|
+
// Release any ImageBitmap resources
|
|
449
|
+
if (customBackgroundImage) {
|
|
450
|
+
customBackgroundImage.close();
|
|
451
|
+
customBackgroundImage = null;
|
|
452
|
+
}
|
|
453
|
+
processTextures = [];
|
|
454
|
+
processFramebuffers = [];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { render, setBackgroundImage, setBlurRadius, cleanup };
|
|
458
|
+
};
|