feat: layer effects, bucket fill, simple brush

This commit is contained in:
2026-05-21 00:02:12 +07:00
parent bcf4650c70
commit e7dd9420e7
47 changed files with 1675 additions and 1038 deletions
+11 -30
View File
@@ -1,5 +1,5 @@
import type { Layer, Project } from "@pien-studio/types";
import { renderImageWithFaceBlur } from "./face-blur-renderer";
import { renderLayerWithEffects } from "./effects/registry";
type ExportOptions = {
isDark: boolean;
@@ -20,11 +20,7 @@ function loadImage(src: string) {
});
}
function drawFallbackLayer(
ctx: CanvasRenderingContext2D,
layer: Layer,
isDark: boolean,
) {
function drawFallbackLayer(ctx: CanvasRenderingContext2D, layer: Layer, isDark: boolean) {
const text = layer.name ?? layer.type;
const width = Math.max(80, layer.width ?? 120);
const height = Math.max(34, layer.height ?? 40);
@@ -49,24 +45,15 @@ function drawFallbackLayer(
ctx.stroke();
ctx.fillStyle = isDark ? "#d7dae0" : "#1f2430";
ctx.font =
"600 12px ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif";
ctx.font = "600 12px ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, width / 2, height / 2);
}
async function drawLayer(
ctx: CanvasRenderingContext2D,
layer: Layer,
isDark: boolean,
) {
const width =
layer.width ??
(layer.type === "image" ? Math.round(200 * layer.scale) : 120);
const height =
layer.height ??
(layer.type === "image" ? Math.round(150 * layer.scale) : 40);
async function drawLayer(ctx: CanvasRenderingContext2D, layer: Layer, isDark: boolean) {
const width = layer.width ?? (layer.type === "raster" ? Math.round(200 * layer.scale) : 120);
const height = layer.height ?? (layer.type === "raster" ? Math.round(150 * layer.scale) : 40);
ctx.save();
ctx.globalAlpha = clampOpacity(layer.opacity);
@@ -74,10 +61,10 @@ async function drawLayer(
ctx.rotate((layer.rotation * Math.PI) / 180);
ctx.translate(-width / 2, -height / 2);
if ((layer.type === "image" || layer.type === "sticker") && layer.sourceUri) {
if ((layer.type === "raster" || layer.type === "sticker") && layer.sourceUri) {
try {
const image = await loadImage(layer.sourceUri);
renderImageWithFaceBlur(ctx, image, layer.faceBlur, width, height);
renderLayerWithEffects(ctx, image, layer.effects, width, height);
} catch {
drawFallbackLayer(ctx, layer, isDark);
}
@@ -88,14 +75,8 @@ async function drawLayer(
ctx.restore();
}
export async function exportProjectAsPng(
project: Project,
options: ExportOptions,
) {
const pixelRatio = Math.max(
1,
Math.floor(options.pixelRatio ?? window.devicePixelRatio ?? 1),
);
export async function exportProjectAsPng(project: Project, options: ExportOptions) {
const pixelRatio = Math.max(1, Math.floor(options.pixelRatio ?? window.devicePixelRatio ?? 1));
const { width, height } = project.canvas;
const canvas = document.createElement("canvas");
canvas.width = width * pixelRatio;
@@ -103,10 +84,10 @@ export async function exportProjectAsPng(
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Cannot create export canvas context");
ctx.scale(pixelRatio, pixelRatio);
for (const layer of project.layers) {
if (layer.visible === false) continue;
await drawLayer(ctx, layer, options.isDark);
}