@stream-io/video-filters-web 0.0.1

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +202 -0
  3. package/README.md +50 -0
  4. package/dist/index.cjs.js +1558 -0
  5. package/dist/index.cjs.js.map +1 -0
  6. package/dist/index.d.ts +4 -0
  7. package/dist/index.es.js +1554 -0
  8. package/dist/index.es.js.map +1 -0
  9. package/dist/src/compatibility.d.ts +5 -0
  10. package/dist/src/createRenderer.d.ts +17 -0
  11. package/dist/src/helpers/webglHelper.d.ts +17 -0
  12. package/dist/src/segmentation.d.ts +9 -0
  13. package/dist/src/tflite.d.ts +19 -0
  14. package/dist/src/version.d.ts +2 -0
  15. package/dist/src/webgl2/backgroundBlurStage.d.ts +7 -0
  16. package/dist/src/webgl2/backgroundImageStage.d.ts +8 -0
  17. package/dist/src/webgl2/jointBilateralFilterStage.d.ts +7 -0
  18. package/dist/src/webgl2/resizingStage.d.ts +6 -0
  19. package/dist/src/webgl2/softmaxStage.d.ts +6 -0
  20. package/dist/src/webgl2/webgl2Pipeline.d.ts +8 -0
  21. package/index.ts +4 -0
  22. package/package.json +40 -0
  23. package/src/compatibility.ts +23 -0
  24. package/src/createRenderer.ts +63 -0
  25. package/src/helpers/webglHelper.ts +142 -0
  26. package/src/segmentation.ts +18 -0
  27. package/src/tflite-simd.js +874 -0
  28. package/src/tflite.ts +59 -0
  29. package/src/version.ts +3 -0
  30. package/src/webgl2/backgroundBlurStage.ts +307 -0
  31. package/src/webgl2/backgroundImageStage.ts +222 -0
  32. package/src/webgl2/jointBilateralFilterStage.ts +159 -0
  33. package/src/webgl2/resizingStage.ts +103 -0
  34. package/src/webgl2/softmaxStage.ts +103 -0
  35. package/src/webgl2/webgl2Pipeline.ts +200 -0
  36. package/tf/models/segm_full_v679.tflite +0 -0
  37. package/tf/tflite/tflite-simd.wasm +0 -0
