border radius for all shape
This commit is contained in:
@@ -88,7 +88,7 @@ const ObjectRenderer = ({
|
||||
groupNode?.clearCache();
|
||||
};
|
||||
// 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, obj.blur]);
|
||||
}, [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, obj.blur, obj.borderRadius]);
|
||||
|
||||
// Refresh cache after transformer is attached (when isSelected changes)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -31,6 +31,17 @@ const SizeSettings = ({ selectedObject, onUpdate, defaultWidth = 100, defaultHei
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label="گردی گوشه"
|
||||
type="number"
|
||||
value={selectedObject.borderRadius ?? 0}
|
||||
onChange={(e) =>
|
||||
onUpdate(selectedObject.id, {
|
||||
borderRadius: Math.max(0, parseInt(e.target.value) || 0),
|
||||
})
|
||||
}
|
||||
min={0}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef } from "react";
|
||||
import { Rect, Group, Circle, Text as KonvaText } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import type { ShapeProps } from "./types";
|
||||
import { getObjectBorderRadius } from "../../utils/borderRadius";
|
||||
|
||||
const AudioShape = ({
|
||||
obj,
|
||||
@@ -15,6 +16,7 @@ const AudioShape = ({
|
||||
|
||||
const containerWidth = obj.width || 320;
|
||||
const containerHeight = obj.height || 56;
|
||||
const cornerRadius = getObjectBorderRadius(obj.borderRadius ?? 8);
|
||||
const barY = containerHeight * 0.55;
|
||||
const barHeight = Math.max(4, containerHeight * 0.12);
|
||||
const playRadius = Math.min(18, containerHeight * 0.35);
|
||||
@@ -70,7 +72,7 @@ const AudioShape = ({
|
||||
width={containerWidth}
|
||||
height={containerHeight}
|
||||
fill="#f3f4f6"
|
||||
cornerRadius={8}
|
||||
cornerRadius={cornerRadius}
|
||||
stroke={isSelected ? "#3b82f6" : "#d1d5db"}
|
||||
strokeWidth={isSelected ? 3 : 1}
|
||||
onClick={handleGroupClick}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Image as KonvaImage } from "react-konva";
|
||||
import { Image as KonvaImage, Group, Rect } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import useImage from "use-image";
|
||||
import type { ShapeProps } from "./types";
|
||||
import { clampPositionToStage } from "../../utils/stageBounds";
|
||||
import { createRoundedRectClipFunc, getObjectBorderRadius } from "../../utils/borderRadius";
|
||||
|
||||
const ImageObject = ({
|
||||
obj,
|
||||
@@ -17,9 +18,8 @@ const ImageObject = ({
|
||||
}: ShapeProps) => {
|
||||
const [image, status] = useImage(obj.imageUrl || "");
|
||||
const shapeRef = useRef<Konva.Image>(null);
|
||||
const groupRef = useRef<Konva.Group>(null);
|
||||
|
||||
// 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();
|
||||
@@ -31,24 +31,12 @@ const ImageObject = ({
|
||||
const width = obj.width || image.width;
|
||||
const height = obj.height || image.height;
|
||||
const hasStageBounds = stageWidth != null && stageHeight != null;
|
||||
const cornerRadius = getObjectBorderRadius(obj.borderRadius);
|
||||
const clipFunc = createRoundedRectClipFunc(width, height, cornerRadius);
|
||||
|
||||
return (
|
||||
<KonvaImage
|
||||
ref={shapeRef}
|
||||
id={obj.id}
|
||||
name="canvas-object"
|
||||
x={obj.x}
|
||||
y={obj.y}
|
||||
image={image}
|
||||
width={width}
|
||||
height={height}
|
||||
stroke={isSelected ? "#3b82f6" : undefined}
|
||||
strokeWidth={isSelected ? 3 : 0}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
dragBoundFunc={
|
||||
const dragBoundFunc =
|
||||
draggable && hasStageBounds
|
||||
? (pos) =>
|
||||
? (pos: { x: number; y: number }) =>
|
||||
clampPositionToStage(
|
||||
pos.x,
|
||||
pos.y,
|
||||
@@ -57,14 +45,9 @@ const ImageObject = ({
|
||||
stageWidth!,
|
||||
stageHeight!,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
: undefined;
|
||||
|
||||
const handleDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
|
||||
const node = e.target;
|
||||
let x = node.x();
|
||||
let y = node.y();
|
||||
@@ -80,10 +63,72 @@ const ImageObject = ({
|
||||
node.position({ x, y });
|
||||
}
|
||||
onUpdate(obj.id, { x, y });
|
||||
}}
|
||||
};
|
||||
|
||||
const handleClick = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
const node = cornerRadius > 0 ? groupRef.current : shapeRef.current;
|
||||
if (node) {
|
||||
onSelect(obj.id, node, e);
|
||||
}
|
||||
};
|
||||
|
||||
if (cornerRadius > 0) {
|
||||
return (
|
||||
<Group
|
||||
ref={groupRef}
|
||||
id={obj.id}
|
||||
name="canvas-object"
|
||||
x={obj.x}
|
||||
y={obj.y}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
clipFunc={clipFunc}
|
||||
dragBoundFunc={dragBoundFunc}
|
||||
onClick={handleClick}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<KonvaImage
|
||||
x={0}
|
||||
y={0}
|
||||
image={image}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
{isSelected && (
|
||||
<Rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={width}
|
||||
height={height}
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={3}
|
||||
cornerRadius={cornerRadius}
|
||||
listening={false}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KonvaImage
|
||||
ref={shapeRef}
|
||||
id={obj.id}
|
||||
name="canvas-object"
|
||||
x={obj.x}
|
||||
y={obj.y}
|
||||
image={image}
|
||||
width={width}
|
||||
height={height}
|
||||
stroke={isSelected ? "#3b82f6" : undefined}
|
||||
strokeWidth={isSelected ? 3 : 0}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
dragBoundFunc={dragBoundFunc}
|
||||
onClick={handleClick}
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageObject;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import Konva from "konva";
|
||||
import useImage from "use-image";
|
||||
import type { ShapeProps } from "./types";
|
||||
import { createPortal } from "react-dom";
|
||||
import { createRoundedRectClipFunc, getObjectBorderRadius } from "../../utils/borderRadius";
|
||||
|
||||
const VideoShape = ({
|
||||
obj,
|
||||
@@ -72,6 +73,8 @@ const VideoShape = ({
|
||||
|
||||
const containerWidth = obj.width || 400;
|
||||
const containerHeight = obj.height || 300;
|
||||
const cornerRadius = getObjectBorderRadius(obj.borderRadius);
|
||||
const clipFunc = createRoundedRectClipFunc(containerWidth, containerHeight, cornerRadius);
|
||||
|
||||
// همتراز با viewer که object-fit: contain دارد
|
||||
const imageLayout = useMemo(() => {
|
||||
@@ -151,10 +154,12 @@ const VideoShape = ({
|
||||
width={containerWidth}
|
||||
height={containerHeight}
|
||||
fill="transparent"
|
||||
cornerRadius={cornerRadius}
|
||||
stroke={isSelected ? "#3b82f6" : "#666666"}
|
||||
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||
onClick={handleGroupClick}
|
||||
/>
|
||||
<Group clipFunc={clipFunc}>
|
||||
{image ? (
|
||||
<>
|
||||
<Rect
|
||||
@@ -184,6 +189,7 @@ const VideoShape = ({
|
||||
onClick={handleGroupClick}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<Group
|
||||
name="playButton"
|
||||
onClick={handlePlayClick}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type Konva from "konva";
|
||||
|
||||
export const getObjectBorderRadius = (borderRadius?: number): number =>
|
||||
Math.max(0, borderRadius ?? 0);
|
||||
|
||||
export const clampBorderRadius = (
|
||||
radius: number,
|
||||
width: number,
|
||||
height: number,
|
||||
): number => Math.max(0, Math.min(radius, width / 2, height / 2));
|
||||
|
||||
export const createRoundedRectClipFunc = (
|
||||
width: number,
|
||||
height: number,
|
||||
radius: number,
|
||||
): Konva.ContainerConfig["clipFunc"] => {
|
||||
const r = clampBorderRadius(radius, width, height);
|
||||
if (r <= 0) return undefined;
|
||||
|
||||
return (ctx) => {
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, 0, width, height, r);
|
||||
ctx.closePath();
|
||||
};
|
||||
};
|
||||
|
||||
export const getCssBorderRadius = (
|
||||
borderRadius: number | undefined,
|
||||
scale: number,
|
||||
): string | undefined => {
|
||||
const r = getObjectBorderRadius(borderRadius) * scale;
|
||||
return r > 0 ? `${r}px` : undefined;
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
usesWrappedLayout,
|
||||
} from '@/pages/editor/utils/textStyle';
|
||||
import { getCssBlurStyle } from '@/pages/editor/utils/shapeBlur';
|
||||
import { getCssBorderRadius } from '@/pages/editor/utils/borderRadius';
|
||||
import '@/pages/viewer/styles/entranceAnimations.css';
|
||||
import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle';
|
||||
import { getMaskImageStyle, getMaskedLayout } from '@/pages/viewer/utils/maskStyle';
|
||||
@@ -27,6 +28,7 @@ const getRasterObjectStyle = (
|
||||
const scaleY = obj.scaleY ?? 1;
|
||||
const width = obj.width != null ? obj.width * scaleX * scale : undefined;
|
||||
const height = obj.height != null ? obj.height * scaleY * scale : undefined;
|
||||
const borderRadius = getCssBorderRadius(obj.borderRadius, scale);
|
||||
|
||||
return {
|
||||
...baseStyle,
|
||||
@@ -36,6 +38,7 @@ const getRasterObjectStyle = (
|
||||
height: height != null ? `${height}px` : 'auto',
|
||||
objectFit: 'fill',
|
||||
zIndex: index,
|
||||
...(borderRadius ? { borderRadius, overflow: 'hidden' as const } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -277,7 +280,17 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
||||
/>
|
||||
);
|
||||
|
||||
case 'video':
|
||||
case 'video': {
|
||||
const videoBorderRadius = getCssBorderRadius(obj.borderRadius, scale);
|
||||
const videoStyle: React.CSSProperties = {
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
||||
objectFit: 'contain',
|
||||
zIndex: index,
|
||||
...(videoBorderRadius ? { borderRadius: videoBorderRadius, overflow: 'hidden' } : {}),
|
||||
};
|
||||
|
||||
if (options.staticMedia) {
|
||||
return (
|
||||
<video
|
||||
@@ -288,11 +301,7 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
||||
preload="metadata"
|
||||
style={applyStyle(
|
||||
{
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
||||
objectFit: 'contain',
|
||||
zIndex: index,
|
||||
...videoStyle,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
obj,
|
||||
@@ -314,11 +323,7 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
||||
controls
|
||||
style={applyStyle(
|
||||
{
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
||||
objectFit: 'contain',
|
||||
zIndex: index,
|
||||
...videoStyle,
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
obj,
|
||||
@@ -335,20 +340,26 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'audio': {
|
||||
const audioBorderRadius = getCssBorderRadius(obj.borderRadius ?? 8, scale);
|
||||
const audioBaseStyle: React.CSSProperties = {
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * scale}px` : `${320 * scale}px`,
|
||||
height: obj.height ? `${obj.height * scale}px` : `${56 * scale}px`,
|
||||
zIndex: index,
|
||||
...(audioBorderRadius ? { borderRadius: audioBorderRadius, overflow: 'hidden' } : {}),
|
||||
};
|
||||
|
||||
case 'audio':
|
||||
if (options.staticMedia) {
|
||||
return (
|
||||
<div
|
||||
key={obj.id || index}
|
||||
style={applyStyle(
|
||||
{
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * scale}px` : `${320 * scale}px`,
|
||||
height: obj.height ? `${obj.height * scale}px` : `${56 * scale}px`,
|
||||
...audioBaseStyle,
|
||||
backgroundColor: '#f3f4f6',
|
||||
borderRadius: `${4 * scale}px`,
|
||||
zIndex: index,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
obj,
|
||||
@@ -365,10 +376,7 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
||||
preload="metadata"
|
||||
style={applyStyle(
|
||||
{
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * scale}px` : `${320 * scale}px`,
|
||||
height: obj.height ? `${obj.height * scale}px` : `${56 * scale}px`,
|
||||
zIndex: index,
|
||||
...audioBaseStyle,
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
obj,
|
||||
@@ -385,6 +393,7 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'link': {
|
||||
const isInternalLink = obj.linkUrl?.startsWith('page://');
|
||||
|
||||
@@ -127,7 +127,7 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
||||
if (obj.type === "rectangle" && obj.shapeType) {
|
||||
baseObject.shapeType = obj.shapeType as EditorObject["shapeType"];
|
||||
}
|
||||
if (obj.type === "rectangle" && obj.borderRadius !== undefined) {
|
||||
if (obj.borderRadius !== undefined) {
|
||||
baseObject.borderRadius = obj.borderRadius;
|
||||
}
|
||||
if (obj.type === "rectangle" && obj.blur !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user