diff --git a/src/vis/vishooks/hooks/VisHooks.stories.tsx b/src/vis/vishooks/hooks/VisHooks.stories.tsx index f4c06ef02..a0be0f07e 100644 --- a/src/vis/vishooks/hooks/VisHooks.stories.tsx +++ b/src/vis/vishooks/hooks/VisHooks.stories.tsx @@ -1,11 +1,16 @@ import React from 'react'; import { StoryObj, Meta } from '@storybook/react'; +import { Center, Stack, Paper, Button, Text, Group } from '@mantine/core'; +import { lassoToSvgPath, useLasso } from './useLasso'; import { Center, Stack, Paper } from '@mantine/core'; import { useLasso } from './useLasso'; import { SVGLasso } from '../components/SVGLasso'; import { useBrush } from './useBrush'; import { SVGBrush } from '../components'; import { useCanvas } from './useCanvas'; +import { m4 } from '../math'; +import { ZoomTransform } from '../interfaces'; +import { useAnimatedTransform } from './useAnimatedTransform'; function UseLassoComponent() { const { setRef, value } = useLasso(); @@ -62,6 +67,54 @@ function UseCanvasComponent() { ); } +function UseAnimatedTransformComponent() { + const [toggled, setToggled] = React.useState(false); + + const [animatedTransform, setAnimatedTransform] = React.useState(m4.identityMatrix4x4()); + + const { animate } = useAnimatedTransform({ + onIntermediate: (newT) => { + setAnimatedTransform(newT); + }, + }); + + return ( +
+ + + + + Animated transform: + t12: {animatedTransform[12]?.toPrecision(3)} + t13: {animatedTransform[13]?.toPrecision(3)} + + + + + + +
+ ); +} + function VisHooksComponent() { const [element, setElement] = React.useState(); @@ -97,3 +150,9 @@ export const UseCanvas: Story = { return ; }, }; + +export const UseAnimated: Story = { + render: () => { + return ; + }, +}; diff --git a/src/vis/vishooks/hooks/index.ts b/src/vis/vishooks/hooks/index.ts index 203cf3ac5..601c8afa5 100644 --- a/src/vis/vishooks/hooks/index.ts +++ b/src/vis/vishooks/hooks/index.ts @@ -1,6 +1,7 @@ export * from './useBandScale'; export * from './usePan'; export * from './useTransformScale'; +export * from './useAnimatedTransform'; export * from './useZoom'; export * from './useWheel'; export * from './useInteractions'; diff --git a/src/vis/vishooks/hooks/useAnimatedTransform.ts b/src/vis/vishooks/hooks/useAnimatedTransform.ts new file mode 100644 index 000000000..e828d7c60 --- /dev/null +++ b/src/vis/vishooks/hooks/useAnimatedTransform.ts @@ -0,0 +1,83 @@ +/* eslint-disable react-compiler/react-compiler */ +import * as React from 'react'; +import { ZoomTransform } from '../interfaces'; +import { useSyncedRef } from '../../../hooks'; + +function linearInterpolate(startMatrix: ZoomTransform, endMatrix: ZoomTransform, t: number) { + return startMatrix.map((startValue, index) => { + const endValue = endMatrix[index]; + + if (endValue === undefined) { + throw new Error('Supplied matrices are not of the same length'); + } + + const cosT = (1 - Math.cos(t * Math.PI)) / 2; + return startValue * (1 - cosT) + endValue * cosT; + }); +} + +/** + * Hook that returns an animate function that can be used to animate between two zoom transforms (keyframes). + * After calling animate, the onIntermediate callback will be called with the monitors refresh rate (requestAnimationFrame) + * with the intermediate transform values (cosine interpolated). + */ +export function useAnimatedTransform({ onIntermediate }: { onIntermediate: (intermediateTransform: ZoomTransform) => void }) { + const stateRef = React.useRef({ + start: undefined as ZoomTransform | undefined, + end: undefined as ZoomTransform | undefined, + t0: performance.now(), + }); + + const animationFrameRef = React.useRef(undefined); + const onIntermediateRef = useSyncedRef(onIntermediate); + + const requestFrame = () => { + animationFrameRef.current = requestAnimationFrame((t1) => { + if (stateRef.current.start && stateRef.current.end) { + const t = (t1 - stateRef.current.t0) / 1000; + // End of animation + if (t >= 1) { + animationFrameRef.current = undefined; + onIntermediateRef.current(stateRef.current.end); + return; + } + + const newMatrix = linearInterpolate(stateRef.current.start, stateRef.current.end, t); + onIntermediateRef.current(newMatrix); + + requestFrame(); + } + }); + }; + + const requestFrameRef = useSyncedRef(requestFrame); + + const animate = React.useCallback( + (start: ZoomTransform, end: ZoomTransform) => { + stateRef.current = { + start, + end, + t0: performance.now(), + }; + + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + + requestFrameRef.current(); + }, + [requestFrameRef], + ); + + React.useEffect(() => { + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + }; + }, []); + + return { + animate, + }; +}