mfx
Version:
In-browser video editing toolkit, with effects accelerated by WebGL
244 lines (236 loc) • 6.32 kB
text/typescript
import { convolution3x3Kernels } from "./convolution";
import { MFXGLEffect, u } from "./Effect";
import type { Uniform } from "./shaders";
import * as shaders from "./shaders/raw";
import { rotate, scale, translate } from "./matrix";
import { coalesce, createEmptyFrame } from "./coalesce";
const repeat = (n: number, fn: () => MFXGLEffect) => [...new Array(n)].map(fn);
const conv =
(kernel: number[]) =>
({ passes = 1 } = {}) =>
repeat(passes, () => new MFXGLEffect(shaders.convolution, { kernel }));
export type Vec3 = [number, number, number];
export type Origin = Vec3;
export const visual = {
adjustment: ({
saturation = 1,
brightness = 1,
contrast = 1,
}: {
saturation?: Uniform<number>;
brightness?: Uniform<number>;
contrast?: Uniform<number>;
}) => [
new MFXGLEffect(shaders.adjustment, { saturation, brightness, contrast }),
],
mask: (video: ReadableStream<VideoFrame>) => {
return [
new MFXGLEffect(shaders.mask, {
mask: coalesce(video),
maskSize: (f) => [f.displayWidth, f.displayHeight],
}),
];
},
add: (
video: ReadableStream<VideoFrame> | ((_: VideoFrame) => VideoFrame),
{
offset = [0, 0],
// Normal blend
normal = 0,
// Additive blend
additive = 0,
// Multiply blend
multiply = 0,
// Screen blend
screen = 0,
// Alpha blend, applies to alpha channel of frame
alpha = 0,
}: {
offset?: Uniform<number[]>;
normal?: Uniform<number>;
additive?: Uniform<number>;
multiply?: Uniform<number>;
screen?: Uniform<number>;
alpha?: Uniform<number>;
} = {},
) => {
const coalescer = typeof video === "function" ? video : coalesce(video);
return [
new MFXGLEffect(
shaders.composition,
async (f) => {
const newFrame = await coalescer(f);
return {
layer: newFrame,
layerSize: [newFrame.displayWidth, newFrame.displayHeight],
offset,
normal,
additive,
multiply,
screen,
alpha,
};
},
{
isDirty: false,
},
),
];
},
scale: ({
values = [1, 1, 1], // Provided as an example
origin = [0.5, 0.5, 0],
}: {
values: Uniform<Vec3>;
origin?: Uniform<Origin>;
}) => [
new MFXGLEffect(shaders.paint, {
transform: async (f) => scale(await u(values, f), await u(origin, f)),
}),
],
crop: ({
values = [1, 1, 1], // Provided as an example
origin = [0.5, 0.5, 0],
}: {
values: Uniform<Vec3>;
origin?: Uniform<Origin>;
}) => [
new MFXGLEffect(
shaders.paint,
{},
{
transformContext: async (ctx, frame) => {
const scale = await u(values, frame);
const resolvedOrigin = await u(origin, frame);
const width = scale[0] * ctx.gl.canvas.width;
const height = scale[1] * ctx.gl.canvas.height;
const x =
ctx.gl.canvas.width * resolvedOrigin[0] - width * resolvedOrigin[0];
const y =
ctx.gl.canvas.height * resolvedOrigin[1] -
height * resolvedOrigin[1];
ctx.setCrop({
x,
y,
width,
height,
});
},
},
),
],
translate: ({
values = [0, 0, 0], // Provided as an example
}: {
values: Uniform<Vec3>;
}) => [
new MFXGLEffect(shaders.paint, {
transform: async (f) => translate(await u(values, f)),
}),
],
rotate: ({
angle = 45, // Provided as an example
values = [1, 1, 1], // Provided as an example
origin = [0.5, 0.5, 0],
}: {
angle: Uniform<number>;
values: Uniform<Vec3>;
origin?: Uniform<Origin>;
}) => [
new MFXGLEffect(shaders.paint, {
transform: async (f) =>
rotate(await u(angle, f), await u(values, f), await u(origin, f)),
}),
],
zoom: (
{
factor = 1,
x = 0.5,
y = 0.5,
}: {
factor?: Uniform<number>;
x?: Uniform<number>;
y?: Uniform<number>;
},
{
isDirty,
}: {
isDirty?: boolean;
} = {},
) => [
new MFXGLEffect(
shaders.paint,
{
transform: async (f) =>
scale(
[await u(factor, f), await u(factor, f), 1],
[await u(x, f), await u(y, f), 0],
),
},
{
isDirty,
},
),
],
blur: ({
passes = 5,
quality = 0.5,
dirty = false,
}: {
// Indicates whether background of the frame should be cleared after each draw, apply to remove vignetting effect on full-screen blurs
dirty?: boolean;
passes?: number;
// Between 0 to 1, The higher the quality the more passes you need to blur the image
quality?: Uniform<number>;
}) => [
...visual.zoom({ factor: quality, x: 0.5, y: 0.5 }, { isDirty: dirty }),
...repeat(
passes,
() =>
new MFXGLEffect(
shaders.convolution,
{
kernel: convolution3x3Kernels.boxBlur,
},
{
// Ensures vignetting effect is reduced while maintaining optimal performance
isDirty: dirty,
},
),
),
...repeat(
Math.round(Math.log(passes)),
() =>
new MFXGLEffect(
shaders.convolution,
{
kernel: convolution3x3Kernels.gaussianBlur,
},
{
// Ensures vignetting effect is reduced while maintaining optimal performance
isDirty: dirty,
},
),
),
...visual.zoom(
{ factor: async (f) => 1 / (await u(quality, f)), x: 0.5, y: 0.5 },
{
isDirty: dirty,
},
),
],
edge: conv(convolution3x3Kernels.edge0),
edge1: conv(convolution3x3Kernels.edge1),
edge2: conv(convolution3x3Kernels.edge2),
sharpen: conv(convolution3x3Kernels.sharpen),
boxBlur: conv(convolution3x3Kernels.boxBlur),
gaussianBlur: conv(convolution3x3Kernels.gaussianBlur),
emboss: conv(convolution3x3Kernels.emboss),
convolution3x3: ({
passes = 1,
kernel,
}: {
passes?: number;
kernel: Uniform<number[]>;
}) => repeat(passes, () => new MFXGLEffect(shaders.convolution, { kernel })),
};