diff --git a/public/viewer-data.json b/public/viewer-data.json index 85a40c5..fc87a17 100644 --- a/public/viewer-data.json +++ b/public/viewer-data.json @@ -14,7 +14,7 @@ "fontFamily": "irancell", "fontWeight": "bold", "fill": "#1a1a1a", - "opacity": 1 + "opacity": 50 }, { "id": "image-1", @@ -24,7 +24,7 @@ "width": 300, "height": 300, "imageUrl": "https://picsum.photos/200/300", - "opacity": 1 + "opacity": 100 }, { "id": "rectangle-1", @@ -37,7 +37,7 @@ "stroke": "#1e40af", "strokeWidth": 2, "shapeType": "square", - "opacity": 1 + "opacity": 100 }, { "id": "circle-1", @@ -50,7 +50,7 @@ "stroke": "#dc2626", "strokeWidth": 2, "shapeType": "circle", - "opacity": 1 + "opacity": 100 }, { "id": "video-1", @@ -60,7 +60,7 @@ "width": 400, "height": 300, "videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4", - "opacity": 1 + "opacity": 100 } ] }, @@ -78,7 +78,7 @@ "fontFamily": "irancell", "fontWeight": "bold", "fill": "#000000", - "opacity": 1 + "opacity": 100 }, { "id": "line-1", @@ -89,7 +89,7 @@ "height": 2, "stroke": "#64748b", "strokeWidth": 3, - "opacity": 1 + "opacity": 100 }, { "id": "arrow-1", @@ -100,7 +100,7 @@ "height": 100, "stroke": "#f59e0b", "strokeWidth": 4, - "opacity": 1 + "opacity": 100 }, { "id": "triangle-1", @@ -113,7 +113,7 @@ "stroke": "#059669", "strokeWidth": 2, "shapeType": "triangle", - "opacity": 1 + "opacity": 100 }, { "id": "link-1", @@ -126,7 +126,7 @@ "fontSize": 18, "linkUrl": "https://example.com", "fill": "#3b82f6", - "opacity": 1 + "opacity": 100 }, { "id": "image-2", @@ -136,7 +136,7 @@ "width": 250, "height": 200, "imageUrl": "https://picsum.photos/200/300", - "opacity": 1 + "opacity": 100 } ] }, @@ -154,7 +154,7 @@ "fontFamily": "irancell", "fontWeight": "normal", "fill": "#1a1a1a", - "opacity": 1 + "opacity": 100 }, { "id": "grid-1", @@ -314,7 +314,7 @@ "stroke": "#808080", "strokeWidth": 1 }, - "opacity": 1 + "opacity": 100 }, { "id": "abstract-1", @@ -327,7 +327,7 @@ "stroke": "#7c3aed", "strokeWidth": 2, "shapeType": "abstract", - "opacity": 1 + "opacity": 100 } ] }, @@ -345,7 +345,7 @@ "fontFamily": "irancell", "fontWeight": "bold", "fill": "#1a1a1a", - "opacity": 1 + "opacity": 100 }, { "id": "video-2", @@ -355,7 +355,7 @@ "width": 350, "height": 250, "videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4", - "opacity": 1 + "opacity": 100 }, { "id": "image-3", @@ -365,7 +365,7 @@ "width": 250, "height": 250, "imageUrl": "https://picsum.photos/200/300", - "opacity": 1 + "opacity": 100 }, { "id": "line-2", @@ -376,7 +376,7 @@ "height": 2, "stroke": "#94a3b8", "strokeWidth": 2, - "opacity": 1 + "opacity": 100 }, { "id": "arrow-2", @@ -387,7 +387,7 @@ "height": 80, "stroke": "#ec4899", "strokeWidth": 5, - "opacity": 1 + "opacity": 100 }, { "id": "circle-2", @@ -400,7 +400,7 @@ "stroke": "#d97706", "strokeWidth": 3, "shapeType": "circle", - "opacity": 1 + "opacity": 100 } ] }, @@ -418,7 +418,7 @@ "fontFamily": "irancell", "fontWeight": "bold", "fill": "#1a1a1a", - "opacity": 1 + "opacity": 100 }, { "id": "rectangle-2", @@ -432,7 +432,7 @@ "strokeWidth": 2, "shapeType": "square", "rotation": 15, - "opacity": 1 + "opacity": 100 }, { "id": "triangle-2", @@ -446,7 +446,7 @@ "strokeWidth": 2, "shapeType": "triangle", "rotation": 30, - "opacity": 1 + "opacity": 100 }, { "id": "abstract-2", @@ -460,7 +460,7 @@ "strokeWidth": 2, "shapeType": "abstract", "rotation": -20, - "opacity": 1 + "opacity": 100 }, { "id": "image-4", @@ -471,7 +471,7 @@ "height": 150, "imageUrl": "https://picsum.photos/200/300", "rotation": 10, - "opacity": 1 + "opacity": 100 }, { "id": "video-3", @@ -481,7 +481,7 @@ "width": 300, "height": 200, "videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4", - "opacity": 1 + "opacity": 100 }, { "id": "link-2", @@ -494,7 +494,7 @@ "fontSize": 20, "linkUrl": "https://example.com", "fill": "#6366f1", - "opacity": 1 + "opacity": 100 }, { "id": "line-3", @@ -506,7 +506,7 @@ "stroke": "#f97316", "strokeWidth": 4, "rotation": 45, - "opacity": 1 + "opacity": 100 }, { "id": "arrow-3", @@ -518,7 +518,7 @@ "stroke": "#84cc16", "strokeWidth": 6, "rotation": -30, - "opacity": 1 + "opacity": 100 }, { "id": "text-6", @@ -531,7 +531,7 @@ "fontWeight": "normal", "fill": "#475569", "letterSpacing": 1, - "opacity": 1 + "opacity": 100 } ] } diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx index 8545915..8553936 100644 --- a/src/pages/viewer/components/BookPage.tsx +++ b/src/pages/viewer/components/BookPage.tsx @@ -15,6 +15,35 @@ type BookPageProps = { * نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند */ const BookPage = forwardRef(({ page, scale = 1 }, ref) => { + // تابع برای تبدیل opacity به رنگ (مثل editor) + // در editor، getColorWithOpacity انتظار opacity 0-100 دارد + // در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است + // پس opacity را مستقیماً استفاده می‌کنیم + const getColorWithOpacity = (fill?: string, opacity?: number): string => { + if (!fill) return '#000000'; + + // opacity در EditorObject به صورت 0-100 است (بعد از تبدیل در dataTransformer) + // در editor getColorWithOpacity انتظار 0-100 دارد + const opacityValue = opacity !== undefined ? opacity : 100; + + // اگر opacity 100% باشد، رنگ را بدون تغییر برگردان (مثل editor) + if (opacityValue >= 100) return fill; + + const hex = fill.replace('#', ''); + // اگر hex کوتاه‌تر از 6 کاراکتر باشد، padding اضافه کن + const paddedHex = hex.length === 3 + ? hex.split('').map(char => char + char).join('') + : hex; + + const r = parseInt(paddedHex.substring(0, 2), 16); + const g = parseInt(paddedHex.substring(2, 4), 16); + const b = parseInt(paddedHex.substring(4, 6), 16); + // opacityValue را به alpha تبدیل کن (0-100 -> 0-1) مثل editor + const alpha = opacityValue / 100; + + return `rgba(${r}, ${g}, ${b}, ${alpha})`; + }; + const renderObject = (obj: EditorObject, index: number) => { const baseStyle: React.CSSProperties = { position: 'absolute', @@ -30,16 +59,19 @@ const BookPage = forwardRef(({ page, scale = 1 }, } switch (obj.type) { - case 'text': + case 'text': { + // برای text، opacity در رنگ اعمال می‌شود، پس باید opacity را از baseStyle حذف کنیم + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { opacity, ...textBaseStyle } = baseStyle; return (
(({ page, scale = 1 }, {obj.text || ''}
); + } case 'image': return ( @@ -58,7 +91,8 @@ const BookPage = forwardRef(({ page, scale = 1 }, ...baseStyle, width: obj.width ? `${obj.width * scale}px` : 'auto', height: obj.height ? `${obj.height * scale}px` : 'auto', - objectFit: 'contain', + objectFit: 'fill', // مشابه Konva که image را با width و height مشخص شده رندر می‌کند + zIndex: index, // برای حفظ ترتیب رندر }} /> ); @@ -102,15 +136,14 @@ const BookPage = forwardRef(({ page, scale = 1 }, ); - case 'rectangle': + case 'rectangle': { if (obj.shapeType === 'circle') { // در editor، circle با مرکز در x, y رندر می‌شود (radius = width/2) - // اما در viewer-data.json ممکن است x, y به عنوان گوشه بالا-چپ bounding box باشد - // برای سازگاری با editor، فرض می‌کنیم x, y مرکز است + // در dataTransformer از گوشه بالا-چپ به مرکز تبدیل شده است const radius = ((obj.width || 100) / 2) * scale; const centerX = (obj.x || 0) * scale; const centerY = (obj.y || 0) * scale; - + return (
(({ page, scale = 1 }, opacity: obj.opacity ?? 1, transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined, transformOrigin: 'center center', + zIndex: index, // برای حفظ ترتیب رندر }} /> ); @@ -204,9 +238,11 @@ const BookPage = forwardRef(({ page, scale = 1 }, border: obj.stroke ? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}` : 'none', + zIndex: index, // برای حفظ ترتیب رندر }; return
; + } case 'line': { // در EditorObject، width و height به عنوان مختصات انتهایی استفاده می‌شوند @@ -283,7 +319,7 @@ const BookPage = forwardRef(({ page, scale = 1 }, ); } - case 'grid': + case 'grid': { if (!obj.tableData) return null; const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } = @@ -325,6 +361,7 @@ const BookPage = forwardRef(({ page, scale = 1 }, )}
); + } default: return null; diff --git a/src/pages/viewer/utils/dataTransformer.ts b/src/pages/viewer/utils/dataTransformer.ts index 2ada0fc..0b1578a 100644 --- a/src/pages/viewer/utils/dataTransformer.ts +++ b/src/pages/viewer/utils/dataTransformer.ts @@ -44,7 +44,10 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] { type: obj.type as EditorObject['type'], x: obj.x, y: obj.y, - opacity: obj.opacity ?? 1, + // در JSON، opacity به صورت 0-100 است (درصد) + // در editor هم opacity به صورت 0-100 است + // پس opacity را مستقیماً استفاده می‌کنیم + opacity: obj.opacity !== undefined ? obj.opacity : 100, visible: true, }; @@ -66,11 +69,12 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] { // برای circle در viewer-data.json، x, y به عنوان گوشه بالا-چپ bounding box است // اما در editor، circle با مرکز در x, y ذخیره می‌شود // پس باید تبدیل کنیم - if (obj.type === 'rectangle' && obj.shapeType === 'circle') { - // تبدیل از گوشه بالا-چپ به مرکز - baseObject.x = obj.x + (obj.width || 100) / 2; - baseObject.y = obj.y + (obj.height || 100) / 2; - } + // اما بر اساس بررسی، در JSON موقعیت به عنوان مرکز ذخیره شده است + // پس نباید تبدیل کنیم - مستقیماً استفاده می‌کنیم + // اگر در آینده JSON به صورت گوشه بالا-چپ export شود، باید تبدیل کنیم: + // baseObject.x = obj.x + (obj.width || 100) / 2; + // baseObject.y = obj.y + (obj.height || 100) / 2; + // اما فعلاً JSON به صورت مرکز است، پس تبدیل نمی‌کنیم if (obj.fill !== undefined) baseObject.fill = obj.fill; if (obj.stroke !== undefined) baseObject.stroke = obj.stroke; if (obj.strokeWidth !== undefined) baseObject.strokeWidth = obj.strokeWidth;