Skip to content

Commit

Permalink
Added useAnimatedTransform hook (#620)
Browse files Browse the repository at this point in the history
Co-authored-by: dv-raghad-jamalaldeen <[email protected]>
Co-authored-by: Holger Stitz <[email protected]>
  • Loading branch information
3 people authored Nov 6, 2024
1 parent 5e4dc82 commit 352ecbf
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 0 deletions.
59 changes: 59 additions & 0 deletions src/vis/vishooks/hooks/VisHooks.stories.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -62,6 +67,54 @@ function UseCanvasComponent() {
);
}

function UseAnimatedTransformComponent() {
const [toggled, setToggled] = React.useState(false);

const [animatedTransform, setAnimatedTransform] = React.useState<ZoomTransform>(m4.identityMatrix4x4());

const { animate } = useAnimatedTransform({
onIntermediate: (newT) => {
setAnimatedTransform(newT);
},
});

return (
<Center w={800} h={600}>
<Group>
<Button
onClick={() => {
if (toggled) {
const id = m4.identityMatrix4x4();
m4.setTranslation(id, 100, 100, 0);

animate(m4.identityMatrix4x4(), id);
} else {
const id = m4.identityMatrix4x4();
m4.setTranslation(id, 100, 100, 0);

animate(id, m4.identityMatrix4x4());
}

setToggled(!toggled);
}}
>
Toggle Transform
</Button>
<Stack>
<Stack>
<Text fw="bold">Animated transform:</Text>
<Text>t12: {animatedTransform[12]?.toPrecision(3)}</Text>
<Text>t13: {animatedTransform[13]?.toPrecision(3)}</Text>
</Stack>
</Stack>
<svg width={300} height={300}>
<circle cx={50 + animatedTransform[12]!} cy={50 + animatedTransform[13]!} r={32} fill="red" />
</svg>
</Group>
</Center>
);
}

function VisHooksComponent() {
const [element, setElement] = React.useState<HTMLElement>();

Expand Down Expand Up @@ -97,3 +150,9 @@ export const UseCanvas: Story = {
return <UseCanvasComponent />;
},
};

export const UseAnimated: Story = {
render: () => {
return <UseAnimatedTransformComponent />;
},
};
1 change: 1 addition & 0 deletions src/vis/vishooks/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
83 changes: 83 additions & 0 deletions src/vis/vishooks/hooks/useAnimatedTransform.ts
Original file line number Diff line number Diff line change
@@ -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<number | undefined>(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,
};
}

0 comments on commit 352ecbf

Please sign in to comment.