Compare commits
3 Commits
7f35f5b9dd
...
879bc0ae49
| Author | SHA1 | Date | |
|---|---|---|---|
| 879bc0ae49 | |||
| 5174d0371d | |||
| ef4ddeb3fa |
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"sidebar": {
|
"sidebar": {
|
||||||
"menu": "منو",
|
"menu": "منو",
|
||||||
"home_page": "صفحه اصلی",
|
"home_page": "صفحه اصلی",
|
||||||
"catalog": "کاتالوگ",
|
"catalog": "کاتالوگ های من",
|
||||||
"logout": "خروج",
|
"logout": "خروج",
|
||||||
"request_design": "درخواست طراحی"
|
"request_design": "درخواست طراحی"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,8 +24,13 @@ type ObjectSettingsProps = {
|
|||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const roundNumbersDeep = <T,>(value: T): T => {
|
const DECIMAL_KEYS = new Set(["lineHeight"]);
|
||||||
|
|
||||||
|
const roundNumbersDeep = <T,>(value: T, parentKey?: string): T => {
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
|
if (parentKey && DECIMAL_KEYS.has(parentKey)) {
|
||||||
|
return value as T;
|
||||||
|
}
|
||||||
return Math.round(value) as T;
|
return Math.round(value) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +40,7 @@ const roundNumbersDeep = <T,>(value: T): T => {
|
|||||||
|
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
const roundedObject = Object.entries(value).reduce<Record<string, unknown>>((acc, [key, nestedValue]) => {
|
const roundedObject = Object.entries(value).reduce<Record<string, unknown>>((acc, [key, nestedValue]) => {
|
||||||
acc[key] = roundNumbersDeep(nestedValue);
|
acc[key] = roundNumbersDeep(nestedValue, key);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ import {
|
|||||||
} from '@/pages/editor/utils/textStyle';
|
} from '@/pages/editor/utils/textStyle';
|
||||||
import '@/pages/viewer/styles/entranceAnimations.css';
|
import '@/pages/viewer/styles/entranceAnimations.css';
|
||||||
import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle';
|
import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle';
|
||||||
|
import { getMaskImageStyle, getMaskedLayout } from '@/pages/viewer/utils/maskStyle';
|
||||||
import type { EntrancePhase } from '@/pages/viewer/hooks/useBookEntranceController';
|
import type { EntrancePhase } from '@/pages/viewer/hooks/useBookEntranceController';
|
||||||
|
|
||||||
|
type RenderObjectOptions = {
|
||||||
|
skipEntrance?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type BookPageProps = {
|
type BookPageProps = {
|
||||||
page: PageData;
|
page: PageData;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
@@ -94,7 +99,24 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
flyLayoutPx,
|
flyLayoutPx,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderObject = (obj: EditorObject, index: number) => {
|
const renderObjectContent = (
|
||||||
|
obj: EditorObject,
|
||||||
|
index: number,
|
||||||
|
options: RenderObjectOptions = {},
|
||||||
|
) => {
|
||||||
|
const applyStyle = (
|
||||||
|
style: React.CSSProperties,
|
||||||
|
styleObj: EditorObject,
|
||||||
|
styleIndex: number,
|
||||||
|
extra?: {
|
||||||
|
transformOrigin?: React.CSSProperties['transformOrigin'];
|
||||||
|
rotationDeg?: number;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
options.skipEntrance
|
||||||
|
? style
|
||||||
|
: withEntrance(style, styleObj, styleIndex, extra);
|
||||||
|
|
||||||
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
||||||
const hasStroke = actualStrokeWidth > 0;
|
const hasStroke = actualStrokeWidth > 0;
|
||||||
const baseStyle: React.CSSProperties = {
|
const baseStyle: React.CSSProperties = {
|
||||||
@@ -146,7 +168,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
...textBaseStyle,
|
...textBaseStyle,
|
||||||
left: `${textLayout.leftPx}px`,
|
left: `${textLayout.leftPx}px`,
|
||||||
@@ -182,7 +204,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
src={obj.imageUrl || ''}
|
src={obj.imageUrl || ''}
|
||||||
alt=""
|
alt=""
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
...baseStyle,
|
...baseStyle,
|
||||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||||
@@ -202,7 +224,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
src={obj.videoUrl || ''}
|
src={obj.videoUrl || ''}
|
||||||
controls
|
controls
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
...baseStyle,
|
...baseStyle,
|
||||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||||
@@ -233,7 +255,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
src={obj.audioUrl || ''}
|
src={obj.audioUrl || ''}
|
||||||
controls
|
controls
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
...baseStyle,
|
...baseStyle,
|
||||||
width: obj.width ? `${obj.width * scale}px` : `${320 * scale}px`,
|
width: obj.width ? `${obj.width * scale}px` : `${320 * scale}px`,
|
||||||
@@ -279,7 +301,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
<button
|
<button
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
type="button"
|
type="button"
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
...linkStyle,
|
...linkStyle,
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
@@ -317,7 +339,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
href={obj.linkUrl || '#'}
|
href={obj.linkUrl || '#'}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
...linkStyle,
|
...linkStyle,
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
@@ -350,7 +372,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${centerX - radius}px`,
|
left: `${centerX - radius}px`,
|
||||||
@@ -413,7 +435,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${centerX - radius}px`,
|
left: `${centerX - radius}px`,
|
||||||
@@ -471,7 +493,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${centerX - radius}px`,
|
left: `${centerX - radius}px`,
|
||||||
@@ -510,7 +532,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
zIndex: index, // برای حفظ ترتیب رندر
|
zIndex: index, // برای حفظ ترتیب رندر
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div key={obj.id || index} style={withEntrance(shapeStyle, obj, index)} />;
|
return <div key={obj.id || index} style={applyStyle(shapeStyle, obj, index)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'line': {
|
case 'line': {
|
||||||
@@ -529,7 +551,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${startX}px`,
|
left: `${startX}px`,
|
||||||
@@ -566,7 +588,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${startX}px`,
|
left: `${startX}px`,
|
||||||
@@ -609,7 +631,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={withEntrance(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
...baseStyle,
|
...baseStyle,
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -653,6 +675,50 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderObject = (obj: EditorObject, index: number, allElements: EditorObject[]) => {
|
||||||
|
if (obj.isMask) return null;
|
||||||
|
|
||||||
|
const maskShape = obj.maskId
|
||||||
|
? allElements.find((m) => m.id === obj.maskId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!maskShape) {
|
||||||
|
return renderObjectContent(obj, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = getMaskedLayout(obj, maskShape, scale);
|
||||||
|
const unionLeft = layout.left / scale;
|
||||||
|
const unionTop = layout.top / scale;
|
||||||
|
|
||||||
|
const content = renderObjectContent(
|
||||||
|
{ ...obj, x: (obj.x || 0) - unionLeft, y: (obj.y || 0) - unionTop },
|
||||||
|
index,
|
||||||
|
{ skipEntrance: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!content) return null;
|
||||||
|
|
||||||
|
const wrapperStyle: React.CSSProperties = {
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${layout.left}px`,
|
||||||
|
top: `${layout.top}px`,
|
||||||
|
width: `${layout.width}px`,
|
||||||
|
height: `${layout.height}px`,
|
||||||
|
overflow: 'visible',
|
||||||
|
zIndex: index,
|
||||||
|
...getMaskImageStyle(obj, maskShape, scale, layout),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`masked-${obj.id || index}`}
|
||||||
|
style={withEntrance(wrapperStyle, obj, index)}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const width = pageWidth ?? 794 * scale;
|
const width = pageWidth ?? 794 * scale;
|
||||||
const height = pageHeight ?? 1123 * scale;
|
const height = pageHeight ?? 1123 * scale;
|
||||||
|
|
||||||
@@ -706,7 +772,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div style={{ position: 'absolute', inset: 0, zIndex: 1 }}>
|
<div style={{ position: 'absolute', inset: 0, zIndex: 1 }}>
|
||||||
{page.elements.map((element, index) => renderObject(element, index))}
|
{page.elements.map((element, index) => renderObject(element, index, page.elements))}
|
||||||
</div>
|
</div>
|
||||||
{useHardPage && showPaperShadow && <div aria-hidden className="page-paper-shadow" />}
|
{useHardPage && showPaperShadow && <div aria-hidden className="page-paper-shadow" />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ type ViewerDataPage = {
|
|||||||
entranceAnimation?: EditorObject["entranceAnimation"];
|
entranceAnimation?: EditorObject["entranceAnimation"];
|
||||||
entranceDurationMs?: number;
|
entranceDurationMs?: number;
|
||||||
entranceDelayMs?: number;
|
entranceDelayMs?: number;
|
||||||
|
isMask?: boolean;
|
||||||
|
maskId?: string;
|
||||||
|
maskInvert?: boolean;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,6 +155,16 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
|||||||
baseObject.entranceDelayMs = obj.entranceDelayMs;
|
baseObject.entranceDelayMs = obj.entranceDelayMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (obj.isMask !== undefined) {
|
||||||
|
baseObject.isMask = obj.isMask;
|
||||||
|
}
|
||||||
|
if (obj.maskId !== undefined) {
|
||||||
|
baseObject.maskId = obj.maskId;
|
||||||
|
}
|
||||||
|
if (obj.maskInvert !== undefined) {
|
||||||
|
baseObject.maskInvert = obj.maskInvert;
|
||||||
|
}
|
||||||
|
|
||||||
return baseObject;
|
return baseObject;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import type { EditorObject } from '@/pages/editor/store/editorStore';
|
||||||
|
|
||||||
|
type Bounds = {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaskedLayout = {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
maskRelX: number;
|
||||||
|
maskRelY: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodeSvg = (svg: string) =>
|
||||||
|
`url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
||||||
|
|
||||||
|
const getObjectBounds = (obj: EditorObject): Bounds => {
|
||||||
|
if (obj.type === 'rectangle' && obj.shapeType === 'circle') {
|
||||||
|
const radius = (obj.width || 100) / 2;
|
||||||
|
const cx = obj.x || 0;
|
||||||
|
const cy = obj.y || 0;
|
||||||
|
return {
|
||||||
|
left: cx - radius,
|
||||||
|
top: cy - radius,
|
||||||
|
right: cx + radius,
|
||||||
|
bottom: cy + radius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.type === 'rectangle' && (obj.shapeType === 'triangle' || obj.shapeType === 'abstract')) {
|
||||||
|
const width = obj.width || 100;
|
||||||
|
const height = obj.height || 100;
|
||||||
|
const radius = Math.min(width, height) / 2;
|
||||||
|
const cx = (obj.x || 0) + width / 2;
|
||||||
|
const cy = (obj.y || 0) + height / 2;
|
||||||
|
return {
|
||||||
|
left: cx - radius,
|
||||||
|
top: cy - radius,
|
||||||
|
right: cx + radius,
|
||||||
|
bottom: cy + radius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: obj.x || 0,
|
||||||
|
top: obj.y || 0,
|
||||||
|
right: (obj.x || 0) + (obj.width || 100),
|
||||||
|
bottom: (obj.y || 0) + (obj.height || 100),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMaskBounds = (mask: EditorObject): Bounds => {
|
||||||
|
if (mask.type === 'rectangle' && mask.shapeType === 'circle') {
|
||||||
|
const radius = (mask.width || 100) / 2;
|
||||||
|
const cx = mask.x || 0;
|
||||||
|
const cy = mask.y || 0;
|
||||||
|
return {
|
||||||
|
left: cx - radius,
|
||||||
|
top: cy - radius,
|
||||||
|
right: cx + radius,
|
||||||
|
bottom: cy + radius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask.type === 'rectangle' && mask.shapeType === 'triangle') {
|
||||||
|
const width = mask.width || 100;
|
||||||
|
const height = mask.height || 100;
|
||||||
|
const radius = Math.min(width, height) / 2;
|
||||||
|
const cx = (mask.x || 0) + width / 2;
|
||||||
|
const cy = (mask.y || 0) + height / 2;
|
||||||
|
return {
|
||||||
|
left: cx - radius,
|
||||||
|
top: cy - radius,
|
||||||
|
right: cx + radius,
|
||||||
|
bottom: cy + radius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask.type === 'rectangle' && mask.shapeType === 'abstract') {
|
||||||
|
const width = mask.width || 100;
|
||||||
|
const height = mask.height || 100;
|
||||||
|
const baseRadius = Math.min(width, height) / 2;
|
||||||
|
const radius = baseRadius * 0.85;
|
||||||
|
const cx = (mask.x || 0) + width / 2;
|
||||||
|
const cy = (mask.y || 0) + height / 2;
|
||||||
|
return {
|
||||||
|
left: cx - radius,
|
||||||
|
top: cy - radius,
|
||||||
|
right: cx + radius,
|
||||||
|
bottom: cy + radius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: mask.x || 0,
|
||||||
|
top: mask.y || 0,
|
||||||
|
right: (mask.x || 0) + (mask.width || 100),
|
||||||
|
bottom: (mask.y || 0) + (mask.height || 100),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMaskedLayout = (
|
||||||
|
obj: EditorObject,
|
||||||
|
mask: EditorObject,
|
||||||
|
scale: number,
|
||||||
|
): MaskedLayout => {
|
||||||
|
const objBounds = getObjectBounds(obj);
|
||||||
|
const maskBounds = getMaskBounds(mask);
|
||||||
|
|
||||||
|
const unionLeft = Math.min(objBounds.left, maskBounds.left);
|
||||||
|
const unionTop = Math.min(objBounds.top, maskBounds.top);
|
||||||
|
const unionRight = Math.max(objBounds.right, maskBounds.right);
|
||||||
|
const unionBottom = Math.max(objBounds.bottom, maskBounds.bottom);
|
||||||
|
|
||||||
|
let maskRelX: number;
|
||||||
|
let maskRelY: number;
|
||||||
|
|
||||||
|
if (mask.type === 'rectangle' && mask.shapeType === 'circle') {
|
||||||
|
maskRelX = (mask.x || 0) - unionLeft;
|
||||||
|
maskRelY = (mask.y || 0) - unionTop;
|
||||||
|
} else if (
|
||||||
|
mask.type === 'rectangle' &&
|
||||||
|
(mask.shapeType === 'triangle' || mask.shapeType === 'abstract')
|
||||||
|
) {
|
||||||
|
maskRelX = (mask.x || 0) - unionLeft;
|
||||||
|
maskRelY = (mask.y || 0) - unionTop;
|
||||||
|
} else {
|
||||||
|
maskRelX = (mask.x || 0) - unionLeft;
|
||||||
|
maskRelY = (mask.y || 0) - unionTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: unionLeft * scale,
|
||||||
|
top: unionTop * scale,
|
||||||
|
width: Math.max(1, (unionRight - unionLeft) * scale),
|
||||||
|
height: Math.max(1, (unionBottom - unionTop) * scale),
|
||||||
|
maskRelX: maskRelX * scale,
|
||||||
|
maskRelY: maskRelY * scale,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMaskShapeSvg = (
|
||||||
|
mask: EditorObject,
|
||||||
|
relX: number,
|
||||||
|
relY: number,
|
||||||
|
scale: number,
|
||||||
|
fill: string,
|
||||||
|
): string => {
|
||||||
|
const width = (mask.width || 100) * scale;
|
||||||
|
const height = (mask.height || 100) * scale;
|
||||||
|
const rotation = mask.rotation || 0;
|
||||||
|
|
||||||
|
if (mask.type === 'rectangle' && mask.shapeType === 'circle') {
|
||||||
|
const radius = width / 2;
|
||||||
|
return `<circle cx="${relX}" cy="${relY}" r="${radius}" fill="${fill}" transform="rotate(${rotation} ${relX} ${relY})" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask.type === 'rectangle' && mask.shapeType === 'triangle') {
|
||||||
|
const radius = Math.min(width, height) / 2;
|
||||||
|
const cx = relX + width / 2;
|
||||||
|
const cy = relY + height / 2;
|
||||||
|
const size = radius * 2;
|
||||||
|
const triangleHeight = size * 0.8660254;
|
||||||
|
const topY = (size - triangleHeight) / 2;
|
||||||
|
const bottomY = size - topY;
|
||||||
|
const halfWidth = triangleHeight * 0.5773503;
|
||||||
|
const topX = size / 2;
|
||||||
|
const bottomLeftX = size / 2 - halfWidth;
|
||||||
|
const bottomRightX = size / 2 + halfWidth;
|
||||||
|
const offsetX = cx - radius;
|
||||||
|
const offsetY = cy - radius;
|
||||||
|
return `<polygon points="${topX + offsetX},${topY + offsetY} ${bottomLeftX + offsetX},${bottomY + offsetY} ${bottomRightX + offsetX},${bottomY + offsetY}" fill="${fill}" transform="rotate(${rotation} ${cx} ${cy})" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask.type === 'rectangle' && mask.shapeType === 'abstract') {
|
||||||
|
const baseRadius = Math.min(width, height) / 2;
|
||||||
|
const radius = baseRadius * 0.85;
|
||||||
|
const cx = relX + width / 2;
|
||||||
|
const cy = relY + height / 2;
|
||||||
|
const left = cx - radius;
|
||||||
|
const top = cy - radius;
|
||||||
|
const size = radius * 2;
|
||||||
|
return `<polygon points="${left + size * 0.5},${top} ${left + size * 0.61},${top + size * 0.35} ${left + size * 0.98},${top + size * 0.35} ${left + size * 0.68},${top + size * 0.57} ${left + size * 0.79},${top + size * 0.91} ${left + size * 0.5},${top + size * 0.7} ${left + size * 0.21},${top + size * 0.91} ${left + size * 0.32},${top + size * 0.57} ${left + size * 0.02},${top + size * 0.35} ${left + size * 0.39},${top + size * 0.35}" fill="${fill}" transform="rotate(${rotation} ${cx} ${cy})" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cornerRadius = Math.max(0, (mask.borderRadius ?? 0) * scale);
|
||||||
|
const rectX = relX;
|
||||||
|
const rectY = relY;
|
||||||
|
const pivotX = rectX;
|
||||||
|
const pivotY = rectY;
|
||||||
|
return `<rect x="${rectX}" y="${rectY}" width="${width}" height="${height}" rx="${cornerRadius}" ry="${cornerRadius}" fill="${fill}" transform="rotate(${rotation} ${pivotX} ${pivotY})" />`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds CSS mask properties matching editor Konva destination-in / destination-out.
|
||||||
|
* maskInvert === false → destination-in (show overlap only)
|
||||||
|
* otherwise → destination-out (hide overlap)
|
||||||
|
*/
|
||||||
|
export const getMaskImageStyle = (
|
||||||
|
obj: EditorObject,
|
||||||
|
mask: EditorObject,
|
||||||
|
scale: number,
|
||||||
|
layout: MaskedLayout,
|
||||||
|
): CSSProperties => {
|
||||||
|
const canvasW = layout.width;
|
||||||
|
const canvasH = layout.height;
|
||||||
|
|
||||||
|
const showOverlapOnly = obj.maskInvert === false;
|
||||||
|
const bgFill = showOverlapOnly ? 'black' : 'white';
|
||||||
|
const shapeFill = showOverlapOnly ? 'white' : 'black';
|
||||||
|
|
||||||
|
const maskShapeX = layout.maskRelX;
|
||||||
|
const maskShapeY = layout.maskRelY;
|
||||||
|
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${canvasW} ${canvasH}"><rect width="100%" height="100%" fill="${bgFill}"/>${renderMaskShapeSvg(mask, maskShapeX, maskShapeY, scale, shapeFill)}</svg>`;
|
||||||
|
const maskUrl = encodeSvg(svg);
|
||||||
|
|
||||||
|
return {
|
||||||
|
WebkitMaskImage: maskUrl,
|
||||||
|
maskImage: maskUrl,
|
||||||
|
WebkitMaskSize: `${canvasW}px ${canvasH}px`,
|
||||||
|
maskSize: `${canvasW}px ${canvasH}px`,
|
||||||
|
WebkitMaskPosition: '0 0',
|
||||||
|
maskPosition: '0 0',
|
||||||
|
WebkitMaskRepeat: 'no-repeat',
|
||||||
|
maskRepeat: 'no-repeat',
|
||||||
|
maskMode: 'luminance',
|
||||||
|
// Safari needs luminance mode for white/black SVG fills
|
||||||
|
['WebkitMaskMode' as string]: 'luminance',
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user