Files
Pien-Studio/apps/web/lib/effects/face-blur.ts
T

146 lines
5.9 KiB
TypeScript
Raw Normal View History

2026-05-21 00:02:12 +07:00
import type { FaceBlurEffect } from "@pien-studio/types";
import type { EffectRenderer, EffectRenderContext } from "./types";
2026-05-10 03:06:54 +07:00
function drawPixelatedRegion(
ctx: CanvasRenderingContext2D,
source: CanvasImageSource,
sourceX: number,
sourceY: number,
sourceWidth: number,
sourceHeight: number,
targetX: number,
targetY: number,
targetWidth: number,
targetHeight: number,
blockSize: number,
) {
const sampleCanvas = document.createElement("canvas");
sampleCanvas.width = Math.max(1, Math.round(targetWidth / blockSize));
sampleCanvas.height = Math.max(1, Math.round(targetHeight / blockSize));
const sampleCtx = sampleCanvas.getContext("2d");
if (!sampleCtx) return;
sampleCtx.imageSmoothingEnabled = false;
2026-05-21 00:02:12 +07:00
sampleCtx.drawImage(source, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, sampleCanvas.width, sampleCanvas.height);
2026-05-10 03:06:54 +07:00
ctx.imageSmoothingEnabled = false;
2026-05-21 00:02:12 +07:00
ctx.drawImage(sampleCanvas, 0, 0, sampleCanvas.width, sampleCanvas.height, targetX, targetY, targetWidth, targetHeight);
2026-05-10 03:06:54 +07:00
ctx.imageSmoothingEnabled = true;
}
function drawBlurredRegionFallback(
ctx: CanvasRenderingContext2D,
source: CanvasImageSource,
sourceX: number,
sourceY: number,
sourceWidth: number,
sourceHeight: number,
targetX: number,
targetY: number,
targetWidth: number,
targetHeight: number,
amount: number,
) {
const regionCanvas = document.createElement("canvas");
regionCanvas.width = Math.max(1, Math.round(targetWidth));
regionCanvas.height = Math.max(1, Math.round(targetHeight));
const regionCtx = regionCanvas.getContext("2d");
if (!regionCtx) return;
2026-05-21 00:02:12 +07:00
regionCtx.drawImage(source, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, regionCanvas.width, regionCanvas.height);
const scale = Math.max(0.04, Math.min(0.5, 1 / Math.max(2, amount / 2)));
const blurCanvas = document.createElement("canvas");
blurCanvas.width = Math.max(1, Math.round(regionCanvas.width * scale));
blurCanvas.height = Math.max(1, Math.round(regionCanvas.height * scale));
const blurCtx = blurCanvas.getContext("2d");
if (!blurCtx) return;
blurCtx.imageSmoothingEnabled = true;
blurCtx.drawImage(regionCanvas, 0, 0, blurCanvas.width, blurCanvas.height);
for (let i = 0; i < 3; i++) {
regionCtx.clearRect(0, 0, regionCanvas.width, regionCanvas.height);
2026-05-21 00:02:12 +07:00
regionCtx.drawImage(blurCanvas, 0, 0, blurCanvas.width, blurCanvas.height, 0, 0, regionCanvas.width, regionCanvas.height);
blurCtx.clearRect(0, 0, blurCanvas.width, blurCanvas.height);
2026-05-21 00:02:12 +07:00
blurCtx.drawImage(regionCanvas, 0, 0, regionCanvas.width, regionCanvas.height, 0, 0, blurCanvas.width, blurCanvas.height);
}
2026-05-21 00:02:12 +07:00
ctx.drawImage(regionCanvas, 0, 0, regionCanvas.width, regionCanvas.height, targetX, targetY, targetWidth, targetHeight);
}
2026-05-21 00:02:12 +07:00
function renderRegions(
2026-05-10 03:06:54 +07:00
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
2026-05-21 00:02:12 +07:00
effect: FaceBlurEffect,
2026-05-10 03:06:54 +07:00
targetWidth: number,
targetHeight: number,
2026-05-21 00:02:12 +07:00
) {
if (!effect.regions.length) return;
2026-05-10 03:06:54 +07:00
const legacyScaleX = image.naturalWidth / Math.max(1, targetWidth);
const legacyScaleY = image.naturalHeight / Math.max(1, targetHeight);
2026-05-21 00:02:12 +07:00
for (const region of effect.regions) {
2026-05-10 12:11:58 +07:00
const sourceWidth = region.sourceWidth ?? 0;
const sourceHeight = region.sourceHeight ?? 0;
const hasSourceDims = sourceWidth > 0 && sourceHeight > 0;
const scaleX = hasSourceDims ? targetWidth / sourceWidth : 1;
const scaleY = hasSourceDims ? targetHeight / sourceHeight : 1;
2026-05-10 03:06:54 +07:00
const x = Math.max(0, Math.floor(region.x * scaleX));
const y = Math.max(0, Math.floor(region.y * scaleY));
const w = Math.max(1, Math.floor(region.width * scaleX));
const h = Math.max(1, Math.floor(region.height * scaleY));
2026-05-21 00:02:12 +07:00
const sx0 = hasSourceDims ? region.x : Math.max(0, Math.floor(region.x * legacyScaleX));
const sy0 = hasSourceDims ? region.y : Math.max(0, Math.floor(region.y * legacyScaleY));
const sw = hasSourceDims ? region.width : Math.max(1, Math.floor(region.width * legacyScaleX));
const sh = hasSourceDims ? region.height : Math.max(1, Math.floor(region.height * legacyScaleY));
2026-05-10 03:06:54 +07:00
2026-05-21 00:02:12 +07:00
if (effect.method === "censor") {
ctx.fillStyle = region.censorColor ?? effect.censorColor ?? "#111111";
2026-05-10 03:06:54 +07:00
ctx.fillRect(x, y, w, h);
continue;
}
2026-05-21 00:02:12 +07:00
if (effect.method === "pixelate") {
drawPixelatedRegion(ctx, image, sx0, sy0, sw, sh, x, y, w, h, Math.max(4, Math.round(effect.amount / 2)));
2026-05-10 03:06:54 +07:00
continue;
}
2026-05-21 00:02:12 +07:00
if ("filter" in ctx && typeof ctx.filter === "string") {
ctx.save();
2026-05-21 00:02:12 +07:00
ctx.filter = `blur(${effect.amount}px)`;
ctx.drawImage(image, sx0, sy0, sw, sh, x, y, w, h);
ctx.restore();
continue;
}
2026-05-21 00:02:12 +07:00
drawBlurredRegionFallback(ctx, image, sx0, sy0, sw, sh, x, y, w, h, effect.amount);
2026-05-10 03:06:54 +07:00
}
}
2026-05-21 00:02:12 +07:00
function renderLayer(context: EffectRenderContext, effect: FaceBlurEffect) {
const { ctx, image, targetWidth, targetHeight } = context;
if (!effect.regions.length) {
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
return;
}
2026-05-21 00:02:12 +07:00
const allHaveSourceDims = effect.regions.every((r) => (r.sourceWidth ?? 0) > 0 && (r.sourceHeight ?? 0) > 0);
if (!allHaveSourceDims) {
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
2026-05-21 00:02:12 +07:00
renderRegions(ctx, image, effect, targetWidth, targetHeight);
return;
}
const sourceCanvas = document.createElement("canvas");
sourceCanvas.width = Math.max(1, image.naturalWidth);
sourceCanvas.height = Math.max(1, image.naturalHeight);
const sourceCtx = sourceCanvas.getContext("2d");
if (!sourceCtx) {
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
return;
}
sourceCtx.drawImage(image, 0, 0, sourceCanvas.width, sourceCanvas.height);
2026-05-21 00:02:12 +07:00
renderRegions(sourceCtx, image, effect, sourceCanvas.width, sourceCanvas.height);
ctx.drawImage(sourceCanvas, 0, 0, sourceCanvas.width, sourceCanvas.height, 0, 0, targetWidth, targetHeight);
}
2026-05-21 00:02:12 +07:00
2026-05-24 21:05:21 +07:00
export const faceBlurRenderer: EffectRenderer = {
2026-05-21 00:02:12 +07:00
kind: "face-blur",
render: ({ ctx, image, targetWidth, targetHeight }, effect) => {
renderRegions(ctx, image, effect, targetWidth, targetHeight);
},
renderLayer,
};