fix animation

This commit is contained in:
Hamid
2026-05-16 04:50:17 -07:00
parent 75c9a2515b
commit 0628340a5d
3 changed files with 88 additions and 86 deletions
+9 -3
View File
@@ -23,7 +23,7 @@ const Editor: FC = () => {
}, []) }, [])
const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false) const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false)
const [isInitialContentReady, setIsInitialContentReady] = useState(false) const [isInitialContentReady, setIsInitialContentReady] = useState(false)
const { loadPages, pages, documentSettings } = useEditorStore() const { loadPages, pages, documentSettings, objects, currentPageId } = useEditorStore()
const { data, isFetched } = useGetCatalogById(id!) const { data, isFetched } = useGetCatalogById(id!)
const { scheduleUpdate, isSaving } = useUpdateCatalog() const { scheduleUpdate, isSaving } = useUpdateCatalog()
@@ -51,13 +51,19 @@ const Editor: FC = () => {
if (!isInitialContentReady) return if (!isInitialContentReady) return
if (!pages || pages.length === 0) return if (!pages || pages.length === 0) return
const pagesToSave = currentPageId
? pages.map((p) =>
p.id === currentPageId ? { ...p, objects } : p,
)
: pages
scheduleUpdate({ scheduleUpdate({
id, id,
content: JSON.stringify({ pages, documentSettings }), content: JSON.stringify({ pages: pagesToSave, documentSettings }),
}) })
// عمداً scheduleUpdate را در وابستگی‌ها نیاوردیم تا از لوپ ذخیره‌سازی جلوگیری شود // عمداً scheduleUpdate را در وابستگی‌ها نیاوردیم تا از لوپ ذخیره‌سازی جلوگیری شود
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, isInitialContentReady, pages, documentSettings]) }, [id, isInitialContentReady, pages, documentSettings, objects, currentPageId])
return ( return (
@@ -1,6 +1,7 @@
import Select from "@/components/Select"; import Select from "@/components/Select";
import Input from "@/components/Input"; import Input from "@/components/Input";
import type { EditorObject } from "../../../store/editorStore"; import type { EditorObject } from "../../../store/editorStore";
import { useEditorStore } from "../../../store/editorStore";
import { ENTRANCE_ANIMATION_SELECT_ITEMS } from "../constants/entranceAnimationOptions"; import { ENTRANCE_ANIMATION_SELECT_ITEMS } from "../constants/entranceAnimationOptions";
import { import {
DEFAULT_ENTRANCE_DURATION_MS, DEFAULT_ENTRANCE_DURATION_MS,
@@ -13,10 +14,34 @@ type Props = {
onUpdate: (id: string, updates: Partial<EditorObject>) => void; onUpdate: (id: string, updates: Partial<EditorObject>) => void;
}; };
/** اعضای گروه (نه خود باکس group) برای تنظیم انیمیشن ورود */
function getAnimationTargets(
selectedObject: EditorObject,
objects: EditorObject[],
): EditorObject[] {
if (selectedObject.type === "group") {
return objects.filter(
(o) => o.groupId === selectedObject.id && o.type !== "group",
);
}
return [selectedObject];
}
const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => { const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => {
const anim = selectedObject.entranceAnimation ?? "none"; const objects = useEditorStore((s) => s.objects);
const duration = selectedObject.entranceDurationMs ?? DEFAULT_ENTRANCE_DURATION_MS; const targets = getAnimationTargets(selectedObject, objects);
const delayMs = selectedObject.entranceDelayMs ?? DEFAULT_ENTRANCE_DELAY_MS; const primary = targets[0] ?? selectedObject;
const anim = primary.entranceAnimation ?? "none";
const duration = primary.entranceDurationMs ?? DEFAULT_ENTRANCE_DURATION_MS;
const delayMs = primary.entranceDelayMs ?? DEFAULT_ENTRANCE_DELAY_MS;
const applyToTargets = (updates: Partial<EditorObject>) => {
const ids =
targets.length > 0 ? targets.map((t) => t.id) : [selectedObject.id];
ids.forEach((id) => onUpdate(id, updates));
};
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">
@@ -26,7 +51,7 @@ const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => {
value={anim} value={anim}
onChange={(e) => { onChange={(e) => {
const v = e.target.value as EditorObject["entranceAnimation"]; const v = e.target.value as EditorObject["entranceAnimation"];
onUpdate(selectedObject.id, { applyToTargets({
entranceAnimation: v === "none" ? undefined : v, entranceAnimation: v === "none" ? undefined : v,
...(v === "none" || !v ...(v === "none" || !v
? { entranceDurationMs: undefined, entranceDelayMs: undefined } ? { entranceDurationMs: undefined, entranceDelayMs: undefined }
@@ -35,19 +60,22 @@ const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => {
}} }}
/> />
{anim !== "none" && ( {anim !== "none" && (
<><Input <>
label="مدت انیمیشن (میلی‌ثانیه)" <Input
type="number" label="مدت انیمیشن (میلی‌ثانیه)"
min={100} type="number"
max={4000} min={100}
value={duration} max={4000}
onChange={(e) => { value={duration}
const n = parseInt(e.target.value, 10); onChange={(e) => {
if (Number.isNaN(n)) return; const n = parseInt(e.target.value, 10);
onUpdate(selectedObject.id, { if (Number.isNaN(n)) return;
entranceDurationMs: Math.min(4000, Math.max(100, n)), applyToTargets({
}); entranceDurationMs: Math.min(4000, Math.max(100, n)),
}} /><Input });
}}
/>
<Input
label="تأخیر قبل از شروع (میلی‌ثانیه)" label="تأخیر قبل از شروع (میلی‌ثانیه)"
type="number" type="number"
min={0} min={0}
@@ -57,13 +85,15 @@ const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => {
const n = parseInt(e.target.value, 10); const n = parseInt(e.target.value, 10);
if (Number.isNaN(n)) return; if (Number.isNaN(n)) return;
const clamped = Math.min(MAX_ENTRANCE_DELAY_MS, Math.max(0, n)); const clamped = Math.min(MAX_ENTRANCE_DELAY_MS, Math.max(0, n));
onUpdate(selectedObject.id, { applyToTargets({
entranceDelayMs: clamped === 0 ? undefined : clamped, entranceDelayMs: clamped === 0 ? undefined : clamped,
}); });
}} /></> }}
/>
</>
)} )}
</div> </div>
); );
}; };
export default EntranceAnimationSettings; export default EntranceAnimationSettings;
+29 -63
View File
@@ -30,6 +30,22 @@ const cloneObjectList = (objects: EditorObject[]): EditorObject[] =>
const objectListsEqual = (a: EditorObject[], b: EditorObject[]) => const objectListsEqual = (a: EditorObject[], b: EditorObject[]) =>
JSON.stringify(a) === JSON.stringify(b); JSON.stringify(a) === JSON.stringify(b);
/** یک آرایهٔ objects برای state و صفحهٔ جاری در pages (جلوگیری از از دست رفتن فیلدهایی مثل entranceAnimation) */
const withSyncedCurrentPageObjects = (
state: { objects: EditorObject[]; pages: Page[]; currentPageId: string | null },
nextObjects: EditorObject[],
) => {
if (!state.currentPageId) {
return { objects: nextObjects };
}
return {
objects: nextObjects,
pages: state.pages.map((p) =>
p.id === state.currentPageId ? { ...p, objects: nextObjects } : p,
),
};
};
let objectGestureSnapshot: EditorObject[] | null = null; let objectGestureSnapshot: EditorObject[] | null = null;
export type { export type {
@@ -248,7 +264,6 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
}); });
}, },
beginObjectGesture: () => { beginObjectGesture: () => {
if (objectGestureSnapshot !== null) return;
objectGestureSnapshot = cloneObjectList(get().objects); objectGestureSnapshot = cloneObjectList(get().objects);
}, },
endObjectGesture: () => { endObjectGesture: () => {
@@ -336,12 +351,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
if (i === -1 || i >= objs.length - 1) return; if (i === -1 || i >= objs.length - 1) return;
[objs[i], objs[i + 1]] = [objs[i + 1]!, objs[i]!]; [objs[i], objs[i + 1]] = [objs[i + 1]!, objs[i]!];
if (state.currentPageId) { if (state.currentPageId) {
set({ set(withSyncedCurrentPageObjects(state, objs));
objects: objs,
pages: state.pages.map((p) =>
p.id === state.currentPageId ? { ...p, objects: objs } : p,
),
});
return; return;
} }
set({ objects: objs }); set({ objects: objs });
@@ -354,12 +364,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
if (i <= 0) return; if (i <= 0) return;
[objs[i], objs[i - 1]] = [objs[i - 1]!, objs[i]!]; [objs[i], objs[i - 1]] = [objs[i - 1]!, objs[i]!];
if (state.currentPageId) { if (state.currentPageId) {
set({ set(withSyncedCurrentPageObjects(state, objs));
objects: objs,
pages: state.pages.map((p) =>
p.id === state.currentPageId ? { ...p, objects: objs } : p,
),
});
return; return;
} }
set({ objects: objs }); set({ objects: objs });
@@ -398,30 +403,12 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
set({ guides }); set({ guides });
}, },
updateObject: (id, updates) => { updateObject: (id, updates) => {
const state = get(); set((state) => {
if (state.currentPageId) { const nextObjects = state.objects.map((obj) =>
set((state) => ({
objects: state.objects.map((obj) =>
obj.id === id ? { ...obj, ...updates } : obj,
),
pages: state.pages.map((p) =>
p.id === state.currentPageId
? {
...p,
objects: p.objects.map((obj) =>
obj.id === id ? { ...obj, ...updates } : obj,
),
}
: p,
),
}));
return;
}
set((state) => ({
objects: state.objects.map((obj) =>
obj.id === id ? { ...obj, ...updates } : obj, obj.id === id ? { ...obj, ...updates } : obj,
), );
})); return withSyncedCurrentPageObjects(state, nextObjects);
});
}, },
deleteObject: (id, options) => { deleteObject: (id, options) => {
if (!options?.skipHistory) { if (!options?.skipHistory) {
@@ -535,17 +522,12 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
get().commitObjectHistoryBeforeChange(); get().commitObjectHistoryBeforeChange();
const state = get(); const state = get();
if (state.currentPageId) { if (state.currentPageId) {
set((state) => ({ set((state) =>
objects: reorderByIds(state.objects, draggedId, targetId), withSyncedCurrentPageObjects(
pages: state.pages.map((page) => state,
page.id === state.currentPageId reorderByIds(state.objects, draggedId, targetId),
? {
...page,
objects: reorderByIds(page.objects, draggedId, targetId),
}
: page,
), ),
})); );
return; return;
} }
@@ -962,24 +944,8 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
if (!result) return; if (!result) return;
if (state.currentPageId) { if (state.currentPageId) {
const pageToGroup = state.pages.find(
(p) => p.id === state.currentPageId,
);
if (!pageToGroup) return;
const pageResult = buildGroupResult(
pageToGroup.objects,
objectIds,
result.groupId,
);
if (!pageResult) return;
set((state) => ({ set((state) => ({
objects: result.nextObjects, ...withSyncedCurrentPageObjects(state, result.nextObjects),
pages: state.pages.map((p) =>
p.id === state.currentPageId
? { ...p, objects: pageResult.nextObjects }
: p,
),
selectedObjectId: result.groupId, selectedObjectId: result.groupId,
selectedObjectIds: [result.groupId], selectedObjectIds: [result.groupId],
})); }));