Compare commits
9 Commits
3c764a1652
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 677c62e6eb | |||
| e0eb29488a | |||
| ae3787241a | |||
| 9e105606a3 | |||
| c6bb275d05 | |||
| 4273e899ca | |||
| 0361e05acb | |||
| 33cce93064 | |||
| a2a591420c |
@@ -0,0 +1,35 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
.cache/
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Environment / secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Editor / IDE
|
||||||
|
.vscode/
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
Generated
+11
@@ -23,6 +23,7 @@
|
|||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"page-flip": "^2.0.7",
|
"page-flip": "^2.0.7",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
"react-colorful": "^5.7.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-i18next": "^16.3.0",
|
"react-i18next": "^16.3.0",
|
||||||
@@ -4556,6 +4557,16 @@
|
|||||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-colorful": {
|
||||||
|
"version": "5.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.7.0.tgz",
|
||||||
|
"integrity": "sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-date-object": {
|
"node_modules/react-date-object": {
|
||||||
"version": "2.1.9",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"page-flip": "^2.0.7",
|
"page-flip": "^2.0.7",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
"react-colorful": "^5.7.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-i18next": "^16.3.0",
|
"react-i18next": "^16.3.0",
|
||||||
|
|||||||
+182
-103
@@ -1,127 +1,206 @@
|
|||||||
import { type FC, type InputHTMLAttributes, useState, useRef, useEffect } from 'react'
|
import { type FC, type InputHTMLAttributes, useEffect, useRef, useState } from "react";
|
||||||
import { clx } from '../helpers/utils'
|
import { HexAlphaColorPicker, HexColorInput, HexColorPicker } from "react-colorful";
|
||||||
import Error from './Error'
|
import { clx } from "../helpers/utils";
|
||||||
|
import Error from "./Error";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label?: string
|
label?: string;
|
||||||
className?: string
|
className?: string;
|
||||||
error_text?: string
|
error_text?: string;
|
||||||
isNotRequired?: boolean
|
isNotRequired?: boolean;
|
||||||
value?: string
|
value?: string;
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void;
|
||||||
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'>
|
opacity?: number;
|
||||||
|
onOpacityChange?: (value: number) => void;
|
||||||
|
opacityLabel?: string;
|
||||||
|
} & Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value">;
|
||||||
|
|
||||||
const ColorPicker: FC<Props> = (props: Props) => {
|
const ColorPicker: FC<Props> = (props: Props) => {
|
||||||
const [color, setColor] = useState<string>(props.value || '#000000')
|
const [color, setColor] = useState<string>(props.value || "#000000");
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null)
|
const [opacityDisplay, setOpacityDisplay] = useState<string>(`${props.opacity ?? 100}`);
|
||||||
const pickerOpenGuardRef = useRef(false)
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const showOpacity = props.onOpacityChange !== undefined;
|
||||||
|
|
||||||
const inputClass = clx(
|
const clampOpacity = (rawValue: number): number => Math.min(100, Math.max(0, rawValue));
|
||||||
'w-full bg-white h-[34px] text-black block pl-10 pr-10 text-xs rounded-xl border border-border',
|
|
||||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
|
||||||
props.className
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const normalizeHex = (value: string): string => {
|
||||||
const newColor = event.target.value
|
if (!value) return "#000000";
|
||||||
setColor(newColor)
|
const withHash = value.startsWith("#") ? value : `#${value}`;
|
||||||
props.onChange?.(newColor)
|
const shortMatch = /^#([0-9A-Fa-f]{3})$/.exec(withHash);
|
||||||
|
if (shortMatch) {
|
||||||
|
const [r, g, b] = shortMatch[1]!.split("");
|
||||||
|
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
||||||
}
|
}
|
||||||
|
const longMatch = /^#([0-9A-Fa-f]{6})$/.exec(withHash);
|
||||||
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
if (longMatch) {
|
||||||
const value = event.target.value
|
return `#${longMatch[1]}`.toLowerCase();
|
||||||
if (/^#[0-9A-Fa-f]{0,6}$/.test(value) || value === '') {
|
|
||||||
const finalColor = value || '#000000'
|
|
||||||
setColor(finalColor)
|
|
||||||
props.onChange?.(finalColor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return "#000000";
|
||||||
|
};
|
||||||
|
|
||||||
const openNativeColorPicker = () => {
|
const toHexAlpha = (hex: string, opacity: number): string => {
|
||||||
if (props.readOnly) return
|
const normalizedHex = normalizeHex(hex);
|
||||||
if (pickerOpenGuardRef.current) return
|
const alpha = Math.round((clampOpacity(opacity) / 100) * 255)
|
||||||
pickerOpenGuardRef.current = true
|
.toString(16)
|
||||||
colorInputRef.current?.click()
|
.padStart(2, "0");
|
||||||
window.setTimeout(() => {
|
return `${normalizedHex}${alpha}`;
|
||||||
pickerOpenGuardRef.current = false
|
};
|
||||||
}, 400)
|
|
||||||
|
const fromHexAlpha = (hexAlpha: string): { hex: string; opacity: number } => {
|
||||||
|
const match = /^#([0-9A-Fa-f]{8})$/.exec(hexAlpha);
|
||||||
|
if (!match) {
|
||||||
|
return {
|
||||||
|
hex: normalizeHex(hexAlpha),
|
||||||
|
opacity: 100,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
const rgbaHex = match[1]!.toLowerCase();
|
||||||
|
const rgb = `#${rgbaHex.slice(0, 6)}`;
|
||||||
|
const alphaHex = rgbaHex.slice(6, 8);
|
||||||
|
const opacity = Math.round((parseInt(alphaHex, 16) / 255) * 100);
|
||||||
|
return { hex: rgb, opacity: clampOpacity(opacity) };
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const inputClass = clx("w-full bg-white h-[34px] text-black block pl-10 pr-10 text-xs rounded-xl border border-border", props.readOnly && "bg-gray-100 border-0 text-description");
|
||||||
if (props.value !== undefined) {
|
|
||||||
setColor(props.value)
|
|
||||||
}
|
|
||||||
}, [props.value])
|
|
||||||
|
|
||||||
return (
|
const handleColorChange = (newColor: string) => {
|
||||||
<div className='w-full'>
|
const normalized = normalizeHex(newColor);
|
||||||
<label className='text-xs'>
|
setColor(normalized);
|
||||||
{props.label}
|
props.onChange?.(normalized);
|
||||||
</label>
|
};
|
||||||
|
|
||||||
<div
|
const togglePicker = () => {
|
||||||
className={clx(
|
if (props.readOnly) return;
|
||||||
'w-full relative mt-1 rounded-xl',
|
setIsOpen((prev) => !prev);
|
||||||
!props.readOnly && 'cursor-pointer'
|
};
|
||||||
)}
|
|
||||||
onClick={!props.readOnly ? openNativeColorPicker : undefined}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
value={color}
|
|
||||||
onChange={handleTextChange}
|
|
||||||
placeholder='#000000'
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
title={
|
|
||||||
!props.readOnly
|
|
||||||
? 'کلیک: پالت رنگ — با Tab وارد فیلد شوید تا کد را تایپ کنید'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={`${inputClass} cursor-pointer`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span
|
useEffect(() => {
|
||||||
aria-hidden
|
if (props.value !== undefined) {
|
||||||
className={clx(
|
setColor(normalizeHex(props.value));
|
||||||
'absolute top-0 bottom-0 my-auto left-1 flex items-center justify-center min-h-[34px] min-w-[34px] rounded-lg pointer-events-none',
|
}
|
||||||
props.readOnly && 'opacity-50'
|
}, [props.value]);
|
||||||
)}
|
|
||||||
>
|
useEffect(() => {
|
||||||
{/* <img
|
if (props.opacity !== undefined) {
|
||||||
|
setOpacityDisplay(`${props.opacity}`);
|
||||||
|
}
|
||||||
|
}, [props.opacity]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const handlePointerDownOutside = (event: MouseEvent) => {
|
||||||
|
if (!wrapperRef.current?.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousedown", handlePointerDownOutside);
|
||||||
|
return () => window.removeEventListener("mousedown", handlePointerDownOutside);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleOpacityChange = (rawValue: string) => {
|
||||||
|
setOpacityDisplay(rawValue);
|
||||||
|
const numValue = parseInt(rawValue, 10);
|
||||||
|
if (!Number.isNaN(numValue)) {
|
||||||
|
props.onOpacityChange?.(clampOpacity(numValue));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpacityBlur = (rawValue: string) => {
|
||||||
|
const numValue = parseInt(rawValue, 10) || 0;
|
||||||
|
const clampedValue = clampOpacity(numValue);
|
||||||
|
setOpacityDisplay(`${clampedValue}`);
|
||||||
|
props.onOpacityChange?.(clampedValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAlphaPickerChange = (hexAlpha: string) => {
|
||||||
|
const { hex, opacity } = fromHexAlpha(hexAlpha);
|
||||||
|
setColor(hex);
|
||||||
|
setOpacityDisplay(`${opacity}`);
|
||||||
|
props.onChange?.(hex);
|
||||||
|
props.onOpacityChange?.(opacity);
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorField = (
|
||||||
|
<div className={clx("w-full", showOpacity && "flex-1")}>
|
||||||
|
<label className="text-xs">{props.label}</label>
|
||||||
|
|
||||||
|
<div className={clx("w-full relative mt-1 rounded-xl", !props.readOnly && "cursor-pointer")}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={color}
|
||||||
|
onChange={(event) => handleColorChange(event.target.value)}
|
||||||
|
placeholder="#000000"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
title={!props.readOnly ? "کلیک برای باز کردن انتخابگر رنگ" : undefined}
|
||||||
|
onClick={togglePicker}
|
||||||
|
className={`${inputClass} ${props.readOnly ? "" : "cursor-pointer"}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className={clx("absolute top-0 bottom-0 my-auto left-1 flex items-center justify-center min-h-[34px] min-w-[34px] rounded-lg pointer-events-none", props.readOnly && "opacity-50")}
|
||||||
|
>
|
||||||
|
{/* <img
|
||||||
src={ColorfilterIcon}
|
src={ColorfilterIcon}
|
||||||
alt=''
|
alt=''
|
||||||
className='size-7 select-none'
|
className='size-7 select-none'
|
||||||
/> */}
|
/> */}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={clx(
|
className={clx("absolute top-0 bottom-0 my-auto right-1 flex items-center justify-center min-h-[34px] min-w-[34px] rounded-lg pointer-events-none", props.readOnly && "opacity-50")}
|
||||||
'absolute top-0 bottom-0 my-auto right-1 flex items-center justify-center min-h-[34px] min-w-[34px] rounded-lg pointer-events-none',
|
>
|
||||||
props.readOnly && 'opacity-50'
|
<span className="block w-5 h-5 rounded-full border border-border shadow-sm" style={{ backgroundColor: color }} />
|
||||||
)}
|
</span>
|
||||||
>
|
|
||||||
<span
|
|
||||||
className='block w-5 h-5 rounded-full border border-border shadow-sm'
|
|
||||||
style={{ backgroundColor: color }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<input
|
{isOpen && !props.readOnly && (
|
||||||
ref={colorInputRef}
|
<div className="absolute right-0 z-50 mt-2 w-full rounded-xl border border-border bg-white p-3 shadow-lg space-y-3">
|
||||||
type='color'
|
<div className="space-y-1">
|
||||||
value={color}
|
{showOpacity ? (
|
||||||
onChange={handleColorChange}
|
<HexAlphaColorPicker color={toHexAlpha(color, Number(opacityDisplay) || 100)} onChange={handleAlphaPickerChange} />
|
||||||
className='absolute opacity-0 pointer-events-none w-0 h-0'
|
) : (
|
||||||
/>
|
<HexColorPicker color={color} onChange={handleColorChange} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
{props.error_text && (
|
<HexColorInput color={color} onChange={handleColorChange} prefixed className="h-9 max-w-[115px] flex-1 rounded-lg border border-border px-2 text-xs" />
|
||||||
<Error errorText={props.error_text} />
|
{showOpacity && (
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={opacityDisplay}
|
||||||
|
onChange={(e) => handleOpacityChange(e.target.value)}
|
||||||
|
onBlur={(e) => handleOpacityBlur(e.target.value)}
|
||||||
|
className="h-9 w-full rounded-lg border border-border px-2 text-xs"
|
||||||
|
title={props.opacityLabel ?? "شفافیت"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showOpacity && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{props.opacityLabel ?? "شفافیت"}: {opacityDisplay}%
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default ColorPicker
|
return (
|
||||||
|
<div ref={wrapperRef} className={clx("w-full", props.className)}>
|
||||||
|
{colorField}
|
||||||
|
|
||||||
|
{props.error_text && <Error errorText={props.error_text} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorPicker;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import { Group, Rect, Text } from "react-konva";
|
import { Group, Rect, Text } from "react-konva";
|
||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import type { EditorObject } from "@/pages/editor/store/editorStore";
|
import type { EditorObject } from "@/pages/editor/store/editorStore";
|
||||||
|
import { getColorWithOpacity } from "@/pages/editor/utils/colorOpacity";
|
||||||
|
|
||||||
type EditorTableProps = {
|
type EditorTableProps = {
|
||||||
obj: EditorObject;
|
obj: EditorObject;
|
||||||
@@ -80,7 +81,7 @@ const EditorTable = ({ obj, selectedCellId, onCellClick, onCellDblClick, onDragE
|
|||||||
y={y}
|
y={y}
|
||||||
width={cellWidth}
|
width={cellWidth}
|
||||||
height={cellHeight}
|
height={cellHeight}
|
||||||
fill={cell.background}
|
fill={getColorWithOpacity(cell.background, cell.backgroundOpacity ?? 100)}
|
||||||
stroke={isCellSelected ? "#3b82f6" : stroke || "#808080"}
|
stroke={isCellSelected ? "#3b82f6" : stroke || "#808080"}
|
||||||
strokeWidth={isCellSelected ? 3 : strokeWidth || 1}
|
strokeWidth={isCellSelected ? 3 : strokeWidth || 1}
|
||||||
cornerRadius={isTopLeft ? [8, 0, 0, 0] : isTopRight ? [0, 8, 0, 0] : 0}
|
cornerRadius={isTopLeft ? [8, 0, 0, 0] : isTopRight ? [0, 8, 0, 0] : 0}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useCustomShapeDrawing } from "./canvas/useCustomShapeDrawing";
|
|||||||
import { usePencilDrawing } from "./canvas/usePencilDrawing";
|
import { usePencilDrawing } from "./canvas/usePencilDrawing";
|
||||||
import ObjectsLayer from "./canvas/ObjectsLayer";
|
import ObjectsLayer from "./canvas/ObjectsLayer";
|
||||||
import GuidesLayer from "./canvas/GuidesLayer";
|
import GuidesLayer from "./canvas/GuidesLayer";
|
||||||
|
import BlurBackdropLayer from "./canvas/BlurBackdropLayer";
|
||||||
import CustomShapeDrawingLayer from "./canvas/CustomShapeDrawingLayer";
|
import CustomShapeDrawingLayer from "./canvas/CustomShapeDrawingLayer";
|
||||||
import PencilDrawingLayer from "./canvas/PencilDrawingLayer";
|
import PencilDrawingLayer from "./canvas/PencilDrawingLayer";
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +28,7 @@ import ZoomControls from "./ZoomControls";
|
|||||||
import EditorCanvasToolbar from "./EditorCanvasToolbar";
|
import EditorCanvasToolbar from "./EditorCanvasToolbar";
|
||||||
import { createScopedId } from "../store/editorStore.helpers";
|
import { createScopedId } from "../store/editorStore.helpers";
|
||||||
import { getKonvaGradientProps } from "../utils/gradient";
|
import { getKonvaGradientProps } from "../utils/gradient";
|
||||||
|
import { getColorWithOpacity } from "../utils/colorOpacity";
|
||||||
import { STICKER_DRAG_MIME } from "../types/IconTypes";
|
import { STICKER_DRAG_MIME } from "../types/IconTypes";
|
||||||
import { GALLERY_DRAG_MIME } from "../types/GalleryTypes";
|
import { GALLERY_DRAG_MIME } from "../types/GalleryTypes";
|
||||||
import {
|
import {
|
||||||
@@ -72,7 +74,12 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
|||||||
const currentPage = pages.find((page) => page.id === currentPageId);
|
const currentPage = pages.find((page) => page.id === currentPageId);
|
||||||
|
|
||||||
const bgColor =
|
const bgColor =
|
||||||
currentPage?.backgroundType === 'color' || currentPage?.backgroundType === 'gradient'
|
currentPage?.backgroundType === 'color'
|
||||||
|
? getColorWithOpacity(
|
||||||
|
currentPage.backgroundColor,
|
||||||
|
currentPage.backgroundOpacity ?? 100,
|
||||||
|
)
|
||||||
|
: currentPage?.backgroundType === 'gradient'
|
||||||
? currentPage.backgroundColor
|
? currentPage.backgroundColor
|
||||||
: '#ffffff';
|
: '#ffffff';
|
||||||
const bgGradient = currentPage?.backgroundGradient;
|
const bgGradient = currentPage?.backgroundGradient;
|
||||||
@@ -621,6 +628,13 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
|||||||
<CustomShapeDrawingLayer drawingState={drawingState} />
|
<CustomShapeDrawingLayer drawingState={drawingState} />
|
||||||
<PencilDrawingLayer drawingState={pencilDrawingState} />
|
<PencilDrawingLayer drawingState={pencilDrawingState} />
|
||||||
</Stage>
|
</Stage>
|
||||||
|
<BlurBackdropLayer
|
||||||
|
objects={objects}
|
||||||
|
stageWidth={stageSize.width}
|
||||||
|
stageHeight={stageSize.height}
|
||||||
|
scale={finalScale}
|
||||||
|
stageRef={stageRef}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
import { useEditorStore } from '../store/editorStore'
|
import { useEditorStore } from '../store/editorStore'
|
||||||
import type { DisplayStyle, BackgroundType } from '../store/editorStore'
|
import type { DisplayStyle, BackgroundType } from '../store/editorStore'
|
||||||
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
||||||
import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData'
|
import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData'
|
||||||
import ColorsImage from '@/assets/images/colors.png'
|
|
||||||
import ColorPicker from '@/components/ColorPicker'
|
import ColorPicker from '@/components/ColorPicker'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
import { toCssLinearGradient } from '../utils/gradient'
|
import { toCssLinearGradient } from '../utils/gradient'
|
||||||
@@ -39,11 +38,11 @@ const SettingsPanel = () => {
|
|||||||
updateDocumentSettings,
|
updateDocumentSettings,
|
||||||
updateCurrentPageBackground,
|
updateCurrentPageBackground,
|
||||||
} = useEditorStore()
|
} = useEditorStore()
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const { mutateAsync: uploadFile, isPending: mediaLoading } = useSingleUpload()
|
const { mutateAsync: uploadFile, isPending: mediaLoading } = useSingleUpload()
|
||||||
const currentPage = pages.find((page) => page.id === currentPageId)
|
const currentPage = pages.find((page) => page.id === currentPageId)
|
||||||
const backgroundType = currentPage?.backgroundType ?? 'color'
|
const backgroundType = currentPage?.backgroundType ?? 'color'
|
||||||
const backgroundColor = currentPage?.backgroundColor ?? '#ffffff'
|
const backgroundColor = currentPage?.backgroundColor ?? '#ffffff'
|
||||||
|
const backgroundOpacity = currentPage?.backgroundOpacity ?? 100
|
||||||
const backgroundGradient = currentPage?.backgroundGradient ?? {
|
const backgroundGradient = currentPage?.backgroundGradient ?? {
|
||||||
from: '#ffffff',
|
from: '#ffffff',
|
||||||
to: '#e2e8f0',
|
to: '#e2e8f0',
|
||||||
@@ -234,30 +233,29 @@ const SettingsPanel = () => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Color Wheel */}
|
<div className="w-full mt-2">
|
||||||
<button
|
<ColorPicker
|
||||||
onClick={() => colorInputRef.current?.click()}
|
label="رنگ دلخواه"
|
||||||
className="w-9 h-9 rounded-xl border-2 border-transparent hover:scale-105 transition-all overflow-hidden relative"
|
|
||||||
title="رنگ دلخواه"
|
|
||||||
>
|
|
||||||
<img src={ColorsImage} alt="انتخاب رنگ" className="w-full h-full object-cover" />
|
|
||||||
<input
|
|
||||||
ref={colorInputRef}
|
|
||||||
type="color"
|
|
||||||
value={backgroundColor}
|
value={backgroundColor}
|
||||||
onChange={(e) => updateCurrentPageBackground({ backgroundColor: e.target.value })}
|
opacity={backgroundOpacity}
|
||||||
className="absolute inset-0 opacity-0 w-full h-full cursor-pointer"
|
onChange={(value) =>
|
||||||
|
updateCurrentPageBackground({ backgroundColor: value })
|
||||||
|
}
|
||||||
|
onOpacityChange={(value) =>
|
||||||
|
updateCurrentPageBackground({ backgroundOpacity: value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* نمایش رنگ انتخابشده */}
|
{/* نمایش رنگ انتخابشده */}
|
||||||
<div className="mt-3 flex items-center gap-2">
|
<div className="mt-3 flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="w-6 h-6 rounded-lg border border-border"
|
className="w-6 h-6 rounded-lg border border-border"
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor, opacity: backgroundOpacity / 100 }}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500 font-mono">{backgroundColor}</span>
|
<span className="text-xs text-gray-500 font-mono">{backgroundColor}</span>
|
||||||
|
<span className="text-xs text-gray-400">{backgroundOpacity}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import type Konva from "konva";
|
||||||
|
import type { EditorObject } from "../../store/editorStore";
|
||||||
|
import {
|
||||||
|
buildBlurOverlayStyle,
|
||||||
|
buildBlurStackEntries,
|
||||||
|
isBlurBackdropObject,
|
||||||
|
} from "../../utils/blurOverlayLayout";
|
||||||
|
import OcclusionObjectsStage from "./OcclusionObjectsStage";
|
||||||
|
|
||||||
|
type BlurBackdropLayerProps = {
|
||||||
|
objects: EditorObject[];
|
||||||
|
stageWidth: number;
|
||||||
|
stageHeight: number;
|
||||||
|
/** Stage scale applied to Konva (editor internal scale + zoom). */
|
||||||
|
scale: number;
|
||||||
|
stageRef: React.RefObject<Konva.Stage | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BlurBackdropLayer = ({ objects, stageWidth, stageHeight, scale, stageRef }: BlurBackdropLayerProps) => {
|
||||||
|
const [dragRevision, setDragRevision] = useState(0);
|
||||||
|
const [draggedBlurObjectId, setDraggedBlurObjectId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stage = stageRef.current;
|
||||||
|
if (!stage) return;
|
||||||
|
|
||||||
|
let raf = 0;
|
||||||
|
const bump = () => setDragRevision((v) => v + 1);
|
||||||
|
|
||||||
|
const scheduleBump = () => {
|
||||||
|
if (raf) return;
|
||||||
|
raf = window.requestAnimationFrame(() => {
|
||||||
|
raf = 0;
|
||||||
|
bump();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveBlurObjectId = (nodeId: string): string | null => {
|
||||||
|
if (!nodeId) return null;
|
||||||
|
const direct = objects.find((obj) => obj.id === nodeId && isBlurBackdropObject(obj));
|
||||||
|
if (direct) return direct.id;
|
||||||
|
if (nodeId.startsWith("masked-")) {
|
||||||
|
const maskedId = nodeId.slice("masked-".length);
|
||||||
|
const masked = objects.find((obj) => obj.id === maskedId && isBlurBackdropObject(obj));
|
||||||
|
if (masked) return masked.id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragStart = (event: Konva.KonvaEventObject<DragEvent>) => {
|
||||||
|
const target = event.target;
|
||||||
|
const id = typeof target.id === "function" ? target.id() : "";
|
||||||
|
const blurObjectId = resolveBlurObjectId(id);
|
||||||
|
if (blurObjectId) {
|
||||||
|
setDraggedBlurObjectId(blurObjectId);
|
||||||
|
}
|
||||||
|
scheduleBump();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragMove = () => scheduleBump();
|
||||||
|
|
||||||
|
const onDragEnd = () => {
|
||||||
|
setDraggedBlurObjectId(null);
|
||||||
|
scheduleBump();
|
||||||
|
};
|
||||||
|
|
||||||
|
stage.on("dragstart", onDragStart);
|
||||||
|
stage.on("dragmove", onDragMove);
|
||||||
|
stage.on("dragend", onDragEnd);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stage.off("dragstart", onDragStart);
|
||||||
|
stage.off("dragmove", onDragMove);
|
||||||
|
stage.off("dragend", onDragEnd);
|
||||||
|
if (raf) window.cancelAnimationFrame(raf);
|
||||||
|
};
|
||||||
|
}, [objects, stageRef]);
|
||||||
|
|
||||||
|
const stackEntries = useMemo(() => buildBlurStackEntries(objects), [objects]);
|
||||||
|
|
||||||
|
const livePositions = useMemo(() => {
|
||||||
|
if (!draggedBlurObjectId) return new Map<string, { x: number; y: number }>();
|
||||||
|
|
||||||
|
const stage = stageRef.current;
|
||||||
|
if (!stage) return new Map<string, { x: number; y: number }>();
|
||||||
|
|
||||||
|
const node =
|
||||||
|
stage.findOne(`#${draggedBlurObjectId}`) ??
|
||||||
|
stage.findOne(`#masked-${draggedBlurObjectId}`);
|
||||||
|
if (node && "getAbsolutePosition" in node) {
|
||||||
|
return new Map([[draggedBlurObjectId, node.getAbsolutePosition()]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Map<string, { x: number; y: number }>();
|
||||||
|
}, [draggedBlurObjectId, dragRevision, stageRef]);
|
||||||
|
|
||||||
|
if (stackEntries.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
width: stageWidth * scale,
|
||||||
|
height: stageHeight * scale,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stackEntries.map((entry) => {
|
||||||
|
const livePosition = livePositions.get(entry.blurObject.id) ?? null;
|
||||||
|
const style = buildBlurOverlayStyle(entry.blurObject, scale, livePosition);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={entry.key} style={{ position: "absolute", inset: 0, pointerEvents: "none" }}>
|
||||||
|
<div style={style} />
|
||||||
|
<OcclusionObjectsStage
|
||||||
|
objects={entry.occlusionObjects}
|
||||||
|
allObjects={objects}
|
||||||
|
stageWidth={stageWidth}
|
||||||
|
stageHeight={stageHeight}
|
||||||
|
scale={scale}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlurBackdropLayer;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Stage, Layer, Group } from "react-konva";
|
||||||
|
import ObjectRenderer from "./ObjectRenderer";
|
||||||
|
import type { EditorObject } from "../../store/editorStore";
|
||||||
|
|
||||||
|
type OcclusionObjectsStageProps = {
|
||||||
|
objects: EditorObject[];
|
||||||
|
allObjects: EditorObject[];
|
||||||
|
stageWidth: number;
|
||||||
|
stageHeight: number;
|
||||||
|
scale: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Re-renders objects above a blur overlay so they stay crisp (display-only). */
|
||||||
|
const OcclusionObjectsStage = ({
|
||||||
|
objects,
|
||||||
|
allObjects,
|
||||||
|
stageWidth,
|
||||||
|
stageHeight,
|
||||||
|
scale,
|
||||||
|
}: OcclusionObjectsStageProps) => {
|
||||||
|
if (objects.length === 0) return null;
|
||||||
|
|
||||||
|
const renderObject = (obj: EditorObject) => (
|
||||||
|
<ObjectRenderer
|
||||||
|
key={`occlusion-${obj.id}`}
|
||||||
|
obj={obj}
|
||||||
|
isSelected={false}
|
||||||
|
transformerRef={{ current: null }}
|
||||||
|
layerRef={{ current: null }}
|
||||||
|
onSelect={() => {}}
|
||||||
|
onUpdate={() => {}}
|
||||||
|
draggable={false}
|
||||||
|
allObjects={allObjects}
|
||||||
|
stageWidth={stageWidth}
|
||||||
|
stageHeight={stageHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stage
|
||||||
|
width={stageWidth * scale}
|
||||||
|
height={stageHeight * scale}
|
||||||
|
scaleX={scale}
|
||||||
|
scaleY={scale}
|
||||||
|
listening={false}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer listening={false}>
|
||||||
|
{objects
|
||||||
|
.filter((obj) => obj.type === "group")
|
||||||
|
.map(renderObject)}
|
||||||
|
{objects
|
||||||
|
.filter((obj) => !obj.groupId && obj.type !== "group")
|
||||||
|
.map(renderObject)}
|
||||||
|
{objects
|
||||||
|
.filter((obj) => obj.groupId && obj.type !== "group")
|
||||||
|
.map((obj) => {
|
||||||
|
const groupObject = objects.find((o) => o.id === obj.groupId);
|
||||||
|
if (!groupObject) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group key={`occlusion-group-member-${obj.id}`} listening={false}>
|
||||||
|
{renderObject(obj)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OcclusionObjectsStage;
|
||||||
@@ -8,6 +8,10 @@ interface TransformerControlsProps {
|
|||||||
isGroupedWithMask: boolean;
|
isGroupedWithMask: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SELECTION_BLUE = "#93c5fd";
|
||||||
|
const ANCHOR_SIZE = 5;
|
||||||
|
const ROTATE_ANCHOR_OFFSET = 22;
|
||||||
|
|
||||||
const TransformerControls = ({ transformerRef, selectedObject, isGroupedWithMask }: TransformerControlsProps) => {
|
const TransformerControls = ({ transformerRef, selectedObject, isGroupedWithMask }: TransformerControlsProps) => {
|
||||||
if (!selectedObject) return null;
|
if (!selectedObject) return null;
|
||||||
|
|
||||||
@@ -43,12 +47,18 @@ const TransformerControls = ({ transformerRef, selectedObject, isGroupedWithMask
|
|||||||
}}
|
}}
|
||||||
ignoreStroke={isText}
|
ignoreStroke={isText}
|
||||||
perfectDrawEnabled={false}
|
perfectDrawEnabled={false}
|
||||||
anchorFill="#3b82f6"
|
anchorFill={SELECTION_BLUE}
|
||||||
anchorStroke="#ffffff"
|
anchorStroke="#ffffff"
|
||||||
borderStroke="#3b82f6"
|
borderStroke={SELECTION_BLUE}
|
||||||
borderStrokeWidth={1}
|
borderStrokeWidth={1}
|
||||||
padding={-2}
|
padding={-2}
|
||||||
anchorSize={8}
|
anchorSize={ANCHOR_SIZE}
|
||||||
|
rotateAnchorOffset={ROTATE_ANCHOR_OFFSET}
|
||||||
|
anchorStyleFunc={(anchor) => {
|
||||||
|
if (anchor.hasName("rotater")) {
|
||||||
|
anchor.cornerRadius(ANCHOR_SIZE / 2);
|
||||||
|
}
|
||||||
|
}}
|
||||||
rotateEnabled={!isGroup && !isGroupedWithMask}
|
rotateEnabled={!isGroup && !isGroupedWithMask}
|
||||||
enabledAnchors={enabledAnchors}
|
enabledAnchors={enabledAnchors}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const buildPencilObject = (
|
|||||||
points: DrawPoint[],
|
points: DrawPoint[],
|
||||||
stroke: string,
|
stroke: string,
|
||||||
strokeWidth: number,
|
strokeWidth: number,
|
||||||
|
opacity: number,
|
||||||
): EditorObject => {
|
): EditorObject => {
|
||||||
const xs = points.map((p) => p.x);
|
const xs = points.map((p) => p.x);
|
||||||
const ys = points.map((p) => p.y);
|
const ys = points.map((p) => p.y);
|
||||||
@@ -89,7 +90,7 @@ const buildPencilObject = (
|
|||||||
stroke,
|
stroke,
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
tension: DEFAULT_TENSION,
|
tension: DEFAULT_TENSION,
|
||||||
opacity: 100,
|
opacity,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -100,6 +101,7 @@ export const usePencilDrawing = (stageRef: React.RefObject<Konva.Stage | null>)
|
|||||||
const pointerIdRef = useRef<number | null>(null);
|
const pointerIdRef = useRef<number | null>(null);
|
||||||
const strokeRef = useRef("#000000");
|
const strokeRef = useRef("#000000");
|
||||||
const strokeWidthRef = useRef(3);
|
const strokeWidthRef = useRef(3);
|
||||||
|
const opacityRef = useRef(100);
|
||||||
|
|
||||||
const [drawingState, setDrawingState] = useState<PencilDrawingState>(INITIAL_STATE);
|
const [drawingState, setDrawingState] = useState<PencilDrawingState>(INITIAL_STATE);
|
||||||
|
|
||||||
@@ -141,7 +143,12 @@ export const usePencilDrawing = (stageRef: React.RefObject<Konva.Stage | null>)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = buildPencilObject(points, strokeRef.current, strokeWidthRef.current);
|
const obj = buildPencilObject(
|
||||||
|
points,
|
||||||
|
strokeRef.current,
|
||||||
|
strokeWidthRef.current,
|
||||||
|
opacityRef.current,
|
||||||
|
);
|
||||||
useEditorStore.getState().commitObjectHistoryBeforeChange();
|
useEditorStore.getState().commitObjectHistoryBeforeChange();
|
||||||
addObject(obj);
|
addObject(obj);
|
||||||
reset();
|
reset();
|
||||||
@@ -165,6 +172,7 @@ export const usePencilDrawing = (stageRef: React.RefObject<Konva.Stage | null>)
|
|||||||
(pos: DrawPoint, pointerId: number) => {
|
(pos: DrawPoint, pointerId: number) => {
|
||||||
strokeRef.current = defaults.stroke;
|
strokeRef.current = defaults.stroke;
|
||||||
strokeWidthRef.current = defaults.strokeWidth;
|
strokeWidthRef.current = defaults.strokeWidth;
|
||||||
|
opacityRef.current = defaults.opacity;
|
||||||
isDrawingRef.current = true;
|
isDrawingRef.current = true;
|
||||||
pointerIdRef.current = pointerId;
|
pointerIdRef.current = pointerId;
|
||||||
pointsRef.current = [pos];
|
pointsRef.current = [pos];
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const PencilInstruction: FC = () => {
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="رنگ قلم"
|
label="رنگ قلم"
|
||||||
value={defaults.stroke}
|
value={defaults.stroke}
|
||||||
|
opacity={defaults.opacity}
|
||||||
|
onOpacityChange={(value) => updateDefaults({ opacity: value })}
|
||||||
onChange={(value) => updateDefaults({ stroke: value })}
|
onChange={(value) => updateDefaults({ stroke: value })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -106,13 +106,6 @@ const TextInstruction: FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// State محلی برای نمایش شفافیت
|
|
||||||
const [opacityDisplay, setOpacityDisplay] = useState<string>(`${formState.opacity}`);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOpacityDisplay(`${formState.opacity}`);
|
|
||||||
}, [formState.opacity]);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -164,41 +157,14 @@ const TextInstruction: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex flex-col gap-2">
|
<div className="mt-4">
|
||||||
<div className="flex gap-3">
|
<ColorPicker
|
||||||
<ColorPicker
|
label="رنگ"
|
||||||
label="رنگ"
|
value={formState.fill}
|
||||||
className="flex-1"
|
opacity={formState.opacity}
|
||||||
value={formState.fill}
|
onOpacityChange={(value) => handleChange("opacity", value)}
|
||||||
onChange={(value) => handleChange("fill", value)}
|
onChange={(value) => handleChange("fill", value)}
|
||||||
/>
|
/>
|
||||||
<div className="w-28">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="شفافیت"
|
|
||||||
className="px-3"
|
|
||||||
value={opacityDisplay}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setOpacityDisplay(value);
|
|
||||||
|
|
||||||
// اعمال تغییرات به صورت لایو اگر مقدار معتبر است
|
|
||||||
const numValue = parseInt(value);
|
|
||||||
if (!isNaN(numValue) && numValue >= 0 && numValue <= 100) {
|
|
||||||
handleChange("opacity", numValue);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
const numValue = parseInt(e.target.value) || 0;
|
|
||||||
const clampedValue = Math.min(100, Math.max(0, numValue));
|
|
||||||
setOpacityDisplay(`${clampedValue}`);
|
|
||||||
handleChange("opacity", clampedValue);
|
|
||||||
}}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className="mt-4 flex gap-2">
|
{/* <div className="mt-4 flex gap-2">
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const CustomShapeSettings = ({ selectedObject, onUpdate }: CustomShapeSettingsPr
|
|||||||
};
|
};
|
||||||
const stroke = selectedObject.stroke ?? "#1e40af";
|
const stroke = selectedObject.stroke ?? "#1e40af";
|
||||||
const strokeWidth = selectedObject.strokeWidth ?? 2;
|
const strokeWidth = selectedObject.strokeWidth ?? 2;
|
||||||
|
const opacity = selectedObject.opacity ?? 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -29,6 +30,8 @@ const CustomShapeSettings = ({ selectedObject, onUpdate }: CustomShapeSettingsPr
|
|||||||
label="رنگ پر"
|
label="رنگ پر"
|
||||||
value={fill}
|
value={fill}
|
||||||
readOnly={fillType === "gradient"}
|
readOnly={fillType === "gradient"}
|
||||||
|
opacity={opacity}
|
||||||
|
onOpacityChange={(value) => onUpdate(selectedObject.id, { opacity: value })}
|
||||||
onChange={(value) => onUpdate(selectedObject.id, { fill: value })}
|
onChange={(value) => onUpdate(selectedObject.id, { fill: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const GridSettings = ({ selectedObject }: GridSettingsProps) => {
|
|||||||
removeRow,
|
removeRow,
|
||||||
removeColumn,
|
removeColumn,
|
||||||
changeCellBackground,
|
changeCellBackground,
|
||||||
|
changeCellBackgroundOpacity,
|
||||||
selectedCellId,
|
selectedCellId,
|
||||||
} = useEditorStore();
|
} = useEditorStore();
|
||||||
|
|
||||||
@@ -50,6 +51,12 @@ const GridSettings = ({ selectedObject }: GridSettingsProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCellBackgroundOpacityChange = (opacity: number) => {
|
||||||
|
if (selectedCellId && tableDataObj) {
|
||||||
|
changeCellBackgroundOpacity(selectedObject.id, selectedCellId, opacity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const selectedCell = selectedCellId && tableDataObj ? tableDataObj.cells[selectedCellId] : null;
|
const selectedCell = selectedCellId && tableDataObj ? tableDataObj.cells[selectedCellId] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -111,6 +118,8 @@ const GridSettings = ({ selectedObject }: GridSettingsProps) => {
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="رنگ پسزمینه سلول"
|
label="رنگ پسزمینه سلول"
|
||||||
value={selectedCell.background}
|
value={selectedCell.background}
|
||||||
|
opacity={selectedCell.backgroundOpacity ?? 100}
|
||||||
|
onOpacityChange={handleCellBackgroundOpacityChange}
|
||||||
onChange={handleCellBackgroundChange}
|
onChange={handleCellBackgroundChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const LineSettings = ({ selectedObject, onUpdate }: LineSettingsProps) => {
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="رنگ خط"
|
label="رنگ خط"
|
||||||
value={selectedObject.stroke || "#000000"}
|
value={selectedObject.stroke || "#000000"}
|
||||||
|
opacity={selectedObject.opacity ?? 100}
|
||||||
|
onOpacityChange={(value) =>
|
||||||
|
onUpdate(selectedObject.id, { opacity: value })
|
||||||
|
}
|
||||||
onChange={(value) => onUpdate(selectedObject.id, { stroke: value })}
|
onChange={(value) => onUpdate(selectedObject.id, { stroke: value })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ const LinkSettings = ({ selectedObject, onUpdate }: LinkSettingsProps) => {
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="رنگ متن"
|
label="رنگ متن"
|
||||||
value={selectedObject.fill || "#0000ff"}
|
value={selectedObject.fill || "#0000ff"}
|
||||||
|
opacity={selectedObject.opacity ?? 100}
|
||||||
|
onOpacityChange={(value) =>
|
||||||
|
onUpdate(selectedObject.id, { opacity: value })
|
||||||
|
}
|
||||||
onChange={(value) => onUpdate(selectedObject.id, { fill: value })}
|
onChange={(value) => onUpdate(selectedObject.id, { fill: value })}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type ShapeSettingsProps = {
|
|||||||
const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
|
const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
|
||||||
const baseWidth = selectedObject.width ?? 100;
|
const baseWidth = selectedObject.width ?? 100;
|
||||||
const baseHeight = selectedObject.height ?? 100;
|
const baseHeight = selectedObject.height ?? 100;
|
||||||
|
const baseOpacity = selectedObject.opacity ?? 100;
|
||||||
const baseFill = selectedObject.fill ?? "#3b82f6";
|
const baseFill = selectedObject.fill ?? "#3b82f6";
|
||||||
const fillType = selectedObject.fillType ?? "solid";
|
const fillType = selectedObject.fillType ?? "solid";
|
||||||
const gradient = selectedObject.gradient ?? {
|
const gradient = selectedObject.gradient ?? {
|
||||||
@@ -50,6 +51,12 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
|
|||||||
label="رنگ پسزمینه"
|
label="رنگ پسزمینه"
|
||||||
value={baseFill}
|
value={baseFill}
|
||||||
readOnly={fillType === "gradient"}
|
readOnly={fillType === "gradient"}
|
||||||
|
opacity={baseOpacity}
|
||||||
|
onOpacityChange={(value) =>
|
||||||
|
onUpdate(selectedObject.id, {
|
||||||
|
opacity: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
onUpdate(selectedObject.id, {
|
onUpdate(selectedObject.id, {
|
||||||
fill: value,
|
fill: value,
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ const TextSettings = ({ selectedObject, onUpdate }: TextSettingsProps) => {
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="رنگ متن"
|
label="رنگ متن"
|
||||||
value={selectedObject.fill || "#000000"}
|
value={selectedObject.fill || "#000000"}
|
||||||
|
opacity={selectedObject.opacity ?? 100}
|
||||||
|
onOpacityChange={(value) =>
|
||||||
|
onUpdate(selectedObject.id, { opacity: value })
|
||||||
|
}
|
||||||
onChange={(value) => onUpdate(selectedObject.id, { fill: value })}
|
onChange={(value) => onUpdate(selectedObject.id, { fill: value })}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,47 +1,48 @@
|
|||||||
import type { EditorObject } from "../../../store/editorStore";
|
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
|
import type { EditorObject } from "../../../store/editorStore";
|
||||||
|
|
||||||
type TransformSettingsProps = {
|
type TransformSettingsProps = {
|
||||||
selectedObject: EditorObject;
|
selectedObject: EditorObject;
|
||||||
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
|
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TransformSettings = ({ selectedObject, onUpdate }: TransformSettingsProps) => {
|
const TransformSettings = ({ selectedObject, onUpdate }: TransformSettingsProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="pt-4 flex flex-col gap-4 border-t border-border">
|
<div className="pt-4 flex flex-col gap-4 border-t border-border">
|
||||||
<Input
|
<div className="flex gap-4">
|
||||||
label="موقعیت X"
|
<Input
|
||||||
type="number"
|
label="موقعیت X"
|
||||||
value={Math.round(selectedObject.x)}
|
type="number"
|
||||||
onChange={(e) =>
|
value={Math.round(selectedObject.x)}
|
||||||
onUpdate(selectedObject.id, {
|
onChange={(e) =>
|
||||||
x: parseInt(e.target.value) || 0,
|
onUpdate(selectedObject.id, {
|
||||||
})
|
x: parseInt(e.target.value) || 0,
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
<Input
|
/>
|
||||||
label="موقعیت Y"
|
<Input
|
||||||
type="number"
|
label="موقعیت Y"
|
||||||
value={Math.round(selectedObject.y)}
|
type="number"
|
||||||
onChange={(e) =>
|
value={Math.round(selectedObject.y)}
|
||||||
onUpdate(selectedObject.id, {
|
onChange={(e) =>
|
||||||
y: parseInt(e.target.value) || 0,
|
onUpdate(selectedObject.id, {
|
||||||
})
|
y: parseInt(e.target.value) || 0,
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
<Input
|
/>
|
||||||
label="چرخش (درجه)"
|
</div>
|
||||||
type="number"
|
<Input
|
||||||
value={selectedObject.rotation || 0}
|
label="چرخش (درجه)"
|
||||||
onChange={(e) =>
|
type="number"
|
||||||
onUpdate(selectedObject.id, {
|
value={selectedObject.rotation || 0}
|
||||||
rotation: parseInt(e.target.value) || 0,
|
onChange={(e) =>
|
||||||
})
|
onUpdate(selectedObject.id, {
|
||||||
}
|
rotation: parseInt(e.target.value) || 0,
|
||||||
/>
|
})
|
||||||
</div>
|
}
|
||||||
);
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TransformSettings;
|
export default TransformSettings;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Star } from "react-konva";
|
|||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import type { ShapeProps } from "./types";
|
import type { ShapeProps } from "./types";
|
||||||
import { getKonvaGradientProps } from "../../utils/gradient";
|
import { getKonvaGradientProps } from "../../utils/gradient";
|
||||||
import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
|
import { getColorWithOpacity } from "../../utils/colorOpacity";
|
||||||
|
import { useShapeBlurCache } from "../../utils/shapeBlur";
|
||||||
|
|
||||||
const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||||
const shapeRef = useRef<Konva.Star>(null);
|
const shapeRef = useRef<Konva.Star>(null);
|
||||||
@@ -19,9 +20,13 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
|||||||
|
|
||||||
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
||||||
const hasStroke = actualStrokeWidth > 0;
|
const hasStroke = actualStrokeWidth > 0;
|
||||||
const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
|
const blurRadius = obj.blur ?? 0;
|
||||||
|
const blurHitFill = blurRadius > 0 ? getColorWithOpacity(obj.fill ?? "#000000", 0.5) : undefined;
|
||||||
|
// Editor blur is implemented via backdrop-filter (see BlurBackdropLayer).
|
||||||
|
// Avoid applying Konva blur filters on the element itself.
|
||||||
|
const blurProps = {};
|
||||||
|
|
||||||
useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
|
useShapeBlurCache(shapeRef, 0, [
|
||||||
obj.width,
|
obj.width,
|
||||||
obj.height,
|
obj.height,
|
||||||
obj.fill,
|
obj.fill,
|
||||||
@@ -29,7 +34,6 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
|||||||
obj.gradient,
|
obj.gradient,
|
||||||
obj.stroke,
|
obj.stroke,
|
||||||
actualStrokeWidth,
|
actualStrokeWidth,
|
||||||
obj.blur,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
||||||
@@ -44,14 +48,14 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
|||||||
const displayStrokeWidth = isSelected
|
const displayStrokeWidth = isSelected
|
||||||
? (hasStroke ? actualStrokeWidth : 3)
|
? (hasStroke ? actualStrokeWidth : 3)
|
||||||
: (isMask && showGuide ? 2 : actualStrokeWidth);
|
: (isMask && showGuide ? 2 : actualStrokeWidth);
|
||||||
const gradientProps = isMask
|
const gradientProps = isMask || blurRadius > 0
|
||||||
? {}
|
? {}
|
||||||
: getKonvaGradientProps(
|
: getKonvaGradientProps(
|
||||||
obj.fillType,
|
obj.fillType,
|
||||||
obj.gradient,
|
obj.gradient,
|
||||||
obj.width || 100,
|
obj.width || 100,
|
||||||
obj.height || 100,
|
obj.height || 100,
|
||||||
"centered",
|
"centered",
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -64,12 +68,14 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
|||||||
numPoints={5}
|
numPoints={5}
|
||||||
innerRadius={innerRadius}
|
innerRadius={innerRadius}
|
||||||
outerRadius={radius}
|
outerRadius={radius}
|
||||||
fill={isMask ? "transparent" : obj.fill}
|
// When blur is enabled, tint is rendered by BlurBackdropLayer.
|
||||||
|
fill={isMask ? "transparent" : blurRadius > 0 ? blurHitFill : (obj.fill ?? "#000000")}
|
||||||
{...gradientProps}
|
{...gradientProps}
|
||||||
stroke={displayStroke}
|
stroke={displayStroke}
|
||||||
strokeWidth={displayStrokeWidth}
|
strokeWidth={displayStrokeWidth}
|
||||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
|
opacity={(obj.opacity ?? 100) / 100}
|
||||||
{...blurProps}
|
{...blurProps}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const ArrowShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePro
|
|||||||
hitStrokeWidth={20}
|
hitStrokeWidth={20}
|
||||||
perfectDrawEnabled={false}
|
perfectDrawEnabled={false}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
|
opacity={(obj.opacity ?? 100) / 100}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (shapeRef.current) {
|
if (shapeRef.current) {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Circle } from "react-konva";
|
|||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import type { ShapeProps } from "./types";
|
import type { ShapeProps } from "./types";
|
||||||
import { getKonvaGradientProps } from "../../utils/gradient";
|
import { getKonvaGradientProps } from "../../utils/gradient";
|
||||||
import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
|
import { getColorWithOpacity } from "../../utils/colorOpacity";
|
||||||
|
import { useShapeBlurCache } from "../../utils/shapeBlur";
|
||||||
|
|
||||||
const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||||
const shapeRef = useRef<Konva.Circle>(null);
|
const shapeRef = useRef<Konva.Circle>(null);
|
||||||
@@ -15,16 +16,19 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
|
|||||||
|
|
||||||
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
||||||
const hasStroke = actualStrokeWidth > 0;
|
const hasStroke = actualStrokeWidth > 0;
|
||||||
const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
|
const blurRadius = obj.blur ?? 0;
|
||||||
|
const blurHitFill = blurRadius > 0 ? getColorWithOpacity(obj.fill ?? "#000000", 0.5) : undefined;
|
||||||
|
// Editor blur is implemented via backdrop-filter (see BlurBackdropLayer).
|
||||||
|
// Avoid applying Konva blur filters on the element itself.
|
||||||
|
const blurProps = {};
|
||||||
|
|
||||||
useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
|
useShapeBlurCache(shapeRef, 0, [
|
||||||
obj.width,
|
obj.width,
|
||||||
obj.fill,
|
obj.fill,
|
||||||
obj.fillType,
|
obj.fillType,
|
||||||
obj.gradient,
|
obj.gradient,
|
||||||
obj.stroke,
|
obj.stroke,
|
||||||
actualStrokeWidth,
|
actualStrokeWidth,
|
||||||
obj.blur,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
||||||
@@ -40,9 +44,9 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
|
|||||||
? (hasStroke ? actualStrokeWidth : 3)
|
? (hasStroke ? actualStrokeWidth : 3)
|
||||||
: (isMask && showGuide ? 2 : actualStrokeWidth);
|
: (isMask && showGuide ? 2 : actualStrokeWidth);
|
||||||
const diameter = obj.width || 100;
|
const diameter = obj.width || 100;
|
||||||
const gradientProps = isMask
|
const gradientProps = isMask || blurRadius > 0
|
||||||
? {}
|
? {}
|
||||||
: getKonvaGradientProps(obj.fillType, obj.gradient, diameter, diameter, "centered");
|
: getKonvaGradientProps(obj.fillType, obj.gradient, diameter, diameter, "centered");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Circle
|
<Circle
|
||||||
@@ -52,12 +56,14 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
|
|||||||
x={obj.x}
|
x={obj.x}
|
||||||
y={obj.y}
|
y={obj.y}
|
||||||
radius={(obj.width || 50) / 2}
|
radius={(obj.width || 50) / 2}
|
||||||
fill={isMask ? "transparent" : obj.fill}
|
// When blur is enabled, tint is rendered by BlurBackdropLayer.
|
||||||
|
fill={isMask ? "transparent" : blurRadius > 0 ? blurHitFill : (obj.fill ?? "#000000")}
|
||||||
{...gradientProps}
|
{...gradientProps}
|
||||||
stroke={displayStroke}
|
stroke={displayStroke}
|
||||||
strokeWidth={displayStrokeWidth}
|
strokeWidth={displayStrokeWidth}
|
||||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
|
opacity={(obj.opacity ?? 100) / 100}
|
||||||
{...blurProps}
|
{...blurProps}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const LineShape = ({ obj, onSelect, onUpdate, draggable }: ShapeProps) => {
|
|||||||
hitStrokeWidth={20}
|
hitStrokeWidth={20}
|
||||||
perfectDrawEnabled={false}
|
perfectDrawEnabled={false}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
|
opacity={(obj.opacity ?? 100) / 100}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (shapeRef.current) {
|
if (shapeRef.current) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const LinkShape = ({
|
|||||||
fontStyle={fontWeight}
|
fontStyle={fontWeight}
|
||||||
underline={true}
|
underline={true}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
|
opacity={(obj.opacity ?? 100) / 100}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// در حالت editor، لینکها قابل کلیک نیستند
|
// در حالت editor، لینکها قابل کلیک نیستند
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Rect } from "react-konva";
|
|||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import type { ShapeProps } from "./types";
|
import type { ShapeProps } from "./types";
|
||||||
import { getKonvaGradientProps } from "../../utils/gradient";
|
import { getKonvaGradientProps } from "../../utils/gradient";
|
||||||
import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
|
import { getColorWithOpacity } from "../../utils/colorOpacity";
|
||||||
|
import { useShapeBlurCache } from "../../utils/shapeBlur";
|
||||||
|
|
||||||
const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||||
const shapeRef = useRef<Konva.Rect>(null);
|
const shapeRef = useRef<Konva.Rect>(null);
|
||||||
@@ -16,9 +17,13 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
|
|||||||
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
||||||
const hasStroke = actualStrokeWidth > 0;
|
const hasStroke = actualStrokeWidth > 0;
|
||||||
const cornerRadius = Math.max(0, obj.borderRadius ?? 0);
|
const cornerRadius = Math.max(0, obj.borderRadius ?? 0);
|
||||||
const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
|
const blurRadius = obj.blur ?? 0;
|
||||||
|
const blurHitFill = blurRadius > 0 ? getColorWithOpacity(obj.fill ?? "#000000", 0.5) : undefined;
|
||||||
|
// Editor blur is implemented via backdrop-filter (see BlurBackdropLayer),
|
||||||
|
// so we intentionally do not apply Konva blur filters here.
|
||||||
|
const blurProps = {};
|
||||||
|
|
||||||
useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
|
useShapeBlurCache(shapeRef, 0, [
|
||||||
obj.width,
|
obj.width,
|
||||||
obj.height,
|
obj.height,
|
||||||
cornerRadius,
|
cornerRadius,
|
||||||
@@ -27,7 +32,6 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
|
|||||||
obj.gradient,
|
obj.gradient,
|
||||||
obj.stroke,
|
obj.stroke,
|
||||||
actualStrokeWidth,
|
actualStrokeWidth,
|
||||||
obj.blur,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
||||||
@@ -42,7 +46,7 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
|
|||||||
const displayStrokeWidth = isSelected
|
const displayStrokeWidth = isSelected
|
||||||
? (hasStroke ? actualStrokeWidth : 3)
|
? (hasStroke ? actualStrokeWidth : 3)
|
||||||
: (isMask && showGuide ? 2 : actualStrokeWidth);
|
: (isMask && showGuide ? 2 : actualStrokeWidth);
|
||||||
const gradientProps = isMask
|
const gradientProps = isMask || blurRadius > 0
|
||||||
? {}
|
? {}
|
||||||
: getKonvaGradientProps(
|
: getKonvaGradientProps(
|
||||||
obj.fillType,
|
obj.fillType,
|
||||||
@@ -62,12 +66,15 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
|
|||||||
width={obj.width || 100}
|
width={obj.width || 100}
|
||||||
height={obj.height || 100}
|
height={obj.height || 100}
|
||||||
cornerRadius={cornerRadius}
|
cornerRadius={cornerRadius}
|
||||||
fill={isMask ? "transparent" : obj.fill}
|
// When blur is enabled, visual tint is rendered by BlurBackdropLayer.
|
||||||
|
// Use a tiny non-zero alpha so Konva hit-detection still works for dragging.
|
||||||
|
fill={isMask ? "transparent" : blurRadius > 0 ? blurHitFill : (obj.fill ?? "#000000")}
|
||||||
{...gradientProps}
|
{...gradientProps}
|
||||||
stroke={displayStroke}
|
stroke={displayStroke}
|
||||||
strokeWidth={displayStrokeWidth}
|
strokeWidth={displayStrokeWidth}
|
||||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
|
opacity={(obj.opacity ?? 100) / 100}
|
||||||
{...blurProps}
|
{...blurProps}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import Konva from "konva";
|
|||||||
import type { TextShapeProps } from "./types";
|
import type { TextShapeProps } from "./types";
|
||||||
import { getFontFamily } from "@/pages/editor/utils/fontFamily";
|
import { getFontFamily } from "@/pages/editor/utils/fontFamily";
|
||||||
import {
|
import {
|
||||||
|
buildCanvasFont,
|
||||||
|
buildKonvaCanvasFont,
|
||||||
DEFAULT_TEXT_MAX_WIDTH_PX,
|
DEFAULT_TEXT_MAX_WIDTH_PX,
|
||||||
getEffectiveTextWidth,
|
getEffectiveTextWidth,
|
||||||
|
getKonvaFontStyle,
|
||||||
measureTextBlock,
|
measureTextBlock,
|
||||||
resolveTextMaxWidth,
|
resolveTextMaxWidth,
|
||||||
usesWrappedLayout,
|
usesWrappedLayout,
|
||||||
} from "@/pages/editor/utils/textStyle";
|
} from "@/pages/editor/utils/textStyle";
|
||||||
|
import { getColorWithOpacity } from "@/pages/editor/utils/colorOpacity";
|
||||||
|
|
||||||
const getFontWeight = (fontWeight?: string): string => {
|
const getFontWeight = (fontWeight?: string): string => {
|
||||||
if (!fontWeight) return "normal";
|
if (!fontWeight) return "normal";
|
||||||
@@ -29,28 +33,6 @@ const getFontWeight = (fontWeight?: string): string => {
|
|||||||
return weightMap[fontWeight] || "normal";
|
return weightMap[fontWeight] || "normal";
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Konva.Text فقط `fontStyle` دارد (نه fontWeight جداگانه)؛ مقدار باید normal|bold|italic باشد. */
|
|
||||||
const getKonvaFontStyle = (fontWeight?: string): string => {
|
|
||||||
const w = getFontWeight(fontWeight);
|
|
||||||
if (w === "bold" || w === "bolder") return "bold";
|
|
||||||
const n = parseInt(w, 10);
|
|
||||||
if (!Number.isNaN(n) && n >= 600) return "bold";
|
|
||||||
return "normal";
|
|
||||||
};
|
|
||||||
|
|
||||||
const getColorWithOpacity = (fill?: string, opacity?: number): string => {
|
|
||||||
if (!fill) return "#000000";
|
|
||||||
if (opacity === undefined || opacity === 100) return fill;
|
|
||||||
|
|
||||||
const hex = fill.replace("#", "");
|
|
||||||
const r = parseInt(hex.substring(0, 2), 16);
|
|
||||||
const g = parseInt(hex.substring(2, 4), 16);
|
|
||||||
const b = parseInt(hex.substring(4, 6), 16);
|
|
||||||
const alpha = opacity / 100;
|
|
||||||
|
|
||||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TextShape = ({
|
const TextShape = ({
|
||||||
obj,
|
obj,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -184,11 +166,9 @@ const TextShape = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const specs = [
|
const specs = [
|
||||||
`${fontSize}px ${resolvedFamily}`,
|
buildCanvasFont(fontSize, obj.fontFamily, obj.fontWeight),
|
||||||
|
buildKonvaCanvasFont(fontSize, obj.fontFamily, obj.fontWeight),
|
||||||
`${konvaStyle} normal ${fontSize}px ${resolvedFamily}`,
|
`${konvaStyle} normal ${fontSize}px ${resolvedFamily}`,
|
||||||
`bold ${fontSize}px ${resolvedFamily}`,
|
|
||||||
`300 ${fontSize}px ${resolvedFamily}`,
|
|
||||||
`200 ${fontSize}px ${resolvedFamily}`,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
void Promise.all(
|
void Promise.all(
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { RegularPolygon } from "react-konva";
|
|||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import type { ShapeProps } from "./types";
|
import type { ShapeProps } from "./types";
|
||||||
import { getKonvaGradientProps } from "../../utils/gradient";
|
import { getKonvaGradientProps } from "../../utils/gradient";
|
||||||
import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
|
import { getColorWithOpacity } from "../../utils/colorOpacity";
|
||||||
|
import { useShapeBlurCache } from "../../utils/shapeBlur";
|
||||||
|
|
||||||
const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||||
const shapeRef = useRef<Konva.RegularPolygon>(null);
|
const shapeRef = useRef<Konva.RegularPolygon>(null);
|
||||||
@@ -16,17 +17,20 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
|||||||
|
|
||||||
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
const actualStrokeWidth = obj.strokeWidth ?? 0;
|
||||||
const hasStroke = actualStrokeWidth > 0;
|
const hasStroke = actualStrokeWidth > 0;
|
||||||
const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
|
const blurRadius = obj.blur ?? 0;
|
||||||
|
const blurHitFill = blurRadius > 0 ? getColorWithOpacity(obj.fill ?? "#000000", 0.5) : undefined;
|
||||||
|
// Editor blur is implemented via backdrop-filter (see BlurBackdropLayer).
|
||||||
|
// Avoid applying Konva blur filters on the element itself.
|
||||||
|
const blurProps = {};
|
||||||
|
|
||||||
useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
|
useShapeBlurCache(shapeRef, 0, [
|
||||||
obj.width,
|
obj.width,
|
||||||
obj.height,
|
|
||||||
obj.fill,
|
obj.fill,
|
||||||
obj.fillType,
|
obj.fillType,
|
||||||
obj.gradient,
|
obj.gradient,
|
||||||
obj.stroke,
|
obj.stroke,
|
||||||
actualStrokeWidth,
|
actualStrokeWidth,
|
||||||
obj.blur,
|
obj.height,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
|
||||||
@@ -41,14 +45,14 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
|||||||
const displayStrokeWidth = isSelected
|
const displayStrokeWidth = isSelected
|
||||||
? (hasStroke ? actualStrokeWidth : 3)
|
? (hasStroke ? actualStrokeWidth : 3)
|
||||||
: (isMask && showGuide ? 2 : actualStrokeWidth);
|
: (isMask && showGuide ? 2 : actualStrokeWidth);
|
||||||
const gradientProps = isMask
|
const gradientProps = isMask || blurRadius > 0
|
||||||
? {}
|
? {}
|
||||||
: getKonvaGradientProps(
|
: getKonvaGradientProps(
|
||||||
obj.fillType,
|
obj.fillType,
|
||||||
obj.gradient,
|
obj.gradient,
|
||||||
obj.width || 100,
|
obj.width || 100,
|
||||||
obj.height || 100,
|
obj.height || 100,
|
||||||
"centered",
|
"centered",
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,12 +64,14 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
|
|||||||
y={obj.y + (obj.height || 100) / 2}
|
y={obj.y + (obj.height || 100) / 2}
|
||||||
sides={3}
|
sides={3}
|
||||||
radius={radius}
|
radius={radius}
|
||||||
fill={isMask ? "transparent" : obj.fill}
|
// When blur is enabled, tint is rendered by BlurBackdropLayer.
|
||||||
|
fill={isMask ? "transparent" : blurRadius > 0 ? blurHitFill : (obj.fill ?? "#000000")}
|
||||||
{...gradientProps}
|
{...gradientProps}
|
||||||
stroke={displayStroke}
|
stroke={displayStroke}
|
||||||
strokeWidth={displayStrokeWidth}
|
strokeWidth={displayStrokeWidth}
|
||||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||||
rotation={obj.rotation || 0}
|
rotation={obj.rotation || 0}
|
||||||
|
opacity={(obj.opacity ?? 100) / 100}
|
||||||
{...blurProps}
|
{...blurProps}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const createInitialCells = (
|
|||||||
id: cellId,
|
id: cellId,
|
||||||
text: "",
|
text: "",
|
||||||
background: "#f5f5f5",
|
background: "#f5f5f5",
|
||||||
|
backgroundOpacity: 100,
|
||||||
width: cellWidth,
|
width: cellWidth,
|
||||||
height: cellHeight,
|
height: cellHeight,
|
||||||
};
|
};
|
||||||
@@ -59,6 +60,7 @@ export const createInitialPage = (name: string): Page => ({
|
|||||||
guides: [],
|
guides: [],
|
||||||
backgroundType: "color",
|
backgroundType: "color",
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
|
backgroundOpacity: 100,
|
||||||
backgroundGradient: {
|
backgroundGradient: {
|
||||||
from: "#ffffff",
|
from: "#ffffff",
|
||||||
to: "#e2e8f0",
|
to: "#e2e8f0",
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type PageBackgroundSettings = Pick<
|
|||||||
Page,
|
Page,
|
||||||
| "backgroundType"
|
| "backgroundType"
|
||||||
| "backgroundColor"
|
| "backgroundColor"
|
||||||
|
| "backgroundOpacity"
|
||||||
| "backgroundGradient"
|
| "backgroundGradient"
|
||||||
| "backgroundImageUrl"
|
| "backgroundImageUrl"
|
||||||
| "backgroundVideoUrl"
|
| "backgroundVideoUrl"
|
||||||
@@ -127,6 +128,11 @@ type EditorStoreType = {
|
|||||||
cellId: string,
|
cellId: string,
|
||||||
color: string,
|
color: string,
|
||||||
) => void;
|
) => void;
|
||||||
|
changeCellBackgroundOpacity: (
|
||||||
|
tableId: string,
|
||||||
|
cellId: string,
|
||||||
|
opacity: number,
|
||||||
|
) => void;
|
||||||
applyTableResize: (
|
applyTableResize: (
|
||||||
tableId: string,
|
tableId: string,
|
||||||
newWidth: number,
|
newWidth: number,
|
||||||
@@ -186,6 +192,7 @@ const DEFAULT_DOCUMENT_SETTINGS: DocumentSettings = {
|
|||||||
smartGuide: true,
|
smartGuide: true,
|
||||||
backgroundType: "color",
|
backgroundType: "color",
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
|
backgroundOpacity: 100,
|
||||||
backgroundGradient: {
|
backgroundGradient: {
|
||||||
from: "#ffffff",
|
from: "#ffffff",
|
||||||
to: "#e2e8f0",
|
to: "#e2e8f0",
|
||||||
@@ -640,6 +647,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
id: cellId,
|
id: cellId,
|
||||||
text: "",
|
text: "",
|
||||||
background: "#f5f5f5",
|
background: "#f5f5f5",
|
||||||
|
backgroundOpacity: 100,
|
||||||
width: tableData.cellWidth,
|
width: tableData.cellWidth,
|
||||||
height: tableData.cellHeight,
|
height: tableData.cellHeight,
|
||||||
};
|
};
|
||||||
@@ -670,6 +678,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
id: cellId,
|
id: cellId,
|
||||||
text: "",
|
text: "",
|
||||||
background: "#f5f5f5",
|
background: "#f5f5f5",
|
||||||
|
backgroundOpacity: 100,
|
||||||
width: tableData.cellWidth,
|
width: tableData.cellWidth,
|
||||||
height: tableData.cellHeight,
|
height: tableData.cellHeight,
|
||||||
};
|
};
|
||||||
@@ -776,6 +785,28 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
changeCellBackgroundOpacity: (tableId, cellId, opacity) => {
|
||||||
|
get().commitObjectHistoryBeforeChange();
|
||||||
|
const state = get();
|
||||||
|
const obj = state.objects.find((o) => o.id === tableId);
|
||||||
|
if (!obj || !obj.tableData) return;
|
||||||
|
|
||||||
|
const { tableData } = obj;
|
||||||
|
const updatedCells = {
|
||||||
|
...tableData.cells,
|
||||||
|
[cellId]: {
|
||||||
|
...tableData.cells[cellId],
|
||||||
|
backgroundOpacity: Math.min(100, Math.max(0, opacity)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
state.updateObject(tableId, {
|
||||||
|
tableData: {
|
||||||
|
...tableData,
|
||||||
|
cells: updatedCells,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
applyTableResize: (tableId, newWidth, newHeight) => {
|
applyTableResize: (tableId, newWidth, newHeight) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const obj = state.objects.find((o) => o.id === tableId);
|
const obj = state.objects.find((o) => o.id === tableId);
|
||||||
@@ -858,6 +889,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
guides: [...pageToDuplicate.guides],
|
guides: [...pageToDuplicate.guides],
|
||||||
backgroundType: pageToDuplicate.backgroundType,
|
backgroundType: pageToDuplicate.backgroundType,
|
||||||
backgroundColor: pageToDuplicate.backgroundColor,
|
backgroundColor: pageToDuplicate.backgroundColor,
|
||||||
|
backgroundOpacity: pageToDuplicate.backgroundOpacity ?? 100,
|
||||||
backgroundGradient: pageToDuplicate.backgroundGradient,
|
backgroundGradient: pageToDuplicate.backgroundGradient,
|
||||||
backgroundImageUrl: pageToDuplicate.backgroundImageUrl,
|
backgroundImageUrl: pageToDuplicate.backgroundImageUrl,
|
||||||
backgroundVideoUrl: pageToDuplicate.backgroundVideoUrl,
|
backgroundVideoUrl: pageToDuplicate.backgroundVideoUrl,
|
||||||
@@ -1053,6 +1085,8 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
page.backgroundType ?? fallbackSettings.backgroundType ?? "color",
|
page.backgroundType ?? fallbackSettings.backgroundType ?? "color",
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
page.backgroundColor ?? fallbackSettings.backgroundColor ?? "#ffffff",
|
page.backgroundColor ?? fallbackSettings.backgroundColor ?? "#ffffff",
|
||||||
|
backgroundOpacity:
|
||||||
|
page.backgroundOpacity ?? fallbackSettings.backgroundOpacity ?? 100,
|
||||||
backgroundGradient: page.backgroundGradient ??
|
backgroundGradient: page.backgroundGradient ??
|
||||||
fallbackSettings.backgroundGradient ?? {
|
fallbackSettings.backgroundGradient ?? {
|
||||||
from: "#ffffff",
|
from: "#ffffff",
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export type TableCell = {
|
|||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
background: string;
|
background: string;
|
||||||
|
/** شفافیت پسزمینه سلول (۰–۱۰۰) */
|
||||||
|
backgroundOpacity?: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
@@ -121,6 +123,7 @@ export type Page = {
|
|||||||
guides: PageGuide[];
|
guides: PageGuide[];
|
||||||
backgroundType: BackgroundType;
|
backgroundType: BackgroundType;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: LinearGradient;
|
backgroundGradient?: LinearGradient;
|
||||||
backgroundImageUrl: string;
|
backgroundImageUrl: string;
|
||||||
backgroundVideoUrl: string;
|
backgroundVideoUrl: string;
|
||||||
@@ -137,6 +140,7 @@ export type DocumentSettings = {
|
|||||||
smartGuide: boolean;
|
smartGuide: boolean;
|
||||||
backgroundType: BackgroundType;
|
backgroundType: BackgroundType;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: LinearGradient;
|
backgroundGradient?: LinearGradient;
|
||||||
backgroundImageUrl: string;
|
backgroundImageUrl: string;
|
||||||
backgroundVideoUrl: string;
|
backgroundVideoUrl: string;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { create } from "zustand";
|
|||||||
export type PencilDefaults = {
|
export type PencilDefaults = {
|
||||||
stroke: string;
|
stroke: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
|
opacity: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PencilDefaultsStoreType = {
|
type PencilDefaultsStoreType = {
|
||||||
@@ -14,6 +15,7 @@ export const usePencilDefaultsStore = create<PencilDefaultsStoreType>((set) => (
|
|||||||
defaults: {
|
defaults: {
|
||||||
stroke: "#000000",
|
stroke: "#000000",
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
|
opacity: 100,
|
||||||
},
|
},
|
||||||
updateDefaults: (updates) =>
|
updateDefaults: (updates) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import type { CSSProperties } from "react";
|
||||||
|
import type { EditorObject } from "../store/editorStore";
|
||||||
|
import { getColorWithOpacity } from "./colorOpacity";
|
||||||
|
import { toCssGradientAngle, type LinearGradient } from "./gradient";
|
||||||
|
|
||||||
|
const TRIANGLE_TOP_Y_PERCENT = 6.69873;
|
||||||
|
const TRIANGLE_BOTTOM_Y_PERCENT = 93.30127;
|
||||||
|
const BLUR_TINT_OPACITY_FACTOR = 0.25;
|
||||||
|
|
||||||
|
export type BlurOverlayGeometry = {
|
||||||
|
leftPx: number;
|
||||||
|
topPx: number;
|
||||||
|
widthPx: number;
|
||||||
|
heightPx: number;
|
||||||
|
borderRadiusPx: number;
|
||||||
|
clipPath?: string;
|
||||||
|
transformOrigin: CSSProperties["transformOrigin"];
|
||||||
|
rotation: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isBlurBackdropObject(obj: EditorObject): boolean {
|
||||||
|
if (obj.visible === false) return false;
|
||||||
|
if (obj.isMask) return false;
|
||||||
|
if (obj.type !== "rectangle") return false;
|
||||||
|
return (obj.blur ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logical stage coords → overlay pixel space (matches Konva getAbsolutePosition after stage scale). */
|
||||||
|
export function computeBlurOverlayGeometry(
|
||||||
|
obj: EditorObject,
|
||||||
|
scale: number,
|
||||||
|
livePosition?: { x: number; y: number } | null,
|
||||||
|
): BlurOverlayGeometry {
|
||||||
|
const rotation = obj.rotation ?? 0;
|
||||||
|
const shapeType = obj.shapeType ?? "square";
|
||||||
|
const anchorX =
|
||||||
|
livePosition?.x ??
|
||||||
|
(shapeType === "triangle" || shapeType === "abstract"
|
||||||
|
? ((obj.x ?? 0) + (obj.width ?? 100) / 2) * scale
|
||||||
|
: (obj.x ?? 0) * scale);
|
||||||
|
const anchorY =
|
||||||
|
livePosition?.y ??
|
||||||
|
(shapeType === "triangle" || shapeType === "abstract"
|
||||||
|
? ((obj.y ?? 0) + (obj.height ?? 100) / 2) * scale
|
||||||
|
: (obj.y ?? 0) * scale);
|
||||||
|
|
||||||
|
if (shapeType === "circle") {
|
||||||
|
const sizePx = (obj.width ?? 100) * scale;
|
||||||
|
const radiusPx = sizePx / 2;
|
||||||
|
return {
|
||||||
|
leftPx: anchorX - radiusPx,
|
||||||
|
topPx: anchorY - radiusPx,
|
||||||
|
widthPx: sizePx,
|
||||||
|
heightPx: sizePx,
|
||||||
|
borderRadiusPx: radiusPx,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
rotation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shapeType === "triangle") {
|
||||||
|
const side = Math.min(obj.width ?? 100, obj.height ?? 100);
|
||||||
|
const radiusPx = (side / 2) * scale;
|
||||||
|
return {
|
||||||
|
leftPx: anchorX - radiusPx,
|
||||||
|
topPx: anchorY - radiusPx,
|
||||||
|
widthPx: side * scale,
|
||||||
|
heightPx: side * scale,
|
||||||
|
borderRadiusPx: 0,
|
||||||
|
clipPath: `polygon(50% ${TRIANGLE_TOP_Y_PERCENT}%, 0% ${TRIANGLE_BOTTOM_Y_PERCENT}%, 100% ${TRIANGLE_BOTTOM_Y_PERCENT}%)`,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
rotation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shapeType === "abstract") {
|
||||||
|
const rawW = obj.width ?? 100;
|
||||||
|
const rawH = obj.height ?? 100;
|
||||||
|
const baseRadius = (Math.min(rawW, rawH) / 2) * scale;
|
||||||
|
const radius = baseRadius * 0.85;
|
||||||
|
return {
|
||||||
|
leftPx: anchorX - radius,
|
||||||
|
topPx: anchorY - radius,
|
||||||
|
widthPx: radius * 2,
|
||||||
|
heightPx: radius * 2,
|
||||||
|
borderRadiusPx: 0,
|
||||||
|
clipPath:
|
||||||
|
"polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)",
|
||||||
|
transformOrigin: "center center",
|
||||||
|
rotation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
leftPx: anchorX,
|
||||||
|
topPx: anchorY,
|
||||||
|
widthPx: (obj.width ?? 100) * scale,
|
||||||
|
heightPx: (obj.height ?? 100) * scale,
|
||||||
|
borderRadiusPx: Math.max(0, obj.borderRadius ?? 0) * scale,
|
||||||
|
transformOrigin: "0% 0%",
|
||||||
|
rotation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBlurOverlayStyle(
|
||||||
|
obj: EditorObject,
|
||||||
|
scale: number,
|
||||||
|
livePosition?: { x: number; y: number } | null,
|
||||||
|
): CSSProperties {
|
||||||
|
const blur = obj.blur ?? 0;
|
||||||
|
const blurPx = blur * scale;
|
||||||
|
const geometry = computeBlurOverlayGeometry(obj, scale, livePosition);
|
||||||
|
const objOpacity = obj.opacity ?? 100;
|
||||||
|
const tintOpacity = Math.max(0, Math.min(100, objOpacity * BLUR_TINT_OPACITY_FACTOR));
|
||||||
|
const defaultFill = "#3b82f6";
|
||||||
|
|
||||||
|
const overlayBackground: CSSProperties =
|
||||||
|
obj.fillType === "gradient" && obj.gradient
|
||||||
|
? (() => {
|
||||||
|
const g = obj.gradient as LinearGradient;
|
||||||
|
const angle = toCssGradientAngle(g.angle);
|
||||||
|
const from = getColorWithOpacity(g.from, tintOpacity);
|
||||||
|
const to = getColorWithOpacity(g.to, tintOpacity);
|
||||||
|
return {
|
||||||
|
backgroundImage: `linear-gradient(${angle}deg, ${from} 0%, ${to} 100%)`,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
: {
|
||||||
|
backgroundColor: getColorWithOpacity(obj.fill ?? defaultFill, tintOpacity),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: "absolute",
|
||||||
|
left: `${geometry.leftPx}px`,
|
||||||
|
top: `${geometry.topPx}px`,
|
||||||
|
width: `${geometry.widthPx}px`,
|
||||||
|
height: `${geometry.heightPx}px`,
|
||||||
|
pointerEvents: "none",
|
||||||
|
backdropFilter: `blur(${blurPx}px)`,
|
||||||
|
WebkitBackdropFilter: `blur(${blurPx}px)`,
|
||||||
|
...overlayBackground,
|
||||||
|
overflow:
|
||||||
|
geometry.clipPath
|
||||||
|
? "hidden"
|
||||||
|
: geometry.borderRadiusPx > 0
|
||||||
|
? "hidden"
|
||||||
|
: undefined,
|
||||||
|
borderRadius: geometry.borderRadiusPx > 0 ? `${geometry.borderRadiusPx}px` : undefined,
|
||||||
|
clipPath: geometry.clipPath,
|
||||||
|
transform: geometry.rotation ? `rotate(${geometry.rotation}deg)` : undefined,
|
||||||
|
transformOrigin: geometry.transformOrigin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlurStackEntry = {
|
||||||
|
key: string;
|
||||||
|
blurObject: EditorObject;
|
||||||
|
/** Objects rendered above this blur so they stay crisp. */
|
||||||
|
occlusionObjects: EditorObject[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildBlurStackEntries(objects: EditorObject[]): BlurStackEntry[] {
|
||||||
|
const entries: BlurStackEntry[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < objects.length; i++) {
|
||||||
|
const obj = objects[i];
|
||||||
|
if (!isBlurBackdropObject(obj)) continue;
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
key: obj.id,
|
||||||
|
blurObject: obj,
|
||||||
|
occlusionObjects: objects.slice(i + 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/** Converts a hex color + 0–100 opacity to rgba (editor/viewer convention). */
|
||||||
|
export const getColorWithOpacity = (fill?: string, opacity?: number): string => {
|
||||||
|
if (!fill) return "#000000";
|
||||||
|
if (opacity === undefined || opacity >= 100) return fill;
|
||||||
|
|
||||||
|
const hex = fill.replace("#", "");
|
||||||
|
if (!/^[0-9A-Fa-f]{3,8}$/.test(hex)) return fill;
|
||||||
|
|
||||||
|
const paddedHex =
|
||||||
|
hex.length === 3
|
||||||
|
? hex
|
||||||
|
.split("")
|
||||||
|
.map((c) => c + c)
|
||||||
|
.join("")
|
||||||
|
: hex.length === 4
|
||||||
|
? hex
|
||||||
|
.split("")
|
||||||
|
.map((c) => c + c)
|
||||||
|
.join("")
|
||||||
|
: hex.padEnd(6, "0").slice(0, 6);
|
||||||
|
|
||||||
|
const r = parseInt(paddedHex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(paddedHex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(paddedHex.substring(4, 6), 16);
|
||||||
|
const alpha = opacity / 100;
|
||||||
|
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
};
|
||||||
@@ -11,12 +11,18 @@ export const getCssFontWeight = (fontWeight?: string): number => {
|
|||||||
return 400;
|
return 400;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Konva.Text uses `fontStyle: bold` (canvas keyword), not numeric weight. */
|
/**
|
||||||
export const usesKonvaBoldStyle = (fontWeight?: string): boolean => {
|
* Konva.Text `fontStyle` — supports `normal`, `bold`, or numeric strings like `200`.
|
||||||
if (!fontWeight || fontWeight === "normal") return false;
|
* @see https://konvajs.org/api/Konva.Text.html#fontStyle
|
||||||
if (fontWeight === "bold" || fontWeight === "bolder") return true;
|
*/
|
||||||
|
export const getKonvaFontStyle = (fontWeight?: string): string => {
|
||||||
|
if (!fontWeight || fontWeight === "normal") return "normal";
|
||||||
|
if (fontWeight === "bold" || fontWeight === "bolder") return "bold";
|
||||||
|
|
||||||
const n = parseInt(fontWeight, 10);
|
const n = parseInt(fontWeight, 10);
|
||||||
return !Number.isNaN(n) && n >= 600;
|
if (!Number.isNaN(n)) return n.toString();
|
||||||
|
|
||||||
|
return "normal";
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Canvas/CSS `font` string shared by editor measurement and viewer layout. */
|
/** Canvas/CSS `font` string shared by editor measurement and viewer layout. */
|
||||||
@@ -30,15 +36,15 @@ export const buildCanvasFont = (
|
|||||||
return `${weight} ${fontSize}px ${family}`;
|
return `${weight} ${fontSize}px ${family}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Canvas font string that matches Konva.Text (`fontStyle: bold`). */
|
/** Canvas font string that matches Konva.Text `_getContextFont()`. */
|
||||||
export const buildKonvaCanvasFont = (
|
export const buildKonvaCanvasFont = (
|
||||||
fontSize: number,
|
fontSize: number,
|
||||||
fontFamily?: string,
|
fontFamily?: string,
|
||||||
fontWeight?: string,
|
fontWeight?: string,
|
||||||
): string => {
|
): string => {
|
||||||
const family = getFontFamily(fontFamily);
|
const family = getFontFamily(fontFamily);
|
||||||
const style = usesKonvaBoldStyle(fontWeight) ? "bold" : "normal";
|
const style = getKonvaFontStyle(fontWeight);
|
||||||
return `${style} ${fontSize}px ${family}`;
|
return `${style} normal ${fontSize}px ${family}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LINE_BREAK_RE = /[\r\n\u2028\u2029]/;
|
const LINE_BREAK_RE = /[\r\n\u2028\u2029]/;
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import {
|
|||||||
resolveTextMaxWidth,
|
resolveTextMaxWidth,
|
||||||
usesWrappedLayout,
|
usesWrappedLayout,
|
||||||
} from '@/pages/editor/utils/textStyle';
|
} from '@/pages/editor/utils/textStyle';
|
||||||
import { getCssBlurStyle } from '@/pages/editor/utils/shapeBlur';
|
import {
|
||||||
|
buildBlurOverlayStyle,
|
||||||
|
buildBlurStackEntries,
|
||||||
|
isBlurBackdropObject,
|
||||||
|
} from '@/pages/editor/utils/blurOverlayLayout';
|
||||||
import { getCssBorderRadius } from '@/pages/editor/utils/borderRadius';
|
import { getCssBorderRadius } from '@/pages/editor/utils/borderRadius';
|
||||||
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';
|
||||||
@@ -57,6 +61,7 @@ type BookPageProps = {
|
|||||||
onLinkClick?: (linkUrl: string) => void;
|
onLinkClick?: (linkUrl: string) => void;
|
||||||
backgroundType?: 'color' | 'gradient' | 'image' | 'video';
|
backgroundType?: 'color' | 'gradient' | 'image' | 'video';
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: {
|
backgroundGradient?: {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
@@ -94,9 +99,10 @@ type BookPageProps = {
|
|||||||
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
||||||
*/
|
*/
|
||||||
const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
||||||
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl, entrancePhase = 'idle', disableEntranceAnimations = false, onDeferredEntranceReady, hideMediaLoadingOverlay = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => {
|
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundOpacity, backgroundGradient, backgroundImageUrl, backgroundVideoUrl, entrancePhase = 'idle', disableEntranceAnimations = false, onDeferredEntranceReady, hideMediaLoadingOverlay = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => {
|
||||||
const effectiveBackgroundType = backgroundType ?? page.backgroundType ?? 'color';
|
const effectiveBackgroundType = backgroundType ?? page.backgroundType ?? 'color';
|
||||||
const effectiveBackgroundColor = backgroundColor ?? page.backgroundColor ?? '#ffffff';
|
const effectiveBackgroundColor = backgroundColor ?? page.backgroundColor ?? '#ffffff';
|
||||||
|
const effectiveBackgroundOpacity = backgroundOpacity ?? page.backgroundOpacity ?? 100;
|
||||||
const effectiveBackgroundGradient = backgroundGradient ?? page.backgroundGradient;
|
const effectiveBackgroundGradient = backgroundGradient ?? page.backgroundGradient;
|
||||||
const effectiveBackgroundImageUrl = backgroundImageUrl ?? page.backgroundImageUrl ?? '';
|
const effectiveBackgroundImageUrl = backgroundImageUrl ?? page.backgroundImageUrl ?? '';
|
||||||
const effectiveBackgroundVideoUrl = backgroundVideoUrl ?? page.backgroundVideoUrl ?? '';
|
const effectiveBackgroundVideoUrl = backgroundVideoUrl ?? page.backgroundVideoUrl ?? '';
|
||||||
@@ -211,11 +217,15 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blur shapes use backdrop-filter overlays (see blur stack below), not CSS filter on the shape.
|
||||||
|
if (isBlurBackdropObject(obj)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const objectFillStyle: React.CSSProperties =
|
const objectFillStyle: React.CSSProperties =
|
||||||
obj.fillType === 'gradient' && obj.gradient
|
obj.fillType === 'gradient' && obj.gradient
|
||||||
? { backgroundImage: toCssLinearGradient(obj.gradient) }
|
? { backgroundImage: toCssLinearGradient(obj.gradient) }
|
||||||
: { backgroundColor: obj.fill || 'transparent' };
|
: { backgroundColor: obj.fill || 'transparent' };
|
||||||
const shapeBlurStyle = getCssBlurStyle(obj.blur, scale);
|
|
||||||
|
|
||||||
switch (obj.type) {
|
switch (obj.type) {
|
||||||
case 'text': {
|
case 'text': {
|
||||||
@@ -535,7 +545,6 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
height: `${radius * 2}px`,
|
height: `${radius * 2}px`,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
...objectFillStyle,
|
...objectFillStyle,
|
||||||
...shapeBlurStyle,
|
|
||||||
border: hasStroke && obj.stroke
|
border: hasStroke && obj.stroke
|
||||||
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
|
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
|
||||||
: 'none',
|
: 'none',
|
||||||
@@ -608,7 +617,6 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
width: `${size}px`,
|
width: `${size}px`,
|
||||||
height: `${size}px`,
|
height: `${size}px`,
|
||||||
opacity: (obj.opacity ?? 100) / 100,
|
opacity: (obj.opacity ?? 100) / 100,
|
||||||
...shapeBlurStyle,
|
|
||||||
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
||||||
transformOrigin: 'center center',
|
transformOrigin: 'center center',
|
||||||
zIndex: index,
|
zIndex: index,
|
||||||
@@ -667,7 +675,6 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
height: `${radius * 2}px`,
|
height: `${radius * 2}px`,
|
||||||
clipPath: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
|
clipPath: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
|
||||||
...objectFillStyle,
|
...objectFillStyle,
|
||||||
...shapeBlurStyle,
|
|
||||||
border: obj.stroke
|
border: obj.stroke
|
||||||
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
|
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
|
||||||
: 'none',
|
: 'none',
|
||||||
@@ -690,7 +697,6 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
width: `${(obj.width || 100) * scale}px`,
|
width: `${(obj.width || 100) * scale}px`,
|
||||||
height: `${(obj.height || 100) * scale}px`,
|
height: `${(obj.height || 100) * scale}px`,
|
||||||
...objectFillStyle,
|
...objectFillStyle,
|
||||||
...shapeBlurStyle,
|
|
||||||
borderRadius: `${Math.max(0, (obj.borderRadius || 0) * scale)}px`,
|
borderRadius: `${Math.max(0, (obj.borderRadius || 0) * scale)}px`,
|
||||||
border: hasStroke && obj.stroke
|
border: hasStroke && obj.stroke
|
||||||
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
|
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
|
||||||
@@ -950,7 +956,10 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
key={cellId}
|
key={cellId}
|
||||||
style={{
|
style={{
|
||||||
border: stroke ? `${(strokeWidth || 1) * scale}px solid ${stroke}` : `${1 * scale}px solid #ccc`,
|
border: stroke ? `${(strokeWidth || 1) * scale}px solid ${stroke}` : `${1 * scale}px solid #ccc`,
|
||||||
backgroundColor: cell?.background || '#ffffff',
|
backgroundColor: getColorWithOpacity(
|
||||||
|
cell?.background || '#ffffff',
|
||||||
|
cell?.backgroundOpacity,
|
||||||
|
),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -1031,7 +1040,7 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
? {
|
? {
|
||||||
backgroundImage: toCssLinearGradient(effectiveBackgroundGradient),
|
backgroundImage: toCssLinearGradient(effectiveBackgroundGradient),
|
||||||
}
|
}
|
||||||
: { backgroundColor: effectiveBackgroundColor };
|
: { backgroundColor: getColorWithOpacity(effectiveBackgroundColor, effectiveBackgroundOpacity) };
|
||||||
|
|
||||||
const showLoadingOverlay = !hideMediaLoadingOverlay && !isMediaReady;
|
const showLoadingOverlay = !hideMediaLoadingOverlay && !isMediaReady;
|
||||||
|
|
||||||
@@ -1097,6 +1106,34 @@ const BookPage = memo(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))}
|
{page.elements.map((element, index) => renderObject(element, index, page.elements))}
|
||||||
|
{buildBlurStackEntries(page.elements).map((entry) => {
|
||||||
|
const blurIndex = page.elements.findIndex((el) => el.id === entry.blurObject.id);
|
||||||
|
const blurOverlayStyle = buildBlurOverlayStyle(entry.blurObject, scale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`blur-stack-${entry.key}`}
|
||||||
|
style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={withEntrance(
|
||||||
|
blurOverlayStyle,
|
||||||
|
entry.blurObject,
|
||||||
|
blurIndex >= 0 ? blurIndex : 0,
|
||||||
|
{
|
||||||
|
transformOrigin: blurOverlayStyle.transformOrigin,
|
||||||
|
rotationDeg: entry.blurObject.rotation ?? 0,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{entry.occlusionObjects.map((obj) => {
|
||||||
|
const index = page.elements.findIndex((el) => el.id === obj.id);
|
||||||
|
if (index < 0) return null;
|
||||||
|
return renderObject(obj, index, page.elements);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showLoadingOverlay && (
|
{showLoadingOverlay && (
|
||||||
|
|||||||
@@ -193,12 +193,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
);
|
);
|
||||||
const backgroundType = documentSettings?.backgroundType ?? "color";
|
const backgroundType = documentSettings?.backgroundType ?? "color";
|
||||||
const backgroundColor = documentSettings?.backgroundColor ?? "#ffffff";
|
const backgroundColor = documentSettings?.backgroundColor ?? "#ffffff";
|
||||||
|
const backgroundOpacity = documentSettings?.backgroundOpacity ?? 100;
|
||||||
const backgroundGradient = documentSettings?.backgroundGradient;
|
const backgroundGradient = documentSettings?.backgroundGradient;
|
||||||
const backgroundImageUrl = documentSettings?.backgroundImageUrl ?? "";
|
const backgroundImageUrl = documentSettings?.backgroundImageUrl ?? "";
|
||||||
const backgroundVideoUrl = documentSettings?.backgroundVideoUrl ?? "";
|
const backgroundVideoUrl = documentSettings?.backgroundVideoUrl ?? "";
|
||||||
const pageBackgroundDefaults = useMemo(
|
const pageBackgroundDefaults = useMemo(
|
||||||
() => ({ backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl }),
|
() => ({ backgroundType, backgroundColor, backgroundOpacity, backgroundGradient, backgroundImageUrl, backgroundVideoUrl }),
|
||||||
[backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl],
|
[backgroundType, backgroundColor, backgroundOpacity, backgroundGradient, backgroundImageUrl, backgroundVideoUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type PageData = {
|
|||||||
elements: EditorObject[];
|
elements: EditorObject[];
|
||||||
backgroundType?: "color" | "gradient" | "image" | "video";
|
backgroundType?: "color" | "gradient" | "image" | "video";
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: {
|
backgroundGradient?: {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type ViewerDataPage = {
|
|||||||
name: string;
|
name: string;
|
||||||
backgroundType?: "color" | "gradient" | "image" | "video";
|
backgroundType?: "color" | "gradient" | "image" | "video";
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: {
|
backgroundGradient?: {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
@@ -203,6 +204,7 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
|||||||
elements: objects,
|
elements: objects,
|
||||||
backgroundType: page.backgroundType,
|
backgroundType: page.backgroundType,
|
||||||
backgroundColor: page.backgroundColor,
|
backgroundColor: page.backgroundColor,
|
||||||
|
backgroundOpacity: page.backgroundOpacity,
|
||||||
backgroundGradient: page.backgroundGradient,
|
backgroundGradient: page.backgroundGradient,
|
||||||
backgroundImageUrl: page.backgroundImageUrl,
|
backgroundImageUrl: page.backgroundImageUrl,
|
||||||
backgroundVideoUrl: page.backgroundVideoUrl,
|
backgroundVideoUrl: page.backgroundVideoUrl,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { PageData } from "@/pages/viewer/types";
|
|||||||
export type PageBackgroundDefaults = {
|
export type PageBackgroundDefaults = {
|
||||||
backgroundType?: "color" | "gradient" | "image" | "video";
|
backgroundType?: "color" | "gradient" | "image" | "video";
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: { from: string; to: string; angle: number };
|
backgroundGradient?: { from: string; to: string; angle: number };
|
||||||
backgroundImageUrl?: string;
|
backgroundImageUrl?: string;
|
||||||
backgroundVideoUrl?: string;
|
backgroundVideoUrl?: string;
|
||||||
@@ -13,6 +14,7 @@ export function resolvePageBackground(page: PageData, defaults: PageBackgroundDe
|
|||||||
return {
|
return {
|
||||||
backgroundType: page.backgroundType ?? defaults.backgroundType ?? "color",
|
backgroundType: page.backgroundType ?? defaults.backgroundType ?? "color",
|
||||||
backgroundColor: page.backgroundColor ?? defaults.backgroundColor ?? "#ffffff",
|
backgroundColor: page.backgroundColor ?? defaults.backgroundColor ?? "#ffffff",
|
||||||
|
backgroundOpacity: page.backgroundOpacity ?? defaults.backgroundOpacity ?? 100,
|
||||||
backgroundGradient: page.backgroundGradient ?? defaults.backgroundGradient ?? { from: "#ffffff", to: "#ffffff", angle: 0 },
|
backgroundGradient: page.backgroundGradient ?? defaults.backgroundGradient ?? { from: "#ffffff", to: "#ffffff", angle: 0 },
|
||||||
backgroundImageUrl: page.backgroundImageUrl ?? defaults.backgroundImageUrl ?? "",
|
backgroundImageUrl: page.backgroundImageUrl ?? defaults.backgroundImageUrl ?? "",
|
||||||
backgroundVideoUrl: page.backgroundVideoUrl ?? defaults.backgroundVideoUrl ?? "",
|
backgroundVideoUrl: page.backgroundVideoUrl ?? defaults.backgroundVideoUrl ?? "",
|
||||||
|
|||||||
Reference in New Issue
Block a user