base animation

This commit is contained in:
hamid zarghami
2026-05-12 15:19:44 +03:30
parent 8cfe559fb6
commit 6a8eaab8cb
13 changed files with 548 additions and 122 deletions
+208 -122
View File
@@ -1,7 +1,10 @@
import { forwardRef } from 'react';
import { forwardRef, useCallback } from 'react';
import { type PageData } from '../types';
import type { EditorObject } from '@/pages/editor/store/editorStore';
import { toCssLinearGradient } from '@/pages/editor/utils/gradient';
import '@/pages/viewer/styles/entranceAnimations.css';
import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle';
import { useBookPageEntranceSequence } from '@/pages/viewer/hooks/useBookPageEntranceSequence';
type BookPageProps = {
page: PageData;
@@ -17,6 +20,8 @@ type BookPageProps = {
angle: number;
};
backgroundImageUrl?: string;
/** در پیش‌نمایش کاتالوگ انیمیشن ورود غیرفعال باشد */
disableEntranceAnimations?: boolean;
};
/**
@@ -27,7 +32,20 @@ type BookPageProps = {
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
*/
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl }, ref) => {
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, disableEntranceAnimations = false }, ref) => {
const { attachRootRef, entranceSeq } = useBookPageEntranceSequence(disableEntranceAnimations);
const setHostRef = useCallback(
(node: HTMLDivElement | null) => {
attachRootRef(node);
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
}
},
[ref, attachRootRef],
);
// تابع برای تبدیل opacity به رنگ (مثل editor)
// در editor، getColorWithOpacity انتظار opacity 0-100 دارد
// در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است
@@ -57,6 +75,25 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
const flyLayoutPx = {
width: pageWidth ?? 794 * scale,
height: pageHeight ?? 1123 * scale,
};
const withEntrance = (
style: React.CSSProperties,
obj: EditorObject,
index: number,
extra?: {
transformOrigin?: React.CSSProperties['transformOrigin'];
rotationDeg?: number;
},
) =>
mergeEntranceAnimationStyle(style, obj, scale, index, entranceSeq, {
...extra,
flyLayoutPx,
});
const renderObject = (obj: EditorObject, index: number) => {
const actualStrokeWidth = obj.strokeWidth ?? 0;
const hasStroke = actualStrokeWidth > 0;
@@ -90,21 +127,25 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return (
<div
key={obj.id || index}
style={{
...textBaseStyle,
left: `${textLeft}px`,
width: textWidth ? `${textWidth}px` : undefined,
fontSize: `${(obj.fontSize || 16) * scale}px`,
fontFamily: obj.fontFamily || 'irancell, sans-serif',
fontWeight: obj.fontWeight || 'normal',
lineHeight: obj.lineHeight ?? 1.2,
textAlign: obj.textAlign ?? 'right',
direction: 'rtl',
color: getColorWithOpacity(obj.fill, obj.opacity),
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
}}
style={withEntrance(
{
...textBaseStyle,
left: `${textLeft}px`,
width: textWidth ? `${textWidth}px` : undefined,
fontSize: `${(obj.fontSize || 16) * scale}px`,
fontFamily: obj.fontFamily || 'irancell, sans-serif',
fontWeight: obj.fontWeight || 'normal',
lineHeight: obj.lineHeight ?? 1.2,
textAlign: obj.textAlign ?? 'right',
direction: 'rtl',
color: getColorWithOpacity(obj.fill, obj.opacity),
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
},
obj,
index,
)}
>
{obj.text || ''}
</div>
@@ -117,13 +158,17 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
key={obj.id || index}
src={obj.imageUrl || ''}
alt=""
style={{
...baseStyle,
width: obj.width ? `${obj.width * scale}px` : 'auto',
height: obj.height ? `${obj.height * scale}px` : 'auto',
objectFit: 'fill', // مشابه Konva که image را با width و height مشخص شده رندر می‌کند
zIndex: index, // برای حفظ ترتیب رندر
}}
style={withEntrance(
{
...baseStyle,
width: obj.width ? `${obj.width * scale}px` : 'auto',
height: obj.height ? `${obj.height * scale}px` : 'auto',
objectFit: 'fill', // مشابه Konva که image را با width و height مشخص شده رندر می‌کند
zIndex: index, // برای حفظ ترتیب رندر
},
obj,
index,
)}
/>
);
@@ -133,14 +178,18 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
key={obj.id || index}
src={obj.videoUrl || ''}
controls
style={{
...baseStyle,
width: obj.width ? `${obj.width * scale}px` : 'auto',
height: obj.height ? `${obj.height * scale}px` : 'auto',
objectFit: 'contain',
zIndex: index,
pointerEvents: 'auto',
}}
style={withEntrance(
{
...baseStyle,
width: obj.width ? `${obj.width * scale}px` : 'auto',
height: obj.height ? `${obj.height * scale}px` : 'auto',
objectFit: 'contain',
zIndex: index,
pointerEvents: 'auto',
},
obj,
index,
)}
onClick={(e) => {
e.stopPropagation();
}}
@@ -175,15 +224,19 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
<button
key={obj.id || index}
type="button"
style={{
...linkStyle,
zIndex: 9999,
pointerEvents: 'auto',
border: 'none',
background: 'none',
padding: 0,
cursor: 'pointer',
}}
style={withEntrance(
{
...linkStyle,
zIndex: 9999,
pointerEvents: 'auto',
border: 'none',
background: 'none',
padding: 0,
cursor: 'pointer',
},
obj,
index,
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -209,11 +262,15 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
href={obj.linkUrl || '#'}
target="_blank"
rel="noopener noreferrer"
style={{
...linkStyle,
zIndex: 9999,
pointerEvents: 'auto',
}}
style={withEntrance(
{
...linkStyle,
zIndex: 9999,
pointerEvents: 'auto',
},
obj,
index,
)}
onClick={(e) => {
e.stopPropagation();
}}
@@ -238,23 +295,28 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return (
<div
key={obj.id || index}
style={{
position: 'absolute',
left: `${centerX - radius}px`,
top: `${centerY - radius}px`,
width: `${radius * 2}px`,
height: `${radius * 2}px`,
borderRadius: '50%',
...objectFillStyle,
border: hasStroke && obj.stroke
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
: 'none',
boxSizing: 'border-box',
opacity: (obj.opacity ?? 100) / 100,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
zIndex: index, // برای حفظ ترتیب رندر
}}
style={withEntrance(
{
position: 'absolute',
left: `${centerX - radius}px`,
top: `${centerY - radius}px`,
width: `${radius * 2}px`,
height: `${radius * 2}px`,
borderRadius: '50%',
...objectFillStyle,
border: hasStroke && obj.stroke
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
: 'none',
boxSizing: 'border-box',
opacity: (obj.opacity ?? 100) / 100,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
zIndex: index, // برای حفظ ترتیب رندر
},
obj,
index,
{ transformOrigin: 'center center' },
)}
/>
);
}
@@ -296,18 +358,23 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return (
<svg
key={obj.id || index}
style={{
position: 'absolute',
left: `${centerX - radius}px`,
top: `${centerY - radius}px`,
width: `${size}px`,
height: `${size}px`,
opacity: (obj.opacity ?? 100) / 100,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
zIndex: index,
overflow: 'visible',
}}
style={withEntrance(
{
position: 'absolute',
left: `${centerX - radius}px`,
top: `${centerY - radius}px`,
width: `${size}px`,
height: `${size}px`,
opacity: (obj.opacity ?? 100) / 100,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
zIndex: index,
overflow: 'visible',
},
obj,
index,
{ transformOrigin: 'center center' },
)}
viewBox={`0 0 ${size} ${size}`}
>
{obj.fillType === 'gradient' && obj.gradient ? (
@@ -349,22 +416,27 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return (
<div
key={obj.id || index}
style={{
position: 'absolute',
left: `${centerX - radius}px`,
top: `${centerY - radius}px`,
width: `${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%)',
...objectFillStyle,
border: obj.stroke
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
: 'none',
boxSizing: 'border-box',
opacity: (obj.opacity ?? 100) / 100,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
}}
style={withEntrance(
{
position: 'absolute',
left: `${centerX - radius}px`,
top: `${centerY - radius}px`,
width: `${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%)',
...objectFillStyle,
border: obj.stroke
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
: 'none',
boxSizing: 'border-box',
opacity: (obj.opacity ?? 100) / 100,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
},
obj,
index,
{ transformOrigin: 'center center' },
)}
/>
);
}
@@ -383,7 +455,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
zIndex: index, // برای حفظ ترتیب رندر
};
return <div key={obj.id || index} style={shapeStyle} />;
return <div key={obj.id || index} style={withEntrance(shapeStyle, obj, index)} />;
}
case 'line': {
@@ -402,17 +474,22 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return (
<div
key={obj.id || index}
style={{
position: 'absolute',
left: `${startX}px`,
top: `${startY}px`,
width: `${lineLength}px`,
height: `${(obj.strokeWidth || 2) * scale}px`,
backgroundColor: obj.stroke || '#000000',
opacity: (obj.opacity ?? 100) / 100,
transform: `rotate(${lineAngle}deg)`,
transformOrigin: 'left center',
}}
style={withEntrance(
{
position: 'absolute',
left: `${startX}px`,
top: `${startY}px`,
width: `${lineLength}px`,
height: `${(obj.strokeWidth || 2) * scale}px`,
backgroundColor: obj.stroke || '#000000',
opacity: (obj.opacity ?? 100) / 100,
transform: `rotate(${lineAngle}deg)`,
transformOrigin: 'left center',
},
obj,
index,
{ rotationDeg: lineAngle, transformOrigin: 'left center' },
)}
/>
);
}
@@ -434,17 +511,22 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return (
<div
key={obj.id || index}
style={{
position: 'absolute',
left: `${startX}px`,
top: `${startY}px`,
width: `${arrowLength}px`,
height: `${strokeWidth}px`,
backgroundColor: obj.stroke || '#000000',
opacity: (obj.opacity ?? 100) / 100,
transform: `rotate(${arrowAngle}deg)`,
transformOrigin: 'left center',
}}
style={withEntrance(
{
position: 'absolute',
left: `${startX}px`,
top: `${startY}px`,
width: `${arrowLength}px`,
height: `${strokeWidth}px`,
backgroundColor: obj.stroke || '#000000',
opacity: (obj.opacity ?? 100) / 100,
transform: `rotate(${arrowAngle}deg)`,
transformOrigin: 'left center',
},
obj,
index,
{ rotationDeg: arrowAngle, transformOrigin: 'left center' },
)}
>
<div
style={{
@@ -472,13 +554,17 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return (
<div
key={obj.id || index}
style={{
...baseStyle,
display: 'grid',
gridTemplateRows: `repeat(${rows}, ${cellHeight * scale}px)`,
gridTemplateColumns: `repeat(${cols}, ${cellWidth * scale}px)`,
border: stroke ? `${(strokeWidth || 1) * scale}px solid ${stroke}` : 'none',
}}
style={withEntrance(
{
...baseStyle,
display: 'grid',
gridTemplateRows: `repeat(${rows}, ${cellHeight * scale}px)`,
gridTemplateColumns: `repeat(${cols}, ${cellWidth * scale}px)`,
border: stroke ? `${(strokeWidth || 1) * scale}px solid ${stroke}` : 'none',
},
obj,
index,
)}
>
{Array.from({ length: rows }).map((_, rowIndex) =>
Array.from({ length: cols }).map((_, colIndex) => {
@@ -536,7 +622,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
return (
<div
ref={ref}
ref={setHostRef}
dir="rtl"
className="page"
data-density="soft"