/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import invariant from 'invariant'; import {Group} from 'pts'; import {EffectFrameContext} from './Effect'; export type MaskCanvas = { maskCanvas: OffscreenCanvas; bounds: number[][]; scaleX: number; scaleY: number; }; import {Effects} from '@/common/components/video/effects/Effects'; import type {CarbonIconType} from '@carbon/icons-react'; import { AppleDash, Asterisk, Barcode, CenterCircle, ColorPalette, ColorSwitch, Development, Erase, FaceWink, Humidity, Image, Overlay, TextFont, } from '@carbon/icons-react'; export type DemoEffect = { title: string; Icon: CarbonIconType; effectName: keyof Effects; }; export const backgroundEffects: DemoEffect[] = [ {title: 'Original', Icon: Image, effectName: 'Original'}, {title: 'Erase', Icon: Erase, effectName: 'EraseBackground'}, { title: 'Gradient', Icon: ColorPalette, effectName: 'Gradient', }, { title: 'Pixelate', Icon: Development, effectName: 'Pixelate', }, {title: 'Desaturate', Icon: ColorSwitch, effectName: 'Desaturate'}, {title: 'Text', Icon: TextFont, effectName: 'BackgroundText'}, {title: 'Blur', Icon: Humidity, effectName: 'BackgroundBlur'}, {title: 'Outline', Icon: AppleDash, effectName: 'Sobel'}, ]; export const highlightEffects: DemoEffect[] = [ {title: 'Original', Icon: Image, effectName: 'Cutout'}, {title: 'Erase', Icon: Erase, effectName: 'EraseForeground'}, {title: 'Gradient', Icon: ColorPalette, effectName: 'VibrantMask'}, {title: 'Pixelate', Icon: Development, effectName: 'PixelateMask'}, { title: 'Overlay', Icon: Overlay, effectName: 'Overlay', }, {title: 'Emoji', Icon: FaceWink, effectName: 'Replace'}, {title: 'Burst', Icon: Asterisk, effectName: 'Burst'}, {title: 'Spotlight', Icon: CenterCircle, effectName: 'Scope'}, ]; export const moreEffects: DemoEffect[] = [ {title: 'Noisy', Icon: Barcode, effectName: 'NoisyMask'}, ]; // Store existing content in a temporary canvas // This can be used in HighlightEffect composite blending, so that the existing background effect can be put back via "destination-over" export function copyCanvasContent( ctx: CanvasRenderingContext2D, effectContext: EffectFrameContext, ): OffscreenCanvas { const {width, height} = effectContext; const previousContent = ctx.getImageData(0, 0, width, height); const tempCanvas = new OffscreenCanvas(width, height); const tempCtx = tempCanvas.getContext('2d'); tempCtx?.putImageData(previousContent, 0, 0); return tempCanvas; } export function isInvalidMask(bound: number[][] | Group) { return ( bound[0].length < 2 || bound[1].length < 2 || bound[1][0] - bound[0][0] < 1 || bound[1][1] - bound[0][1] < 1 ); } export type MaskRenderingData = { canvas: OffscreenCanvas; scale: number[]; bounds: number[][]; }; export class EffectLayer { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; width: number; height: number; constructor(context: EffectFrameContext) { this.canvas = new OffscreenCanvas(context.width, context.height); const ctx = this.canvas.getContext('2d'); invariant(ctx !== null, 'context cannot be null'); this.ctx = ctx; this.width = context.width; this.height = context.height; } image(source: CanvasImageSourceWebCodecs) { this.ctx.drawImage(source, 0, 0); } filter(filterString: string) { this.ctx.filter = filterString; } composite(blend: GlobalCompositeOperation) { this.ctx.globalCompositeOperation = blend; } fill(color: string) { this.ctx.fillStyle = color; this.ctx.fillRect(0, 0, this.width, this.height); } clear() { this.ctx.clearRect(0, 0, this.width, this.height); } }