import { ChangeEventHandler, FocusEventHandler, FormEventHandler, MouseEventHandler, useEffect, useId, useRef, useState, } from 'react'; import { getFormValues, setFormValues } from '@theoryofnekomata/formxtra'; import { Button } from '~/components/Button'; import {FileButton} from '~/components/FileButton'; type SvgGroup = { id: string; name: string; } type KeyframeAttribute = { group: SvgGroup; translateX: number; translateY: number; rotate: number; } type Keyframe = { time: number; attributes: KeyframeAttribute[]; } const useAnimationForm = () => { const [loadedFileName, setLoadedFileName] = useState(); const [loadedFileContents, setLoadedFileContents] = useState(); const [svgAnimations, setSvgAnimations] = useState(); const [svgGroups, setSvgGroups] = useState(); const [isNewAnimationId, setIsNewAnimationId] = useState(); const [currentAnimationId, setCurrentAnimationId] = useState(); const [enabledGroupIds, setEnabledGroupIds] = useState(); const [savedSvgKeyframes, setSavedSvgKeyframes] = useState(); const svgAnimationIdsListId = useId(); const svgPreviewRef = useRef(null); const readFile = async (file: File) => { const fileReader = new FileReader() return new Promise((resolve, reject) => { fileReader.addEventListener('load', (e) => { if (!e.target) { reject(new Error('Invalid target')) return; } resolve(e.target.result as string); }) fileReader.addEventListener('error', () => { reject(new Error('Error reading file')) }); fileReader.readAsText(file); }); } const loadSvgFile: ChangeEventHandler = async (e) => { const filePicker = e.currentTarget; if (!filePicker.files) { return; } const [file] = Array.from(filePicker.files); if (!file) { return; } setLoadedFileName(file.name); setIsNewAnimationId(false); const result = await readFile(file); setLoadedFileContents(result); filePicker.value = '' } const handleAnimationIdInput: FormEventHandler = (e) => { const nativeEvent = e.nativeEvent as unknown as { inputType?: string }; switch (nativeEvent.inputType) { case 'deleteContentBackward': case 'deleteContentForward': case 'insertText': // we type into the input setIsNewAnimationId(true); break; default: // we select from the datalist const target = e.currentTarget; setIsNewAnimationId(false); setCurrentAnimationId(e.currentTarget.value); setTimeout(() => { target.blur(); }); } } const cancelAnimationIdChange: MouseEventHandler = (e) => { e.preventDefault(); setIsNewAnimationId(false); setFormValues(e.currentTarget.form!, { animationId: currentAnimationId, }) } useEffect(() => { if (!loadedFileContents) { return } if (!svgPreviewRef.current) { return } const svgPreviewRoot = svgPreviewRef.current.children[0]; setSvgAnimations(['default']); setSvgGroups( Array .from(svgPreviewRoot.children) .map((g) => ({ id: g.id, name: g.id, })) ); setCurrentAnimationId('default'); setEnabledGroupIds([]); // TODO - compute enabled groupIds based on current animation id setSavedSvgKeyframes([]); // TODO - load keyframes from file }, [loadedFileContents, svgPreviewRef.current]); const focusAnimationIdInput: FocusEventHandler = (e) => { e.currentTarget.value = ''; } const blurAnimationIdInput: FocusEventHandler = (e) => { e.currentTarget.value = currentAnimationId as string; setIsNewAnimationId(false); } const addKeyframeAttribute: ChangeEventHandler = (e) => { const { currentTarget } = e; if (currentTarget.checked) { setEnabledGroupIds((oldEnabledGroupIds = []) => [...oldEnabledGroupIds, currentTarget.value]); return } setEnabledGroupIds((oldEnabledGroupIds = []) => oldEnabledGroupIds.filter((i) => i !== currentTarget.value)); } const translateXGroup = (group: SvgGroup): ChangeEventHandler => (e) => { const { current: svgPreviewWrapper } = svgPreviewRef; if (!svgPreviewWrapper) { return; } const svgPreviewRoot = svgPreviewWrapper.children[0] as SVGSVGElement | null; if (!svgPreviewRoot) { return; } const targetGroup = Array.from(svgPreviewRoot.children).find(g => g.id === group.id) as SVGGraphicsElement; if (!targetGroup) { return; } const rotationCenterReference = Array.from(targetGroup.children).find(e => e.tagName === 'circle') as SVGCircleElement | undefined const rotationCenter = { cx: rotationCenterReference?.getAttribute?.('cx') ?? 0, cy: rotationCenterReference?.getAttribute?.('cy') ?? 0, }; const values = getFormValues(e.currentTarget.form!, { forceNumberValues: true }); const changed = Array.isArray(values.changed) ? values.changed : [values.changed]; const changedIndex = changed.indexOf(group.id); const rotate = Array.isArray(values.rotate) ? values.rotate : [values.rotate]; const translateY = Array.isArray(values.translateY) ? values.translateY : [values.translateY]; const currentRotate = rotate[changedIndex]; const currentTranslateX = e.currentTarget.value; const currentTranslateY = translateY[changedIndex]; targetGroup.setAttribute('transform', `translate(${currentTranslateX} ${currentTranslateY}) rotate(${currentRotate} ${rotationCenter.cx} ${rotationCenter.cy})`); } const translateYGroup = (group: SvgGroup): ChangeEventHandler => (e) => { const { current: svgPreviewWrapper } = svgPreviewRef; if (!svgPreviewWrapper) { return; } const svgPreviewRoot = svgPreviewWrapper.children[0] as SVGSVGElement | null; if (!svgPreviewRoot) { return; } const targetGroup = Array.from(svgPreviewRoot.children).find(g => g.id === group.id) as SVGGraphicsElement; if (!targetGroup) { return; } const rotationCenterReference = Array.from(targetGroup.children).find(e => e.tagName === 'circle') as SVGCircleElement | undefined const rotationCenter = { cx: rotationCenterReference?.getAttribute?.('cx') ?? 0, cy: rotationCenterReference?.getAttribute?.('cy') ?? 0, }; const values = getFormValues(e.currentTarget.form!, { forceNumberValues: true }); const changed = Array.isArray(values.changed) ? values.changed : [values.changed]; const changedIndex = changed.indexOf(group.id); const rotate = Array.isArray(values.rotate) ? values.rotate : [values.rotate]; const translateX = Array.isArray(values.translateX) ? values.translateX : [values.translateX]; const currentRotate = rotate[changedIndex]; const currentTranslateX = translateX[changedIndex]; const currentTranslateY = e.currentTarget.value; targetGroup.setAttribute('transform', `translate(${currentTranslateX} ${currentTranslateY}) rotate(${currentRotate} ${rotationCenter.cx} ${rotationCenter.cy})`); } const rotateGroup = (group: SvgGroup): ChangeEventHandler => (e) => { const { current: svgPreviewWrapper } = svgPreviewRef; if (!svgPreviewWrapper) { return; } const svgPreviewRoot = svgPreviewWrapper.children[0] as SVGSVGElement | null; if (!svgPreviewRoot) { return; } const targetGroup = Array.from(svgPreviewRoot.children).find(g => g.id === group.id) as SVGGraphicsElement; if (!targetGroup) { return; } const rotationCenterReference = Array.from(targetGroup.children).find(e => e.tagName === 'circle') as SVGCircleElement | undefined const rotationCenter = { cx: rotationCenterReference?.getAttribute?.('cx') ?? 0, cy: rotationCenterReference?.getAttribute?.('cy') ?? 0, }; const values = getFormValues(e.currentTarget.form!, { forceNumberValues: true }); setFormValues(e.currentTarget.form!, { rotateNumber: values.rotate }); const changed = Array.isArray(values.changed) ? values.changed : [values.changed]; const changedIndex = changed.indexOf(group.id); const translateX = Array.isArray(values.translateX) ? values.translateX : [values.translateX]; const translateY = Array.isArray(values.translateY) ? values.translateY : [values.translateY]; const currentRotate = e.currentTarget.value; const currentTranslateX = translateX[changedIndex]; const currentTranslateY = translateY[changedIndex]; targetGroup.setAttribute('transform', `translate(${currentTranslateX} ${currentTranslateY}) rotate(${currentRotate} ${rotationCenter.cx} ${rotationCenter.cy})`); } const rotateGroupThroughNumber = (group: SvgGroup): ChangeEventHandler => (e) => { const { current: svgPreviewWrapper } = svgPreviewRef; if (!svgPreviewWrapper) { return; } const svgPreviewRoot = svgPreviewWrapper.children[0] as SVGSVGElement | null; if (!svgPreviewRoot) { return; } const targetGroup = Array.from(svgPreviewRoot.children).find(g => g.id === group.id) as SVGGraphicsElement; if (!targetGroup) { return; } const rotationCenterReference = Array.from(targetGroup.children).find(e => e.tagName === 'circle') as SVGCircleElement | undefined const rotationCenter = { cx: rotationCenterReference?.getAttribute?.('cx') ?? 0, cy: rotationCenterReference?.getAttribute?.('cy') ?? 0, }; const values = getFormValues(e.currentTarget.form!, { forceNumberValues: true }); setFormValues(e.currentTarget.form!, { rotate: values.rotateNumber }); const changed = Array.isArray(values.changed) ? values.changed : [values.changed]; const changedIndex = changed.indexOf(group.id); const translateX = Array.isArray(values.translateX) ? values.translateX : [values.translateX]; const translateY = Array.isArray(values.translateY) ? values.translateY : [values.translateY]; const currentRotate = e.currentTarget.value; const currentTranslateX = translateX[changedIndex]; const currentTranslateY = translateY[changedIndex]; targetGroup.setAttribute('transform', `translate(${currentTranslateX} ${currentTranslateY}) rotate(${currentRotate} ${rotationCenter.cx} ${rotationCenter.cy})`); } const handleFormSubmit: FormEventHandler = (e) => { e.preventDefault(); const { submitter } = e.nativeEvent as unknown as { submitter: HTMLButtonElement }; const values = getFormValues(e.currentTarget, { submitter, forceNumberValues: true }); const changed = Array.isArray(values.changed) ? values.changed : [values.changed]; const translateX = Array.isArray(values.translateX) ? values.translateX : [values.translateX]; const translateY = Array.isArray(values.translateY) ? values.translateY : [values.translateY]; const rotate = Array.isArray(values.rotate) ? values.rotate : [values.rotate]; switch (values.action) { case 'updateTimeKeyframes': if ('changed' in values) { // TODO handle keyframes on different animations // TODO update SVG preview when updating current time setSavedSvgKeyframes((oldSavedSvgKeyframes = []) => { const newKeyframe = { time: values.time as number, attributes: changed.map((attribute, i) => ({ group: { id: attribute, name: attribute, }, translateX: translateX[i], translateY: translateY[i], rotate: rotate[i], })), }; if (oldSavedSvgKeyframes.some((k) => k.time === values.time as number)) { return oldSavedSvgKeyframes.map((k) => k.time === values.time ? newKeyframe : k); } return [ ...oldSavedSvgKeyframes, newKeyframe, ] }) return; } setSavedSvgKeyframes((oldSavedSvgKeyframes = []) => oldSavedSvgKeyframes.filter(k => k.time !== values.time as number)); return; default: break; } } const updateTime: ChangeEventHandler = (e) => { const value = e.currentTarget.valueAsNumber; const form = e.currentTarget.form as HTMLFormElement const keyframe = savedSvgKeyframes?.find((k) => k.time === value); if (!keyframe) { setEnabledGroupIds([]); setFormValues(form, { changed: svgGroups?.map(() => false), translateX: svgGroups?.map(() => 0), translateY: svgGroups?.map(() => 0), rotate: svgGroups?.map(() => 0), rotateNumber: svgGroups?.map(() => 0), }) return; } const changed = keyframe.attributes.map((a) => a.group.id); setEnabledGroupIds(changed); setTimeout(() => { setFormValues(form, { changed: changed, translateX: keyframe.attributes.map((a) => a.translateX), translateY: keyframe.attributes.map((a) => a.translateY), rotate: keyframe.attributes.map((a) => a.rotate), rotateNumber: keyframe.attributes.map((a) => a.rotate), }); }); } return { isNewAnimationId, svgAnimations, loadedFileName, loadedFileContents, loadSvgFile, svgPreviewRef, svgGroups, svgAnimationIdsListId, handleAnimationIdInput, cancelAnimationIdChange, focusAnimationIdInput, blurAnimationIdInput, addKeyframeAttribute, enabledGroupIds, rotateGroup, rotateGroupThroughNumber, translateXGroup, translateYGroup, handleFormSubmit, savedSvgKeyframes, updateTime, } } const IndexPage = () => { const { loadedFileName, loadedFileContents, loadSvgFile, svgPreviewRef, svgGroups, svgAnimations, svgAnimationIdsListId, handleAnimationIdInput, isNewAnimationId, cancelAnimationIdChange, focusAnimationIdInput, blurAnimationIdInput, addKeyframeAttribute, enabledGroupIds, rotateGroup, rotateGroupThroughNumber, translateXGroup, translateYGroup, handleFormSubmit, savedSvgKeyframes, updateTime, } = useAnimationForm(); return (
{savedSvgKeyframes?.map((k) => ( ))}
Load SVG File { loadedFileContents && ( <>
{loadedFileName}
) }
{ loadedFileContents && (
{svgAnimations?.map(s => ( ))} { isNewAnimationId && ( <> ) }
{ Array.isArray(svgGroups) && ( <> {svgGroups.map((group) => { return (
{group.name}
) })} ) }
) } ); } export default IndexPage;