Compare commits

...

47 Commits

Author SHA1 Message Date
bodymovin
e4fcef529e chore: release 4.11.3 2024-06-08 03:56:42 +00:00
Hernan Torrisi
f51fe6ee2a update rive canvas to 2.17.3 2024-06-07 22:53:12 -05:00
bodymovin
0f558e66e1 chore: release 4.11.2 2024-06-07 01:10:59 +00:00
Hernan Torrisi
7a67fb14f7 bump rive 2024-06-06 20:06:49 -05:00
bodymovin
da3fc317ff chore: release 4.11.1 2024-06-06 18:08:51 +00:00
Hernan Torrisi
9b40a1a02e update rive version 2024-06-06 13:04:18 -05:00
Maxwell Talbot
aa92c01329 chore: release 4.11.0 2024-06-06 15:07:03 +01:00
Gordon Hayes
99a8a42a15 chore: bump web to v2.17.0 2024-06-06 10:24:12 +02:00
HayesGordon
f0ea7add89 chore: release 4.10.0 2024-05-28 08:45:14 +00:00
Hernan Torrisi
ac9f322ccb update comments 2024-05-27 20:19:23 -05:00
Hernan Torrisi
e1c2d108e9 add missing mocked methods 2024-05-27 20:19:23 -05:00
Hernan Torrisi
3aaee0bcf8 update canvas package version 2024-05-27 20:19:23 -05:00
Hernan Torrisi
e3739f1a1f request instance of rive file 2024-05-27 20:19:23 -05:00
Hernan Torrisi
0e0a2bd972 add status handling 2024-05-27 20:19:23 -05:00
Adam
5c47a411f2 test: update useRiveFile tests to check buffer changes 2024-05-27 20:19:23 -05:00
Adam
16dc257b2f feat: create useRiveFile hook 2024-05-27 20:19:23 -05:00
Hernan Torrisi
4678ea9ecf interpret undefined definition for shouldUseIntersectionObserver as true 2024-05-27 18:24:51 -05:00
Hernan Torrisi
d58963e29d edit hook description 2024-05-27 18:24:51 -05:00
Hernan Torrisi
c32433284a fix observer and use single instance 2024-05-27 18:24:51 -05:00
Hernan Torrisi
8bb5652df0 improve tests 2024-05-14 12:02:47 -05:00
Hernan Torrisi
9ecacf37fe fix tests 2024-05-14 12:02:47 -05:00
Hernan Torrisi
f837fbe0d4 WIP 2024-05-14 12:02:47 -05:00
Hernan Torrisi
8e181d6ae2 change hooks lifecycle to account for component reloading 2024-05-14 12:02:47 -05:00
mjtalbot
a3a41dca40 chore: release 4.9.5 2024-05-10 10:00:11 +00:00
Maxwell Talbot
b78c9715d7 bump canvas to 2.15.6 2024-05-10 10:51:08 +01:00
bodymovin
520e5bb51d chore: release 4.9.4 2024-05-09 00:49:46 +00:00
Hernan Torrisi
586ee4c002 bump version to 2.15.5 2024-05-08 19:46:15 -05:00
bodymovin
3d6c7ed499 chore: release 4.9.3 2024-05-08 17:09:41 +00:00
Hernan Torrisi
c3900b7845 bump version to 2.15.4 2024-05-08 12:05:23 -05:00
bodymovin
d369817113 chore: release 4.9.2 2024-04-30 20:06:06 +00:00
Hernan Torrisi
d68302ccb3 bump rive canvas to 2.15.2 2024-04-30 14:51:06 -05:00
bodymovin
bbd4cc7af6 chore: release 4.9.1 2024-04-30 16:46:29 +00:00
Hernan Torrisi
3936277f65 update to version 2.15.1 2024-04-30 11:37:50 -05:00
HayesGordon
aa2a783d1c chore: release 4.9.0 2024-04-24 14:04:19 +00:00
Gordon Hayes
d8d7d64749 chore: bump rive wasm to v2.15.0 2024-04-24 15:57:51 +02:00
mjtalbot
af3edad2c2 chore: release 4.8.10 2024-04-23 09:28:37 +00:00
Maxwell Talbot
5326f800f7 bump rive library dependencies to 2.14.4 2024-04-23 10:20:19 +01:00
mjtalbot
b44f9ad9e1 chore: release 4.8.9 2024-04-18 09:20:12 +00:00
Maxwell Talbot
b2495300b7 Bump rive js libraries to 2.14.3 2024-04-18 10:16:25 +01:00
mjtalbot
eb436263d7 chore: release 4.8.8 2024-04-17 16:24:43 +00:00
Maxwell Talbot
0e6385288e bump canvas and webgl to 2.14.2 2024-04-17 17:17:59 +01:00
bodymovin
78f75434fc chore: release 4.8.7 2024-04-11 19:04:48 +00:00
Hernan Torrisi
a9c2950419 bump react canvas to 2.14.1 2024-04-11 14:00:46 -05:00
avivian
e799f64554 chore: release 4.8.6 2024-04-09 08:54:46 +00:00
Arthur Vivian
95a1daa4ef Bump rive-canvas to 2.13.2 2024-04-09 09:51:08 +01:00
avivian
8a5b88c591 chore: release 4.8.5 2024-04-08 20:01:44 +00:00
Arthur Vivian
d3b29cf7d7 bump version to 2.13.0 2024-04-08 20:52:04 +01:00
17 changed files with 664 additions and 104 deletions

