Skip to content

Commit

Permalink
Feature: Move canvas sizing logic into useResizeCanvas hook and clean…
Browse files Browse the repository at this point in the history
… up util hooks
  • Loading branch information
zplata committed Jun 2, 2023
1 parent 2b24949 commit 2c82fa0
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 255 deletions.
6 changes: 6 additions & 0 deletions src/components/Rive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface RiveProps {
* Specify whether to disable Rive listeners on the canvas, thus preventing any event listeners to be attached to the canvas element
*/
shouldDisableRiveListeners?: boolean;
/**
* Specify whether to resize the canvas to its container automatically
*/
shouldResizeCanvasToContainer?: boolean;
}

const Rive = ({
Expand All @@ -42,6 +46,7 @@ const Rive = ({
layout,
useOffscreenRenderer = true,
shouldDisableRiveListeners = false,
shouldResizeCanvasToContainer = true,
children,
...rest
}: RiveProps & ComponentProps<'canvas'>) => {
Expand All @@ -57,6 +62,7 @@ const Rive = ({

const options = {
useOffscreenRenderer,
shouldResizeCanvasToContainer,
};

const { RiveComponent } = useRive(params, options);
Expand Down
94 changes: 94 additions & 0 deletions src/hooks/useContainerSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useEffect, useRef, useState } from 'react';
import { Dimensions } from '../types';

// There are polyfills for this, but they add hundreds of lines of code
class FakeResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}

function throttle(f: Function, delay: number) {
let timer = 0;
return function (this: Function, ...args: any) {
clearTimeout(timer);
timer = window.setTimeout(() => f.apply(this, args), delay);
};
}

const MyResizeObserver = globalThis.ResizeObserver || FakeResizeObserver;
const hasResizeObserver = globalThis.ResizeObserver !== undefined;

const useResizeObserver = hasResizeObserver;
const useWindowListener = !useResizeObserver;

/**
* Hook to listen for a ref element's resize events being triggered. When resized,
* it sets state to an object of {width: number, height: number} indicating the contentRect
* size of the element at the new resize.
*
* @param containerRef - Ref element to listen for resize events on
* @returns - Size object with width and height attributes
*/
export default function useSize(
containerRef: React.MutableRefObject<HTMLElement | null>,
shouldResizeCanvasToContainer = true
) {
const [size, setSize] = useState<Dimensions>({
width: 0,
height: 0,
});

// internet explorer does not support ResizeObservers.
useEffect(() => {
if (typeof window !== 'undefined' && shouldResizeCanvasToContainer) {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};

if (useWindowListener) {
// only pay attention to window size changes when we do not have the resizeObserver (IE only)
handleResize();
window.addEventListener('resize', handleResize);
}

return () => window.removeEventListener('resize', handleResize);
}
}, []);
const observer = useRef(
new MyResizeObserver(
throttle((entries: any) => {
if (useResizeObserver) {
setSize({
width: entries[entries.length - 1].contentRect.width,
height: entries[entries.length - 1].contentRect.height,
});
}
}, 0)
)
);

useEffect(() => {
const currentObserver = observer.current;
if (!shouldResizeCanvasToContainer) {
currentObserver.disconnect();
return;
}
const containerEl = containerRef.current;
if (containerRef.current && useResizeObserver) {
currentObserver.observe(containerRef.current);
}

return () => {
currentObserver.disconnect();
if (containerEl && useResizeObserver) {
currentObserver.unobserve(containerEl);
}
};
}, [containerRef, observer]);

return size;
}
49 changes: 49 additions & 0 deletions src/hooks/useDevicePixelRatio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useEffect, useState } from 'react';
/**
* Listen for devicePixelRatio changes and set the new value accordingly. This could
* happen for reasons such as:
* - User moves window from retina screen display to a separate monitor
* - User controls zoom settings on the browser
*
* Source: https://github.com/rexxars/use-device-pixel-ratio/blob/main/index.ts
*
* @returns dpr: Number - Device pixel ratio; ratio of physical px to resolution in CSS pixels for current device
*/
export default function useDevicePixelRatio() {
const dpr = getDevicePixelRatio();
const [currentDpr, setCurrentDpr] = useState(dpr);

useEffect(() => {
const canListen = typeof window !== 'undefined' && 'matchMedia' in window;
if (!canListen) {
return;
}

const updateDpr = () => {
const newDpr = getDevicePixelRatio();
setCurrentDpr(newDpr);
};
const mediaMatcher = window.matchMedia(
`screen and (resolution: ${currentDpr}dppx)`
);
mediaMatcher.hasOwnProperty('addEventListener')
? mediaMatcher.addEventListener('change', updateDpr)
: mediaMatcher.addListener(updateDpr);

return () => {
mediaMatcher.hasOwnProperty('removeEventListener')
? mediaMatcher.removeEventListener('change', updateDpr)
: mediaMatcher.removeListener(updateDpr);
};
}, [currentDpr]);

return currentDpr;
}

function getDevicePixelRatio(): number {
const hasDprProp =
typeof window !== 'undefined' &&
typeof window.devicePixelRatio === 'number';
const dpr = hasDprProp ? window.devicePixelRatio : 1;
return Math.min(Math.max(1, dpr), 3);
}
153 changes: 153 additions & 0 deletions src/hooks/useResizeCanvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { useEffect, useState, MutableRefObject, useCallback } from 'react';
import { Bounds } from '@rive-app/canvas';
import { Dimensions, UseRiveOptions } from '../types';
import useDevicePixelRatio from './useDevicePixelRatio';
import useContainerSize from './useContainerSize';
import { getOptions } from '../utils';

interface UseResizeCanvasProps {
riveLoaded: boolean;
canvasRef: MutableRefObject<HTMLCanvasElement | null>;
containerRef: MutableRefObject<HTMLElement | null>;
onCanvasHasResized?: () => void;
options?: Partial<UseRiveOptions>;
artboardBounds?: Bounds;
}

/**
* Helper hook to listen for changes in the <canvas> parent container size and size the <canvas>
* to match. If a resize event has occurred, a supplied callback (onCanvasHasResized)
* will be inokved to allow for any re-calculation needed (i.e. Rive layout on the canvas).
*
* This hook is useful if you are not intending to use the `useRive` hook yourself, but still
* want to use the auto-sizing logic on the canvas/container.
*
* @param props - Object to supply necessary
*/
export default function useResizeCanvas({
riveLoaded = false,
canvasRef,
containerRef,
options = {},
onCanvasHasResized,
artboardBounds,
}: UseResizeCanvasProps) {
const presetOptions = getOptions(options);
const [
{ height: lastContainerHeight, width: lastContainerWidth },
setLastContainerDimensions,
] = useState<Dimensions>({
height: 0,
width: 0,
});
const [
{ height: lastCanvasHeight, width: lastCanvasWidth },
setLastCanvasSize,
] = useState<Dimensions>({
height: 0,
width: 0,
});

const [isFirstSizing, setIsFirstSizing] = useState(true);

const {
fitCanvasToArtboardHeight,
shouldResizeCanvasToContainer,
useDevicePixelRatio: shouldUseDevicePixelRatio,
} = presetOptions;

const containerSize = useContainerSize(
containerRef,
shouldResizeCanvasToContainer
);
const currentDevicePixelRatio = useDevicePixelRatio();

const getCanvasDimensions = useCallback(() => {
const width = containerRef.current?.clientWidth ?? 0;
const height = containerRef.current?.clientHeight ?? 0;
if (fitCanvasToArtboardHeight && artboardBounds) {
const { maxY, maxX } = artboardBounds;
return { width, height: width * (maxY / maxX) };
}
return {
width,
height,
};
}, [containerRef, fitCanvasToArtboardHeight, artboardBounds]);

useEffect(() => {
if (
!shouldResizeCanvasToContainer ||
!containerRef.current ||
!riveLoaded
) {
return;
}

const { width, height } = getCanvasDimensions();
let hasResized = false;
if (canvasRef.current) {
// Check if the canvas parent container bounds have changed and set
// new values accordingly
const boundsChanged =
width !== lastContainerWidth || height !== lastContainerHeight;
if (presetOptions.fitCanvasToArtboardHeight && boundsChanged) {
containerRef.current.style.height = height + 'px';
hasResized = true;
}
if (presetOptions.useDevicePixelRatio) {
// Check if devicePixelRatio may have changed and get new canvas
// width/height values to set the size
const canvasSizeChanged =
width * currentDevicePixelRatio !== lastCanvasWidth ||
height * currentDevicePixelRatio !== lastCanvasHeight;
if (boundsChanged || canvasSizeChanged) {
const newCanvasWidthProp = currentDevicePixelRatio * width;
const newCanvasHeightProp = currentDevicePixelRatio * height;
canvasRef.current.width = newCanvasWidthProp;
canvasRef.current.height = newCanvasHeightProp;
canvasRef.current.style.width = width + 'px';
canvasRef.current.style.height = height + 'px';
setLastCanvasSize({
width: newCanvasWidthProp,
height: newCanvasHeightProp,
});
hasResized = true;
}
} else if (boundsChanged) {
canvasRef.current.width = width;
canvasRef.current.height = height;
setLastCanvasSize({
width: width,
height: height,
});
hasResized = true;
}
setLastContainerDimensions({ width, height });
}

// Callback to perform any Rive-related actions after resizing the canvas
// (i.e., reset the Rive layout in the render loop)
if (onCanvasHasResized && (isFirstSizing || hasResized)) {
onCanvasHasResized && onCanvasHasResized();
}
setIsFirstSizing(false);
}, [
canvasRef,
containerRef,
containerSize,
currentDevicePixelRatio,
getCanvasDimensions,
isFirstSizing,
setIsFirstSizing,
lastCanvasHeight,
lastCanvasWidth,
lastContainerHeight,
lastContainerWidth,
onCanvasHasResized,
shouldResizeCanvasToContainer,
fitCanvasToArtboardHeight,
shouldUseDevicePixelRatio,
riveLoaded,
]);
}
Loading

0 comments on commit 2c82fa0

Please sign in to comment.