|
|
@@ -3,7 +3,6 @@ import { |
|
|
|
FormEventHandler, |
|
|
|
MouseEventHandler, |
|
|
|
useEffect, |
|
|
|
useId, |
|
|
|
useRef, |
|
|
|
useState, |
|
|
|
} from 'react'; |
|
|
@@ -40,7 +39,6 @@ const useAnimationForm = () => { |
|
|
|
const [enabledGroupIds, setEnabledGroupIds] = useState<string[]>(); |
|
|
|
const [savedSvgKeyframes, setSavedSvgKeyframes] = useState<Keyframe[]>(); |
|
|
|
|
|
|
|
const svgAnimationIdsListId = useId(); |
|
|
|
const svgPreviewRef = useRef<HTMLDivElement>(null); |
|
|
|
|
|
|
|
const loadSvgFile: ChangeEventHandler<HTMLInputElement> = async (e) => { |
|
|
@@ -68,7 +66,7 @@ const useAnimationForm = () => { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
const proposeNewAnimationId: FormEventHandler<HTMLInputElement> = (e) => { |
|
|
|
const proposeNewAnimationId: FormEventHandler<HTMLInputElement> = () => { |
|
|
|
setIsNewAnimationId(true); |
|
|
|
} |
|
|
|
|
|
|
@@ -80,17 +78,16 @@ const useAnimationForm = () => { |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
const onLoaded = (loadedFileContents?: string, svgPreviewRefCurrent?: HTMLElement | null) => { |
|
|
|
if (!loadedFileContents) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if (!svgPreviewRef.current) { |
|
|
|
if (!svgPreviewRefCurrent) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
const svgPreviewRoot = svgPreviewRef.current.children[0]; |
|
|
|
|
|
|
|
const svgPreviewRoot = svgPreviewRefCurrent.children[0]; |
|
|
|
setSvgAnimations(['default']); |
|
|
|
setSvgGroups( |
|
|
|
Array |
|
|
@@ -103,6 +100,10 @@ const useAnimationForm = () => { |
|
|
|
setCurrentAnimationId('default'); |
|
|
|
setEnabledGroupIds([]); // TODO - compute enabled groupIds based on current animation id |
|
|
|
setSavedSvgKeyframes([]); // TODO - load keyframes from file |
|
|
|
} |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
onLoaded(loadedFileContents, svgPreviewRef.current); |
|
|
|
}, [loadedFileContents, svgPreviewRef.current]); |
|
|
|
|
|
|
|
const focusAnimationIdInput: FocusEventHandler<HTMLInputElement> = (e) => { |
|
|
@@ -123,40 +124,7 @@ const useAnimationForm = () => { |
|
|
|
setEnabledGroupIds((oldEnabledGroupIds = []) => oldEnabledGroupIds.filter((i) => i !== currentTarget.value)); |
|
|
|
} |
|
|
|
|
|
|
|
const translateXGroup = (group: SvgGroup): ChangeEventHandler<HTMLInputElement> => (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<HTMLInputElement> => (e) => { |
|
|
|
const handleAttributeValueChange = (group: SvgGroup): ChangeEventHandler<HTMLInputElement> => (e) => { |
|
|
|
const { current: svgPreviewWrapper } = svgPreviewRef; |
|
|
|
if (!svgPreviewWrapper) { |
|
|
|
return; |
|
|
@@ -178,82 +146,26 @@ const useAnimationForm = () => { |
|
|
|
cy: rotationCenterReference?.getAttribute?.('cy') ?? 0, |
|
|
|
}; |
|
|
|
|
|
|
|
const values = getFormValues(e.currentTarget.form!, { forceNumberValues: true }); |
|
|
|
const form = e.currentTarget.form as HTMLFormElement |
|
|
|
const values = getFormValues(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<HTMLInputElement> => (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 rotateNumber = Array.isArray(values.rotateNumber) ? values.rotateNumber : [values.rotateNumber]; |
|
|
|
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<HTMLInputElement> => (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 currentRotate = e.currentTarget.name.startsWith('rotate') ? e.currentTarget.valueAsNumber : rotate[changedIndex]; |
|
|
|
const currentTranslateX = e.currentTarget.name.startsWith('translateX') ? e.currentTarget.valueAsNumber : translateX[changedIndex]; |
|
|
|
const currentTranslateY = e.currentTarget.name.startsWith('translateY') ? e.currentTarget.valueAsNumber : translateY[changedIndex]; |
|
|
|
if (e.currentTarget.name === 'rotate') { |
|
|
|
setFormValues(form, { |
|
|
|
rotateNumber: rotate, |
|
|
|
}) |
|
|
|
} else if (e.currentTarget.name === 'rotateNumber') { |
|
|
|
setFormValues(form, { |
|
|
|
rotate: rotateNumber, |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
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})`); |
|
|
|
} |
|
|
|
|
|
|
@@ -304,19 +216,48 @@ const useAnimationForm = () => { |
|
|
|
|
|
|
|
const updateTime: ChangeEventHandler<HTMLInputElement> = (e) => { |
|
|
|
const value = e.currentTarget.valueAsNumber; |
|
|
|
const form = e.currentTarget.form as HTMLFormElement |
|
|
|
const form = e.currentTarget.form as HTMLFormElement; |
|
|
|
const theSavedSvgKeyframes = savedSvgKeyframes ?? []; |
|
|
|
const sortedSavedSvgKeyframes = theSavedSvgKeyframes.sort((k1, k2) => k1.time - k2.time); |
|
|
|
const previousKeyframe = |
|
|
|
[...sortedSavedSvgKeyframes].reverse().find((k) => k.time <= value) |
|
|
|
?? sortedSavedSvgKeyframes[0]; |
|
|
|
const nextKeyframe = |
|
|
|
sortedSavedSvgKeyframes.find((k) => k.time > value) |
|
|
|
?? sortedSavedSvgKeyframes.slice(-1)[0]; |
|
|
|
const keyframe = savedSvgKeyframes?.find((k) => k.time === value); |
|
|
|
const hasKeyframeRange = (previousKeyframe || nextKeyframe); |
|
|
|
if (!keyframe) { |
|
|
|
setEnabledGroupIds([]); |
|
|
|
setFormValues(form, { |
|
|
|
setEnabledGroupIds(svgGroups?.map(g => g.id)); |
|
|
|
// todo interpolate across individual keyframes |
|
|
|
// get previous keyframe with respect to group |
|
|
|
// get next keyframe with respect to group |
|
|
|
// |----------------| |
|
|
|
// |-----| |
|
|
|
|
|
|
|
const interpolatedKeyframe = previousKeyframe; |
|
|
|
const interpolatedValues = { |
|
|
|
changed: svgGroups?.map(() => false), |
|
|
|
translateX: svgGroups?.map(() => 0), |
|
|
|
translateY: svgGroups?.map(() => 0), |
|
|
|
rotate: svgGroups?.map(() => 0), |
|
|
|
rotateNumber: svgGroups?.map(() => 0), |
|
|
|
}) |
|
|
|
translateX: svgGroups?.map((g) => { |
|
|
|
if (!hasKeyframeRange) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
setTimeout(() => { |
|
|
|
// todo set interpolation |
|
|
|
setFormValues(form, { |
|
|
|
changed: svgGroups?.map(() => false), |
|
|
|
translateX: svgGroups?.map(() => 0), |
|
|
|
translateY: svgGroups?.map(() => 0), |
|
|
|
rotate: svgGroups?.map(() => 0), |
|
|
|
rotateNumber: svgGroups?.map(() => 0), |
|
|
|
}); |
|
|
|
setEnabledGroupIds([]); |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const changed = keyframe.attributes.map((a) => a.group.id); |
|
|
|
setEnabledGroupIds(changed); |
|
|
|
setTimeout(() => { |
|
|
@@ -338,7 +279,6 @@ const useAnimationForm = () => { |
|
|
|
loadSvgFile, |
|
|
|
svgPreviewRef, |
|
|
|
svgGroups, |
|
|
|
svgAnimationIdsListId, |
|
|
|
selectNewAnimationId, |
|
|
|
proposeNewAnimationId, |
|
|
|
cancelAnimationIdChange, |
|
|
@@ -346,10 +286,7 @@ const useAnimationForm = () => { |
|
|
|
blurAnimationIdInput, |
|
|
|
addKeyframeAttribute, |
|
|
|
enabledGroupIds, |
|
|
|
rotateGroup, |
|
|
|
rotateGroupThroughNumber, |
|
|
|
translateXGroup, |
|
|
|
translateYGroup, |
|
|
|
handleAttributeValueChange, |
|
|
|
handleFormSubmit, |
|
|
|
savedSvgKeyframes, |
|
|
|
updateTime, |
|
|
@@ -364,7 +301,6 @@ const IndexPage = () => { |
|
|
|
svgPreviewRef, |
|
|
|
svgGroups, |
|
|
|
svgAnimations, |
|
|
|
svgAnimationIdsListId, |
|
|
|
selectNewAnimationId, |
|
|
|
proposeNewAnimationId, |
|
|
|
isNewAnimationId, |
|
|
@@ -373,10 +309,7 @@ const IndexPage = () => { |
|
|
|
blurAnimationIdInput, |
|
|
|
addKeyframeAttribute, |
|
|
|
enabledGroupIds, |
|
|
|
rotateGroup, |
|
|
|
rotateGroupThroughNumber, |
|
|
|
translateXGroup, |
|
|
|
translateYGroup, |
|
|
|
handleAttributeValueChange, |
|
|
|
handleFormSubmit, |
|
|
|
savedSvgKeyframes, |
|
|
|
updateTime, |
|
|
@@ -384,9 +317,6 @@ const IndexPage = () => { |
|
|
|
|
|
|
|
return ( |
|
|
|
<form className="lg:fixed lg:w-full lg:h-full flex flex-col" onSubmit={handleFormSubmit}> |
|
|
|
<datalist id="translationTickMarks"> |
|
|
|
<option value="0">0</option> |
|
|
|
</datalist> |
|
|
|
<datalist id="rotationTickMarks"> |
|
|
|
<option value="-360">-360</option> |
|
|
|
<option value="-270">-270</option> |
|
|
@@ -507,12 +437,24 @@ const IndexPage = () => { |
|
|
|
</div> |
|
|
|
<div |
|
|
|
style={{ |
|
|
|
width: 560 |
|
|
|
width: 700 |
|
|
|
}} |
|
|
|
> |
|
|
|
{ |
|
|
|
Array.isArray(svgGroups) && ( |
|
|
|
<> |
|
|
|
<div className="grid grid-cols-10"> |
|
|
|
<div className="col-span-4"> |
|
|
|
Group |
|
|
|
</div> |
|
|
|
<div> |
|
|
|
X |
|
|
|
</div> |
|
|
|
<div> |
|
|
|
Y |
|
|
|
</div> |
|
|
|
<div className="col-span-4"> |
|
|
|
Rotate |
|
|
|
</div> |
|
|
|
{svgGroups.map((group) => { |
|
|
|
return ( |
|
|
|
<fieldset |
|
|
@@ -522,88 +464,84 @@ const IndexPage = () => { |
|
|
|
<legend className="sr-only"> |
|
|
|
{group.name} |
|
|
|
</legend> |
|
|
|
<div className="flex"> |
|
|
|
<div className="flex-auto"> |
|
|
|
<label |
|
|
|
className="whitespace-nowrap" |
|
|
|
> |
|
|
|
<input type="checkbox" name="changed" value={group.id} onChange={addKeyframeAttribute} /> |
|
|
|
{' '} |
|
|
|
<span> |
|
|
|
{group.name} |
|
|
|
</span> |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
<label |
|
|
|
className="w-16 block" |
|
|
|
> |
|
|
|
<span className="sr-only">Translate X</span> |
|
|
|
<input |
|
|
|
className="w-full block text-right" |
|
|
|
type="number" |
|
|
|
name="translateX" |
|
|
|
min="-360" max="360" |
|
|
|
defaultValue="0" |
|
|
|
step="any" |
|
|
|
disabled={!enabledGroupIds?.includes(group.name)} |
|
|
|
onChange={translateXGroup(group)} |
|
|
|
list="translationTickMarks" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<label |
|
|
|
className="w-16 block" |
|
|
|
> |
|
|
|
<span className="sr-only">Translate Y</span> |
|
|
|
<input |
|
|
|
className="w-full block text-right" |
|
|
|
type="number" |
|
|
|
name="translateY" |
|
|
|
min="-360" max="360" |
|
|
|
defaultValue="0" |
|
|
|
step="any" |
|
|
|
disabled={!enabledGroupIds?.includes(group.name)} |
|
|
|
onChange={translateYGroup(group)} |
|
|
|
list="translationTickMarks" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<label |
|
|
|
className="w-40 block" |
|
|
|
> |
|
|
|
<span className="sr-only">Rotate</span> |
|
|
|
<input |
|
|
|
className="w-full block" |
|
|
|
type="range" |
|
|
|
name="rotate" |
|
|
|
min="-360" |
|
|
|
max="360" |
|
|
|
defaultValue="0" |
|
|
|
step="5" |
|
|
|
disabled={!enabledGroupIds?.includes(group.name)} |
|
|
|
onChange={rotateGroup(group)} |
|
|
|
list="rotationTickMarks" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<div className="col-span-4"> |
|
|
|
<label |
|
|
|
className="w-12 block" |
|
|
|
className="whitespace-nowrap" |
|
|
|
> |
|
|
|
<span className="sr-only">Rotate</span> |
|
|
|
<input |
|
|
|
className="w-full block text-right" |
|
|
|
type="number" |
|
|
|
name="rotateNumber" |
|
|
|
min="-360" |
|
|
|
max="360" |
|
|
|
defaultValue="0" |
|
|
|
step="5" |
|
|
|
disabled={!enabledGroupIds?.includes(group.name)} |
|
|
|
onChange={rotateGroupThroughNumber(group)} |
|
|
|
/> |
|
|
|
<input type="checkbox" name="changed" value={group.id} onChange={addKeyframeAttribute} /> |
|
|
|
{' '} |
|
|
|
<span> |
|
|
|
{group.name} |
|
|
|
</span> |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
<label |
|
|
|
className="block" |
|
|
|
> |
|
|
|
<span className="sr-only">Translate X</span> |
|
|
|
<input |
|
|
|
className="w-full block text-right" |
|
|
|
type="number" |
|
|
|
name="translateX" |
|
|
|
min="-360" max="360" |
|
|
|
defaultValue="0" |
|
|
|
step="any" |
|
|
|
disabled={!enabledGroupIds?.includes(group.name)} |
|
|
|
onChange={handleAttributeValueChange(group)} |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<label |
|
|
|
className="block" |
|
|
|
> |
|
|
|
<span className="sr-only">Translate Y</span> |
|
|
|
<input |
|
|
|
className="w-full block text-right" |
|
|
|
type="number" |
|
|
|
name="translateY" |
|
|
|
min="-360" max="360" |
|
|
|
defaultValue="0" |
|
|
|
step="any" |
|
|
|
disabled={!enabledGroupIds?.includes(group.name)} |
|
|
|
onChange={handleAttributeValueChange(group)} |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<label |
|
|
|
className="block col-span-3" |
|
|
|
> |
|
|
|
<span className="sr-only">Rotate</span> |
|
|
|
<input |
|
|
|
className="w-full block" |
|
|
|
type="range" |
|
|
|
name="rotate" |
|
|
|
min="-360" |
|
|
|
max="360" |
|
|
|
defaultValue="0" |
|
|
|
step="5" |
|
|
|
disabled={!enabledGroupIds?.includes(group.name)} |
|
|
|
onChange={handleAttributeValueChange(group)} |
|
|
|
list="rotationTickMarks" |
|
|
|
/> |
|
|
|
</label> |
|
|
|
<label |
|
|
|
className="block" |
|
|
|
> |
|
|
|
<span className="sr-only">Rotate</span> |
|
|
|
<input |
|
|
|
className="w-full block text-right" |
|
|
|
type="number" |
|
|
|
name="rotateNumber" |
|
|
|
min="-360" |
|
|
|
max="360" |
|
|
|
defaultValue="0" |
|
|
|
step="5" |
|
|
|
disabled={!enabledGroupIds?.includes(group.name)} |
|
|
|
onChange={handleAttributeValueChange(group)} |
|
|
|
/> |
|
|
|
</label> |
|
|
|
</fieldset> |
|
|
|
) |
|
|
|
})} |
|
|
|
</> |
|
|
|
</div> |
|
|
|
) |
|
|
|
} |
|
|
|
</div> |
|
|
|