294 lines
8.3 KiB
TypeScript
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");
|
|
};
|
|
|