From 2b1aa01a87c14f71b980d160f6607edc12d3eed6 Mon Sep 17 00:00:00 2001 From: Zach Plata Date: Tue, 20 Sep 2022 22:57:08 -0500 Subject: [PATCH] fix: Adjust canvas size if devicePixelRatio changes for any reaason --- src/hooks/useRive.tsx | 53 +++++++++++++++++++++++++++++++------------ src/utils.ts | 45 ++++++++++++++++++++++++++++++++++++ test/useRive.test.tsx | 27 ++++++++++++++++++++++ 3 files changed, 111 insertions(+), 14 deletions(-) diff --git a/src/hooks/useRive.tsx b/src/hooks/useRive.tsx index 977fea6..422c73a 100644 --- a/src/hooks/useRive.tsx +++ b/src/hooks/useRive.tsx @@ -13,7 +13,7 @@ import { RiveState, Dimensions, } from '../types'; -import { useSize } from '../utils'; +import { useSize, useDevicePixelRatio } from '../utils'; type RiveComponentProps = { setContainerRef: RefCallback; @@ -86,7 +86,12 @@ export default function useRive( const containerRef = useRef(null); const [rive, setRive] = useState(null); - const [dimensions, setDimensions] = useState({ + const [lastContainerDimensions, setLastContainerDimensions] = + useState({ + height: 0, + width: 0, + }); + const [lastCanvasSize, setLastCanvasSize] = useState({ height: 0, width: 0, }); @@ -94,6 +99,7 @@ export default function useRive( // Listen to changes in the window sizes and update the bounds when changes // occur. const size = useSize(containerRef); + const currentDevicePixelRatio = useDevicePixelRatio(); const isParamsLoaded = Boolean(riveParams); const options = getOptions(opts); @@ -131,23 +137,42 @@ export default function useRive( } const { width, height } = getCanvasDimensions(); - const boundsChanged = - width !== dimensions.width || height !== dimensions.height; - if (canvasRef.current && rive && boundsChanged) { - if (options.fitCanvasToArtboardHeight) { + if (canvasRef.current && rive) { + // Check if the canvas parent container bounds have changed and set + // new values accordingly + const boundsChanged = + width !== lastContainerDimensions.width || + height !== lastContainerDimensions.height; + if (options.fitCanvasToArtboardHeight && boundsChanged) { containerRef.current.style.height = height + 'px'; } if (options.useDevicePixelRatio) { - const dpr = window.devicePixelRatio || 1; - canvasRef.current.width = dpr * width; - canvasRef.current.height = dpr * height; - canvasRef.current.style.width = width + 'px'; - canvasRef.current.style.height = height + 'px'; - } else { + // Check if devicePixelRatio may have changed and get new canvas + // width/height values to set the size + const canvasSizeChanged = + width * currentDevicePixelRatio !== lastCanvasSize.width || + height * currentDevicePixelRatio !== lastCanvasSize.height; + 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, + }); + } + } else if (boundsChanged) { canvasRef.current.width = width; canvasRef.current.height = height; + setLastCanvasSize({ + width: width, + height: height, + }); } - setDimensions({ width, height }); + setLastContainerDimensions({ width, height }); // Updating the canvas width or height will clear the canvas, so call // startRendering() to redraw the current frame as the animation might @@ -171,7 +196,7 @@ export default function useRive( if (rive) { updateBounds(); } - }, [rive, size]); + }, [rive, size, currentDevicePixelRatio]); /** * Ref callback called when the canvas element mounts and unmounts. diff --git a/src/utils.ts b/src/utils.ts index 10f642c..9f00192 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -78,3 +78,48 @@ export function useSize( return size; } + +/** + * 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 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.addEventListener('change', updateDpr); + + return () => { + mediaMatcher.removeEventListener('change', updateDpr); + }; + }, [currentDpr]); + + return currentDpr; +} + +export 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); +} diff --git a/test/useRive.test.tsx b/test/useRive.test.tsx index 678d7e1..59b70ef 100644 --- a/test/useRive.test.tsx +++ b/test/useRive.test.tsx @@ -424,4 +424,31 @@ describe('useRive', () => { expect(canvasSpy).toHaveStyle('height: 100px'); expect(canvasSpy).toHaveStyle('width: 100px'); }); + + it('updates the canvas dimensions and size if there is a new canvas size calculation', async () => { + const params = { + src: 'file-src', + }; + + window.devicePixelRatio = 2; + + // @ts-ignore + mocked(rive.Rive).mockImplementation(() => baseRiveMock); + + const canvasSpy = document.createElement('canvas'); + const containerSpy = document.createElement('div'); + jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(100); + jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(100); + + const { result } = renderHook(() => useRive(params)); + + await act(async () => { + result.current.setCanvasRef(canvasSpy); + result.current.setContainerRef(containerSpy); + controlledRiveloadCb(); + }); + + expect(canvasSpy).toHaveAttribute('width', '200'); + expect(canvasSpy).toHaveAttribute('height', '200'); + }); });