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 => { 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 => { // محاسبه 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> => { const results = new Map(); 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 => { 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"); };