Skip to content

Commit

Permalink
Mimick requestAnimationFrame API and enable loop by default
Browse files Browse the repository at this point in the history
  • Loading branch information
leroykorterink committed May 15, 2023
1 parent de98f6a commit 6a83dd7
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 46 deletions.
14 changes: 4 additions & 10 deletions src/hooks/useAnimationLoop/useAnimationLoop.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,11 @@ export default {
};

function DemoComponent(): ReactElement {
const [delta, setDelta] = useState(0);
const [currentTimestamp, setCurrentTimestamp] = useState(0);

const [timestamp, setTimestamp] = useState(0);
const [isRunning, toggleIsRunning] = useToggle(true);

useAnimationLoop(() => {
const timestamp = Date.now();

setDelta(timestamp - currentTimestamp);
setCurrentTimestamp(timestamp);
useAnimationLoop((_timestamp: DOMHighResTimeStamp) => {
setTimestamp(_timestamp);
}, isRunning);

return (
Expand All @@ -30,8 +25,7 @@ function DemoComponent(): ReactElement {
<div className="card border-dark" data-ref="test-area">
<div className="card-header">Test Area</div>
<div className="card-body">
<p>Current time: {currentTimestamp}</p>
<p>Delta: {delta}</p>
<p>Timestamp: {timestamp}</p>

<button
type="button"
Expand Down
35 changes: 20 additions & 15 deletions src/hooks/useAnimationLoop/useAnimationLoop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ describe('useAnimationLoop', () => {
});
});

it('should not execute the callback function when enabled is not passed', async () => {
it('should not execute the callback function when enabled is set to false', async () => {
const spy = jest.fn();

renderHook(
({ callback }) => {
useAnimationLoop(callback);
useAnimationLoop(callback, false);
},
{
initialProps: {
Expand All @@ -29,35 +30,34 @@ describe('useAnimationLoop', () => {

it('should execute the callback function when useAnimationLoop is enabled', async () => {
const spy = jest.fn();

renderHook(
({ callback, enabled }) => {
useAnimationLoop(callback, enabled);
({ callback }) => {
useAnimationLoop(callback);
},
{
initialProps: {
callback: spy,
enabled: true,
},
},
);

expect(spy).toBeCalledTimes(0);
await waitFor(() => {
expect(spy).toBeCalled();
});
});

it('should execute another callback function when it is passed to useAnimationLoop and not execute previously passed callback function', async () => {
it('should not execute previous callback function when callback function is updated', async () => {
const spyFirstRender = jest.fn();
const spySecondRender = jest.fn();

const { rerender } = renderHook(
({ callback, enabled }) => {
useAnimationLoop(callback, enabled);
({ callback }) => {
useAnimationLoop(callback);
},
{
initialProps: {
callback: spyFirstRender,
enabled: true,
},
},
);
Expand All @@ -67,16 +67,18 @@ describe('useAnimationLoop', () => {
expect(spySecondRender).toBeCalledTimes(0);
});

rerender({ callback: spySecondRender, enabled: true });
rerender({ callback: spySecondRender });
const amountOfCalls = spyFirstRender.mock.calls.length;

await waitFor(() => {
expect(spyFirstRender).toBeCalledTimes(amountOfCalls);
expect(spySecondRender).toBeCalled();
});
});

it('should execute the callback function when useAnimationLoop is enabled on the first render and should not execute the callback function when useAnimationLoop is disabled on the second render', async () => {
it('should execute the callback function when useAnimationLoop is enabled and should not execute the callback function when useAnimationLoop is disabled', async () => {
const spy = jest.fn();

const { rerender } = renderHook(
({ callback, enabled }) => {
useAnimationLoop(callback, enabled);
Expand All @@ -89,13 +91,16 @@ describe('useAnimationLoop', () => {
},
);

waitFor(() => {
await waitFor(() => {
expect(spy).toBeCalled();
});

rerender({ callback: spy, enabled: false });
waitFor(() => {
expect(spy).toBeCalledTimes(0);

const amountOfCalls = spy.mock.calls.length;

await waitFor(() => {
expect(spy).toBeCalledTimes(amountOfCalls);
});
});
});
32 changes: 11 additions & 21 deletions src/hooks/useAnimationLoop/useAnimationLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,27 @@ import { useRefValue } from '../useRefValue/useRefValue.js';
* @param callback - callback function with @param delta which represents time passed since last invocation
* @param enabled - boolean which is used to play and pause the requestAnimationFrame
*/
export function useAnimationLoop(callback: (delta: number) => void, enabled = false): void {
export function useAnimationLoop(callback: FrameRequestCallback, enabled = true): void {
const animationFrameRef = useRef(0);
const lastTimeRef = useRef(0);
const callbackRef = useRefValue(callback);

const tick = useCallback(
(time: number): void => {
const delta = time - lastTimeRef.current;
lastTimeRef.current = time;

callbackRef.current?.(delta);

const tick = useCallback<FrameRequestCallback>(
(time) => {
callbackRef.current?.(time);
animationFrameRef.current = requestAnimationFrame(tick);
},
[callbackRef],
);

const play = useCallback(() => {
lastTimeRef.current = performance.now();
requestAnimationFrame(tick);
}, [tick]);

const pause = useCallback(() => {
cancelAnimationFrame(animationFrameRef.current);
}, []);

useEffect(() => {
if (enabled) {
play();
requestAnimationFrame(tick);
} else {
cancelAnimationFrame(animationFrameRef.current);
}

return pause;
}, [enabled, pause, play]);
return () => {
cancelAnimationFrame(animationFrameRef.current);
};
}, [enabled, tick]);
}

0 comments on commit 6a83dd7

Please sign in to comment.