View File

@@ -4,11 +4,131 @@ 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).
#### [v4.11.3](https://github.com/rive-app/rive-react/compare/v4.11.2...v4.11.3)
- update rive canvas to 2.17.3 [`f51fe6e`](https://github.com/rive-app/rive-react/commit/f51fe6ee2aafef72f9272e49535d4f2c89b9b44f)
#### [v4.11.2](https://github.com/rive-app/rive-react/compare/v4.11.1...v4.11.2)
> 7 June 2024
- chore: release 4.11.2 [`0f558e6`](https://github.com/rive-app/rive-react/commit/0f558e66e1a78aae923bd79c39b25c9725eb76bd)
- bump rive [`7a67fb1`](https://github.com/rive-app/rive-react/commit/7a67fb14f7cb581e2fa823e9d968fc506ce2834e)
#### [v4.11.1](https://github.com/rive-app/rive-react/compare/v4.11.0...v4.11.1)
> 6 June 2024
- chore: release 4.11.1 [`da3fc31`](https://github.com/rive-app/rive-react/commit/da3fc317ff9cc275a30d5a42adad3d0532947f9a)
- chore: release 4.11.0 [`aa92c01`](https://github.com/rive-app/rive-react/commit/aa92c013296bfd4c848e1c41b0c52ad48b1f6ab7)
- update rive version [`9b40a1a`](https://github.com/rive-app/rive-react/commit/9b40a1a02e1877445b3e3dc35b96a4b7e3a54780)
#### [v4.11.0](https://github.com/rive-app/rive-react/compare/v4.10.0...v4.11.0)
> 6 June 2024
- chore: release 4.11.0 [`2142210`](https://github.com/rive-app/rive-react/commit/21422104c9d502887352c40a6e185c69098f438d)
- chore: bump web to v2.17.0 [`99a8a42`](https://github.com/rive-app/rive-react/commit/99a8a42a15969e70c96cc90460ec9fcba514ed4c)
#### [v4.10.0](https://github.com/rive-app/rive-react/compare/v4.9.5...v4.10.0)
> 28 May 2024
- fix observer and use single instance [`c324332`](https://github.com/rive-app/rive-react/commit/c32433284ad4116170ab016139ecba7678e6c21e)
- feat: create useRiveFile hook [`16dc257`](https://github.com/rive-app/rive-react/commit/16dc257b2f904d51101535002b9fb34640a65842)
- change hooks lifecycle to account for component reloading [`8e181d6`](https://github.com/rive-app/rive-react/commit/8e181d6ae2dff82875c60be789dcd63bedaba883)
#### [v4.9.5](https://github.com/rive-app/rive-react/compare/v4.9.4...v4.9.5)
> 10 May 2024
- chore: release 4.9.5 [`a3a41dc`](https://github.com/rive-app/rive-react/commit/a3a41dca404193a900f021a2358d85d4ab44fb26)
- bump canvas to 2.15.6 [`b78c971`](https://github.com/rive-app/rive-react/commit/b78c9715d713031d0dbdacb8c4517be5d5a411b8)
#### [v4.9.4](https://github.com/rive-app/rive-react/compare/v4.9.3...v4.9.4)
> 9 May 2024
- chore: release 4.9.4 [`520e5bb`](https://github.com/rive-app/rive-react/commit/520e5bb51d8c31b769149a6daee2c84ec0a1a3d6)
- bump version to 2.15.5 [`586ee4c`](https://github.com/rive-app/rive-react/commit/586ee4c002a13251d8f13adce11bc6f9cda3a904)
#### [v4.9.3](https://github.com/rive-app/rive-react/compare/v4.9.2...v4.9.3)
> 8 May 2024
- chore: release 4.9.3 [`3d6c7ed`](https://github.com/rive-app/rive-react/commit/3d6c7ed4991da4b729b11e02ea2cda3d3f6b078c)
- bump version to 2.15.4 [`c3900b7`](https://github.com/rive-app/rive-react/commit/c3900b7845a63862b7da0d64d16294dd015e876e)
#### [v4.9.2](https://github.com/rive-app/rive-react/compare/v4.9.1...v4.9.2)
> 30 April 2024
- chore: release 4.9.2 [`d369817`](https://github.com/rive-app/rive-react/commit/d3698171138887e4a27180b31c40750090fb0863)
- bump rive canvas to 2.15.2 [`d68302c`](https://github.com/rive-app/rive-react/commit/d68302ccb3f5756b753f29897033da2c0aa015d5)
#### [v4.9.1](https://github.com/rive-app/rive-react/compare/v4.9.0...v4.9.1)
> 30 April 2024
- chore: release 4.9.1 [`bbd4cc7`](https://github.com/rive-app/rive-react/commit/bbd4cc7af651df5fb5f28313b7e7e55e294a0282)
- update to version 2.15.1 [`3936277`](https://github.com/rive-app/rive-react/commit/3936277f658981f6928bf0ef77ea01bb60ce27c5)
#### [v4.9.0](https://github.com/rive-app/rive-react/compare/v4.8.10...v4.9.0)
> 24 April 2024
- chore: release 4.9.0 [`aa2a783`](https://github.com/rive-app/rive-react/commit/aa2a783d1c5e6560f59fc8299858d68464a28308)
- chore: bump rive wasm to v2.15.0 [`d8d7d64`](https://github.com/rive-app/rive-react/commit/d8d7d64749ff019edd6d50ea98dea9000967071a)
#### [v4.8.10](https://github.com/rive-app/rive-react/compare/v4.8.9...v4.8.10)
> 23 April 2024
- chore: release 4.8.10 [`af3edad`](https://github.com/rive-app/rive-react/commit/af3edad2c20e820569544318bd68bb3c56b9e180)
- bump rive library dependencies to 2.14.4 [`5326f80`](https://github.com/rive-app/rive-react/commit/5326f800f7a99eafa9c2081e22966509f89008da)
#### [v4.8.9](https://github.com/rive-app/rive-react/compare/v4.8.8...v4.8.9)
> 18 April 2024
- chore: release 4.8.9 [`b44f9ad`](https://github.com/rive-app/rive-react/commit/b44f9ad9e157702af5b5a946c8a476f4842392ce)
- Bump rive js libraries to 2.14.3 [`b249530`](https://github.com/rive-app/rive-react/commit/b2495300b7a7badcdfb6ced96c2eabb8779c36f0)
#### [v4.8.8](https://github.com/rive-app/rive-react/compare/v4.8.7...v4.8.8)
> 17 April 2024
- chore: release 4.8.8 [`eb43626`](https://github.com/rive-app/rive-react/commit/eb436263d786ffdee20d39dea0ce16b8aec101a9)
- bump canvas and webgl to 2.14.2 [`0e63852`](https://github.com/rive-app/rive-react/commit/0e6385288ec85c40c5e7f4bc6f15b98a02ec6b88)
#### [v4.8.7](https://github.com/rive-app/rive-react/compare/v4.8.6...v4.8.7)
> 11 April 2024
- chore: release 4.8.7 [`78f7543`](https://github.com/rive-app/rive-react/commit/78f75434fc179326918f6e837058a74fa4a8dbd6)
- bump react canvas to 2.14.1 [`a9c2950`](https://github.com/rive-app/rive-react/commit/a9c29504191d929b7e96587100d375ba6787deb2)
#### [v4.8.6](https://github.com/rive-app/rive-react/compare/v4.8.5...v4.8.6)
> 9 April 2024
- chore: release 4.8.6 [`e799f64`](https://github.com/rive-app/rive-react/commit/e799f64554e05de8c3b0666aa7091859576d37ba)
- Bump rive-canvas to 2.13.2 [`95a1daa`](https://github.com/rive-app/rive-react/commit/95a1daa4efbb3835e886317ad16161fe8ac843a5)
#### [v4.8.5](https://github.com/rive-app/rive-react/compare/v4.8.4...v4.8.5)
> 8 April 2024
- chore: release 4.8.5 [`8a5b88c`](https://github.com/rive-app/rive-react/commit/8a5b88c59132e5d04fdcce1db903397fd15b5a18)
- bump version to 2.13.0 [`d3b29cf`](https://github.com/rive-app/rive-react/commit/d3b29cf7d7c6fe99e4e12f4579ef0876ef875b35)
#### [v4.8.4](https://github.com/rive-app/rive-react/compare/v4.8.3...v4.8.4)
> 29 March 2024
- feat: add new webgl2 package for new Rive Renderer [`a7875b2`](https://github.com/rive-app/rive-react/commit/a7875b26a43342f6380a5009b25b831acfcfb610)
- chore: release 4.8.4 [`611522b`](https://github.com/rive-app/rive-react/commit/611522b3e06c30923b4aefa737310c376dab861c)
- Update README.md [`4653b8b`](https://github.com/rive-app/rive-react/commit/4653b8bea1169408c94962e80fe3f2c34c8b221f)
- bump version to 2.12.0 [`89d3597`](https://github.com/rive-app/rive-react/commit/89d35976d4c8c91eddeb35b9dda5e3073ef45851)
#### [v4.8.3](https://github.com/rive-app/rive-react/compare/v4.8.2...v4.8.3)

View File

@@ -1,6 +1,6 @@
{
"name": "@rive-app/react-canvas-lite",
"version": "4.8.4",
"version": "4.11.3",
"description": "React wrapper around the @rive-app/canvas-lite library",
"main": "dist/index.js",
"typings": "dist/types/index.d.ts",
@@ -18,7 +18,7 @@
},
"homepage": "https://github.com/rive-app/rive-react#readme",
"dependencies": {
"@rive-app/canvas-lite": "2.12.0"
"@rive-app/canvas-lite": "2.17.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@rive-app/react-canvas",
"version": "4.8.4",
"version": "4.11.3",
"description": "React wrapper around the @rive-app/canvas library",
"main": "dist/index.js",
"typings": "dist/types/index.d.ts",
@@ -18,7 +18,7 @@
},
"homepage": "https://github.com/rive-app/rive-react#readme",
"dependencies": {
"@rive-app/canvas": "2.12.0"
"@rive-app/canvas": "2.17.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@rive-app/react-webgl",
"version": "4.8.4",
"version": "4.11.3",
"description": "React wrapper around the @rive-app/webgl library",
"main": "dist/index.js",
"typings": "dist/types/index.d.ts",
@@ -18,7 +18,7 @@
},
"homepage": "https://github.com/rive-app/rive-react#readme",
"dependencies": {
"@rive-app/webgl": "2.12.0"
"@rive-app/webgl": "2.17.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@rive-app/react-webgl2",
"version": "4.8.4",
"version": "4.11.3",
"description": "React wrapper around the @rive-app/webgl2 library",
"main": "dist/index.js",
"typings": "dist/types/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "rive-react",
"version": "4.8.4",
"version": "4.11.3",
"description": "React wrapper around the rive-js library",
"main": "dist/index.js",
"typings": "dist/types/index.d.ts",
@@ -36,9 +36,9 @@
},
"homepage": "https://github.com/rive-app/rive-react#readme",
"dependencies": {
"@rive-app/canvas": "2.12.0",
"@rive-app/canvas-lite": "2.12.0",
"@rive-app/webgl": "2.12.0"
"@rive-app/canvas": "2.17.3",
"@rive-app/canvas-lite": "2.17.3",
"@rive-app/webgl": "2.17.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"

View File

@@ -0,0 +1,38 @@
class FakeIntersectionObserver {
observe() {}
unobserve() {}
disconnect() {}
}
const MyIntersectionObserver =
globalThis.IntersectionObserver || FakeIntersectionObserver;
class ElementObserver {
private observer;
private elementsMap: Map<Element, Function> = new Map();
constructor() {
this.observer = new MyIntersectionObserver(this.onObserved);
}
public onObserved = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
const elementCallback = this.elementsMap.get(entry.target as Element);
if (elementCallback) {
elementCallback(entry);
}
});
};
public registerCallback(element: Element, callback: Function) {
this.observer.observe(element);
this.elementsMap.set(element, callback);
}
public removeCallback(element: Element) {
this.observer.unobserve(element);
this.elementsMap.delete(element);
}
}
export default ElementObserver;

View File

@@ -0,0 +1,32 @@
import { useCallback } from 'react';
import ElementObserver from './elementObserver';
let observer: ElementObserver;
const getObserver = () => {
if(!observer) {
observer = new ElementObserver();
}
return observer;
}
/**
* Hook to observe elements when they are intersecting with the viewport
*
* @returns - API to observer and unobserve elements
*/
export default function useIntersectionObserver() {
const observe = useCallback((element: Element, callback: Function) => {
const observer = getObserver();
observer.registerCallback(element, callback);
}, []);
const unobserve = useCallback((element: Element) => {
const observer = getObserver();
observer.removeCallback(element);
}, []);
return {
observe,
unobserve,
};
}

View File

@@ -13,7 +13,7 @@ interface UseResizeCanvasProps {
/**
* Ref to the canvas element
*/
canvasRef: MutableRefObject<HTMLCanvasElement | null>;
canvasElem: HTMLCanvasElement | null;
/**
* Ref to the container element of the canvas
*/
@@ -55,7 +55,7 @@ interface UseResizeCanvasProps {
*/
export default function useResizeCanvas({
riveLoaded = false,
canvasRef,
canvasElem,
containerRef,
options = {},
onCanvasHasResized,
@@ -120,7 +120,7 @@ export default function useResizeCanvas({
const { width, height } = getContainerDimensions();
let hasResized = false;
if (canvasRef.current) {
if (canvasElem) {
// Check if the canvas parent container bounds have changed and set
// new values accordingly
const boundsChanged =
@@ -138,10 +138,10 @@ export default function useResizeCanvas({
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';
canvasElem.width = newCanvasWidthProp;
canvasElem.height = newCanvasHeightProp;
canvasElem.style.width = width + 'px';
canvasElem.style.height = height + 'px';
setLastCanvasSize({
width: newCanvasWidthProp,
height: newCanvasHeightProp,
@@ -149,8 +149,8 @@ export default function useResizeCanvas({
hasResized = true;
}
} else if (boundsChanged) {
canvasRef.current.width = width;
canvasRef.current.height = height;
canvasElem.width = width;
canvasElem.height = height;
setLastCanvasSize({
width: width,
height: height,
@@ -167,7 +167,7 @@ export default function useResizeCanvas({
}
isFirstSizing && setIsFirstSizing(false);
}, [
canvasRef,
canvasElem,
containerRef,
containerSize,
currentDevicePixelRatio,
@@ -184,4 +184,12 @@ export default function useResizeCanvas({
shouldUseDevicePixelRatio,
riveLoaded,
]);
// Reset width and height values when the canvas changes
useEffect(() => {
setLastCanvasSize({
width: 0,
height: 0,
});
}, [canvasElem]);
}

View File

@@ -10,6 +10,7 @@ import { Rive, EventType } from '@rive-app/canvas';
import { UseRiveParameters, UseRiveOptions, RiveState } from '../types';
import useResizeCanvas from './useResizeCanvas';
import { getOptions } from '../utils';
import useIntersectionObserver from './useIntersectionObserver';
type RiveComponentProps = {
setContainerRef: RefCallback<HTMLElement>;
@@ -65,7 +66,7 @@ export default function useRive(
riveParams?: UseRiveParameters,
opts: Partial<UseRiveOptions> = {}
): RiveState {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [canvasElem, setCanvasElem] = useState<HTMLCanvasElement | null>(null);
const containerRef = useRef<HTMLElement | null>(null);
const [rive, setRive] = useState<Rive | null>(null);
@@ -87,7 +88,7 @@ export default function useRive(
// Watch the canvas parent container resize and size the canvas to match
useResizeCanvas({
riveLoaded: !!rive,
canvasRef,
canvasElem,
containerRef,
options,
onCanvasHasResized,
@@ -99,32 +100,39 @@ export default function useRive(
*/
const setCanvasRef: RefCallback<HTMLCanvasElement> = useCallback(
(canvas: HTMLCanvasElement | null) => {
if (canvas && riveParams && isParamsLoaded) {
const { useOffscreenRenderer } = options;
const r = new Rive({
useOffscreenRenderer,
...riveParams,
canvas,
});
r.on(EventType.Load, () => {
// Check if the component/canvas is mounted before setting state to avoid setState
// on an unmounted component in some rare cases
if (canvasRef.current) {
setRive(r);
} else {
// If unmounted, cleanup the rive object immediately
r.cleanup();
}
});
} else if (canvas === null && canvasRef.current) {
canvasRef.current.height = 0;
canvasRef.current.width = 0;
if (canvas === null && canvasElem) {
canvasElem.height = 0;
canvasElem.width = 0;
}
canvasRef.current = canvas;
setCanvasElem(canvas);
},
[isParamsLoaded]
[]
);
useEffect(() => {
if (!canvasElem || !riveParams) {
return;
}
if (rive == null) {
const { useOffscreenRenderer } = options;
const r = new Rive({
useOffscreenRenderer,
...riveParams,
canvas: canvasElem,
});
r.on(EventType.Load, () => {
// Check if the component/canvas is mounted before setting state to avoid setState
// on an unmounted component in some rare cases
if (canvasElem) {
setRive(r);
} else {
// If unmounted, cleanup the rive object immediately
r.cleanup();
}
});
}
}, [canvasElem, isParamsLoaded, rive]);
/**
* Ref callback called when the container element mounts
*/
@@ -139,21 +147,62 @@ export default function useRive(
* Set up IntersectionObserver to stop rendering if the animation is not in
* view.
*/
const { observe, unobserve } = useIntersectionObserver();
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
let timeoutId: ReturnType<typeof setTimeout>;
let isPaused = false;
// This is a workaround to retest whether an element is offscreen or not.
// There seems to be a bug in Chrome that triggers an intersection change when an element
// is moved within the DOM using insertBefore.
// For some reason, when this is called whithin the context of a React application, the
// intersection callback is called only once reporting isIntersecting as false but never
// triggered back with isIntersecting as true.
// For this reason we retest after 10 millisecond whether the element is actually off the
// viewport or not.
const retestIntersection = () => {
if (canvasElem && isPaused) {
const size = canvasElem.getBoundingClientRect();
const isIntersecting =
size.width > 0 &&
size.height > 0 &&
size.top <
(window.innerHeight || document.documentElement.clientHeight) &&
size.bottom > 0 &&
size.left <
(window.innerWidth || document.documentElement.clientWidth) &&
size.right > 0;
if (isIntersecting) {
rive?.startRendering();
isPaused = false;
}
}
};
const onChange = (entry: IntersectionObserverEntry) => {
entry.isIntersecting
? rive && rive.startRendering()
: rive && rive.stopRendering();
});
if (canvasRef.current) {
observer.observe(canvasRef.current);
}
return () => {
observer.disconnect();
isPaused = !entry.isIntersecting;
clearTimeout(timeoutId);
if (!entry.isIntersecting && entry.boundingClientRect.width === 0) {
timeoutId = setTimeout(retestIntersection, 10);
}
};
}, [rive]);
if (canvasElem && options.shouldUseIntersectionObserver !== false) {
observe(canvasElem, onChange);
}
return () => {
if (canvasElem) {
unobserve(canvasElem);
}
};
}, [
observe,
unobserve,
rive,
canvasElem,
options.shouldUseIntersectionObserver,
]);
/**
* On unmount, call cleanup to cleanup any WASM generated objects that need
@@ -166,7 +215,7 @@ export default function useRive(
setRive(null);
}
};
}, [rive]);
}, [rive, canvasElem]);
/**
* Listen for changes in the animations params
@@ -198,7 +247,7 @@ export default function useRive(
);
return {
canvas: canvasRef.current,
canvas: canvasElem,
container: containerRef.current,
setCanvasRef,
setContainerRef,

52
src/hooks/useRiveFile.ts Normal file
View File

@@ -0,0 +1,52 @@
import { useState, useEffect } from 'react';
import type {
UseRiveFileParameters,
RiveFileState,
FileStatus,
} from '../types';
import { EventType, RiveFile } from '@rive-app/canvas';
/**
* Custom hook for initializing and managing a RiveFile instance within a component.
* It sets up a RiveFile based on provided source parameters (URL or ArrayBuffer) and ensures
* proper cleanup to avoid memory leaks when the component unmounts or inputs change.
*
* @param params - Object containing parameters accepted by the Rive file in the @rive-app/canvas runtime,
*
* @returns {RiveFileState} Contains the active RiveFile instance (`riveFile`) and the loading status.
*/
function useRiveFile(params: UseRiveFileParameters): RiveFileState {
const [riveFile, setRiveFile] = useState<RiveFile | null>(null);
const [status, setStatus] = useState<FileStatus>('idle');
useEffect(() => {
let file: RiveFile | null = null;
const loadRiveFile = async () => {
setStatus('loading');
file = new RiveFile(params);
file.init();
file.on(EventType.Load, () => {
// We request an instance to add +1 to the referencesCount so it doesn't get destroyed
// while this hook is active
file?.getInstance();
setRiveFile(file);
setStatus('success');
});
file.on(EventType.LoadError, () => {
setStatus('failed');
});
setRiveFile(file);
};
loadRiveFile();
return () => {
file?.cleanup();
};
}, [params.src, params.buffer]);
return { riveFile, status };
}
export default useRiveFile;

View File

@@ -2,8 +2,9 @@ import Rive, { RiveProps } from './components/Rive';
import useRive from './hooks/useRive';
import useStateMachineInput from './hooks/useStateMachineInput';
import useResizeCanvas from './hooks/useResizeCanvas';
import useRiveFile from './hooks/useRiveFile';
export default Rive;
export { useRive, useStateMachineInput, useResizeCanvas, RiveProps };
export { RiveState, UseRiveParameters, UseRiveOptions } from './types';
export { useRive, useStateMachineInput, useResizeCanvas, useRiveFile , RiveProps };
export { RiveState, UseRiveParameters, UseRiveFileParameters, UseRiveOptions } from './types';
export * from '@rive-app/canvas';

View File

@@ -1,5 +1,10 @@
import { RefCallback, ComponentProps } from 'react';
import { Rive, RiveParameters } from '@rive-app/canvas';
import {
Rive,
RiveFile,
RiveFileParameters,
RiveParameters,
} from '@rive-app/canvas';
import { ComponentProps, RefCallback } from 'react';
export type UseRiveParameters = Partial<Omit<RiveParameters, 'canvas'>> | null;
@@ -9,6 +14,7 @@ export type UseRiveOptions = {
fitCanvasToArtboardHeight: boolean;
useOffscreenRenderer: boolean;
shouldResizeCanvasToContainer: boolean;
shouldUseIntersectionObserver?: boolean;
};
export type Dimensions = {
@@ -21,9 +27,9 @@ export type Dimensions = {
* @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
* not take care of automatically resizing the canvas to it's outer
* @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 not take care of automatically resizing the canvas to it's outer
* container if the window resizes.
* @property rive - The loaded Rive Animation
*/
@@ -35,3 +41,19 @@ export type RiveState = {
rive: Rive | null;
RiveComponent: (props: ComponentProps<'canvas'>) => JSX.Element;
};
export type UseRiveFileParameters = Partial<
Omit<RiveFileParameters, 'onLoad' | 'onLoadError'>
>;
export type FileStatus = 'idle' | 'loading' | 'failed' | 'success';
/**
* @typedef RiveFileState
* @property data - The RiveFile instance
* @property status - The status of the file
*/
export type RiveFileState = {
riveFile: RiveFile | null;
status: FileStatus;
};

View File

@@ -0,0 +1,39 @@
// TODO move this
const observe = jest.fn();
const unobserve = jest.fn();
const disconnect = jest.fn();
jest.spyOn(globalThis, 'IntersectionObserver').mockImplementation(() => {
return {
observe,
unobserve,
disconnect,
root: null,
thresholds: [],
rootMargin: '',
takeRecords: () => [],
};
});
import ElementObserver from '../src/hooks/elementObserver';
describe('elementObserver', () => {
it('registers a callback and observes the element', () => {
const observer = new ElementObserver();
const element = document.createElement('li');
observer.registerCallback(element, ()=>{});
expect(observe).toHaveBeenCalled();
expect(observe).toHaveBeenCalledWith(element);
});
it('unregisters a callback and unobserves the element', () => {
const observer = new ElementObserver();
const element = document.createElement('li');
observer.removeCallback(element);
expect(unobserve).toHaveBeenCalled();
expect(unobserve).toHaveBeenCalledWith(element);
});
});
jest.clearAllMocks();

View File

@@ -0,0 +1,42 @@
import { renderHook, act } from '@testing-library/react-hooks';
import ElementObserver from '../src/hooks/elementObserver';
jest.mock('../src/hooks/elementObserver');
import useIntersectionObserver from '../src/hooks/useIntersectionObserver';
describe('useIntersectionObserver', () => {
it('returns an object on initialization', () => {
const { result } = renderHook(() => useIntersectionObserver());
expect(result.current).toBeDefined();
});
it('registers a callback', () => {
const { result } = renderHook(() => useIntersectionObserver());
const element = document.createElement('li');
const callback = () => {};
act(() => {
result.current.observe(element, callback);
});
const mockElementObserver = (ElementObserver as jest.Mock).mock
.instances[0];
const registerCallback = mockElementObserver.registerCallback;
expect(registerCallback.mock.calls.length).toBe(1);
expect(registerCallback.mock.calls[0].length).toBe(2);
expect(registerCallback.mock.calls[0][0]).toBe(element);
expect(registerCallback.mock.calls[0][1]).toBe(callback);
});
it('unregisters a callback', () => {
const { result } = renderHook(() => useIntersectionObserver());
const element = document.createElement('li');
act(() => {
result.current.unobserve(element);
});
const mockElementObserver = (ElementObserver as jest.Mock).mock
.instances[0];
const removeCallback = mockElementObserver.removeCallback;
expect(removeCallback.mock.calls.length).toBe(1);
expect(removeCallback.mock.calls[0].length).toBe(1);
expect(removeCallback.mock.calls[0][0]).toBe(element);
});
});

View File

@@ -4,7 +4,7 @@ import { renderHook, act } from '@testing-library/react-hooks';
import useRive from '../src/hooks/useRive';
import * as rive from '@rive-app/canvas';
import { render } from '@testing-library/react';
import { render, waitFor } from '@testing-library/react';
jest.mock('@rive-app/canvas', () => ({
Rive: jest.fn().mockImplementation(() => ({
@@ -65,6 +65,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
expect(result.current.rive).toBe(baseRiveMock);
@@ -93,7 +98,14 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
result.current.setContainerRef(containerSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
await act(async () => {
jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(500);
jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(500);
containerSpy.dispatchEvent(new Event('resize'));
@@ -125,6 +137,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -153,6 +170,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
result.current.setContainerRef(containerSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -188,6 +210,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
result.current.setContainerRef(containerSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -222,6 +249,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
result.current.setContainerRef(containerSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -260,6 +292,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setContainerRef(containerSpy);
result.current.setCanvasRef(canvasSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -271,43 +308,6 @@ describe('useRive', () => {
expect(containerSpy).toHaveAttribute('style', 'height: 50px;');
});
it('configures a IntersectionObserver on mounting', async () => {
const params = {
src: 'file-src',
};
const observeMock = jest.fn();
const restore = global.IntersectionObserver;
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: observeMock,
}));
const riveMock = {
...baseRiveMock,
bounds: {
maxX: 100,
maxY: 50,
},
};
// @ts-ignore
mocked(rive.Rive).mockImplementation(() => riveMock);
const canvasSpy = document.createElement('canvas');
const { result } = renderHook(() => useRive(params));
await act(async () => {
result.current.setCanvasRef(canvasSpy);
controlledRiveloadCb();
});
expect(observeMock).toBeCalledWith(canvasSpy);
global.IntersectionObserver = restore;
});
it('updates the playing animations when the animations param changes', async () => {
const params = {
src: 'file-src',
@@ -336,6 +336,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -379,6 +384,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -405,6 +415,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -428,6 +443,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -456,6 +476,11 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
result.current.setContainerRef(containerSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
@@ -483,9 +508,17 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
result.current.setContainerRef(containerSpy);
controlledRiveloadCb();
jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(200);
jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(200);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
await act(async () => {
containerSpy.dispatchEvent(new Event('resize'));
});
@@ -516,7 +549,16 @@ describe('useRive', () => {
await act(async () => {
result.current.setCanvasRef(canvasSpy);
result.current.setContainerRef(containerSpy);
});
await waitFor(() => {
expect(result.current.canvas).toBe(canvasSpy);
});
await act(async () => {
controlledRiveloadCb();
});
await act(async () => {
jest.spyOn(containerSpy, 'clientWidth', 'get').mockReturnValue(500);
jest.spyOn(containerSpy, 'clientHeight', 'get').mockReturnValue(500);
containerSpy.dispatchEvent(new Event('resize'));

115
test/useRiveFile.test.tsx Normal file
View File

@@ -0,0 +1,115 @@
import { renderHook } from '@testing-library/react-hooks';
import { mocked } from 'jest-mock';
import useRiveFile from '../src/hooks/useRiveFile';
import { RiveFile } from '@rive-app/canvas';
jest.mock('@rive-app/canvas', () => ({
RiveFile: jest.fn().mockImplementation(() => ({
cleanup: jest.fn(),
on: jest.fn(),
init: jest.fn(),
getInstance: jest.fn(),
})),
EventType: {
Load: 'load',
loadError: 'loadError',
},
}));
describe('useRiveFile', () => {
beforeEach(() => {
mocked(RiveFile).mockClear();
});
it('initializes RiveFile with provided parameters', async () => {
const params = {
src: 'file-src',
enableRiveAssetCDN: false
};
const { result } = renderHook(() => useRiveFile(params));
expect(RiveFile).toHaveBeenCalledWith(params);
expect(result.current.riveFile).toBeDefined();
});
it('cleans up RiveFile on unmount', async () => {
const params = {
src: 'file-src',
enableRiveAssetCDN: false
};
const { result, unmount } = renderHook(() => useRiveFile(params));
const riveInstance = result.current.riveFile;
expect(riveInstance).toBeDefined();
unmount();
expect(riveInstance?.cleanup).toHaveBeenCalled();
});
it('does not reinitialize RiveFile if src has not changed', async () => {
const params = {
src: 'file-src',
enableRiveAssetCDN: false
};
const { rerender } = renderHook(() => useRiveFile(params));
rerender();
expect(RiveFile).toHaveBeenCalledTimes(1);
});
it('does not reinitialize RiveFile if buffer has not changed', async () => {
const params = {
buffer: new ArrayBuffer(10),
enableRiveAssetCDN: false
};
const { rerender } = renderHook(() => useRiveFile(params));
rerender();
expect(RiveFile).toHaveBeenCalledTimes(1);
});
it('reinitializes RiveFile if src changes', async () => {
let params = {
src: 'file-src',
enableRiveAssetCDN: false
};
const { rerender } = renderHook(() => useRiveFile(params));
params = {
src: 'new-file-src',
enableRiveAssetCDN: false
};
rerender();
expect(RiveFile).toHaveBeenCalledTimes(2);
});
it('reinitializes RiveFile if buffer changes', async () => {
let params = {
buffer: new ArrayBuffer(10),
enableRiveAssetCDN: false
};
const { rerender } = renderHook(() => useRiveFile(params));
params = {
buffer: new ArrayBuffer(20),
enableRiveAssetCDN: false
};
rerender();
expect(RiveFile).toHaveBeenCalledTimes(2);
});
});