Files
dpage-editor/src/pages/editor/utils/exportUtils.ts
T
hamid zarghami b6bff9956a dpage
2026-05-21 12:21:44 +03:30

294 lines
8.3 KiB
TypeScript

import type { EditorObject } from "../store/editorStore";
/**
* exportUtils - Utilities برای export با real masking
*
* CRITICAL: Real masking فقط در export اعمال می‌شود، نه در editor.
* این تضمین می‌کند که masking هیچ‌وقت روی hit detection، selection،
* grouping یا transforms تاثیر نگذارد.
*/
/**
* رسم یک mask shape روی canvas context
*/
const drawMaskShape = (
ctx: CanvasRenderingContext2D,
maskShape: EditorObject
): void => {
ctx.save();
ctx.translate(maskShape.x || 0, maskShape.y || 0);
ctx.rotate(((maskShape.rotation || 0) * Math.PI) / 180);
ctx.fillStyle = "black";
ctx.strokeStyle = "transparent";
if (maskShape.type === "rectangle" && maskShape.shapeType === "circle") {
const radius = (maskShape.width || 50) / 2;
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
} else if (maskShape.type === "rectangle" && maskShape.shapeType === "triangle") {
const width = maskShape.width || 100;
const height = maskShape.height || 100;
const radius = Math.min(width / 2, height / 2);
ctx.beginPath();
ctx.translate(width / 2, height / 2);
for (let i = 0; i < 3; i++) {
const angle = (i * 2 * Math.PI) / 3 - Math.PI / 2;
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.fill();
} else if (maskShape.type === "rectangle" && maskShape.shapeType === "abstract") {
const width = maskShape.width || 100;
const height = maskShape.height || 100;
const baseRadius = Math.min(width / 2, height / 2);
const abstractScale = 0.85;
const outerRadius = baseRadius * abstractScale;
const innerRadius = outerRadius * 0.5;
const numPoints = 5;
ctx.beginPath();
ctx.translate(width / 2, height / 2);
for (let i = 0; i < numPoints * 2; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const angle = (i * Math.PI) / numPoints - Math.PI / 2;
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.fill();
} else {
// Default: Rectangle
const width = maskShape.width || 100;
const height = maskShape.height || 100;
const radius = Math.max(0, maskShape.borderRadius || 0);
drawRoundedRectPath(ctx, 0, 0, width, height, radius);
ctx.fill();
}
ctx.restore();
};
const drawRoundedRectPath = (
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number
): void => {
const normalizedRadius = Math.max(0, Math.min(radius, width / 2, height / 2));
ctx.beginPath();
if (normalizedRadius === 0) {
ctx.rect(x, y, width, height);
} else {
ctx.roundRect(x, y, width, height, normalizedRadius);
}
ctx.closePath();
};
/**
* رسم یک object روی canvas context
* این تابع یک نسخه ساده است - در پیاده‌سازی واقعی باید
* از Konva stage.toCanvas() یا image rendering استفاده شود
*/
const drawObject = async (
ctx: CanvasRenderingContext2D,
object: EditorObject
): Promise<void> => {
ctx.save();
ctx.translate(object.x || 0, object.y || 0);
ctx.rotate(((object.rotation || 0) * Math.PI) / 180);
ctx.scale(object.scaleX || 1, object.scaleY || 1);
// برای image objects
if (object.type === "image" || object.type === "document" || object.type === "sticker") {
if (object.imageUrl) {
const img = new Image();
img.crossOrigin = "anonymous";
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = object.imageUrl!;
});
ctx.drawImage(img, 0, 0, object.width || 100, object.height || 100);
}
}
// برای shape objects
else if (object.type === "rectangle") {
ctx.fillStyle = object.fill || "#000000";
ctx.strokeStyle = object.stroke || "transparent";
ctx.lineWidth = object.strokeWidth || 0;
if (object.shapeType === "circle") {
const radius = (object.width || 50) / 2;
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
if (ctx.lineWidth > 0) ctx.stroke();
} else {
const width = object.width || 100;
const height = object.height || 100;
const radius = Math.max(0, object.borderRadius || 0);
drawRoundedRectPath(ctx, 0, 0, width, height, radius);
ctx.fill();
if (ctx.lineWidth > 0) ctx.stroke();
}
}
// برای text objects
else if (object.type === "text") {
ctx.fillStyle = object.fill || "#000000";
ctx.font = `${object.fontWeight || "normal"} ${object.fontSize || 16}px ${object.fontFamily || "Arial"}`;
ctx.fillText(object.text || "", 0, 0);
}
ctx.restore();
};
/**
* Export یک object با masking به صورت data URL
*
* @param object - Object برای export
* @param maskShape - Mask shape برای اعمال (optional)
* @returns Data URL (PNG)
*/
export const exportObjectWithMask = async (
object: EditorObject,
maskShape: EditorObject | null
): Promise<string> => {
// محاسبه bounds
const x = object.x || 0;
const y = object.y || 0;
const width = object.width || 100;
const height = object.height || 100;
// ایجاد offscreen canvas
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Failed to get canvas context");
}
// اگر mask وجود ندارد، فقط object را draw کن
if (!maskShape) {
await drawObject(ctx, { ...object, x: 0, y: 0 });
return canvas.toDataURL("image/png");
}
// Step 1: Draw object
await drawObject(ctx, { ...object, x: 0, y: 0 });
// Step 2: Apply mask با destination-in
ctx.globalCompositeOperation = "destination-in";
// Adjust mask position relative to object
const adjustedMaskShape: EditorObject = {
...maskShape,
x: (maskShape.x || 0) - x,
y: (maskShape.y || 0) - y,
};
drawMaskShape(ctx, adjustedMaskShape);
// Return PNG
return canvas.toDataURL("image/png");
};
/**
* Export تمام objects از یک page با masking
*
* @param objects - لیست objects برای export
* @returns Map از object IDs به data URLs
*/
export const exportPageWithMasks = async (
objects: EditorObject[]
): Promise<Map<string, string>> => {
const results = new Map<string, string>();
for (const obj of objects) {
// Skip masks themselves
if (obj.isMask) {
continue;
}
let maskShape: EditorObject | null = null;
if (obj.maskId) {
maskShape = objects.find((m) => m.id === obj.maskId) || null;
}
try {
const dataUrl = await exportObjectWithMask(obj, maskShape);
results.set(obj.id, dataUrl);
} catch (error) {
console.error(`Failed to export object ${obj.id}:`, error);
}
}
return results;
};
/**
* Export کل stage به عنوان یک image با masking اعمال شده
*
* این تابع می‌تواند با Konva Stage API integrate شود
* برای بهتر شدن quality و performance
*/
export const exportStageWithMasks = async (
objects: EditorObject[],
stageWidth: number,
stageHeight: number,
backgroundColor: string = "#ffffff"
): Promise<string> => {
const canvas = document.createElement("canvas");
canvas.width = stageWidth;
canvas.height = stageHeight;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Failed to get canvas context");
}
// Background
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, stageWidth, stageHeight);
// Export هر object با mask
const exports = await exportPageWithMasks(objects);
// Draw exported images روی stage
for (const [objId, dataUrl] of exports.entries()) {
const obj = objects.find((o) => o.id === objId);
if (!obj) continue;
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = dataUrl;
});
ctx.drawImage(img, obj.x || 0, obj.y || 0);
}
return canvas.toDataURL("image/png");
};