import type { ChangeEvent } from 'react'; import type { AppState, BinaryFiles, ExcalidrawImperativeAPI, } from '@excalidraw/excalidraw/types'; import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'; export type Drawing = { elements: readonly ExcalidrawElement[]; appState: AppState; files: BinaryFiles | null; }; const loadAnimateOptions = (): { pointerImg: string | undefined; pointerWidth: string | undefined; pointerHeight: string | undefined; } => { const hash = window.location.hash.slice(1); const searchParams = new URLSearchParams(hash); return { pointerImg: searchParams.get('pointerImg') || undefined, pointerWidth: searchParams.get('pointerWidth') || undefined, pointerHeight: searchParams.get('pointerHeight') || undefined, }; }; const saveAnimateOption = ( name: 'pointerImg' | 'pointerWidth' | 'pointerHeight', value: string, ) => { const hash = window.location.hash.slice(1); const searchParams = new URLSearchParams(hash); searchParams.set(name, value); window.location.hash = searchParams.toString(); }; const extractNumberFromId = (id: string, key: string) => { const match = id.match(new RegExp(`${key}:(-?\\d+)`)); return match === null ? undefined : Number(match[1]) || 0; }; const applyNumberInId = ( drawing: Drawing, ids: string[], key: string, value: number, ): Drawing => { const selectedElementIds = { ...drawing.appState.selectedElementIds }; const elements = drawing.elements.map((element) => { const { id } = element; if (!ids.includes(id)) { return element; } let newId: string; const match = id.match(new RegExp(`${key}:(-?\\d+)`)); if (match) { newId = id.replace(new RegExp(`${key}:(-?\\d+)`), `${key}:${value}`); } else { newId = id + `-${key}:${value}`; } if (id === newId) { return element; } selectedElementIds[newId] = selectedElementIds[id]; delete selectedElementIds[id]; return { ...element, id: newId }; }); return { elements, appState: { ...drawing.appState, selectedElementIds, }, files: drawing.files, }; }; export const AnimateConfig = ({ drawing, api, }: { drawing: Drawing; api: ExcalidrawImperativeAPI; }) => { const defaultAnimateOptions = loadAnimateOptions(); const selectedIds = Object.keys( drawing.appState.selectedElementIds ?? {}, ).filter( (id) => drawing.appState.selectedElementIds[id] && drawing.elements.some((element) => element.id === id), ); const animateOrderSet = new Set(); selectedIds.forEach((id) => { animateOrderSet.add(extractNumberFromId(id, 'animateOrder')); }); const onChangeAnimateOrder = (e: ChangeEvent) => { const value = Math.floor(Number(e.target.value)); if (Number.isFinite(value)) { api.updateScene( applyNumberInId(drawing, selectedIds, 'animateOrder', value), ); } }; const animateOrderDisabled = !animateOrderSet.size; const animateDurationSet = new Set(); selectedIds.forEach((id) => { animateDurationSet.add(extractNumberFromId(id, 'animateDuration')); }); const onChangeAnimateDuration = (e: ChangeEvent) => { const value = Math.floor(Number(e.target.value)); if (Number.isFinite(value)) { api.updateScene( applyNumberInId(drawing, selectedIds, 'animateDuration', value), ); } }; const animateDurationDisabled = !animateDurationSet.size; const onChangeAnimatePointerText = (e: ChangeEvent) => { saveAnimateOption('pointerImg', e.target.value); }; const onChangeAnimatePointerFile = (e: ChangeEvent) => { const file = e.target.files && e.target.files[0]; if (!file) { return; } const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === 'string') { saveAnimateOption('pointerImg', reader.result); } }; reader.readAsDataURL(file); }; const onChangeAnimatePointerWidth = (e: ChangeEvent) => { saveAnimateOption('pointerWidth', e.target.value); }; return (
{/* Animation Section */}
Animation
Order:
1 ? 'Mixed' : undefined} style={{ width: 80, minWidth: 50 }} title="Set animation order" />
Duration:
1 ? 'Mixed' : 'ms'} style={{ width: 80, minWidth: 50 }} title="Set animation duration in milliseconds" />
{/* Pointer Section */}
Pointer
Width:{' '} {' '}

* Values are set to the app’s default settings unless you change them.

); };