-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: Move canvas sizing logic into useResizeCanvas hook and clean…
… up util hooks
- Loading branch information
Showing
9 changed files
with
374 additions
and
255 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
]); | ||
} |
Oops, something went wrong.