mirror of
https://github.com/rive-app/rive-react.git
synced 2026-03-13 08:22:30 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a968ec266 | ||
|
|
b5f00e5c97 | ||
|
|
22e6dd3494 | ||
|
|
08b9f9a2aa | ||
|
|
a24b910096 | ||
|
|
33760042d1 | ||
|
|
2c82fa04e7 |
@@ -4,9 +4,9 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v3.0.50](https://github.com/rive-app/rive-react/compare/v3.0.38...v3.0.50)
|
||||
#### [v3.0.52](https://github.com/rive-app/rive-react/compare/v3.0.38...v3.0.52)
|
||||
|
||||
- feat: allow for children to be set inside the canvas for fallback content when canvas cannot be shown [`b56c17d`](https://github.com/rive-app/rive-react/commit/b56c17d48c51176d7c7b0e10d465548be2538eac)
|
||||
- fix: bump WASM to fix iterator over animatables [`b5f00e5`](https://github.com/rive-app/rive-react/commit/b5f00e5c97146573474ab73773fe189b1bcc5d43)
|
||||
|
||||
#### [v3.0.38](https://github.com/rive-app/rive-react/compare/v3.0.37...v3.0.38)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rive-react",
|
||||
"version": "3.0.50",
|
||||
"version": "3.0.52",
|
||||
"description": "React wrapper around the rive-js library",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/types/index.d.ts",
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/rive-app/rive-react#readme",
|
||||
"dependencies": {
|
||||
"@rive-app/canvas": "1.1.9",
|
||||
"@rive-app/webgl": "1.1.9"
|
||||
"@rive-app/canvas": "1.1.10",
|
||||
"@rive-app/webgl": "1.1.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
|
||||
@@ -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 = ({
|
||||
@@ -42,6 +46,7 @@ const Rive = ({
|
||||
layout,
|
||||
useOffscreenRenderer = true,
|
||||
shouldDisableRiveListeners = false,
|
||||
shouldResizeCanvasToContainer = true,
|
||||
children,
|
||||
...rest
|
||||
}: RiveProps & ComponentProps<'canvas'>) => {
|
||||
@@ -57,6 +62,7 @@ const Rive = ({
|
||||
|
||||
const options = {
|
||||
useOffscreenRenderer,
|
||||
shouldResizeCanvasToContainer,
|
||||
};
|
||||
|
||||
const { RiveComponent } = useRive(params, options);
|
||||
|
||||
94
src/hooks/useContainerSize.ts
Normal file
94
src/hooks/useContainerSize.ts
Normal file
@@ -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
src/hooks/useDevicePixelRatio.ts
Normal file
49
src/hooks/useDevicePixelRatio.ts
Normal file
@@ -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);
|
||||
}
|
||||
186
src/hooks/useResizeCanvas.ts
Normal file
186
src/hooks/useResizeCanvas.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
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 {
|
||||
/**
|
||||
* Whether or not Rive is loaded and renderer is associated with the canvas
|
||||
*/
|
||||
riveLoaded: boolean;
|
||||
/**
|
||||
* Ref to the canvas element
|
||||
*/
|
||||
canvasRef: MutableRefObject<HTMLCanvasElement | null>;
|
||||
/**
|
||||
* Ref to the container element of the canvas
|
||||
*/
|
||||
containerRef: MutableRefObject<HTMLElement | null>;
|
||||
/**
|
||||
* (Optional) Callback to be invoked after the canvas has been resized due to a resize
|
||||
* of its parent container. This is where you would want to reset the layout
|
||||
* dimensions for the Rive renderer to dictate the new min/max bounds of the
|
||||
* canvas.
|
||||
*
|
||||
* Using the high-level JS runtime, this might be a simple call to `rive.resizeToCanvas()`
|
||||
* Using the low-level JSruntime, this might be invoking the renderer's `.align()` method
|
||||
* with the Layout and min/max X/Y values of the canvas.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
onCanvasHasResized?: () => void;
|
||||
/**
|
||||
* (Optional) Options passed to the useRive hook, including the shouldResizeCanvasToContainer option
|
||||
* which prevents the canvas element from resizing to its parent container
|
||||
*/
|
||||
options?: Partial<UseRiveOptions>;
|
||||
/**
|
||||
* (Optional) AABB bounds of the artboard. If provided, the canvas will be sized to the artboard
|
||||
* height if the fitCanvasToArtboardHeight option is true.
|
||||
*/
|
||||
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 props to the hook
|
||||
*/
|
||||
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 { maxX, maxY } = artboardBounds ?? {};
|
||||
|
||||
const getContainerDimensions = 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, maxX, maxY]);
|
||||
|
||||
useEffect(() => {
|
||||
// If Rive is not ready, the container is not ready, or the user supplies a flag
|
||||
// to not resize the canvas to the container, then return early
|
||||
if (
|
||||
!shouldResizeCanvasToContainer ||
|
||||
!containerRef.current ||
|
||||
!riveLoaded
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height } = getContainerDimensions();
|
||||
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();
|
||||
}
|
||||
isFirstSizing && setIsFirstSizing(false);
|
||||
}, [
|
||||
canvasRef,
|
||||
containerRef,
|
||||
containerSize,
|
||||
currentDevicePixelRatio,
|
||||
getContainerDimensions,
|
||||
isFirstSizing,
|
||||
setIsFirstSizing,
|
||||
lastCanvasHeight,
|
||||
lastCanvasWidth,
|
||||
lastContainerHeight,
|
||||
lastContainerWidth,
|
||||
onCanvasHasResized,
|
||||
shouldResizeCanvasToContainer,
|
||||
fitCanvasToArtboardHeight,
|
||||
shouldUseDevicePixelRatio,
|
||||
riveLoaded,
|
||||
]);
|
||||
}
|
||||
@@ -7,13 +7,9 @@ import React, {
|
||||
RefCallback,
|
||||
} from 'react';
|
||||
import { Rive, EventType } from '@rive-app/canvas';
|
||||
import {
|
||||
UseRiveParameters,
|
||||
UseRiveOptions,
|
||||
RiveState,
|
||||
Dimensions,
|
||||
} from '../types';
|
||||
import { useSize, useDevicePixelRatio } from '../utils';
|
||||
import { UseRiveParameters, UseRiveOptions, RiveState } from '../types';
|
||||
import useResizeCanvas from './useResizeCanvas';
|
||||
import { getOptions } from '../utils';
|
||||
|
||||
type RiveComponentProps = {
|
||||
setContainerRef: RefCallback<HTMLElement>;
|
||||
@@ -51,22 +47,6 @@ function RiveComponent({
|
||||
);
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
useDevicePixelRatio: true,
|
||||
fitCanvasToArtboardHeight: false,
|
||||
useOffscreenRenderer: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns options, with defaults set.
|
||||
*
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
function getOptions(opts: Partial<UseRiveOptions>) {
|
||||
return Object.assign({}, defaultOptions, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Hook for loading a Rive file.
|
||||
*
|
||||
@@ -89,117 +69,30 @@ export default function useRive(
|
||||
const containerRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const [rive, setRive] = useState<Rive | null>(null);
|
||||
const [lastContainerDimensions, setLastContainerDimensions] =
|
||||
useState<Dimensions>({
|
||||
height: 0,
|
||||
width: 0,
|
||||
});
|
||||
const [lastCanvasSize, setLastCanvasSize] = useState<Dimensions>({
|
||||
height: 0,
|
||||
width: 0,
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
/**
|
||||
* Gets the intended dimensions of the canvas element.
|
||||
*
|
||||
* The intended dimensions are those of the container element, unless the
|
||||
* option `fitCanvasToArtboardHeight` is true, then they are adjusted to
|
||||
* the height of the artboard.
|
||||
*
|
||||
* @returns Dimensions object.
|
||||
* When the canvas/parent container resize, reset the Rive layout to match the
|
||||
* new (0, 0, canvas.width, canvas.height) bounds in the render loop
|
||||
*/
|
||||
function getCanvasDimensions() {
|
||||
// getBoundingClientRect returns the scaled width and height
|
||||
// this will result in double scaling
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/Determining_the_dimensions_of_elements
|
||||
|
||||
const width = containerRef.current?.clientWidth ?? 0;
|
||||
const height = containerRef.current?.clientHeight ?? 0;
|
||||
|
||||
if (rive && options.fitCanvasToArtboardHeight) {
|
||||
const { maxY, maxX } = rive.bounds;
|
||||
return { width, height: width * (maxY / maxX) };
|
||||
}
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the width and height of the canvas.
|
||||
*/
|
||||
function updateBounds() {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height } = getCanvasDimensions();
|
||||
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) {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
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
|
||||
// be paused and not advancing.
|
||||
rive.startRendering();
|
||||
}
|
||||
|
||||
// Always resize to Canvas
|
||||
const onCanvasHasResized = useCallback(() => {
|
||||
if (rive) {
|
||||
rive.startRendering();
|
||||
rive.resizeToCanvas();
|
||||
}
|
||||
}
|
||||
}, [rive]);
|
||||
|
||||
/**
|
||||
* Listen to changes on the windowSize and the rive file being loaded
|
||||
* and update the canvas bounds as needed.
|
||||
*
|
||||
* ie does not support ResizeObservers, so we fallback to the window listener there
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (rive) {
|
||||
updateBounds();
|
||||
}
|
||||
}, [rive, size, currentDevicePixelRatio]);
|
||||
// Watch the canvas parent container resize and size the canvas to match
|
||||
useResizeCanvas({
|
||||
riveLoaded: !!rive,
|
||||
canvasRef,
|
||||
containerRef,
|
||||
options,
|
||||
onCanvasHasResized,
|
||||
artboardBounds: rive?.bounds,
|
||||
});
|
||||
|
||||
/**
|
||||
* Ref callback called when the canvas element mounts and unmounts.
|
||||
@@ -301,11 +194,12 @@ export default function useRive(
|
||||
/>
|
||||
);
|
||||
},
|
||||
[]
|
||||
[setCanvasRef, setContainerRef]
|
||||
);
|
||||
|
||||
return {
|
||||
canvas: canvasRef.current,
|
||||
container: containerRef.current,
|
||||
setCanvasRef,
|
||||
setContainerRef,
|
||||
rive,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Rive from './components/Rive';
|
||||
import useRive from './hooks/useRive';
|
||||
import useStateMachineInput from './hooks/useStateMachineInput';
|
||||
import useResizeCanvas from './hooks/useResizeCanvas';
|
||||
|
||||
export default Rive;
|
||||
export { useRive, useStateMachineInput };
|
||||
export { useRive, useStateMachineInput, useResizeCanvas };
|
||||
export { RiveState, UseRiveParameters, UseRiveOptions } from './types';
|
||||
export * from '@rive-app/canvas';
|
||||
|
||||
@@ -7,6 +7,7 @@ export type UseRiveOptions = {
|
||||
useDevicePixelRatio: boolean;
|
||||
fitCanvasToArtboardHeight: boolean;
|
||||
useOffscreenRenderer: boolean;
|
||||
shouldResizeCanvasToContainer: boolean;
|
||||
};
|
||||
|
||||
export type Dimensions = {
|
||||
@@ -17,6 +18,7 @@ export type Dimensions = {
|
||||
/**
|
||||
* @typedef RiveState
|
||||
* @property canvas - Canvas element the Rive Animation is attached to.
|
||||
* @property container - Container element of the canvas.
|
||||
* @property setCanvasRef - Ref callback to be passed to the canvas element.
|
||||
* @property setContainerRef - Ref callback to be passed to the container element
|
||||
* of the canvas. This is optional, however if not used then the hook will
|
||||
@@ -26,6 +28,7 @@ export type Dimensions = {
|
||||
*/
|
||||
export type RiveState = {
|
||||
canvas: HTMLCanvasElement | null;
|
||||
container: HTMLElement | null;
|
||||
setCanvasRef: RefCallback<HTMLCanvasElement>;
|
||||
setContainerRef: RefCallback<HTMLElement>;
|
||||
rive: Rive | null;
|
||||
|
||||
137
src/utils.ts
137
src/utils.ts
@@ -1,129 +1,12 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Dimensions } from './types';
|
||||
import { UseRiveOptions } 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;
|
||||
|
||||
export function useSize(
|
||||
containerRef: React.MutableRefObject<HTMLElement | null>
|
||||
) {
|
||||
const [size, setSize] = useState<Dimensions>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
// internet explorer does not support ResizeObservers.
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
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 current = observer.current;
|
||||
if (containerRef.current && useResizeObserver) {
|
||||
current.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
current.disconnect();
|
||||
if (containerRef.current && useResizeObserver) {
|
||||
current.unobserve(containerRef.current);
|
||||
}
|
||||
};
|
||||
}, [containerRef, observer]);
|
||||
|
||||
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.hasOwnProperty('addEventListener')
|
||||
? mediaMatcher.addEventListener('change', updateDpr)
|
||||
: mediaMatcher.addListener(updateDpr);
|
||||
|
||||
return () => {
|
||||
mediaMatcher.hasOwnProperty('removeEventListener')
|
||||
? mediaMatcher.removeEventListener('change', updateDpr)
|
||||
: mediaMatcher.removeListener(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);
|
||||
const defaultOptions = {
|
||||
useDevicePixelRatio: true,
|
||||
fitCanvasToArtboardHeight: false,
|
||||
useOffscreenRenderer: true,
|
||||
shouldResizeCanvasToContainer: true,
|
||||
};
|
||||
|
||||
export function getOptions(opts: Partial<UseRiveOptions>) {
|
||||
return Object.assign({}, defaultOptions, opts);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,9 @@ describe('useRive', () => {
|
||||
result.current.setCanvasRef(canvasSpy);
|
||||
result.current.setContainerRef(containerSpy);
|
||||
controlledRiveloadCb();
|
||||
jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(500);
|
||||
jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(500);
|
||||
containerSpy.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
expect(result.current.rive).toBe(riveMock);
|
||||
@@ -192,7 +195,7 @@ describe('useRive', () => {
|
||||
expect(canvasSpy).toHaveAttribute('width', '100');
|
||||
});
|
||||
|
||||
it('uses artbound height to set bounds if fitCanvasToArtboardHeight is true', async () => {
|
||||
it('uses artboard height to set bounds if fitCanvasToArtboardHeight is true', async () => {
|
||||
const params = {
|
||||
src: 'file-src',
|
||||
};
|
||||
@@ -446,9 +449,45 @@ describe('useRive', () => {
|
||||
result.current.setCanvasRef(canvasSpy);
|
||||
result.current.setContainerRef(containerSpy);
|
||||
controlledRiveloadCb();
|
||||
jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(200);
|
||||
jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(200);
|
||||
containerSpy.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
expect(canvasSpy).toHaveAttribute('width', '200');
|
||||
expect(canvasSpy).toHaveAttribute('height', '200');
|
||||
expect(canvasSpy).toHaveAttribute('width', '400');
|
||||
expect(canvasSpy).toHaveAttribute('height', '400');
|
||||
});
|
||||
|
||||
it('prevents resizing if shouldResizeCanvasToContainer option is false', async () => {
|
||||
const params = {
|
||||
src: 'file-src',
|
||||
};
|
||||
const options = {
|
||||
shouldResizeCanvasToContainer: false,
|
||||
};
|
||||
|
||||
window.devicePixelRatio = 2;
|
||||
|
||||
// @ts-ignore
|
||||
mocked(rive.Rive).mockImplementation(() => baseRiveMock);
|
||||
|
||||
const canvasSpy = document.createElement('canvas');
|
||||
canvasSpy.width = 200;
|
||||
canvasSpy.height = 200;
|
||||
const containerSpy = document.createElement('div');
|
||||
|
||||
const { result } = renderHook(() => useRive(params, options));
|
||||
|
||||
await act(async () => {
|
||||
result.current.setCanvasRef(canvasSpy);
|
||||
result.current.setContainerRef(containerSpy);
|
||||
controlledRiveloadCb();
|
||||
jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(500);
|
||||
jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(500);
|
||||
containerSpy.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
expect(canvasSpy.width).toBe(200);
|
||||
expect(canvasSpy.height).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user