@remotion/effects 4.0.513 → 4.0.515
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/effect-internals.d.ts +8 -0
- package/dist/entrypoints/blur.d.ts +6 -0
- package/dist/esm/outline.mjs +703 -0
- package/dist/esm/page-turn.mjs +521 -0
- package/dist/outline/polygonize-alpha.d.ts +7 -0
- package/dist/outline.d.ts +54 -0
- package/dist/page-turn.d.ts +91 -0
- package/package.json +11 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const EffectInternals: {
|
|
2
|
+
readonly halftone: (params?: (import("./halftone.js").HalftoneParams & {
|
|
3
|
+
readonly disabled?: boolean | undefined;
|
|
4
|
+
}) | undefined) => import("remotion").EffectDescriptor<unknown>;
|
|
5
|
+
readonly tint: (params: import("./tint.js").TintParams & {
|
|
6
|
+
readonly disabled?: boolean | undefined;
|
|
7
|
+
}) => import("remotion").EffectDescriptor<unknown>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as blurExports from '../blur/index.js';
|
|
2
|
+
export type { BlurParams } from '../blur/index.js';
|
|
3
|
+
declare const blur: (params: blurExports.BlurParams & {
|
|
4
|
+
readonly disabled?: boolean | undefined;
|
|
5
|
+
}) => import("remotion").EffectDescriptor<unknown>;
|
|
6
|
+
export { blur };
|
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
// src/outline.ts
|
|
2
|
+
import { Internals } from "remotion";
|
|
3
|
+
|
|
4
|
+
// src/validate-effect-param.ts
|
|
5
|
+
var assertEffectParamsObject = (params, effectLabel) => {
|
|
6
|
+
if (params === null || typeof params !== "object") {
|
|
7
|
+
throw new TypeError(`${effectLabel} effect requires a parameters object, but got ${JSON.stringify(params)}`);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var assertRequiredFiniteNumber = (value, name) => {
|
|
11
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
12
|
+
throw new TypeError(`"${name}" must be a finite number, but got ${JSON.stringify(value)}`);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var assertRequiredColor = (value, name) => {
|
|
16
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
17
|
+
throw new TypeError(`"${name}" must be a non-empty string, but got ${JSON.stringify(value)}`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var assertOptionalColor = (value, name) => {
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
assertRequiredColor(value, name);
|
|
25
|
+
};
|
|
26
|
+
var assertOptionalBoolean = (value, name) => {
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value !== "boolean") {
|
|
31
|
+
throw new TypeError(`"${name}" must be a boolean, but got ${JSON.stringify(value)}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/color-utils.ts
|
|
36
|
+
var DEFAULT_AMOUNT = 1;
|
|
37
|
+
var DEFAULT_BRIGHTNESS_AMOUNT = 0;
|
|
38
|
+
var DEFAULT_HUE_DEGREES = 0;
|
|
39
|
+
var colorAmountSchema = {
|
|
40
|
+
type: "number",
|
|
41
|
+
min: 0,
|
|
42
|
+
max: 1,
|
|
43
|
+
step: 0.01,
|
|
44
|
+
default: DEFAULT_AMOUNT,
|
|
45
|
+
description: "Amount",
|
|
46
|
+
hiddenFromList: false
|
|
47
|
+
};
|
|
48
|
+
var colorMultiplierSchema = {
|
|
49
|
+
type: "number",
|
|
50
|
+
min: 0,
|
|
51
|
+
step: 0.01,
|
|
52
|
+
default: DEFAULT_AMOUNT,
|
|
53
|
+
description: "Amount",
|
|
54
|
+
hiddenFromList: false
|
|
55
|
+
};
|
|
56
|
+
var brightnessAmountSchema = {
|
|
57
|
+
type: "number",
|
|
58
|
+
min: -1,
|
|
59
|
+
max: 1,
|
|
60
|
+
step: 0.01,
|
|
61
|
+
default: DEFAULT_BRIGHTNESS_AMOUNT,
|
|
62
|
+
description: "Amount",
|
|
63
|
+
hiddenFromList: false
|
|
64
|
+
};
|
|
65
|
+
var hueDegreesSchema = {
|
|
66
|
+
type: "rotation-degrees",
|
|
67
|
+
step: 1,
|
|
68
|
+
default: DEFAULT_HUE_DEGREES,
|
|
69
|
+
description: "Degrees"
|
|
70
|
+
};
|
|
71
|
+
var assertOptionalFiniteNumber = (value, name) => {
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
assertRequiredFiniteNumber(value, name);
|
|
76
|
+
};
|
|
77
|
+
var validateUnitInterval = (value, name) => {
|
|
78
|
+
if (value < 0) {
|
|
79
|
+
throw new TypeError(`"${name}" must be >= 0, but got ${JSON.stringify(value)}`);
|
|
80
|
+
}
|
|
81
|
+
if (value > 1) {
|
|
82
|
+
throw new TypeError(`"${name}" must be <= 1, but got ${JSON.stringify(value)}`);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var validateNonNegative = (value, name) => {
|
|
86
|
+
if (value < 0) {
|
|
87
|
+
throw new TypeError(`"${name}" must be >= 0, but got ${JSON.stringify(value)}`);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var validateSignedUnitInterval = (value, name) => {
|
|
91
|
+
if (value < -1) {
|
|
92
|
+
throw new TypeError(`"${name}" must be >= -1, but got ${JSON.stringify(value)}`);
|
|
93
|
+
}
|
|
94
|
+
if (value > 1) {
|
|
95
|
+
throw new TypeError(`"${name}" must be <= 1, but got ${JSON.stringify(value)}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var clampColorChannel = (value) => {
|
|
99
|
+
return Math.max(0, Math.min(255, value));
|
|
100
|
+
};
|
|
101
|
+
var parseColorRgba = (ctx, color) => {
|
|
102
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
103
|
+
ctx.fillStyle = color;
|
|
104
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
105
|
+
const { data } = ctx.getImageData(0, 0, 1, 1);
|
|
106
|
+
return [data[0], data[1], data[2], data[3]];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/outline/polygonize-alpha.ts
|
|
110
|
+
var pointKey = ([x, y]) => `${x},${y}`;
|
|
111
|
+
var edgeKey = (a, b) => a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
112
|
+
var squaredDistance = (a, b) => {
|
|
113
|
+
const dx = a[0] - b[0];
|
|
114
|
+
const dy = a[1] - b[1];
|
|
115
|
+
return dx * dx + dy * dy;
|
|
116
|
+
};
|
|
117
|
+
var squaredSegmentDistance = (point, start, end) => {
|
|
118
|
+
let x = start[0];
|
|
119
|
+
let y = start[1];
|
|
120
|
+
let dx = end[0] - x;
|
|
121
|
+
let dy = end[1] - y;
|
|
122
|
+
if (dx !== 0 || dy !== 0) {
|
|
123
|
+
const progress = ((point[0] - x) * dx + (point[1] - y) * dy) / (dx * dx + dy * dy);
|
|
124
|
+
if (progress > 1) {
|
|
125
|
+
x = end[0];
|
|
126
|
+
y = end[1];
|
|
127
|
+
} else if (progress > 0) {
|
|
128
|
+
x += dx * progress;
|
|
129
|
+
y += dy * progress;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
dx = point[0] - x;
|
|
133
|
+
dy = point[1] - y;
|
|
134
|
+
return dx * dx + dy * dy;
|
|
135
|
+
};
|
|
136
|
+
var simplifyOpenContour = (points, toleranceSquared) => {
|
|
137
|
+
if (points.length <= 2) {
|
|
138
|
+
return [...points];
|
|
139
|
+
}
|
|
140
|
+
const keep = new Uint8Array(points.length);
|
|
141
|
+
keep[0] = 1;
|
|
142
|
+
keep[points.length - 1] = 1;
|
|
143
|
+
const stack = [[0, points.length - 1]];
|
|
144
|
+
while (stack.length > 0) {
|
|
145
|
+
const range = stack.pop();
|
|
146
|
+
if (!range) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
const [startIndex, endIndex] = range;
|
|
150
|
+
let furthestIndex = -1;
|
|
151
|
+
let furthestDistance = toleranceSquared;
|
|
152
|
+
for (let index = startIndex + 1;index < endIndex; index++) {
|
|
153
|
+
const distance = squaredSegmentDistance(points[index], points[startIndex], points[endIndex]);
|
|
154
|
+
if (distance > furthestDistance) {
|
|
155
|
+
furthestDistance = distance;
|
|
156
|
+
furthestIndex = index;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (furthestIndex !== -1) {
|
|
160
|
+
keep[furthestIndex] = 1;
|
|
161
|
+
stack.push([startIndex, furthestIndex], [furthestIndex, endIndex]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return points.filter((_, index) => keep[index] === 1);
|
|
165
|
+
};
|
|
166
|
+
var simplifyClosedContour = (points, tolerance) => {
|
|
167
|
+
if (points.length <= 3 || tolerance <= 0) {
|
|
168
|
+
return [...points];
|
|
169
|
+
}
|
|
170
|
+
let oppositeIndex = 1;
|
|
171
|
+
let oppositeDistance = 0;
|
|
172
|
+
for (let index = 1;index < points.length; index++) {
|
|
173
|
+
const distance = squaredDistance(points[0], points[index]);
|
|
174
|
+
if (distance > oppositeDistance) {
|
|
175
|
+
oppositeDistance = distance;
|
|
176
|
+
oppositeIndex = index;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const firstHalf = points.slice(0, oppositeIndex + 1);
|
|
180
|
+
const secondHalf = [...points.slice(oppositeIndex), points[0]];
|
|
181
|
+
const toleranceSquared = tolerance * tolerance;
|
|
182
|
+
const simplified = [
|
|
183
|
+
...simplifyOpenContour(firstHalf, toleranceSquared).slice(0, -1),
|
|
184
|
+
...simplifyOpenContour(secondHalf, toleranceSquared).slice(0, -1)
|
|
185
|
+
];
|
|
186
|
+
return simplified.length >= 3 ? simplified : [...points];
|
|
187
|
+
};
|
|
188
|
+
var polygonizeAlpha = ({
|
|
189
|
+
data,
|
|
190
|
+
width,
|
|
191
|
+
height,
|
|
192
|
+
simplification
|
|
193
|
+
}) => {
|
|
194
|
+
const adjacency = new Map;
|
|
195
|
+
const points = new Map;
|
|
196
|
+
const addSegment = (a, b) => {
|
|
197
|
+
const aKey = pointKey(a);
|
|
198
|
+
const bKey = pointKey(b);
|
|
199
|
+
points.set(aKey, a);
|
|
200
|
+
points.set(bKey, b);
|
|
201
|
+
const aNeighbors = adjacency.get(aKey);
|
|
202
|
+
if (aNeighbors) {
|
|
203
|
+
aNeighbors.push(bKey);
|
|
204
|
+
} else {
|
|
205
|
+
adjacency.set(aKey, [bKey]);
|
|
206
|
+
}
|
|
207
|
+
const bNeighbors = adjacency.get(bKey);
|
|
208
|
+
if (bNeighbors) {
|
|
209
|
+
bNeighbors.push(aKey);
|
|
210
|
+
} else {
|
|
211
|
+
adjacency.set(bKey, [aKey]);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const isFilled = (x, y) => x >= 0 && y >= 0 && x < width && y < height && data[(y * width + x) * 4 + 3] > 0;
|
|
215
|
+
for (let y = -1;y < height; y++) {
|
|
216
|
+
for (let x = -1;x < width; x++) {
|
|
217
|
+
const configuration = (isFilled(x, y) ? 1 : 0) | (isFilled(x + 1, y) ? 2 : 0) | (isFilled(x + 1, y + 1) ? 4 : 0) | (isFilled(x, y + 1) ? 8 : 0);
|
|
218
|
+
if (configuration === 0 || configuration === 15) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const top = [x * 2 + 2, y * 2 + 1];
|
|
222
|
+
const right = [x * 2 + 3, y * 2 + 2];
|
|
223
|
+
const bottom = [x * 2 + 2, y * 2 + 3];
|
|
224
|
+
const left = [x * 2 + 1, y * 2 + 2];
|
|
225
|
+
switch (configuration) {
|
|
226
|
+
case 1:
|
|
227
|
+
case 14:
|
|
228
|
+
addSegment(top, left);
|
|
229
|
+
break;
|
|
230
|
+
case 2:
|
|
231
|
+
case 13:
|
|
232
|
+
addSegment(top, right);
|
|
233
|
+
break;
|
|
234
|
+
case 3:
|
|
235
|
+
case 12:
|
|
236
|
+
addSegment(left, right);
|
|
237
|
+
break;
|
|
238
|
+
case 4:
|
|
239
|
+
case 11:
|
|
240
|
+
addSegment(right, bottom);
|
|
241
|
+
break;
|
|
242
|
+
case 5:
|
|
243
|
+
addSegment(top, left);
|
|
244
|
+
addSegment(right, bottom);
|
|
245
|
+
break;
|
|
246
|
+
case 6:
|
|
247
|
+
case 9:
|
|
248
|
+
addSegment(top, bottom);
|
|
249
|
+
break;
|
|
250
|
+
case 7:
|
|
251
|
+
case 8:
|
|
252
|
+
addSegment(bottom, left);
|
|
253
|
+
break;
|
|
254
|
+
case 10:
|
|
255
|
+
addSegment(top, right);
|
|
256
|
+
addSegment(bottom, left);
|
|
257
|
+
break;
|
|
258
|
+
default:
|
|
259
|
+
throw new Error(`Unexpected marching squares case: ${configuration}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const visitedEdges = new Set;
|
|
264
|
+
const contours = [];
|
|
265
|
+
for (const [start, neighbors] of adjacency) {
|
|
266
|
+
for (const firstNeighbor of neighbors) {
|
|
267
|
+
if (visitedEdges.has(edgeKey(start, firstNeighbor))) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const contour = [start];
|
|
271
|
+
let previous = start;
|
|
272
|
+
let current = firstNeighbor;
|
|
273
|
+
visitedEdges.add(edgeKey(previous, current));
|
|
274
|
+
while (current !== start) {
|
|
275
|
+
contour.push(current);
|
|
276
|
+
const candidates = adjacency.get(current) ?? [];
|
|
277
|
+
const next = candidates.find((candidate) => candidate !== previous && !visitedEdges.has(edgeKey(current, candidate)));
|
|
278
|
+
if (!next) {
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
previous = current;
|
|
282
|
+
current = next;
|
|
283
|
+
visitedEdges.add(edgeKey(previous, current));
|
|
284
|
+
}
|
|
285
|
+
if (current !== start || contour.length < 3) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const decoded = contour.map((key) => {
|
|
289
|
+
const point = points.get(key);
|
|
290
|
+
if (!point) {
|
|
291
|
+
throw new Error(`Missing outline point: ${key}`);
|
|
292
|
+
}
|
|
293
|
+
return [point[0] / 2, point[1] / 2];
|
|
294
|
+
});
|
|
295
|
+
contours.push(simplifyClosedContour(decoded, simplification));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return contours;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/outline.ts
|
|
302
|
+
var { createEffect, createWebGL2ContextError } = Internals;
|
|
303
|
+
var DEFAULT_WIDTH = 8;
|
|
304
|
+
var DEFAULT_EDGE_SIMPLIFICATION = 0;
|
|
305
|
+
var DEFAULT_COLOR = "#ffffff";
|
|
306
|
+
var DEFAULT_OPACITY = 1;
|
|
307
|
+
var DEFAULT_OUTLINE_ONLY = false;
|
|
308
|
+
var outlineSchema = {
|
|
309
|
+
width: {
|
|
310
|
+
type: "number",
|
|
311
|
+
min: 0,
|
|
312
|
+
max: 100,
|
|
313
|
+
step: 1,
|
|
314
|
+
default: DEFAULT_WIDTH,
|
|
315
|
+
description: "Width",
|
|
316
|
+
hiddenFromList: false
|
|
317
|
+
},
|
|
318
|
+
edgeSimplification: {
|
|
319
|
+
type: "number",
|
|
320
|
+
min: 0,
|
|
321
|
+
max: 100,
|
|
322
|
+
step: 1,
|
|
323
|
+
default: DEFAULT_EDGE_SIMPLIFICATION,
|
|
324
|
+
description: "Edge simplification",
|
|
325
|
+
hiddenFromList: false
|
|
326
|
+
},
|
|
327
|
+
color: {
|
|
328
|
+
type: "color",
|
|
329
|
+
default: DEFAULT_COLOR,
|
|
330
|
+
description: "Color"
|
|
331
|
+
},
|
|
332
|
+
opacity: {
|
|
333
|
+
type: "number",
|
|
334
|
+
min: 0,
|
|
335
|
+
max: 1,
|
|
336
|
+
step: 0.01,
|
|
337
|
+
default: DEFAULT_OPACITY,
|
|
338
|
+
description: "Opacity",
|
|
339
|
+
hiddenFromList: false
|
|
340
|
+
},
|
|
341
|
+
outlineOnly: {
|
|
342
|
+
type: "boolean",
|
|
343
|
+
default: DEFAULT_OUTLINE_ONLY,
|
|
344
|
+
description: "Outline only"
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
var resolve = (params) => ({
|
|
348
|
+
width: params.width ?? DEFAULT_WIDTH,
|
|
349
|
+
edgeSimplification: params.edgeSimplification ?? DEFAULT_EDGE_SIMPLIFICATION,
|
|
350
|
+
color: params.color ?? DEFAULT_COLOR,
|
|
351
|
+
opacity: params.opacity ?? DEFAULT_OPACITY,
|
|
352
|
+
outlineOnly: params.outlineOnly ?? DEFAULT_OUTLINE_ONLY
|
|
353
|
+
});
|
|
354
|
+
var validateOutlineParams = (params) => {
|
|
355
|
+
assertEffectParamsObject(params, "Outline");
|
|
356
|
+
assertOptionalFiniteNumber(params.width, "width");
|
|
357
|
+
assertOptionalFiniteNumber(params.edgeSimplification, "edgeSimplification");
|
|
358
|
+
assertOptionalColor(params.color, "color");
|
|
359
|
+
assertOptionalFiniteNumber(params.opacity, "opacity");
|
|
360
|
+
assertOptionalBoolean(params.outlineOnly, "outlineOnly");
|
|
361
|
+
const resolved = resolve(params);
|
|
362
|
+
validateNonNegative(resolved.width, "width");
|
|
363
|
+
validateNonNegative(resolved.edgeSimplification, "edgeSimplification");
|
|
364
|
+
validateUnitInterval(resolved.opacity, "opacity");
|
|
365
|
+
};
|
|
366
|
+
var OUTLINE_VS = `#version 300 es
|
|
367
|
+
in vec2 aPos;
|
|
368
|
+
in vec2 aUv;
|
|
369
|
+
out vec2 vUv;
|
|
370
|
+
|
|
371
|
+
void main() {
|
|
372
|
+
vUv = aUv;
|
|
373
|
+
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
374
|
+
}
|
|
375
|
+
`;
|
|
376
|
+
var OUTLINE_FS = `#version 300 es
|
|
377
|
+
precision highp float;
|
|
378
|
+
|
|
379
|
+
in vec2 vUv;
|
|
380
|
+
out vec4 fragColor;
|
|
381
|
+
|
|
382
|
+
uniform sampler2D uSource;
|
|
383
|
+
uniform sampler2D uPolygonMask;
|
|
384
|
+
uniform bool uUsePolygonMask;
|
|
385
|
+
uniform float uWidth;
|
|
386
|
+
uniform vec4 uColor;
|
|
387
|
+
uniform float uOpacity;
|
|
388
|
+
uniform bool uOutlineOnly;
|
|
389
|
+
|
|
390
|
+
const float TAU = 6.283185307179586;
|
|
391
|
+
const float MIN_ALPHA = 0.5 / 255.0;
|
|
392
|
+
const int SAMPLE_DIRECTIONS = 32;
|
|
393
|
+
const int SAMPLE_RINGS = 3;
|
|
394
|
+
|
|
395
|
+
void main() {
|
|
396
|
+
vec4 source = texture(uSource, vUv);
|
|
397
|
+
|
|
398
|
+
if (uOpacity <= 0.0 || uColor.a <= 0.0) {
|
|
399
|
+
fragColor = uOutlineOnly ? vec4(0.0) : source;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (uWidth <= 0.0 && !uOutlineOnly) {
|
|
404
|
+
fragColor = source;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
float outlineMaskAlpha = 0.0;
|
|
409
|
+
if (uUsePolygonMask) {
|
|
410
|
+
outlineMaskAlpha = step(MIN_ALPHA, texture(uPolygonMask, vUv).a);
|
|
411
|
+
} else if (uWidth > 0.0) {
|
|
412
|
+
ivec2 sourceSize = textureSize(uSource, 0);
|
|
413
|
+
ivec2 sourcePosition = clamp(
|
|
414
|
+
ivec2(vUv * vec2(sourceSize)),
|
|
415
|
+
ivec2(0),
|
|
416
|
+
sourceSize - ivec2(1)
|
|
417
|
+
);
|
|
418
|
+
for (int ring = 1; ring <= SAMPLE_RINGS; ring++) {
|
|
419
|
+
float distancePx = uWidth * float(ring) / float(SAMPLE_RINGS);
|
|
420
|
+
for (int direction = 0; direction < SAMPLE_DIRECTIONS; direction++) {
|
|
421
|
+
float angle = TAU * float(direction) / float(SAMPLE_DIRECTIONS);
|
|
422
|
+
ivec2 offset = ivec2(round(
|
|
423
|
+
vec2(cos(angle), sin(angle)) * distancePx
|
|
424
|
+
));
|
|
425
|
+
ivec2 samplePosition = clamp(
|
|
426
|
+
sourcePosition + offset,
|
|
427
|
+
ivec2(0),
|
|
428
|
+
sourceSize - ivec2(1)
|
|
429
|
+
);
|
|
430
|
+
outlineMaskAlpha = max(
|
|
431
|
+
outlineMaskAlpha,
|
|
432
|
+
step(MIN_ALPHA, texelFetch(uSource, samplePosition, 0).a)
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (uOutlineOnly) {
|
|
439
|
+
float filledAlpha = (
|
|
440
|
+
uUsePolygonMask
|
|
441
|
+
? outlineMaskAlpha
|
|
442
|
+
: max(step(MIN_ALPHA, source.a), outlineMaskAlpha)
|
|
443
|
+
) * uColor.a * uOpacity;
|
|
444
|
+
fragColor = vec4(uColor.rgb * filledAlpha, filledAlpha);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
float outlineAlpha = outlineMaskAlpha * uColor.a * uOpacity * (1.0 - source.a);
|
|
449
|
+
vec3 outlineRgb = uColor.rgb * outlineAlpha;
|
|
450
|
+
fragColor = vec4(source.rgb + outlineRgb, source.a + outlineAlpha);
|
|
451
|
+
}
|
|
452
|
+
`;
|
|
453
|
+
var compileShader = (gl, type, source) => {
|
|
454
|
+
const shader = gl.createShader(type);
|
|
455
|
+
if (!shader) {
|
|
456
|
+
throw new Error("Failed to create WebGL shader");
|
|
457
|
+
}
|
|
458
|
+
gl.shaderSource(shader, source);
|
|
459
|
+
gl.compileShader(shader);
|
|
460
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
461
|
+
const log = gl.getShaderInfoLog(shader);
|
|
462
|
+
gl.deleteShader(shader);
|
|
463
|
+
throw new Error(`Outline shader compile failed: ${log ?? "(no log)"}`);
|
|
464
|
+
}
|
|
465
|
+
return shader;
|
|
466
|
+
};
|
|
467
|
+
var createProgram = (gl) => {
|
|
468
|
+
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, OUTLINE_VS);
|
|
469
|
+
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, OUTLINE_FS);
|
|
470
|
+
const program = gl.createProgram();
|
|
471
|
+
if (!program) {
|
|
472
|
+
throw new Error("Failed to create WebGL program");
|
|
473
|
+
}
|
|
474
|
+
gl.attachShader(program, vertexShader);
|
|
475
|
+
gl.attachShader(program, fragmentShader);
|
|
476
|
+
gl.linkProgram(program);
|
|
477
|
+
gl.deleteShader(vertexShader);
|
|
478
|
+
gl.deleteShader(fragmentShader);
|
|
479
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
480
|
+
const log = gl.getProgramInfoLog(program);
|
|
481
|
+
gl.deleteProgram(program);
|
|
482
|
+
throw new Error(`Outline program link failed: ${log ?? "(no log)"}`);
|
|
483
|
+
}
|
|
484
|
+
return program;
|
|
485
|
+
};
|
|
486
|
+
var createTexture = (gl) => {
|
|
487
|
+
const texture = gl.createTexture();
|
|
488
|
+
if (!texture) {
|
|
489
|
+
throw new Error("Failed to create WebGL texture");
|
|
490
|
+
}
|
|
491
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
492
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
493
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
494
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
495
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
496
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
497
|
+
return texture;
|
|
498
|
+
};
|
|
499
|
+
var updatePolygonMask = ({
|
|
500
|
+
source,
|
|
501
|
+
width,
|
|
502
|
+
height,
|
|
503
|
+
simplification,
|
|
504
|
+
outlineWidth,
|
|
505
|
+
state
|
|
506
|
+
}) => {
|
|
507
|
+
if (state.alphaCanvas.width !== width || state.alphaCanvas.height !== height || state.polygonMaskCanvas.width !== width || state.polygonMaskCanvas.height !== height) {
|
|
508
|
+
state.alphaCanvas.width = width;
|
|
509
|
+
state.alphaCanvas.height = height;
|
|
510
|
+
state.polygonMaskCanvas.width = width;
|
|
511
|
+
state.polygonMaskCanvas.height = height;
|
|
512
|
+
}
|
|
513
|
+
state.alphaCtx.clearRect(0, 0, width, height);
|
|
514
|
+
state.alphaCtx.drawImage(source, 0, 0, width, height);
|
|
515
|
+
const imageData = state.alphaCtx.getImageData(0, 0, width, height);
|
|
516
|
+
const contours = polygonizeAlpha({
|
|
517
|
+
data: imageData.data,
|
|
518
|
+
width,
|
|
519
|
+
height,
|
|
520
|
+
simplification
|
|
521
|
+
});
|
|
522
|
+
const { polygonMaskCtx: context } = state;
|
|
523
|
+
context.clearRect(0, 0, width, height);
|
|
524
|
+
context.beginPath();
|
|
525
|
+
for (const contour of contours) {
|
|
526
|
+
if (contour.length < 3) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
context.moveTo(contour[0][0], contour[0][1]);
|
|
530
|
+
for (let index = 1;index < contour.length; index++) {
|
|
531
|
+
context.lineTo(contour[index][0], contour[index][1]);
|
|
532
|
+
}
|
|
533
|
+
context.closePath();
|
|
534
|
+
}
|
|
535
|
+
context.fillStyle = "white";
|
|
536
|
+
context.fill("evenodd");
|
|
537
|
+
if (outlineWidth > 0) {
|
|
538
|
+
context.strokeStyle = "white";
|
|
539
|
+
context.lineWidth = outlineWidth * 2;
|
|
540
|
+
context.lineJoin = "miter";
|
|
541
|
+
context.miterLimit = 4;
|
|
542
|
+
context.stroke();
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
var setupOutline = (target) => {
|
|
546
|
+
const gl = target.getContext("webgl2", {
|
|
547
|
+
premultipliedAlpha: true,
|
|
548
|
+
alpha: true,
|
|
549
|
+
preserveDrawingBuffer: true
|
|
550
|
+
});
|
|
551
|
+
if (!gl) {
|
|
552
|
+
throw createWebGL2ContextError("outline effect");
|
|
553
|
+
}
|
|
554
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
555
|
+
const program = createProgram(gl);
|
|
556
|
+
const vao = gl.createVertexArray();
|
|
557
|
+
if (!vao) {
|
|
558
|
+
throw new Error("Failed to create WebGL vertex array");
|
|
559
|
+
}
|
|
560
|
+
gl.bindVertexArray(vao);
|
|
561
|
+
const vbo = gl.createBuffer();
|
|
562
|
+
if (!vbo) {
|
|
563
|
+
throw new Error("Failed to create WebGL buffer");
|
|
564
|
+
}
|
|
565
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
566
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, 1, 1, 1, 1]), gl.STATIC_DRAW);
|
|
567
|
+
const aPos = gl.getAttribLocation(program, "aPos");
|
|
568
|
+
const aUv = gl.getAttribLocation(program, "aUv");
|
|
569
|
+
gl.enableVertexAttribArray(aPos);
|
|
570
|
+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0);
|
|
571
|
+
gl.enableVertexAttribArray(aUv);
|
|
572
|
+
gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 16, 8);
|
|
573
|
+
gl.bindVertexArray(null);
|
|
574
|
+
const textureSource = createTexture(gl);
|
|
575
|
+
const texturePolygonMask = createTexture(gl);
|
|
576
|
+
gl.bindTexture(gl.TEXTURE_2D, texturePolygonMask);
|
|
577
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]));
|
|
578
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
579
|
+
const colorCanvas = target.ownerDocument.createElement("canvas");
|
|
580
|
+
colorCanvas.width = 1;
|
|
581
|
+
colorCanvas.height = 1;
|
|
582
|
+
const colorCtx = colorCanvas.getContext("2d", { willReadFrequently: true });
|
|
583
|
+
if (!colorCtx) {
|
|
584
|
+
throw new Error("Failed to acquire 2D context for color parsing");
|
|
585
|
+
}
|
|
586
|
+
const alphaCanvas = target.ownerDocument.createElement("canvas");
|
|
587
|
+
const alphaCtx = alphaCanvas.getContext("2d", { willReadFrequently: true });
|
|
588
|
+
if (!alphaCtx) {
|
|
589
|
+
throw new Error("Failed to acquire 2D context for outline extraction");
|
|
590
|
+
}
|
|
591
|
+
const polygonMaskCanvas = target.ownerDocument.createElement("canvas");
|
|
592
|
+
polygonMaskCanvas.width = 1;
|
|
593
|
+
polygonMaskCanvas.height = 1;
|
|
594
|
+
const polygonMaskCtx = polygonMaskCanvas.getContext("2d");
|
|
595
|
+
if (!polygonMaskCtx) {
|
|
596
|
+
throw new Error("Failed to acquire 2D context for outline mask");
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
gl,
|
|
600
|
+
program,
|
|
601
|
+
vao,
|
|
602
|
+
vbo,
|
|
603
|
+
textureSource,
|
|
604
|
+
texturePolygonMask,
|
|
605
|
+
uniforms: {
|
|
606
|
+
uSource: gl.getUniformLocation(program, "uSource"),
|
|
607
|
+
uPolygonMask: gl.getUniformLocation(program, "uPolygonMask"),
|
|
608
|
+
uUsePolygonMask: gl.getUniformLocation(program, "uUsePolygonMask"),
|
|
609
|
+
uWidth: gl.getUniformLocation(program, "uWidth"),
|
|
610
|
+
uColor: gl.getUniformLocation(program, "uColor"),
|
|
611
|
+
uOpacity: gl.getUniformLocation(program, "uOpacity"),
|
|
612
|
+
uOutlineOnly: gl.getUniformLocation(program, "uOutlineOnly")
|
|
613
|
+
},
|
|
614
|
+
alphaCanvas,
|
|
615
|
+
alphaCtx,
|
|
616
|
+
polygonMaskCanvas,
|
|
617
|
+
polygonMaskCtx,
|
|
618
|
+
colorCtx,
|
|
619
|
+
cachedColor: "",
|
|
620
|
+
cachedColorRgba: [255, 255, 255, 255]
|
|
621
|
+
};
|
|
622
|
+
};
|
|
623
|
+
var outline = createEffect({
|
|
624
|
+
type: "remotion/outline",
|
|
625
|
+
label: "outline()",
|
|
626
|
+
documentationLink: "https://www.remotion.dev/docs/effects/outline",
|
|
627
|
+
backend: "webgl2",
|
|
628
|
+
calculateKey: (params) => {
|
|
629
|
+
const resolved = resolve(params);
|
|
630
|
+
return `outline-${resolved.width}-${resolved.edgeSimplification}-${resolved.color}-${resolved.opacity}-${resolved.outlineOnly}`;
|
|
631
|
+
},
|
|
632
|
+
setup: setupOutline,
|
|
633
|
+
apply: ({ source, width, height, params, state, flipSourceY }) => {
|
|
634
|
+
const resolved = resolve(params);
|
|
635
|
+
if (state.cachedColor !== resolved.color) {
|
|
636
|
+
state.cachedColor = resolved.color;
|
|
637
|
+
state.cachedColorRgba = parseColorRgba(state.colorCtx, resolved.color);
|
|
638
|
+
}
|
|
639
|
+
const { gl, program, textureSource, texturePolygonMask, uniforms, vao } = state;
|
|
640
|
+
const [red, green, blue, alpha] = state.cachedColorRgba;
|
|
641
|
+
const usePolygonMask = resolved.edgeSimplification > 0 && resolved.opacity > 0 && alpha > 0 && (resolved.width > 0 || resolved.outlineOnly);
|
|
642
|
+
if (usePolygonMask) {
|
|
643
|
+
updatePolygonMask({
|
|
644
|
+
source,
|
|
645
|
+
width,
|
|
646
|
+
height,
|
|
647
|
+
simplification: resolved.edgeSimplification,
|
|
648
|
+
outlineWidth: resolved.width,
|
|
649
|
+
state
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
gl.viewport(0, 0, width, height);
|
|
653
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
654
|
+
gl.clearColor(0, 0, 0, 0);
|
|
655
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
656
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
657
|
+
gl.bindTexture(gl.TEXTURE_2D, textureSource);
|
|
658
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipSourceY);
|
|
659
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
660
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
661
|
+
gl.bindTexture(gl.TEXTURE_2D, texturePolygonMask);
|
|
662
|
+
if (usePolygonMask) {
|
|
663
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
664
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, state.polygonMaskCanvas);
|
|
665
|
+
}
|
|
666
|
+
gl.useProgram(program);
|
|
667
|
+
if (uniforms.uSource)
|
|
668
|
+
gl.uniform1i(uniforms.uSource, 0);
|
|
669
|
+
if (uniforms.uPolygonMask)
|
|
670
|
+
gl.uniform1i(uniforms.uPolygonMask, 1);
|
|
671
|
+
if (uniforms.uUsePolygonMask)
|
|
672
|
+
gl.uniform1i(uniforms.uUsePolygonMask, usePolygonMask ? 1 : 0);
|
|
673
|
+
if (uniforms.uWidth)
|
|
674
|
+
gl.uniform1f(uniforms.uWidth, resolved.width);
|
|
675
|
+
if (uniforms.uColor)
|
|
676
|
+
gl.uniform4f(uniforms.uColor, red / 255, green / 255, blue / 255, alpha / 255);
|
|
677
|
+
if (uniforms.uOpacity)
|
|
678
|
+
gl.uniform1f(uniforms.uOpacity, resolved.opacity);
|
|
679
|
+
if (uniforms.uOutlineOnly)
|
|
680
|
+
gl.uniform1i(uniforms.uOutlineOnly, resolved.outlineOnly ? 1 : 0);
|
|
681
|
+
gl.bindVertexArray(vao);
|
|
682
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
683
|
+
gl.bindVertexArray(null);
|
|
684
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
685
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
686
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
687
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
688
|
+
gl.useProgram(null);
|
|
689
|
+
},
|
|
690
|
+
cleanup: ({ gl, program, vao, vbo, textureSource, texturePolygonMask }) => {
|
|
691
|
+
gl.deleteTexture(textureSource);
|
|
692
|
+
gl.deleteTexture(texturePolygonMask);
|
|
693
|
+
gl.deleteBuffer(vbo);
|
|
694
|
+
gl.deleteProgram(program);
|
|
695
|
+
gl.deleteVertexArray(vao);
|
|
696
|
+
},
|
|
697
|
+
schema: outlineSchema,
|
|
698
|
+
validateParams: validateOutlineParams
|
|
699
|
+
});
|
|
700
|
+
export {
|
|
701
|
+
outlineSchema,
|
|
702
|
+
outline
|
|
703
|
+
};
|