From 6a8eaab8cbe84b95e8ed5daff5c1ed379fec8b38 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Tue, 12 May 2026 15:19:44 +0330 Subject: [PATCH] base animation --- .../catalogue/components/CatalogPreview.tsx | 1 + .../components/sidebar/ObjectSettings.tsx | 5 + .../constants/entranceAnimationOptions.ts | 12 + .../settings/EntranceAnimationSettings.tsx | 50 +++ .../components/sidebar/settings/index.ts | 1 + src/pages/editor/store/editorStore.ts | 1 + src/pages/editor/store/editorStore.types.ts | 15 + src/pages/viewer/components/BookPage.tsx | 330 +++++++++++------- .../hooks/useBookPageEntranceSequence.ts | 56 +++ .../viewer/styles/entranceAnimations.css | 76 ++++ src/pages/viewer/utils/dataTransformer.ts | 9 + .../viewer/utils/entranceAnimationStyle.ts | 109 ++++++ src/shared/entranceAnimation.ts | 5 + 13 files changed, 548 insertions(+), 122 deletions(-) create mode 100644 src/pages/editor/components/sidebar/constants/entranceAnimationOptions.ts create mode 100644 src/pages/editor/components/sidebar/settings/EntranceAnimationSettings.tsx create mode 100644 src/pages/viewer/hooks/useBookPageEntranceSequence.ts create mode 100644 src/pages/viewer/styles/entranceAnimations.css create mode 100644 src/pages/viewer/utils/entranceAnimationStyle.ts create mode 100644 src/shared/entranceAnimation.ts diff --git a/src/pages/catalogue/components/CatalogPreview.tsx b/src/pages/catalogue/components/CatalogPreview.tsx index 6574323..ef32795 100644 --- a/src/pages/catalogue/components/CatalogPreview.tsx +++ b/src/pages/catalogue/components/CatalogPreview.tsx @@ -73,6 +73,7 @@ const CatalogPreview: FC = ({ item }) => { scale={1} pageWidth={pageWidth} pageHeight={pageHeight} + disableEntranceAnimations /> diff --git a/src/pages/editor/components/sidebar/ObjectSettings.tsx b/src/pages/editor/components/sidebar/ObjectSettings.tsx index d9a31b3..5ccebc1 100644 --- a/src/pages/editor/components/sidebar/ObjectSettings.tsx +++ b/src/pages/editor/components/sidebar/ObjectSettings.tsx @@ -11,6 +11,7 @@ import { TransformSettings, GridSettings, MaskSettings, + EntranceAnimationSettings, } from "./settings"; import TextInstruction from "./instructions/TextInstruction"; @@ -117,6 +118,10 @@ const ObjectSettings = ({ )} + diff --git a/src/pages/editor/components/sidebar/constants/entranceAnimationOptions.ts b/src/pages/editor/components/sidebar/constants/entranceAnimationOptions.ts new file mode 100644 index 0000000..cecf5be --- /dev/null +++ b/src/pages/editor/components/sidebar/constants/entranceAnimationOptions.ts @@ -0,0 +1,12 @@ +import type { ItemsSelectType } from "@/components/Select"; + +export const ENTRANCE_ANIMATION_SELECT_ITEMS: ItemsSelectType[] = [ + { value: "none", label: "بدون انیمیشن" }, + { value: "fade", label: "محو شدن (Fade)" }, + { value: "flyInLeft", label: "ورود از چپ" }, + { value: "flyInRight", label: "ورود از راست" }, + { value: "flyInTop", label: "ورود از بالا" }, + { value: "flyInBottom", label: "ورود از پایین" }, + { value: "zoomIn", label: "بزرگ‌نمایی (Zoom in)" }, + { value: "zoomOut", label: "ریزنمایی (Zoom out)" }, +]; diff --git a/src/pages/editor/components/sidebar/settings/EntranceAnimationSettings.tsx b/src/pages/editor/components/sidebar/settings/EntranceAnimationSettings.tsx new file mode 100644 index 0000000..86c4152 --- /dev/null +++ b/src/pages/editor/components/sidebar/settings/EntranceAnimationSettings.tsx @@ -0,0 +1,50 @@ +import Select from "@/components/Select"; +import Input from "@/components/Input"; +import type { EditorObject } from "../../../store/editorStore"; +import { ENTRANCE_ANIMATION_SELECT_ITEMS } from "../constants/entranceAnimationOptions"; +import { DEFAULT_ENTRANCE_DURATION_MS } from "@/shared/entranceAnimation"; + +type Props = { + selectedObject: EditorObject; + onUpdate: (id: string, updates: Partial) => void; +}; + +const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => { + const anim = selectedObject.entranceAnimation ?? "none"; + const duration = selectedObject.entranceDurationMs ?? DEFAULT_ENTRANCE_DURATION_MS; + + return ( +
+ { + const n = parseInt(e.target.value, 10); + if (Number.isNaN(n)) return; + onUpdate(selectedObject.id, { + entranceDurationMs: Math.min(4000, Math.max(100, n)), + }); + }} + /> + )} +
+ ); +}; + +export default EntranceAnimationSettings; diff --git a/src/pages/editor/components/sidebar/settings/index.ts b/src/pages/editor/components/sidebar/settings/index.ts index cd7c37a..843d7e4 100644 --- a/src/pages/editor/components/sidebar/settings/index.ts +++ b/src/pages/editor/components/sidebar/settings/index.ts @@ -8,4 +8,5 @@ export { default as AlignmentSettings } from "./AlignmentSettings"; export { default as TransformSettings } from "./TransformSettings"; export { default as GridSettings } from "./GridSettings"; export { default as MaskSettings } from "./MaskSettings"; +export { default as EntranceAnimationSettings } from "./EntranceAnimationSettings"; diff --git a/src/pages/editor/store/editorStore.ts b/src/pages/editor/store/editorStore.ts index 3417c29..3006922 100644 --- a/src/pages/editor/store/editorStore.ts +++ b/src/pages/editor/store/editorStore.ts @@ -42,6 +42,7 @@ export type { DocumentSettings, DisplayStyle, BackgroundType, + EntranceAnimationType, } from "./editorStore.types"; type PageBackgroundSettings = Pick< diff --git a/src/pages/editor/store/editorStore.types.ts b/src/pages/editor/store/editorStore.types.ts index 39b4031..1c3e0a9 100644 --- a/src/pages/editor/store/editorStore.types.ts +++ b/src/pages/editor/store/editorStore.types.ts @@ -1,6 +1,17 @@ import type { ShapeType } from "./shapeStore"; import type { FillType, LinearGradient } from "../utils/gradient"; +/** انیمیشن ورود (نمایش) شیء در viewer، مشابه PowerPoint */ +export type EntranceAnimationType = + | "none" + | "fade" + | "flyInLeft" + | "flyInRight" + | "flyInTop" + | "flyInBottom" + | "zoomIn" + | "zoomOut"; + export type ToolType = | "select" | "rectangle" @@ -70,6 +81,10 @@ export type EditorObject = { maskId?: string; maskInvert?: boolean; groupId?: string; + /** نحوهٔ ظاهر شدن در نمایشگر؛ پیش‌فرض بدون انیمیشن */ + entranceAnimation?: EntranceAnimationType; + /** مدت انیمیشن ورود به میلی‌ثانیه (پیش‌فرض ۶۰۰) */ + entranceDurationMs?: number; }; export type PageGuide = { diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx index e1a9b11..38fb2a3 100644 --- a/src/pages/viewer/components/BookPage.tsx +++ b/src/pages/viewer/components/BookPage.tsx @@ -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( - ({ 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).current = node; + } + }, + [ref, attachRootRef], + ); // تابع برای تبدیل opacity به رنگ (مثل editor) // در editor، getColorWithOpacity انتظار opacity 0-100 دارد // در dataTransformer، opacity از 0-1 به 0-100 تبدیل شده است @@ -57,6 +75,25 @@ const BookPage = forwardRef( 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( return (
{obj.text || ''}
@@ -117,13 +158,17 @@ const BookPage = forwardRef( 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( 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(