package/src/tflite.ts ADDED
@@ -0,0 +1,59 @@
1
+ // @ts-expect-error - module is not declared
2
+ import { createTFLiteSIMDModule } from './tflite-simd.js';
3
+ import { packageName, version } from './version';
4
+
5
+ // This is a WebAssembly module compiled from the TensorFlow Lite C++ library.
6
+ const createTFLite = createTFLiteSIMDModule as (opts?: any) => Promise<TFLite>;
7
+
8
+ export interface TFLite extends EmscriptenModule {
9
+ _getModelBufferMemoryOffset(): number;
10
+ _getInputMemoryOffset(): number;
11
+ _getInputHeight(): number;
12
+ _getInputWidth(): number;
13
+ _getInputChannelCount(): number;
14
+ _getOutputMemoryOffset(): number;
15
+ _getOutputHeight(): number;
16
+ _getOutputWidth(): number;
17
+ _getOutputChannelCount(): number;
18
+ _loadModel(bufferSize: number): number;
19
+ _runInference(): number;
20
+ }
21
+
22
+ export const loadTFLite = async (
23
+ options: {
24
+ basePath?: string;
25
+ tfFilePath?: string;
26
+ modelFilePath?: string;
27
+ } = {},
28
+ ) => {
29
+ const {
30
+ basePath = `https://unpkg.com/${packageName}@${version}/tf`,
31
+ tfFilePath = `${basePath}/tflite/tflite-simd.wasm`,
32
+ modelFilePath = `${basePath}/models/segm_full_v679.tflite`,
33
+ } = options;
34
+
35
+ const [tfLite, model] = await Promise.all([
36
+ createTFLite({ locateFile: () => tfFilePath }),
37
+ fetchModel(modelFilePath),
38
+ ]);
39
+
40
+ const modelBufferOffset = tfLite._getModelBufferMemoryOffset();
41
+ tfLite.HEAPU8.set(new Uint8Array(model), modelBufferOffset);
42
+ tfLite._loadModel(model.byteLength);
43
+
44
+ return tfLite;
45
+ };
46
+
47
+ let lastModelFilePath = '';
48
+ let modelFileCache: ArrayBuffer | undefined;
49
+ const fetchModel = async (modelFilePath: string) => {
50
+ const model =
51
+ modelFilePath === lastModelFilePath && modelFileCache
52
+ ? modelFileCache
53
+ : await fetch(modelFilePath).then((r) => r.arrayBuffer());
54
+
55
+ // Cache the model file for future use.
56
+ modelFileCache = model;
57
+ lastModelFilePath = modelFilePath;
58
+ return model;
59
+ };
package/src/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ export const version = process.env.PKG_VERSION || '0.0.0-development';
2
+ export const packageName =
3
+ process.env.PKG_NAME || '@stream-io/video-filters-web';
@@ -0,0 +1,307 @@
1
+ import {
2
+ compileShader,
3
+ createPipelineStageProgram,
4
+ createTexture,
5
+ glsl,
6
+ } from '../helpers/webglHelper';
7
+ import type { BackgroundBlurLevel } from '../createRenderer';
8
+
9
+ export type BackgroundBlurStage = {
10
+ render(): void;
11
+ updateCoverage(coverage: [number, number]): void;
12
+ cleanUp(): void;
13
+ };
14
+
15
+ export function buildBackgroundBlurStage(
16
+ gl: WebGL2RenderingContext,
17
+ vertexShader: WebGLShader,
18
+ positionBuffer: WebGLBuffer,
19
+ texCoordBuffer: WebGLBuffer,
20
+ personMaskTexture: WebGLTexture,
21
+ canvas: HTMLCanvasElement,
22
+ blurLevel: BackgroundBlurLevel,
23
+ ): BackgroundBlurStage {
24
+ const blurPass = buildBlurPass(
25
+ gl,
26
+ vertexShader,
27
+ positionBuffer,
28
+ texCoordBuffer,
29
+ personMaskTexture,
30
+ canvas,
31
+ blurLevel,
32
+ );
33
+ const blendPass = buildBlendPass(gl, positionBuffer, texCoordBuffer, canvas);
34
+
35
+ function render() {
36
+ blurPass.render();
37
+ blendPass.render();
38
+ }
39
+
40
+ function updateCoverage(coverage: [number, number]) {
41
+ blendPass.updateCoverage(coverage);
42
+ }
43
+
44
+ function cleanUp() {
45
+ blendPass.cleanUp();
46
+ blurPass.cleanUp();
47
+ }
48
+
49
+ return {
50
+ render,
51
+ updateCoverage,
52
+ cleanUp,
53
+ };
54
+ }
55
+
56
+ function buildBlurPass(
57
+ gl: WebGL2RenderingContext,
58
+ vertexShader: WebGLShader,
59
+ positionBuffer: WebGLBuffer,
60
+ texCoordBuffer: WebGLBuffer,
61
+ personMaskTexture: WebGLTexture,
62
+ canvas: HTMLCanvasElement,
63
+ blurLevel: BackgroundBlurLevel,
64
+ ) {
65
+ const weights =
66
+ blurLevel === 'low'
67
+ ? [0.227027027, 0.1545945946, 0.1016216216, 0.0340540541, 0.0142162162]
68
+ : blurLevel === 'medium'
69
+ ? [0.327027027, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162]
70
+ : [0.627027027, 0.3445945946, 0.2216216216, 0.0540540541, 0.0162162162];
71
+
72
+ const fragmentShaderSource = glsl`#version 300 es
73
+
74
+ precision highp float;
75
+
76
+ uniform sampler2D u_inputFrame;
77
+ uniform sampler2D u_personMask;
78
+ uniform vec2 u_texelSize;
79
+
80
+ in vec2 v_texCoord;
81
+ out vec4 outColor;
82
+
83
+ const float offset[5] = float[](0.0, 1.0, 2.0, 3.0, 4.0);
84
+ const float weight[5] = float[](
85
+ ${weights.join(',')}
86
+ );
87
+
88
+ void main() {
89
+ vec4 centerColor = texture(u_inputFrame, v_texCoord);
90
+ float personMask = texture(u_personMask, v_texCoord).a;
91
+
92
+ vec4 frameColor = centerColor * weight[0] * (1.0 - personMask);
93
+
94
+ for (int i = 1; i < 5; i++) {
95
+ vec2 offset = vec2(offset[i]) * u_texelSize;
96
+
97
+ vec2 texCoord = v_texCoord + offset;
98
+ frameColor += texture(u_inputFrame, texCoord)
99
+ * weight[i]
100
+ * (1.0 - texture(u_personMask, texCoord).a);
101
+
102
+ texCoord = v_texCoord - offset;
103
+ frameColor += texture(u_inputFrame, texCoord)
104
+ * weight[i]
105
+ * (1.0 - texture(u_personMask, texCoord).a);
106
+ }
107
+ outColor = vec4(frameColor.rgb + (1.0 - frameColor.a) * centerColor.rgb, 1.0);
108
+ }
109
+ `;
110
+
111
+ const scale = 0.5;
112
+ const outputWidth = canvas.width * scale;
113
+ const outputHeight = canvas.height * scale;
114
+ const texelWidth = 1 / outputWidth;
115
+ const texelHeight = 1 / outputHeight;
116
+
117
+ const fragmentShader = compileShader(
118
+ gl,
119
+ gl.FRAGMENT_SHADER,
120
+ fragmentShaderSource,
121
+ );
122
+ const program = createPipelineStageProgram(
123
+ gl,
124
+ vertexShader,
125
+ fragmentShader,
126
+ positionBuffer,
127
+ texCoordBuffer,
128
+ );
129
+ const inputFrameLocation = gl.getUniformLocation(program, 'u_inputFrame');
130
+ const personMaskLocation = gl.getUniformLocation(program, 'u_personMask');
131
+ const texelSizeLocation = gl.getUniformLocation(program, 'u_texelSize');
132
+ const texture1 = createTexture(
133
+ gl,
134
+ gl.RGBA8,
135
+ outputWidth,
136
+ outputHeight,
137
+ gl.NEAREST,
138
+ // @ts-expect-error types are incomplete
139
+ gl.LINEAR,
140
+ );
141
+ const texture2 = createTexture(
142
+ gl,
143
+ gl.RGBA8,
144
+ outputWidth,
145
+ outputHeight,
146
+ gl.NEAREST,
147
+ // @ts-expect-error types are incomplete
148
+ gl.LINEAR,
149
+ );
150
+
151
+ const frameBuffer1 = gl.createFramebuffer();
152
+ gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer1);
153
+ gl.framebufferTexture2D(
154
+ gl.FRAMEBUFFER,
155
+ gl.COLOR_ATTACHMENT0,
156
+ gl.TEXTURE_2D,
157
+ texture1,
158
+ 0,
159
+ );
160
+
161
+ const frameBuffer2 = gl.createFramebuffer();
162
+ gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer2);
163
+ gl.framebufferTexture2D(
164
+ gl.FRAMEBUFFER,
165
+ gl.COLOR_ATTACHMENT0,
166
+ gl.TEXTURE_2D,
167
+ texture2,
168
+ 0,
169
+ );
170
+
171
+ gl.useProgram(program);
172
+ gl.uniform1i(personMaskLocation, 1);
173
+
174
+ function render() {
175
+ gl.viewport(0, 0, outputWidth, outputHeight);
176
+ gl.useProgram(program);
177
+ gl.uniform1i(inputFrameLocation, 0);
178
+ gl.activeTexture(gl.TEXTURE1);
179
+ gl.bindTexture(gl.TEXTURE_2D, personMaskTexture);
180
+
181
+ for (let i = 0; i < 3; i++) {
182
+ gl.uniform2f(texelSizeLocation, 0, texelHeight);
183
+ gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer1);
184
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
185
+
186
+ gl.activeTexture(gl.TEXTURE2);
187
+ gl.bindTexture(gl.TEXTURE_2D, texture1);
188
+ gl.uniform1i(inputFrameLocation, 2);
189
+
190
+ gl.uniform2f(texelSizeLocation, texelWidth, 0);
191
+ gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer2);
192
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
193
+
194
+ gl.bindTexture(gl.TEXTURE_2D, texture2);
195
+ }
196
+ }
197
+
198
+ function cleanUp() {
199
+ gl.deleteFramebuffer(frameBuffer2);
200
+ gl.deleteFramebuffer(frameBuffer1);
201
+ gl.deleteTexture(texture2);
202
+ gl.deleteTexture(texture1);
203
+ gl.deleteProgram(program);
204
+ gl.deleteShader(fragmentShader);
205
+ }
206
+
207
+ return {
208
+ render,
209
+ cleanUp,
210
+ };
211
+ }
212
+
213
+ function buildBlendPass(
214
+ gl: WebGL2RenderingContext,
215
+ positionBuffer: WebGLBuffer,
216
+ texCoordBuffer: WebGLBuffer,
217
+ canvas: HTMLCanvasElement,
218
+ ) {
219
+ const vertexShaderSource = glsl`#version 300 es
220
+
221
+ in vec2 a_position;
222
+ in vec2 a_texCoord;
223
+
224
+ out vec2 v_texCoord;
225
+
226
+ void main() {
227
+ // Flipping Y is required when rendering to canvas
228
+ gl_Position = vec4(a_position * vec2(1.0, -1.0), 0.0, 1.0);
229
+ v_texCoord = a_texCoord;
230
+ }
231
+ `;
232
+
233
+ const fragmentShaderSource = glsl`#version 300 es
234
+
235
+ precision highp float;
236
+
237
+ uniform sampler2D u_inputFrame;
238
+ uniform sampler2D u_personMask;
239
+ uniform sampler2D u_blurredInputFrame;
240
+ uniform vec2 u_coverage;
241
+
242
+ in vec2 v_texCoord;
243
+
244
+ out vec4 outColor;
245
+
246
+ void main() {
247
+ vec3 color = texture(u_inputFrame, v_texCoord).rgb;
248
+ vec3 blurredColor = texture(u_blurredInputFrame, v_texCoord).rgb;
249
+ float personMask = texture(u_personMask, v_texCoord).a;
250
+ personMask = smoothstep(u_coverage.x, u_coverage.y, personMask);
251
+ outColor = vec4(mix(blurredColor, color, personMask), 1.0);
252
+ }
253
+ `;
254
+
255
+ const { width: outputWidth, height: outputHeight } = canvas;
256
+
257
+ const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
258
+ const fragmentShader = compileShader(
259
+ gl,
260
+ gl.FRAGMENT_SHADER,
261
+ fragmentShaderSource,
262
+ );
263
+ const program = createPipelineStageProgram(
264
+ gl,
265
+ vertexShader,
266
+ fragmentShader,
267
+ positionBuffer,
268
+ texCoordBuffer,
269
+ );
270
+ const inputFrameLocation = gl.getUniformLocation(program, 'u_inputFrame');
271
+ const personMaskLocation = gl.getUniformLocation(program, 'u_personMask');
272
+ const blurredInputFrame = gl.getUniformLocation(
273
+ program,
274
+ 'u_blurredInputFrame',
275
+ );
276
+ const coverageLocation = gl.getUniformLocation(program, 'u_coverage');
277
+
278
+ gl.useProgram(program);
279
+ gl.uniform1i(inputFrameLocation, 0);
280
+ gl.uniform1i(personMaskLocation, 1);
281
+ gl.uniform1i(blurredInputFrame, 2);
282
+ gl.uniform2f(coverageLocation, 0, 1);
283
+
284
+ function render() {
285
+ gl.viewport(0, 0, outputWidth, outputHeight);
286
+ gl.useProgram(program);
287
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
288
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
289
+ }
290
+
291
+ function updateCoverage(coverage: [number, number]) {
292
+ gl.useProgram(program);
293
+ gl.uniform2f(coverageLocation, coverage[0], coverage[1]);
294
+ }
295
+
296
+ function cleanUp() {
297
+ gl.deleteProgram(program);
298
+ gl.deleteShader(fragmentShader);
299
+ gl.deleteShader(vertexShader);
300
+ }
301
+
302
+ return {
303
+ render,
304
+ updateCoverage,
305
+ cleanUp,
306
+ };
307
+ }
@@ -0,0 +1,222 @@
1
+ import {
2
+ compileShader,
3
+ createPipelineStageProgram,
4
+ createTexture,
5
+ glsl,
6
+ } from '../helpers/webglHelper';
7
+
8
+ export type BackgroundImageStage = {
9
+ render(): void;
10
+ updateCoverage(coverage: [number, number]): void;
11
+ updateLightWrapping(lightWrapping: number): void;
12
+ updateBlendMode(blendMode: 'screen' | 'linearDodge'): void;
13
+ cleanUp(): void;
14
+ };
15
+
16
+ export function buildBackgroundImageStage(
17
+ gl: WebGL2RenderingContext,
18
+ positionBuffer: WebGLBuffer,
19
+ texCoordBuffer: WebGLBuffer,
20
+ personMaskTexture: WebGLTexture,
21
+ backgroundImage: HTMLImageElement | undefined,
22
+ canvas: HTMLCanvasElement,
23
+ ): BackgroundImageStage {
24
+ const vertexShaderSource = glsl`#version 300 es
25
+
26
+ uniform vec2 u_backgroundScale;
27
+ uniform vec2 u_backgroundOffset;
28
+
29
+ in vec2 a_position;
30
+ in vec2 a_texCoord;
31
+
32
+ out vec2 v_texCoord;
33
+ out vec2 v_backgroundCoord;
34
+
35
+ void main() {
36
+ // Flipping Y is required when rendering to canvas
37
+ gl_Position = vec4(a_position * vec2(1.0, -1.0), 0.0, 1.0);
38
+ v_texCoord = a_texCoord;
39
+ v_backgroundCoord = a_texCoord * u_backgroundScale + u_backgroundOffset;
40
+ }
41
+ `;
42
+
43
+ const fragmentShaderSource = glsl`#version 300 es
44
+
45
+ precision highp float;
46
+
47
+ uniform sampler2D u_inputFrame;
48
+ uniform sampler2D u_personMask;
49
+ uniform sampler2D u_background;
50
+ uniform vec2 u_coverage;
51
+ uniform float u_lightWrapping;
52
+ uniform float u_blendMode;
53
+
54
+ in vec2 v_texCoord;
55
+ in vec2 v_backgroundCoord;
56
+
57
+ out vec4 outColor;
58
+
59
+ vec3 screen(vec3 a, vec3 b) {
60
+ return 1.0 - (1.0 - a) * (1.0 - b);
61
+ }
62
+
63
+ vec3 linearDodge(vec3 a, vec3 b) {
64
+ return a + b;
65
+ }
66
+
67
+ void main() {
68
+ vec3 frameColor = texture(u_inputFrame, v_texCoord).rgb;
69
+ vec3 backgroundColor = texture(u_background, v_backgroundCoord).rgb;
70
+ float personMask = texture(u_personMask, v_texCoord).a;
71
+ float lightWrapMask = 1.0 - max(0.0, personMask - u_coverage.y) / (1.0 - u_coverage.y);
72
+ vec3 lightWrap = u_lightWrapping * lightWrapMask * backgroundColor;
73
+
74
+ frameColor = u_blendMode * linearDodge(frameColor, lightWrap)
75
+ + (1.0 - u_blendMode) * screen(frameColor, lightWrap);
76
+ personMask = smoothstep(u_coverage.x, u_coverage.y, personMask);
77
+ outColor = vec4(frameColor * personMask + backgroundColor * (1.0 - personMask), 1.0);
78
+ }
79
+ `;
80
+
81
+ const { width: outputWidth, height: outputHeight } = canvas;
82
+ const outputRatio = outputWidth / outputHeight;
83
+
84
+ const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
85
+ const fragmentShader = compileShader(
86
+ gl,
87
+ gl.FRAGMENT_SHADER,
88
+ fragmentShaderSource,
89
+ );
90
+ const program = createPipelineStageProgram(
91
+ gl,
92
+ vertexShader,
93
+ fragmentShader,
94
+ positionBuffer,
95
+ texCoordBuffer,
96
+ );
97
+ const backgroundScaleLocation = gl.getUniformLocation(
98
+ program,
99
+ 'u_backgroundScale',
100
+ );
101
+ const backgroundOffsetLocation = gl.getUniformLocation(
102
+ program,
103
+ 'u_backgroundOffset',
104
+ );
105
+ const inputFrameLocation = gl.getUniformLocation(program, 'u_inputFrame');
106
+ const personMaskLocation = gl.getUniformLocation(program, 'u_personMask');
107
+ const backgroundLocation = gl.getUniformLocation(program, 'u_background');
108
+ const coverageLocation = gl.getUniformLocation(program, 'u_coverage');
109
+ const lightWrappingLocation = gl.getUniformLocation(
110
+ program,
111
+ 'u_lightWrapping',
112
+ );
113
+ const blendModeLocation = gl.getUniformLocation(program, 'u_blendMode');
114
+
115
+ gl.useProgram(program);
116
+ gl.uniform2f(backgroundScaleLocation, 1, 1);
117
+ gl.uniform2f(backgroundOffsetLocation, 0, 0);
118
+ gl.uniform1i(inputFrameLocation, 0);
119
+ gl.uniform1i(personMaskLocation, 1);
120
+ gl.uniform2f(coverageLocation, 0, 1);
121
+ gl.uniform1f(lightWrappingLocation, 0);
122
+ gl.uniform1f(blendModeLocation, 0);
123
+
124
+ let backgroundTexture: WebGLTexture | null = null;
125
+ // TODO Find a better to handle background being loaded
126
+ if (backgroundImage?.complete) {
127
+ updateBackgroundImage(backgroundImage);
128
+ } else if (backgroundImage) {
129
+ backgroundImage.onload = () => {
130
+ updateBackgroundImage(backgroundImage);
131
+ };
132
+ }
133
+
134
+ function render() {
135
+ gl.viewport(0, 0, outputWidth, outputHeight);
136
+ gl.useProgram(program);
137
+ gl.activeTexture(gl.TEXTURE1);
138
+ gl.bindTexture(gl.TEXTURE_2D, personMaskTexture);
139
+ if (backgroundTexture !== null) {
140
+ gl.activeTexture(gl.TEXTURE2);
141
+ gl.bindTexture(gl.TEXTURE_2D, backgroundTexture);
142
+ // TODO Handle correctly the background not loaded yet
143
+ gl.uniform1i(backgroundLocation, 2);
144
+ }
145
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
146
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
147
+ }
148
+
149
+ function updateBackgroundImage(bgImage: HTMLImageElement) {
150
+ backgroundTexture = createTexture(
151
+ gl,
152
+ gl.RGBA8,
153
+ bgImage.naturalWidth,
154
+ bgImage.naturalHeight,
155
+ // @ts-expect-error types are incomplete
156
+ gl.LINEAR,
157
+ gl.LINEAR,
158
+ );
159
+ gl.texSubImage2D(
160
+ gl.TEXTURE_2D,
161
+ 0,
162
+ 0,
163
+ 0,
164
+ bgImage.naturalWidth,
165
+ bgImage.naturalHeight,
166
+ gl.RGBA,
167
+ gl.UNSIGNED_BYTE,
168
+ bgImage,
169
+ );
170
+
171
+ let xOffset = 0;
172
+ let yOffset = 0;
173
+ let backgroundWidth = bgImage.naturalWidth;
174
+ let backgroundHeight = bgImage.naturalHeight;
175
+ const backgroundRatio = backgroundWidth / backgroundHeight;
176
+ if (backgroundRatio < outputRatio) {
177
+ backgroundHeight = backgroundWidth / outputRatio;
178
+ yOffset = (bgImage.naturalHeight - backgroundHeight) / 2;
179
+ } else {
180
+ backgroundWidth = backgroundHeight * outputRatio;
181
+ xOffset = (bgImage.naturalWidth - backgroundWidth) / 2;
182
+ }
183
+
184
+ const xScale = backgroundWidth / bgImage.naturalWidth;
185
+ const yScale = backgroundHeight / bgImage.naturalHeight;
186
+ xOffset /= bgImage.naturalWidth;
187
+ yOffset /= bgImage.naturalHeight;
188
+
189
+ gl.uniform2f(backgroundScaleLocation, xScale, yScale);
190
+ gl.uniform2f(backgroundOffsetLocation, xOffset, yOffset);
191
+ }
192
+
193
+ function updateCoverage(coverage: [number, number]) {
194
+ gl.useProgram(program);
195
+ gl.uniform2f(coverageLocation, coverage[0], coverage[1]);
196
+ }
197
+
198
+ function updateLightWrapping(lightWrapping: number) {
199
+ gl.useProgram(program);
200
+ gl.uniform1f(lightWrappingLocation, lightWrapping);
201
+ }
202
+
203
+ function updateBlendMode(blendMode: 'screen' | 'linearDodge') {
204
+ gl.useProgram(program);
205
+ gl.uniform1f(blendModeLocation, blendMode === 'screen' ? 0 : 1);
206
+ }
207
+
208
+ function cleanUp() {
209
+ gl.deleteTexture(backgroundTexture);
210
+ gl.deleteProgram(program);
211
+ gl.deleteShader(fragmentShader);
212
+ gl.deleteShader(vertexShader);
213
+ }
214
+
215
+ return {
216
+ render,
217
+ updateCoverage,
218
+ updateLightWrapping,
219
+ updateBlendMode,
220
+ cleanUp,
221
+ };
222
+ }