mask
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Group, Rect, Circle, RegularPolygon, Star } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import type { EditorObject } from "../../store/editorStore";
|
||||
@@ -56,21 +56,26 @@ const ObjectRenderer = ({
|
||||
const maskShape = obj.maskId ? allObjects.find((m) => m.id === obj.maskId) : null;
|
||||
const shouldApplyMask = maskShape && !obj.isMask;
|
||||
|
||||
// Apply caching for masked objects - REQUIRED for destination-in to work
|
||||
// Whether the object and its mask share a groupId (move together as one unit).
|
||||
const isGroupedWithMask = !!(obj.groupId && maskShape && maskShape.groupId === obj.groupId);
|
||||
|
||||
// For GROUPED masks the relative offset is always constant — no need to track it.
|
||||
// For NON-GROUPED masks we must recache when the user repositions just the mask.
|
||||
// Round to integer to suppress floating-point noise that accumulates when
|
||||
// handleObjectUpdate adds a delta to every group member on each drag pixel:
|
||||
// e.g. 281.6692917998996 vs 281.6692917998995 would otherwise fire the effect.
|
||||
const maskRelXDep = (shouldApplyMask && !isGroupedWithMask) ? Math.round((maskShape?.x || 0) - (obj.x || 0)) : 0;
|
||||
const maskRelYDep = (shouldApplyMask && !isGroupedWithMask) ? Math.round((maskShape?.y || 0) - (obj.y || 0)) : 0;
|
||||
|
||||
useEffect(() => {
|
||||
const groupNode = groupRef.current;
|
||||
if (!groupNode || !shouldApplyMask) return;
|
||||
|
||||
// Use double requestAnimationFrame to ensure all changes (including stroke) are rendered before caching
|
||||
let rafId1: number;
|
||||
const rafId2 = requestAnimationFrame(() => {
|
||||
rafId1 = requestAnimationFrame(() => {
|
||||
if (!groupRef.current) return;
|
||||
|
||||
// Clear cache first
|
||||
groupRef.current.clearCache();
|
||||
|
||||
// Apply cache - Konva will automatically calculate bounds
|
||||
groupRef.current.cache();
|
||||
groupRef.current.getLayer()?.batchDraw();
|
||||
});
|
||||
@@ -81,13 +86,13 @@ const ObjectRenderer = ({
|
||||
if (rafId1) cancelAnimationFrame(rafId1);
|
||||
groupNode?.clearCache();
|
||||
};
|
||||
}, [shouldApplyMask, isSelected, maskShape?.x, maskShape?.y, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.x, obj.y, obj.width, obj.height, obj.rotation, obj.maskInvert, obj.strokeWidth, obj.stroke, obj.fill, obj.fillType, obj.gradient]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [shouldApplyMask, isSelected, maskRelXDep, maskRelYDep, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.width, obj.height, obj.rotation, obj.maskInvert, obj.strokeWidth, obj.stroke, obj.fill, obj.fillType, obj.gradient]);
|
||||
|
||||
// Refresh cache after transformer is attached (when isSelected changes)
|
||||
useEffect(() => {
|
||||
if (!groupRef.current || !shouldApplyMask || !isSelected) return;
|
||||
|
||||
// Wait for transformer to attach, then refresh cache
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!groupRef.current) return;
|
||||
groupRef.current.clearCache();
|
||||
@@ -100,6 +105,17 @@ const ObjectRenderer = ({
|
||||
};
|
||||
}, [shouldApplyMask, isSelected, transformerRef]);
|
||||
|
||||
// Called by async shapes (images) once their content is fully decoded and painted.
|
||||
const handleImageReady = useCallback(() => {
|
||||
if (!groupRef.current || !shouldApplyMask) return;
|
||||
requestAnimationFrame(() => {
|
||||
if (!groupRef.current) return;
|
||||
groupRef.current.clearCache();
|
||||
groupRef.current.cache();
|
||||
groupRef.current.getLayer()?.batchDraw();
|
||||
});
|
||||
}, [shouldApplyMask]);
|
||||
|
||||
if (obj.visible === false) {
|
||||
return null;
|
||||
}
|
||||
@@ -216,7 +232,7 @@ const ObjectRenderer = ({
|
||||
shapeElement = <ArrowShape key={obj.id} {...commonProps} />;
|
||||
break;
|
||||
case "image":
|
||||
shapeElement = <ImageObject key={obj.id} {...commonProps} />;
|
||||
shapeElement = <ImageObject key={obj.id} {...commonProps} onImageReady={handleImageReady} />;
|
||||
break;
|
||||
case "link":
|
||||
shapeElement = <LinkShape key={obj.id} {...commonProps} />;
|
||||
@@ -234,10 +250,10 @@ const ObjectRenderer = ({
|
||||
);
|
||||
break;
|
||||
case "document":
|
||||
shapeElement = <ImageObject key={obj.id} {...commonProps} />;
|
||||
shapeElement = <ImageObject key={obj.id} {...commonProps} onImageReady={handleImageReady} />;
|
||||
break;
|
||||
case "sticker":
|
||||
shapeElement = <ImageObject key={obj.id} {...commonProps} />;
|
||||
shapeElement = <ImageObject key={obj.id} {...commonProps} onImageReady={handleImageReady} />;
|
||||
break;
|
||||
case "grid":
|
||||
shapeElement = (
|
||||
@@ -317,8 +333,8 @@ const ObjectRenderer = ({
|
||||
// Apply masking with Group + destination-in or destination-out
|
||||
// IMPORTANT: The shape inside keeps its normal event handlers
|
||||
// The mask layer has listening={false} to not interfere with selection
|
||||
// Default: destination-out (overlap مخفی میشود)
|
||||
const compositeOp = obj.maskInvert === false ? "destination-in" : "destination-out";
|
||||
// Default: destination-in (نمایش فقط داخل ماسک — جاهایی که ماسک هست نمایش میدهد)
|
||||
const compositeOp = obj.maskInvert === false ? "destination-out" : "destination-in";
|
||||
|
||||
const handleGroupDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
|
||||
const node = e.target;
|
||||
@@ -328,18 +344,10 @@ const ObjectRenderer = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleGroupDragMove = (e: Konva.KonvaEventObject<DragEvent>) => {
|
||||
const node = e.target;
|
||||
// بهروزرسانی transformer در حین drag
|
||||
if (transformerRef.current && isSelected) {
|
||||
transformerRef.current.nodes([node]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
const handleGroupDragMove = (_e: Konva.KonvaEventObject<DragEvent>) => {
|
||||
// Intentionally empty — see comment above.
|
||||
};
|
||||
|
||||
// بررسی آیا object با mask گروه شده است
|
||||
const isGroupedWithMask = obj.groupId && maskShape && maskShape.groupId === obj.groupId;
|
||||
|
||||
const handleGroupTransformEnd = () => {
|
||||
if (!groupRef.current || !maskShape) return;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo } from "react";
|
||||
import { Layer, Group } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import ObjectRenderer from "./ObjectRenderer";
|
||||
@@ -152,5 +153,11 @@ const ObjectsLayer = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectsLayer;
|
||||
// Memoized to prevent re-renders when EditorCanvas state that is unrelated to
|
||||
// object rendering changes (e.g. activeGuideIds during drag). All function
|
||||
// props are stabilised via useCallback in their respective hooks so this memo
|
||||
// is effective — preventing the cascade: setActiveGuideIds → EditorCanvas
|
||||
// re-render → ObjectsLayer re-render → every ObjectRenderer re-render →
|
||||
// react-konva reconcile + batchDraw for every drag-move pixel.
|
||||
export default memo(ObjectsLayer);
|
||||
|
||||
|
||||
@@ -1,58 +1,55 @@
|
||||
import { useCallback } from "react";
|
||||
import { useEditorStore, type EditorObject } from "../../store/editorStore";
|
||||
|
||||
export const useObjectHandlers = () => {
|
||||
const { updateObject, groupObjects, ungroupObjects, updateCellValue, setEditingCell, editingCell } =
|
||||
useEditorStore();
|
||||
// All handlers read live state via getState() and are wrapped in useCallback
|
||||
// with empty deps so their references never change between renders.
|
||||
// This prevents ObjectsLayer (and every ObjectRenderer inside it) from
|
||||
// re-rendering whenever unrelated EditorCanvas state (e.g. active guide IDs)
|
||||
// changes during drag — eliminating lag and flickering on masked groups.
|
||||
|
||||
const handleObjectUpdate = (id: string, updates: Partial<EditorObject>) => {
|
||||
// همیشه آخرین state را بخوان تا چند dragmove پشتسرهم قبل از رِندر React، دلتا درست بماند
|
||||
const liveObjects = useEditorStore.getState().objects;
|
||||
const obj = liveObjects.find((o) => o.id === id);
|
||||
const handleObjectUpdate = useCallback((id: string, updates: Partial<EditorObject>) => {
|
||||
const { objects, updateObject } = useEditorStore.getState();
|
||||
const obj = objects.find((o) => o.id === id);
|
||||
if (!obj) return;
|
||||
|
||||
// اگر group object است و position تغییر کرد، همه objectهای داخل group را جابجا کن
|
||||
if (obj.type === "group" && (updates.x !== undefined || updates.y !== undefined)) {
|
||||
const groupMembers = liveObjects.filter((o) => o.groupId === id);
|
||||
const groupMembers = objects.filter((o) => o.groupId === id);
|
||||
const deltaX = updates.x !== undefined ? updates.x - (obj.x || 0) : 0;
|
||||
const deltaY = updates.y !== undefined ? updates.y - (obj.y || 0) : 0;
|
||||
|
||||
// بهروزرسانی group object
|
||||
updateObject(id, updates);
|
||||
|
||||
// بهروزرسانی همه اعضای گروه
|
||||
groupMembers.forEach((member) => {
|
||||
updateObject(member.id, {
|
||||
x: (member.x || 0) + deltaX,
|
||||
y: (member.y || 0) + deltaY,
|
||||
});
|
||||
});
|
||||
} else if (obj.groupId && (updates.x !== undefined || updates.y !== undefined)) {
|
||||
// اگر object در یک گروه است و position تغییر کرد، فقط object را بهروزرسانی کن
|
||||
// bounds group فقط موقع transform بهروزرسانی میشود
|
||||
updateObject(id, updates);
|
||||
} else {
|
||||
updateObject(id, updates);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleGroupWithMask = (objectId: string, maskId: string) => {
|
||||
groupObjects([objectId, maskId]);
|
||||
};
|
||||
const handleGroupWithMask = useCallback((objectId: string, maskId: string) => {
|
||||
useEditorStore.getState().groupObjects([objectId, maskId]);
|
||||
}, []);
|
||||
|
||||
const handleUngroupFromMask = (groupId: string) => {
|
||||
ungroupObjects(groupId);
|
||||
};
|
||||
const handleUngroupFromMask = useCallback((groupId: string) => {
|
||||
useEditorStore.getState().ungroupObjects(groupId);
|
||||
}, []);
|
||||
|
||||
const handleCellEditorSave = (text: string) => {
|
||||
const handleCellEditorSave = useCallback((text: string) => {
|
||||
const { editingCell, updateCellValue, setEditingCell } = useEditorStore.getState();
|
||||
if (editingCell) {
|
||||
updateCellValue(editingCell.tableId, editingCell.cellId, text);
|
||||
setEditingCell(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCellEditorCancel = () => {
|
||||
setEditingCell(null);
|
||||
};
|
||||
const handleCellEditorCancel = useCallback(() => {
|
||||
useEditorStore.getState().setEditingCell(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleObjectUpdate,
|
||||
@@ -62,4 +59,3 @@ export const useObjectHandlers = () => {
|
||||
handleCellEditorCancel,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { useCallback } from "react";
|
||||
import Konva from "konva";
|
||||
import { useEditorStore } from "../../store/editorStore";
|
||||
|
||||
export const useSelectionHandlers = () => {
|
||||
// Read live state via getState() so callbacks never go stale and can have
|
||||
// empty deps — this prevents ObjectsLayer from re-rendering every time
|
||||
// unrelated React state (e.g. active guide IDs) changes in EditorCanvas.
|
||||
|
||||
const handleSelect = useCallback((id: string, _node: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
const {
|
||||
objects,
|
||||
selectedObjectIds,
|
||||
tool,
|
||||
setTool,
|
||||
setSelectedObjectId,
|
||||
addToSelection,
|
||||
removeFromSelection,
|
||||
setSelectedTableId,
|
||||
setSelectedCellId,
|
||||
tool,
|
||||
setTool,
|
||||
} = useEditorStore();
|
||||
} = useEditorStore.getState();
|
||||
|
||||
const handleSelect = (id: string, _node: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
if (tool !== "select") {
|
||||
setTool("select");
|
||||
}
|
||||
@@ -22,7 +27,6 @@ export const useSelectionHandlers = () => {
|
||||
const obj = objects.find((o) => o.id === id);
|
||||
const isMultiSelect = e?.evt?.metaKey || e?.evt?.ctrlKey || e?.evt?.shiftKey;
|
||||
|
||||
// اگر object در یک group است، group را انتخاب کن نه object
|
||||
if (obj?.groupId && obj.type !== "group") {
|
||||
const groupObject = objects.find((o) => o.id === obj.groupId);
|
||||
if (groupObject) {
|
||||
@@ -40,16 +44,12 @@ export const useSelectionHandlers = () => {
|
||||
}
|
||||
|
||||
if (isMultiSelect) {
|
||||
// انتخاب چندتایی
|
||||
if (selectedObjectIds.includes(id)) {
|
||||
// اگر قبلاً انتخاب شده بود، از انتخاب خارج کن
|
||||
removeFromSelection(id);
|
||||
} else {
|
||||
// اگر انتخاب نشده بود، به انتخاب اضافه کن
|
||||
addToSelection(id);
|
||||
}
|
||||
} else {
|
||||
// انتخاب تکتایی
|
||||
setSelectedObjectId(id);
|
||||
}
|
||||
|
||||
@@ -62,13 +62,21 @@ export const useSelectionHandlers = () => {
|
||||
if (!isMultiSelect) {
|
||||
setSelectedCellId(null);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCellClick = useCallback((tableId: string, cellId: string, e?: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
const {
|
||||
selectedObjectIds,
|
||||
addToSelection,
|
||||
removeFromSelection,
|
||||
setSelectedTableId,
|
||||
setSelectedObjectId,
|
||||
setSelectedCellId,
|
||||
} = useEditorStore.getState();
|
||||
|
||||
const handleCellClick = (tableId: string, cellId: string, e?: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
const isMultiSelect = e?.evt?.metaKey || e?.evt?.ctrlKey || e?.evt?.shiftKey;
|
||||
|
||||
if (isMultiSelect) {
|
||||
// انتخاب چندتایی
|
||||
if (selectedObjectIds.includes(tableId)) {
|
||||
removeFromSelection(tableId);
|
||||
} else {
|
||||
@@ -80,20 +88,19 @@ export const useSelectionHandlers = () => {
|
||||
}
|
||||
|
||||
setSelectedCellId(cellId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCellDblClick = (
|
||||
const handleCellDblClick = useCallback((
|
||||
tableId: string,
|
||||
cellId: string,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
text: string
|
||||
text: string,
|
||||
) => {
|
||||
const { setEditingCell } = useEditorStore.getState();
|
||||
setEditingCell({ tableId, cellId, x, y, width, height, value: text });
|
||||
};
|
||||
useEditorStore.getState().setEditingCell({ tableId, cellId, x, y, width, height, value: text });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleSelect,
|
||||
@@ -101,4 +108,3 @@ export const useSelectionHandlers = () => {
|
||||
handleCellDblClick,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -132,7 +132,10 @@ const MaskSettings = ({ objectId }: MaskSettingsProps) => {
|
||||
{object.maskId && (
|
||||
<>
|
||||
<div className="flex items-center justify-between mt-4 pt-3 border-t">
|
||||
<span className="text-xs">نمایش فقط همپوشانی</span>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-xs font-medium">معکوس کردن ماسک</span>
|
||||
<span className="text-[10px] text-gray-400">نمایش خارج از محدوده ماسک</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={object.maskInvert === false}
|
||||
onCheckedChange={(checked) => handleToggleMaskInvert(!checked)}
|
||||
@@ -153,7 +156,7 @@ const MaskSettings = ({ objectId }: MaskSettingsProps) => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
✓ ماسک اعمال شد. {object.maskInvert === false ? "فقط قسمتهای همپوشانی نمایش داده میشوند." : "قسمتهای همپوشانی مخفی میشوند."}
|
||||
✓ ماسک اعمال شد. {object.maskInvert === false ? "قسمتهای خارج از ماسک نمایش داده میشوند." : "فقط داخل محدوده ماسک نمایش داده میشود."}
|
||||
<br />
|
||||
💡 برای گروهبندی با ماسک، object را انتخاب کنید و دکمه "گروهبندی با ماسک" را بزنید.
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Image as KonvaImage } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import useImage from "use-image";
|
||||
@@ -13,11 +13,18 @@ const ImageObject = ({
|
||||
draggable,
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
onImageReady,
|
||||
}: ShapeProps) => {
|
||||
const [image] = useImage(obj.imageUrl || "");
|
||||
const [image, status] = useImage(obj.imageUrl || "");
|
||||
const shapeRef = useRef<Konva.Image>(null);
|
||||
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
// Notify parent (ObjectRenderer) as soon as the image is decoded and ready.
|
||||
// This lets the masked group re-cache itself after the image content appears.
|
||||
useEffect(() => {
|
||||
if (status === "loaded" && onImageReady) {
|
||||
onImageReady();
|
||||
}
|
||||
}, [status, onImageReady]);
|
||||
|
||||
if (!image) return null;
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ export type ShapeProps = {
|
||||
draggable: boolean;
|
||||
stageWidth?: number;
|
||||
stageHeight?: number;
|
||||
/** Called by async shapes (images) once their content is fully ready to render. */
|
||||
onImageReady?: () => void;
|
||||
};
|
||||
|
||||
export type TextShapeProps = ShapeProps & {
|
||||
|
||||
@@ -196,8 +196,9 @@ export const exportObjectWithMask = async (
|
||||
// Step 1: Draw object
|
||||
await drawObject(ctx, { ...object, x: 0, y: 0 });
|
||||
|
||||
// Step 2: Apply mask با destination-in
|
||||
ctx.globalCompositeOperation = "destination-in";
|
||||
// Step 2: Apply mask — default is destination-in (show inside mask).
|
||||
// maskInvert === false → destination-out (show outside mask, hide inside).
|
||||
ctx.globalCompositeOperation = object.maskInvert === false ? "destination-out" : "destination-in";
|
||||
|
||||
// Adjust mask position relative to object
|
||||
const adjustedMaskShape: EditorObject = {
|
||||
|
||||
@@ -105,6 +105,16 @@ const getMaskBounds = (mask: EditorObject): Bounds => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the wrapper bounding box for a masked object.
|
||||
*
|
||||
* Default (maskInvert !== false) → destination-in → show inside mask:
|
||||
* Uses intersection bounds — only the overlap area needs to be rendered,
|
||||
* which is smaller and faster than the union.
|
||||
*
|
||||
* maskInvert === false → destination-out → show outside mask (hide inside):
|
||||
* Uses object bounds — the entire object is visible except where the mask sits.
|
||||
*/
|
||||
export const getMaskedLayout = (
|
||||
obj: EditorObject,
|
||||
mask: EditorObject,
|
||||
@@ -113,33 +123,42 @@ export const getMaskedLayout = (
|
||||
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);
|
||||
const isDestinationIn = obj.maskInvert !== false;
|
||||
|
||||
let maskRelX: number;
|
||||
let maskRelY: number;
|
||||
let left: number;
|
||||
let top: number;
|
||||
let right: number;
|
||||
let bottom: 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;
|
||||
if (isDestinationIn) {
|
||||
// Show inside mask: only the intersection area will be visible.
|
||||
// Use intersection bounds to minimise the wrapper size.
|
||||
left = Math.max(objBounds.left, maskBounds.left);
|
||||
top = Math.max(objBounds.top, maskBounds.top);
|
||||
right = Math.min(objBounds.right, maskBounds.right);
|
||||
bottom = Math.min(objBounds.bottom, maskBounds.bottom);
|
||||
|
||||
if (right <= left || bottom <= top) {
|
||||
// No overlap — nothing to render.
|
||||
return { left: left * scale, top: top * scale, width: 0, height: 0, maskRelX: 0, maskRelY: 0 };
|
||||
}
|
||||
} else {
|
||||
maskRelX = (mask.x || 0) - unionLeft;
|
||||
maskRelY = (mask.y || 0) - unionTop;
|
||||
// Show outside mask: the full object area is the canvas.
|
||||
// The mask shape (drawn black) will punch a hole inside it.
|
||||
left = objBounds.left;
|
||||
top = objBounds.top;
|
||||
right = objBounds.right;
|
||||
bottom = objBounds.bottom;
|
||||
}
|
||||
|
||||
const maskRelX = (mask.x || 0) - left;
|
||||
const maskRelY = (mask.y || 0) - top;
|
||||
|
||||
return {
|
||||
left: unionLeft * scale,
|
||||
top: unionTop * scale,
|
||||
width: Math.max(1, (unionRight - unionLeft) * scale),
|
||||
height: Math.max(1, (unionBottom - unionTop) * scale),
|
||||
left: left * scale,
|
||||
top: top * scale,
|
||||
width: Math.max(1, (right - left) * scale),
|
||||
height: Math.max(1, (bottom - top) * scale),
|
||||
maskRelX: maskRelX * scale,
|
||||
maskRelY: maskRelY * scale,
|
||||
};
|
||||
@@ -199,8 +218,12 @@ const renderMaskShapeSvg = (
|
||||
|
||||
/**
|
||||
* Builds CSS mask properties matching editor Konva destination-in / destination-out.
|
||||
* maskInvert === false → destination-in (show overlap only)
|
||||
* otherwise → destination-out (hide overlap)
|
||||
*
|
||||
* Default (maskInvert !== false) → destination-in → show inside mask:
|
||||
* bg = black (hide everything), shape = white (reveal mask area)
|
||||
*
|
||||
* maskInvert === false → destination-out → show outside mask (hide inside):
|
||||
* bg = white (show everything), shape = black (hide mask area)
|
||||
*/
|
||||
export const getMaskImageStyle = (
|
||||
obj: EditorObject,
|
||||
@@ -211,9 +234,10 @@ export const getMaskImageStyle = (
|
||||
const canvasW = layout.width;
|
||||
const canvasH = layout.height;
|
||||
|
||||
const showOverlapOnly = obj.maskInvert === false;
|
||||
const bgFill = showOverlapOnly ? 'black' : 'white';
|
||||
const shapeFill = showOverlapOnly ? 'white' : 'black';
|
||||
// Default behaviour: show only inside the mask (destination-in)
|
||||
const showInsideMask = obj.maskInvert !== false;
|
||||
const bgFill = showInsideMask ? 'black' : 'white';
|
||||
const shapeFill = showInsideMask ? 'white' : 'black';
|
||||
|
||||
const maskShapeX = layout.maskRelX;
|
||||
const maskShapeY = layout.maskRelY;
|
||||
|
||||
Reference in New Issue
Block a user