mirror of
https://github.com/YuzuZensai/Pien-Studio.git
synced 2026-09-02 14:18:35 +00:00
✨ feat: layer effects, bucket fill, simple brush
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import type { FaceBlurEffect } from "@pien-studio/types";
|
||||
import type { EffectRenderer, EffectRenderContext } from "./types";
|
||||
|
||||
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;
|
||||
sampleCtx.drawImage(source, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, sampleCanvas.width, sampleCanvas.height);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(sampleCanvas, 0, 0, sampleCanvas.width, sampleCanvas.height, targetX, targetY, targetWidth, targetHeight);
|
||||
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;
|
||||
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);
|
||||
regionCtx.drawImage(blurCanvas, 0, 0, blurCanvas.width, blurCanvas.height, 0, 0, regionCanvas.width, regionCanvas.height);
|
||||
blurCtx.clearRect(0, 0, blurCanvas.width, blurCanvas.height);
|
||||
blurCtx.drawImage(regionCanvas, 0, 0, regionCanvas.width, regionCanvas.height, 0, 0, blurCanvas.width, blurCanvas.height);
|
||||
}
|
||||
ctx.drawImage(regionCanvas, 0, 0, regionCanvas.width, regionCanvas.height, targetX, targetY, targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
function renderRegions(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
image: HTMLImageElement,
|
||||
effect: FaceBlurEffect,
|
||||
targetWidth: number,
|
||||
targetHeight: number,
|
||||
) {
|
||||
if (!effect.regions.length) return;
|
||||
const legacyScaleX = image.naturalWidth / Math.max(1, targetWidth);
|
||||
const legacyScaleY = image.naturalHeight / Math.max(1, targetHeight);
|
||||
|
||||
for (const region of effect.regions) {
|
||||
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;
|
||||
|
||||
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));
|
||||
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));
|
||||
|
||||
if (effect.method === "censor") {
|
||||
ctx.fillStyle = region.censorColor ?? effect.censorColor ?? "#111111";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
continue;
|
||||
}
|
||||
if (effect.method === "pixelate") {
|
||||
drawPixelatedRegion(ctx, image, sx0, sy0, sw, sh, x, y, w, h, Math.max(4, Math.round(effect.amount / 2)));
|
||||
continue;
|
||||
}
|
||||
if ("filter" in ctx && typeof ctx.filter === "string") {
|
||||
ctx.save();
|
||||
ctx.filter = `blur(${effect.amount}px)`;
|
||||
ctx.drawImage(image, sx0, sy0, sw, sh, x, y, w, h);
|
||||
ctx.restore();
|
||||
continue;
|
||||
}
|
||||
drawBlurredRegionFallback(ctx, image, sx0, sy0, sw, sh, x, y, w, h, effect.amount);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLayer(context: EffectRenderContext, effect: FaceBlurEffect) {
|
||||
const { ctx, image, targetWidth, targetHeight } = context;
|
||||
if (!effect.regions.length) {
|
||||
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
const allHaveSourceDims = effect.regions.every((r) => (r.sourceWidth ?? 0) > 0 && (r.sourceHeight ?? 0) > 0);
|
||||
if (!allHaveSourceDims) {
|
||||
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
||||
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);
|
||||
renderRegions(sourceCtx, image, effect, sourceCanvas.width, sourceCanvas.height);
|
||||
ctx.drawImage(sourceCanvas, 0, 0, sourceCanvas.width, sourceCanvas.height, 0, 0, targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
export const faceBlurRenderer: EffectRenderer<FaceBlurEffect> = {
|
||||
kind: "face-blur",
|
||||
render: ({ ctx, image, targetWidth, targetHeight }, effect) => {
|
||||
renderRegions(ctx, image, effect, targetWidth, targetHeight);
|
||||
},
|
||||
renderLayer,
|
||||
};
|
||||
Reference in New Issue
Block a user