Compare commits

..

63 Commits

Author SHA1 Message Date
HayesGordon
d80df170aa chore: release 4.23.2 2025-09-04 14:29:04 +00:00
CI
a3118d5984 chore: bump rive wasm to 2.31.4 2025-09-04 16:27:08 +02:00
CI
f732a3b044 docs: update README 2025-09-04 16:27:08 +02:00
bodymovin
57ebc37e3f chore: release 4.23.1 2025-08-13 17:19:55 +00:00
Hernan Torrisi
69a356894d bump rive to 2.31.2 2025-08-13 10:09:58 -07:00
Hernan Torrisi
23d9d7f48b add dependency 2025-08-12 12:27:31 -07:00
Hernan Torrisi
788b7ef68e bump rive canvas to 2.31.1 2025-08-12 12:27:31 -07:00
HayesGordon
69658c204a chore: release 4.23.0 2025-08-08 12:09:50 +00:00
CI
7249fa36e7 chore: bump rive wasm 2.31.0 2025-08-08 12:42:01 +02:00
bodymovin
52dd934e43 chore: release 4.22.1 2025-07-18 14:29:38 +00:00
Hernan Torrisi
c151ee37b5 bump rive to 2.30.4 2025-07-18 07:18:56 -07:00
damzobridge
c660a675c2 chore: release 4.22.0 2025-07-15 20:57:21 +00:00
Adam
74e1d5a5f2 feat: add tests for artboard binding 2025-07-15 12:52:34 -07:00
Adam
963ecc43b8 feat: add useViewModelInstanceArtboard hook 2025-07-15 12:52:34 -07:00
bodymovin
85807f2166 chore: release 4.21.6 2025-07-15 04:51:16 +00:00
Hernan Torrisi
9a33504d3a rive_canvas_2.30.3 2025-07-14 21:48:19 -07:00
HayesGordon
1a4d7e7168 chore: release 4.21.5 2025-07-14 16:12:59 +00:00
CI
b3d0fd4339 chore: bump rive wasm 2.30.2 2025-07-14 17:09:31 +01:00
Adam
c4239ab6b2 fix: lint error with revision 2025-07-07 14:23:22 -07:00
Adam
22f8d5a945 feat: add tests for list property 2025-07-07 14:23:22 -07:00
Adam
721ed786dc feat: add useViewModelInstanceList hook 2025-07-07 14:23:22 -07:00
Adam
eef56fb641 feat: add useViewModelInstanceImage hook 2025-07-07 14:23:22 -07:00
bodymovin
4bc0f496f8 chore: release 4.21.4 2025-06-25 13:39:06 +00:00
Maxwell Talbot
10bb4c69ea fix: update how we use release it with github tokens 2025-06-25 06:26:13 -07:00
Hernan Torrisi
c5b6826996 bump rive to 2.30.1 2025-06-25 06:23:15 -07:00
Adam
ec4875933c refactor: change onLoad to onRiveReady 2025-06-23 11:57:58 -07:00
Adam
d808a8bdea feat: add onLoad callback to useRive 2025-06-23 11:57:58 -07:00
Hernan Torrisi
7b174f7f51 cleanup rive on unmount 2025-06-18 16:25:59 -07:00
bodymovin
eecd0d3c5b chore: release 4.21.3 2025-06-08 17:57:17 +00:00
Hernan Torrisi
6c00364e60 rive react 2.29.3 2025-06-08 10:22:55 -07:00
HayesGordon
d310f1c96d chore: release 4.21.2 2025-06-05 20:43:20 +00:00
CI
68e8fbe46d chore: bump Rive wasm 2.29.2 2025-06-05 22:40:54 +02:00
bodymovin
8ff9a844fe chore: release 4.21.1 2025-05-28 16:15:41 +00:00
Hernan Torrisi
a565795452 bump rive to 2.29.0 2025-05-28 08:10:54 -07:00
HayesGordon
b26280ae12 chore: release 4.21.0 2025-05-23 16:44:06 +00:00
CI
a62e89de94 chore: bump rive wasm 2.28.0 2025-05-23 18:20:22 +02:00
HayesGordon
31255f9746 chore: release 4.20.2 2025-05-23 15:24:28 +00:00
CI
3e768533df chore: bump rive wasm 2.27.5 2025-05-22 14:42:31 +02:00
bodymovin
c790e66723 chore: release 4.20.1 2025-05-14 06:46:19 +00:00
Hernan Torrisi
ab89793032 rive canvas 2.27.3 2025-05-13 23:43:41 -07:00
damzobridge
2088c0bf9b chore: release 4.20.0 2025-05-12 22:51:11 +00:00
Adam
81fdf57736 chore: simplify useViewModelInstance 2025-05-12 14:16:00 -07:00
Adam
3a59585207 fix: implement useViewModel feedback 2025-05-12 14:16:00 -07:00
Adam
46e19874a2 feat: add db test 2025-05-12 14:16:00 -07:00
Lance
d4de776a01 Add onTrigger example 2025-05-12 14:16:00 -07:00
Lance
fb4f543077 Add stocks data binding example 2025-05-12 14:16:00 -07:00
Lance
345905f810 Merge storybook 2025-05-12 14:16:00 -07:00
Adam
0d9dabb135 fix: use default view model if none provide in useViewModelInstance and useViewModel 2025-05-12 14:16:00 -07:00
Adam
ad12fe20d1 fix: unify effects in useViewModelInstance 2025-05-12 14:16:00 -07:00
Adam
4e45f74a47 fix: change useViewModel property hook parameters to match conventions 2025-05-12 14:16:00 -07:00
Adam
efd6c4ce82 chore: simplify useViewModel 2025-05-12 14:16:00 -07:00
Adam
37e379091c fix: avoid rebuilding operations unless necessary 2025-05-12 14:16:00 -07:00
Adam
6d76e9f85d fix: hot reload crash 2025-05-12 14:16:00 -07:00
Adam
e7b64201ca chore: update UseViewModelParameters documentation 2025-05-12 14:16:00 -07:00
Adam
5dc8362107 fix: remove viewmodel hooks from initial release 2025-05-12 14:16:00 -07:00
Adam
56bc96fa76 chore: update color setter methods to be more explicit 2025-05-12 14:16:00 -07:00
Adam
01ab78db97 fix: lint issue 2025-05-12 14:16:00 -07:00
Adam
7800cc041f feat: add instance property hooks 2025-05-12 14:16:00 -07:00
Hernan Torrisi
1f9fc84629 hook with generic 2025-05-12 14:16:00 -07:00
Hernan Torrisi
452eb89e72 inital work for data binding hooks 2025-05-12 14:16:00 -07:00
Lance
3109e45724 Remove GH Action that published Stories to GH Pages 2025-05-12 11:38:30 -06:00
avivian
d303e8c96f chore: release 4.19.1 2025-05-08 11:24:05 +00:00
Arthur Vivian
479d5340e8 chore: rive-wasm -> 2.27.2 2025-05-08 12:20:01 +01:00
23 changed files with 1419 additions and 569 deletions

View File

@@ -14,9 +14,10 @@ jobs:
publish_job:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT_GITHUB }}
- name: Setup Git config
run: |
git config --local user.email 'hello@rive.app'
@@ -39,20 +40,14 @@ jobs:
name: Major Release - Bump version number, update changelog, push and tag
run: npm run release:major
env:
GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }}
PAT_GITHUB: ${{ secrets.PAT_GITHUB }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- if: ${{inputs.major == false && inputs.minor == true}}
name: Minor release - Bump version number, update changelog, push and tag
run: npm run release:minor
env:
GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }}
PAT_GITHUB: ${{ secrets.PAT_GITHUB }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- if: ${{inputs.major == false && inputs.minor == false}}
name: Patch release - Bump version number, update changelog, push and tag
run: npm run release:patch
env:
GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }}
PAT_GITHUB: ${{ secrets.PAT_GITHUB }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,30 +0,0 @@
name: Deploy Storybook
on:
# Testing to see if this job is causing the race condition
workflow_dispatch:
# pull_request:
# types: [closed]
# branches:
# - main
# paths: ['src', 'examples/stories/**'] # Trigger the action only when files change in the folders defined here
jobs:
build-and-deploy:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Install and Build 🔧
run: | # Install npm packages and build the Storybook files
npm install
npm run build-storybook
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.6.2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: main # The branch the action should deploy to.
FOLDER: docs-build # The folder that the build-storybook script generates files.
CLEAN: true # Automatically remove deleted files from the deploy branch
TARGET_FOLDER: docs # The folder that we serve our Storybook files from

View File

@@ -4,8 +4,125 @@ 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.23.2](https://github.com/rive-app/rive-react/compare/v4.23.1...v4.23.2)
- docs: update README [`f732a3b`](https://github.com/rive-app/rive-react/commit/f732a3b044e2a56ed0ea178a43d68612423c0548)
- chore: bump rive wasm to 2.31.4 [`a3118d5`](https://github.com/rive-app/rive-react/commit/a3118d59841c45467b46170e3eed7ba3359d4fea)
#### [v4.23.1](https://github.com/rive-app/rive-react/compare/v4.23.0...v4.23.1)
> 13 August 2025
- chore: release 4.23.1 [`57ebc37`](https://github.com/rive-app/rive-react/commit/57ebc37e3f99eb7fd9673e34441f395c990e312b)
- bump rive to 2.31.2 [`69a3568`](https://github.com/rive-app/rive-react/commit/69a356894d3acf44f4d24b708e9f8d8dda5b3046)
- bump rive canvas to 2.31.1 [`788b7ef`](https://github.com/rive-app/rive-react/commit/788b7ef68e9001460175b596f74d7f54616a69d4)
#### [v4.23.0](https://github.com/rive-app/rive-react/compare/v4.22.1...v4.23.0)
> 8 August 2025
- chore: release 4.23.0 [`69658c2`](https://github.com/rive-app/rive-react/commit/69658c204ad1f70a408bab098136c2b23083fd16)
- chore: bump rive wasm 2.31.0 [`7249fa3`](https://github.com/rive-app/rive-react/commit/7249fa36e7b6a2184ec60fb1e34a68f28b4eeb6d)
#### [v4.22.1](https://github.com/rive-app/rive-react/compare/v4.22.0...v4.22.1)
> 18 July 2025
- chore: release 4.22.1 [`52dd934`](https://github.com/rive-app/rive-react/commit/52dd934e439507d079bf4f5009372857dfbb97a6)
- bump rive to 2.30.4 [`c151ee3`](https://github.com/rive-app/rive-react/commit/c151ee37b5482cb7eee258c84f6c52182dbe9db5)
#### [v4.22.0](https://github.com/rive-app/rive-react/compare/v4.21.6...v4.22.0)
> 15 July 2025
- feat: add tests for artboard binding [`74e1d5a`](https://github.com/rive-app/rive-react/commit/74e1d5a5f29f14f46be3af3d052bb51c3d833799)
- feat: add useViewModelInstanceArtboard hook [`963ecc4`](https://github.com/rive-app/rive-react/commit/963ecc43b80e6465d159621d014b70b8cbfee9d4)
- chore: release 4.22.0 [`c660a67`](https://github.com/rive-app/rive-react/commit/c660a675c246af9fca50795ff88b7935c2d2a101)
#### [v4.21.6](https://github.com/rive-app/rive-react/compare/v4.21.5...v4.21.6)
> 15 July 2025
- chore: release 4.21.6 [`85807f2`](https://github.com/rive-app/rive-react/commit/85807f2166fcfba01e4556ac346b769d6fa08341)
- rive_canvas_2.30.3 [`9a33504`](https://github.com/rive-app/rive-react/commit/9a33504d3a315ce2f3dff753192b0ae491a56a04)
#### [v4.21.5](https://github.com/rive-app/rive-react/compare/v4.21.4...v4.21.5)
> 14 July 2025
- feat: add tests for list property [`22f8d5a`](https://github.com/rive-app/rive-react/commit/22f8d5a945c74974b7dabcfe16aaa019f6141326)
- feat: add useViewModelInstanceImage hook [`eef56fb`](https://github.com/rive-app/rive-react/commit/eef56fb641839b55806296873186aa53b3e1d068)
- feat: add useViewModelInstanceList hook [`721ed78`](https://github.com/rive-app/rive-react/commit/721ed786dc43a526eafb54108bfb54f353d7430d)
#### [v4.21.4](https://github.com/rive-app/rive-react/compare/v4.21.3...v4.21.4)
> 25 June 2025
- chore: release 4.21.4 [`4bc0f49`](https://github.com/rive-app/rive-react/commit/4bc0f496f87a54ffda673acb7b9be4b7a8b311c0)
- cleanup rive on unmount [`7b174f7`](https://github.com/rive-app/rive-react/commit/7b174f7f5106b1b863969bd7318a8a6cb1a12b67)
- refactor: change onLoad to onRiveReady [`ec48759`](https://github.com/rive-app/rive-react/commit/ec4875933cad45a3d338290951d55ac9c72df9d0)
#### [v4.21.3](https://github.com/rive-app/rive-react/compare/v4.21.2...v4.21.3)
> 8 June 2025
- chore: release 4.21.3 [`eecd0d3`](https://github.com/rive-app/rive-react/commit/eecd0d3c5be011fe9865e45b05435fbd45e7395d)
- rive react 2.29.3 [`6c00364`](https://github.com/rive-app/rive-react/commit/6c00364e60e91a7a6556e763ebf9ebee4793b336)
#### [v4.21.2](https://github.com/rive-app/rive-react/compare/v4.21.1...v4.21.2)
> 5 June 2025
- chore: release 4.21.2 [`d310f1c`](https://github.com/rive-app/rive-react/commit/d310f1c96dbff6cbb7397d4bea2687c8d3f271f4)
- chore: bump Rive wasm 2.29.2 [`68e8fbe`](https://github.com/rive-app/rive-react/commit/68e8fbe46d4f1824a6228ce2ea0a02735dced5ba)
#### [v4.21.1](https://github.com/rive-app/rive-react/compare/v4.21.0...v4.21.1)
> 28 May 2025
- chore: release 4.21.1 [`8ff9a84`](https://github.com/rive-app/rive-react/commit/8ff9a844fe5b02a2eb1964cf01814479f6c72248)
- bump rive to 2.29.0 [`a565795`](https://github.com/rive-app/rive-react/commit/a565795452444205e88083cba272bc8ca6c9968f)
#### [v4.21.0](https://github.com/rive-app/rive-react/compare/v4.20.2...v4.21.0)
> 23 May 2025
- chore: release 4.21.0 [`b26280a`](https://github.com/rive-app/rive-react/commit/b26280ae125173b52dd9a6147833f45631c2252f)
- chore: bump rive wasm 2.28.0 [`a62e89d`](https://github.com/rive-app/rive-react/commit/a62e89de9436360e439896a1aa11623e3574897e)
#### [v4.20.2](https://github.com/rive-app/rive-react/compare/v4.20.1...v4.20.2)
> 23 May 2025
- chore: release 4.20.2 [`31255f9`](https://github.com/rive-app/rive-react/commit/31255f974635278aea211dcf827e3a0cd0cc138e)
- chore: bump rive wasm 2.27.5 [`3e76853`](https://github.com/rive-app/rive-react/commit/3e768533df747da69acd392332495303077fa8c6)
#### [v4.20.1](https://github.com/rive-app/rive-react/compare/v4.20.0...v4.20.1)
> 14 May 2025
- chore: release 4.20.1 [`c790e66`](https://github.com/rive-app/rive-react/commit/c790e6672389ea68ee222140a49bcb7e4a7d3ca3)
- rive canvas 2.27.3 [`ab89793`](https://github.com/rive-app/rive-react/commit/ab89793032bcadf58f680610cea2e15fcd76d0b2)
#### [v4.20.0](https://github.com/rive-app/rive-react/compare/v4.19.1...v4.20.0)
> 12 May 2025
- feat: add db test [`46e1987`](https://github.com/rive-app/rive-react/commit/46e19874a2ec5893b5d3365f61db871400327087)
- fix: hot reload crash [`6d76e9f`](https://github.com/rive-app/rive-react/commit/6d76e9f85d949ec1e0e4d29458676efbe1c24d1d)
- inital work for data binding hooks [`452eb89`](https://github.com/rive-app/rive-react/commit/452eb89e72ffb73f837917fd969a51ed238a6d05)
#### [v4.19.1](https://github.com/rive-app/rive-react/compare/v4.19.0...v4.19.1)
> 8 May 2025
- chore: release 4.19.1 [`d303e8c`](https://github.com/rive-app/rive-react/commit/d303e8c96f70fa8886d96aba35afd911f0fcef50)
- chore: rive-wasm -> 2.27.2 [`479d534`](https://github.com/rive-app/rive-react/commit/479d5340e87f5335a2525b547e690be60ebafc00)
#### [v4.19.0](https://github.com/rive-app/rive-react/compare/v4.18.9...v4.19.0)
> 29 April 2025
- Add examples [`5354d1f`](https://github.com/rive-app/rive-react/commit/5354d1f69bfe91dc67c39cee80a6ea00c4c70cb1)
- Storybookk page reloads when the package in the parent changes [`7277ed2`](https://github.com/rive-app/rive-react/commit/7277ed2f0d877150637a69f2ff9122db1b151686)
- Upgrade React and React DOM [`a9a98fe`](https://github.com/rive-app/rive-react/commit/a9a98fece2caf727e941ce645be2f031efaf8a89)

View File

@@ -1,5 +1,4 @@
![Build Status](https://github.com/rive-app/rive-react/actions/workflows/tests.yml/badge.svg)
[![Storybook](https://cdn.jsdelivr.net/gh/storybookjs/brand@main/badge/badge-storybook.svg)](https://rive-app.github.io/rive-react)
![Discord badge](https://img.shields.io/discord/532365473602600965)
![Twitter handle](https://img.shields.io/twitter/follow/rive_app.svg?style=social&label=Follow)
@@ -7,9 +6,16 @@
![Rive hero image](https://cdn.rive.app/rive_logo_dark_bg.png)
A React runtime library for [Rive](https://rive.app).
[Rive](https://rive.app) combines an interactive design tool, a new stateful graphics format, a lightweight multi-platform runtime, and a blazing-fast vector renderer. This end-to-end pipeline guarantees that what you build in the Rive Editor is exactly what ships in your apps, games, and websites.
This library is a wrapper around the [JS/Wasm runtime](https://github.com/rive-app/rive-wasm), giving full control over the js runtime while providing components and hooks for React applications.
This library is a wrapper around the [JS/Wasm runtime](https://github.com/rive-app/rive-wasm), giving full control over the JS/Wasm runtime while providing components and hooks for React applications.
For more information, check out the following resources:
- [Homepage](https://rive.app/)
- [General Docs](https://rive.app/docs/)
- [React Docs](https://rive.app/docs/runtimes/react/react)
- [Rive Community / Support](https://community.rive.app/c/support/)
## Table of contents
@@ -55,8 +61,6 @@ This library supports React versions `^16.8.0` through `^18.0.0`.
Check out our Storybook instance that shows how to use the library in small examples, along with code snippets! This includes examples using the basic component, as well as the convenient hooks exported to take advantage of state machines.
- [Example page](https://rive-app.github.io/rive-react)
- [Login screen w/ input tracking](https://rive-app.github.io/rive-use-cases/?path=/story/example-loginformcomponent--primary)
- [Mouse tracking](https://codesandbox.io/s/rive-mouse-track-test-t0y965?file=/src/App.js)
- [Accessibility concerns](https://rive.app/blog/accesible-web-animations-aria-live-regions)

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { within, expect, waitFor, userEvent } from '@storybook/test';
import { StringPropertyTest, NumberPropertyTest, BooleanPropertyTest, ColorPropertyTest, EnumPropertyTest, NestedViewModelTest, TriggerPropertyTest, PersonForm, PersonInstances } from './DataBindingTests';
import { StringPropertyTest, NumberPropertyTest, BooleanPropertyTest, ColorPropertyTest, EnumPropertyTest, NestedViewModelTest, TriggerPropertyTest, PersonForm, PersonInstances, ImagePropertyTest, TodoListTest, ArtboardPropertyTest } from './DataBindingTests';
const meta: Meta = {
title: 'Tests/DataBinding',
@@ -345,4 +345,183 @@ export const PersonFormStory: StoryObj = {
};
export const ImagePropertyStory: StoryObj = {
name: 'Image Property',
render: () => <ImagePropertyTest src="image_db_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('load-random-image')).toBeTruthy();
expect(canvas.getByTestId('clear-image')).toBeTruthy();
}, { timeout: 3000 });
const loadImageButton = canvas.getByTestId('load-random-image');
const clearImageButton = canvas.getByTestId('clear-image');
expect(canvas.queryByTestId('current-image-url')).toBeNull();
// Load a random image
await userEvent.click(loadImageButton);
// Wait for the image to load and URL to appear
await waitFor(() => {
expect(canvas.getByTestId('current-image-url')).toBeTruthy();
}, { timeout: 5000 });
// Verify the image URL is displayed
const imageUrlElement = canvas.getByTestId('current-image-url');
expect(imageUrlElement.textContent).toContain('Current image: https://picsum.photos');
// Clear the image
await userEvent.click(clearImageButton);
// Load another image to test it works multiple times
await userEvent.click(loadImageButton);
// Wait for the new image to load
await waitFor(() => {
expect(canvas.getByTestId('current-image-url')).toBeTruthy();
}, { timeout: 5000 });
}
};
export const TodoListStory: StoryObj = {
name: 'Todo List Property',
render: () => <TodoListTest src="db_list_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('list-length')).toBeTruthy();
}, { timeout: 3000 });
const initialLengthText = canvas.getByTestId('list-length').textContent;
const initialCount = parseInt(initialLengthText?.match(/Items: (\d+)/)?.[1] || '0');
// Test 1: addInstance - Add item to end
const addButton = canvas.getByTestId('add-item-button');
await userEvent.click(addButton);
await waitFor(() => {
expect(canvas.getByTestId('list-length').textContent).toContain(`Items: ${initialCount + 1}`);
});
// Test 2: addInstanceAt - Add item at specific index (if we have items)
if (initialCount > 0) {
const addAtButton = canvas.getByTestId('add-item-at-button');
await userEvent.click(addAtButton);
await waitFor(() => {
expect(canvas.getByTestId('list-length').textContent).toContain(`Items: ${initialCount + 2}`);
});
}
// Test 3: getInstanceAt - Interact with specific items
const currentCount = initialCount + (initialCount > 0 ? 2 : 1);
if (currentCount > 0) {
await waitFor(() => {
expect(canvas.getByTestId('todo-item-0')).toBeTruthy();
});
// Edit the first item
const todoText = canvas.getByTestId('todo-text-0');
await userEvent.clear(todoText);
// Wait for the input to be cleared to avoid issues with autocomplete
await waitFor(() => {
expect((todoText as HTMLInputElement).value).toBe('');
}, { timeout: 2000 });
await userEvent.click(todoText);
await userEvent.paste('Test Item');
await waitFor(() => {
expect(canvas.getByTestId('todo-text-value-0').textContent).toContain('Test Item');
}, { timeout: 3000 });
}
// Test 4: swap - Swap first two items
if (currentCount >= 2) {
const firstText = canvas.getByTestId<HTMLInputElement>('todo-text-0').value;
const secondText = canvas.getByTestId<HTMLInputElement>('todo-text-1').value;
const swapButton = canvas.getByTestId('swap-button');
await userEvent.click(swapButton);
await waitFor(() => {
expect(canvas.getByTestId('todo-text-0')).toHaveValue(secondText);
expect(canvas.getByTestId('todo-text-1')).toHaveValue(firstText);
}, { timeout: 3000 });
}
// Test 5: removeInstance - Remove by instance reference
if (currentCount > 0) {
const removeInstanceButton = canvas.getByTestId('remove-instance-button');
await userEvent.click(removeInstanceButton);
await waitFor(() => {
expect(canvas.getByTestId('list-length').textContent).toContain(`Items: ${currentCount - 1}`);
}, { timeout: 3000 });
}
// Test 6: removeInstanceAt - Remove by index
const afterRemoveInstance = currentCount > 0 ? currentCount - 1 : 0;
if (afterRemoveInstance > 0) {
const removeIndexButton = canvas.getByTestId('remove-index-button');
await userEvent.click(removeIndexButton);
await waitFor(() => {
expect(canvas.getByTestId('list-length').textContent).toContain(`Items: ${afterRemoveInstance - 1}`);
}, { timeout: 3000 });
}
}
};
export const ArtboardPropertyStory: StoryObj = {
name: 'Artboard Property',
render: () => <ArtboardPropertyTest src="artboard_db_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('set-artboard1-blue')).toBeTruthy();
expect(canvas.getByTestId('set-artboard1-red')).toBeTruthy();
expect(canvas.getByTestId('set-artboard1-green')).toBeTruthy();
}, { timeout: 3000 });
// Initially should show None
expect(canvas.getByTestId('artboard1-current').textContent).toBe('Current: None');
expect(canvas.getByTestId('artboard2-current').textContent).toBe('Current: None');
// Set artboard 1 to blue
await userEvent.click(canvas.getByTestId('set-artboard1-blue'));
await waitFor(() => {
expect(canvas.getByTestId('artboard1-current').textContent).toBe('Current: ArtboardBlue');
});
// Set artboard 2 to red
await userEvent.click(canvas.getByTestId('set-artboard2-red'));
await waitFor(() => {
expect(canvas.getByTestId('artboard2-current').textContent).toBe('Current: ArtboardRed');
});
// Switch artboard 1 to green
await userEvent.click(canvas.getByTestId('set-artboard1-green'));
await waitFor(() => {
expect(canvas.getByTestId('artboard1-current').textContent).toBe('Current: ArtboardGreen');
});
// Switch artboard 2 to blue
await userEvent.click(canvas.getByTestId('set-artboard2-blue'));
await waitFor(() => {
expect(canvas.getByTestId('artboard2-current').textContent).toBe('Current: ArtboardBlue');
});
}
};

View File

@@ -9,7 +9,12 @@ import Rive, {
useViewModelInstanceNumber,
useViewModelInstanceEnum,
useViewModelInstanceColor,
useViewModelInstanceTrigger
useViewModelInstanceTrigger,
useViewModelInstanceImage,
decodeImage,
ViewModelInstance,
useViewModelInstanceList,
useViewModelInstanceArtboard
} from '@rive-app/react-webgl2';
@@ -522,3 +527,355 @@ export const PersonInstances = ({ src }: { src: string }) => {
</div>
);
};
export const ImagePropertyTest = ({ src }: { src: string }) => {
const [currentImageUrl, setCurrentImageUrl] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const { rive, RiveComponent } = useRive({
src,
artboard: "Artboard",
stateMachines: "State Machine 1",
autoplay: true,
autoBind: false,
});
const viewModel = useViewModel(rive, { name: 'Post' });
const viewModelInstance = useViewModelInstance(viewModel, { rive });
const { setValue: setImage } = useViewModelInstanceImage(
'image',
viewModelInstance
);
const loadRandomImage = async () => {
if (!setImage) return;
setIsLoading(true);
try {
const imageUrl = `https://picsum.photos/400/300?random=${Date.now()}`;
setCurrentImageUrl(imageUrl);
const response = await fetch(imageUrl);
const imageBuffer = await response.arrayBuffer();
const decodedImage = await decodeImage(new Uint8Array(imageBuffer));
setImage(decodedImage);
decodedImage.unref();
} catch (error) {
console.error('Failed to load image:', error);
} finally {
setIsLoading(false);
}
};
const clearImage = () => {
if (setImage) {
setImage(null);
setCurrentImageUrl('');
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px' }}>
<div style={{ width: '400px', height: '300px', border: '1px solid #ccc' }}>
<RiveComponent />
</div>
{rive === null ? (
<div data-testid="loading-text">Loading</div>
) : (
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<button
onClick={loadRandomImage}
disabled={isLoading}
data-testid="load-random-image"
>
{isLoading ? 'Loading...' : 'Load Random Image'}
</button>
<button
onClick={clearImage}
disabled={isLoading}
data-testid="clear-image"
>
Clear Image
</button>
</div>
)}
{currentImageUrl && (
<div style={{ fontSize: '12px', color: '#666' }}>
<span data-testid="current-image-url">Current image: {currentImageUrl}</span>
</div>
)}
</div>
);
};
// List Property Test
const TodoItemComponent = ({
index,
todoItem
}: {
index: number;
todoItem: ViewModelInstance | null;
}) => {
const { value: text, setValue: setText } = useViewModelInstanceString('text', todoItem);
const { value: isDone, setValue: setIsDone } = useViewModelInstanceBoolean('isDone', todoItem);
if (!todoItem) {
return <div data-testid={`todo-item-${index}`}>Item not found</div>;
}
return (
<div data-testid={`todo-item-${index}`} style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '8px',
border: '1px solid #ccc',
marginBottom: '4px'
}}>
<input
data-testid={`todo-checkbox-${index}`}
type="checkbox"
checked={isDone ?? false}
onChange={(e) => setIsDone(e.target.checked)}
/>
<input
data-testid={`todo-text-${index}`}
type="text"
value={text || ''}
onChange={(e) => setText(e.target.value)}
style={{ flex: 1 }}
/>
<div data-testid={`todo-text-value-${index}`} style={{ fontSize: '12px', color: '#666' }}>
Text: {text}
</div>
<div data-testid={`todo-done-value-${index}`} style={{ fontSize: '12px', color: '#666' }}>
Done: {isDone ? 'true' : 'false'}
</div>
</div>
);
};
export const TodoListTest = ({ src }: { src: string }) => {
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
artboard: "Artboard",
autoBind: false,
stateMachines: "State Machine 1",
});
const viewModel = useViewModel(rive, { name: 'TodoList' });
const viewModelInstance = useViewModelInstance(viewModel, { rive });
const {
length,
addInstance,
addInstanceAt,
removeInstance,
removeInstanceAt,
getInstanceAt,
swap
} = useViewModelInstanceList('items', viewModelInstance);
const handleAddItem = () => {
const todoItemViewModel = rive?.viewModelByName?.('TodoItem');
if (todoItemViewModel) {
const newTodoItem = todoItemViewModel.instance?.();
if (newTodoItem) {
addInstance(newTodoItem);
}
}
};
const handleAddItemAt = () => {
const todoItemViewModel = rive?.viewModelByName?.('TodoItem');
if (todoItemViewModel && length > 0) {
const newTodoItem = todoItemViewModel.instance?.();
if (newTodoItem) {
addInstanceAt(newTodoItem, 1);
}
}
};
const handleRemoveFirstInstance = () => {
const firstInstance = getInstanceAt(0);
if (firstInstance) {
removeInstance(firstInstance);
}
};
const handleRemoveFirstByIndex = () => {
if (length > 0) {
removeInstanceAt(0);
}
};
const handleSwapItems = () => {
if (length >= 2) {
swap(0, 1);
}
};
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{rive === null ? (
<div data-testid="loading-text">Loading</div>
) : (
<div>
<div data-testid="list-length">Items: {length}</div>
<div style={{ marginBottom: '10px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
data-testid="add-item-button"
onClick={handleAddItem}
>
Add Item (End)
</button>
<button
data-testid="add-item-at-button"
onClick={handleAddItemAt}
disabled={length === 0}
>
Add Item at Index 1
</button>
<button
data-testid="remove-instance-button"
onClick={handleRemoveFirstInstance}
disabled={length === 0}
>
Remove First (by Instance)
</button>
<button
data-testid="remove-index-button"
onClick={handleRemoveFirstByIndex}
disabled={length === 0}
>
Remove First (by Index)
</button>
<button
data-testid="swap-button"
onClick={handleSwapItems}
disabled={length < 2}
>
Swap First Two
</button>
</div>
<div data-testid="todo-items">
{Array.from({ length }, (_, index) => (
<TodoItemComponent
key={index}
index={index}
todoItem={getInstanceAt(index)}
/>
))}
</div>
</div>
)}
</div>
);
};
export const ArtboardPropertyTest = ({ src }: { src: string }) => {
const [currentArtboard1, setCurrentArtboard1] = useState<string>('None');
const [currentArtboard2, setCurrentArtboard2] = useState<string>('None');
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
artboard: "Main",
autoBind: true,
stateMachines: "State Machine 1",
});
const { setValue: setArtboard1 } = useViewModelInstanceArtboard('artboard_1', rive?.viewModelInstance);
const { setValue: setArtboard2 } = useViewModelInstanceArtboard('artboard_2', rive?.viewModelInstance);
const handleSetArtboard1 = (artboardName: string) => {
if (rive) {
const artboard = rive.getArtboard(artboardName);
setArtboard1(artboard);
setCurrentArtboard1(artboardName);
}
};
const handleSetArtboard2 = (artboardName: string) => {
if (rive) {
const artboard = rive.getArtboard(artboardName);
setArtboard2(artboard);
setCurrentArtboard2(artboardName);
}
};
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<div>
<div style={{ marginBottom: '20px' }}>
<h4>Artboard 1:</h4>
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
<button
data-testid="set-artboard1-blue"
onClick={() => handleSetArtboard1('ArtboardBlue')}
>
Set Blue Artboard
</button>
<button
data-testid="set-artboard1-red"
onClick={() => handleSetArtboard1('ArtboardRed')}
>
Set Red Artboard
</button>
<button
data-testid="set-artboard1-green"
onClick={() => handleSetArtboard1('ArtboardGreen')}
>
Set Green Artboard
</button>
</div>
<div data-testid="artboard1-current">Current: {currentArtboard1}</div>
</div>
<div>
<h4>Artboard 2:</h4>
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
<button
data-testid="set-artboard2-blue"
onClick={() => handleSetArtboard2('ArtboardBlue')}
>
Set Blue Artboard
</button>
<button
data-testid="set-artboard2-red"
onClick={() => handleSetArtboard2('ArtboardRed')}
>
Set Red Artboard
</button>
<button
data-testid="set-artboard2-green"
onClick={() => handleSetArtboard2('ArtboardGreen')}
>
Set Green Artboard
</button>
</div>
<div data-testid="artboard2-current">Current: {currentArtboard2}</div>
</div>
</div>
)}
</div>
);
};

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@rive-app/react-canvas-lite",
"version": "4.19.0",
"version": "4.23.2",
"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.27.1"
"@rive-app/canvas-lite": "2.31.4"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@rive-app/react-canvas",
"version": "4.19.0",
"version": "4.23.2",
"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.27.1"
"@rive-app/canvas": "2.31.4"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@rive-app/react-webgl",
"version": "4.19.0",
"version": "4.23.2",
"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.27.1"
"@rive-app/webgl": "2.31.4"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@rive-app/react-webgl2",
"version": "4.19.0",
"version": "4.23.2",
"description": "React wrapper around the @rive-app/webgl2 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/webgl2": "2.27.1"
"@rive-app/webgl2": "2.31.4"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"

View File

@@ -1,6 +1,6 @@
{
"name": "rive-react",
"version": "4.19.0",
"version": "4.23.2",
"description": "React wrapper around the rive-js library",
"main": "dist/index.js",
"typings": "dist/types/index.d.ts",
@@ -35,10 +35,10 @@
},
"homepage": "https://github.com/rive-app/rive-react#readme",
"dependencies": {
"@rive-app/canvas": "2.27.1",
"@rive-app/canvas-lite": "2.27.1",
"@rive-app/webgl": "2.27.1",
"@rive-app/webgl2": "2.27.1"
"@rive-app/canvas": "2.31.4",
"@rive-app/canvas-lite": "2.31.4",
"@rive-app/webgl": "2.31.4",
"@rive-app/webgl2": "2.31.4"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
@@ -48,6 +48,7 @@
"@testing-library/jest-dom": "^5.13.0",
"@testing-library/react": "^16.3.0",
"@types/jest": "^27.0.3",
"@types/node": "^18.17.0",
"@types/offscreencanvas": "^2019.6.4",
"@types/react": "^18.0.0",
"@types/testing-library__jest-dom": "^5.9.5",

View File

@@ -69,6 +69,7 @@ export default function useRive(
): RiveState {
const [canvasElem, setCanvasElem] = useState<HTMLCanvasElement | null>(null);
const containerRef = useRef<HTMLElement | null>(null);
const riveRef = useRef<Rive | null>(null);
const [rive, setRive] = useState<Rive | null>(null);
@@ -130,13 +131,23 @@ export default function useRive(
let r: Rive | null;
if (rive == null) {
const { useOffscreenRenderer } = options;
const { onRiveReady, ...restRiveParams } = riveParams;
r = new Rive({
useOffscreenRenderer,
...riveParams,
...restRiveParams,
canvas: canvasElem,
});
if (riveRef.current != null) {
riveRef.current!.cleanup();
}
riveRef.current = r;
r.on(EventType.Load, () => {
isLoaded = true;
if (onRiveReady) {
onRiveReady(r!);
}
// Check if the component/canvas is mounted before setting state to avoid setState
// on an unmounted component in some rare cases
if (canvasElem) {
@@ -237,6 +248,14 @@ export default function useRive(
};
}, [rive, canvasElem]);
useEffect(() => {
return () => {
if (riveRef.current != null) {
riveRef.current!.cleanup();
}
};
}, []);
/**
* Listen for changes in the animations params
*/

View File

@@ -1,26 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect } from 'react';
import { Rive, ViewModel, EventType } from '@rive-app/canvas';
import { UseViewModelParameters } from '../types';
function areParamsEqual(
prev?: UseViewModelParameters,
next?: UseViewModelParameters
): boolean {
if (prev === next) return true;
if (!prev || !next) return prev === next;
if ('name' in prev && 'name' in next) {
return prev.name === next.name;
}
if ('useDefault' in prev && 'useDefault' in next) {
return prev.useDefault === next.useDefault;
}
return false;
}
/**
* Hook for fetching a ViewModel from a Rive instance.
*
@@ -35,57 +16,37 @@ export default function useViewModel(
params?: UseViewModelParameters
): ViewModel | null {
const { name, useDefault = false } = params ?? {};
const riveRef = useRef<Rive | null>(null);
const paramsRef = useRef<UseViewModelParameters | undefined>(params);
const [viewModel, setViewModel] = useState<ViewModel | null>(null);
const shouldUpdate = useRef(true);
useEffect(() => {
const isRiveChanged = riveRef.current !== rive;
const areParamsChanged = !areParamsEqual(paramsRef.current, params);
shouldUpdate.current = isRiveChanged || areParamsChanged;
riveRef.current = rive;
paramsRef.current = params;
if (!shouldUpdate.current && viewModel) {
return;
}
function fetchViewModel() {
const currentRive = riveRef.current;
const currentParams = paramsRef.current;
if (!currentRive) {
if (!rive) {
setViewModel(null);
return;
}
let model: ViewModel | null = null;
if (currentParams?.name != null) {
model = currentRive.viewModelByName?.(currentParams.name) || null;
} else if (currentParams?.useDefault) {
model = currentRive.defaultViewModel() || null;
if (name != null) {
model = rive.viewModelByName?.(name) || null;
} else if (useDefault) {
model = rive.defaultViewModel() || null;
} else {
model = currentRive.defaultViewModel() || null;
model = rive.defaultViewModel() || null;
}
setViewModel(model);
shouldUpdate.current = false;
}
fetchViewModel();
const currentRive = riveRef.current;
if (currentRive) {
currentRive.on(EventType.Load, fetchViewModel);
if (rive) {
rive.on(EventType.Load, fetchViewModel);
}
return () => {
if (currentRive) {
currentRive.off(EventType.Load, fetchViewModel);
if (rive) {
rive.off(EventType.Load, fetchViewModel);
}
};
}, [rive, name, useDefault]);

View File

@@ -1,30 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect } from 'react';
import { ViewModel, ViewModelInstance } from '@rive-app/canvas';
import { UseViewModelInstanceParameters } from '../types';
function areParamsEqual(
prev?: UseViewModelInstanceParameters,
next?: UseViewModelInstanceParameters
): boolean {
if (prev === next) return true;
if (!prev || !next) return prev === next;
if ('name' in prev && 'name' in next) {
return prev.name === next.name;
}
if ('useDefault' in prev && 'useDefault' in next) {
return prev.useDefault === next.useDefault;
}
if ('useNew' in prev && 'useNew' in next) {
return prev.useNew === next.useNew;
}
return false;
}
/**
* Hook for fetching a ViewModelInstance from a ViewModel.
*
@@ -43,50 +20,26 @@ export default function useViewModelInstance(
const { name, useDefault = false, useNew = false, rive } = params ?? {};
const [instance, setInstance] = useState<ViewModelInstance | null>(null);
const viewModelRef = useRef<ViewModel | null>(viewModel);
const paramsRef = useRef<UseViewModelInstanceParameters | undefined>(params);
const instanceRef = useRef<ViewModelInstance | null>(null);
const shouldUpdate = useRef(true);
useEffect(() => {
const isViewModelChanged = viewModelRef.current !== viewModel;
const areParamsChanged = !areParamsEqual(paramsRef.current, params);
shouldUpdate.current = isViewModelChanged || areParamsChanged;
viewModelRef.current = viewModel;
paramsRef.current = params;
if (!shouldUpdate.current && instanceRef.current) {
return;
}
const currentViewModel = viewModelRef.current;
const currentParams = paramsRef.current;
if (!currentViewModel) {
if (!viewModel) {
setInstance(null);
instanceRef.current = null;
return;
}
let result: ViewModelInstance | null = null;
if (currentParams?.name != null) {
result = currentViewModel.instanceByName(currentParams.name) || null;
} else if (currentParams?.useDefault) {
result = currentViewModel.defaultInstance?.() || null;
} else if (currentParams?.useNew) {
result = currentViewModel.instance?.() || null;
if (name != null) {
result = viewModel.instanceByName(name) || null;
} else if (useDefault) {
result = viewModel.defaultInstance?.() || null;
} else if (useNew) {
result = viewModel.instance?.() || null;
} else {
result = currentViewModel.defaultInstance?.() || null;
result = viewModel.defaultInstance?.() || null;
}
instanceRef.current = result;
setInstance(result);
shouldUpdate.current = false;
// Bind instance to Rive if needed
if (rive && result && rive.viewModelInstance !== result) {
rive.bindViewModelInstance(result);
}

View File

@@ -0,0 +1,35 @@
import { useCallback } from 'react';
import { ViewModelInstance, ViewModelInstanceArtboard } from '@rive-app/canvas';
import { UseViewModelInstanceArtboardResult } from '../types';
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
/**
* Hook for interacting with artboard properties of a ViewModelInstance.
*
* @param path - Path to the artboard property (e.g. "targetArtboard" or "group/artboard")
* @param viewModelInstance - The ViewModelInstance containing the artboard property
* @returns An object with a setter function
*/
export default function useViewModelInstanceArtboard(
path: string,
viewModelInstance?: ViewModelInstance | null
): UseViewModelInstanceArtboardResult {
const result = useViewModelInstanceProperty<ViewModelInstanceArtboard, undefined, UseViewModelInstanceArtboardResult>(
path,
viewModelInstance,
{
getProperty: useCallback((vm, p) => vm.artboard(p), []),
getValue: useCallback(() => undefined, []), // Artboards properties don't currently have a readable value
defaultValue: null,
buildPropertyOperations: useCallback((safePropertyAccess) => ({
setValue: (newValue) => {
safePropertyAccess(prop => { prop.value = newValue; });
}
}), [])
}
);
return {
setValue: result.setValue
};
}

View File

@@ -0,0 +1,35 @@
import { useCallback } from 'react';
import { ViewModelInstance, ViewModelInstanceAssetImage } from '@rive-app/canvas';
import { UseViewModelInstanceImageResult, RiveRenderImage } from '../types';
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
/**
* Hook for interacting with image properties of a ViewModelInstance.
*
* @param path - Path to the image property (e.g. "profileImage" or "group/avatar")
* @param viewModelInstance - The ViewModelInstance containing the image property
* @returns An object with a setter function
*/
export default function useViewModelInstanceImage(
path: string,
viewModelInstance?: ViewModelInstance | null
): UseViewModelInstanceImageResult {
const result = useViewModelInstanceProperty<ViewModelInstanceAssetImage, undefined, UseViewModelInstanceImageResult>(
path,
viewModelInstance,
{
getProperty: useCallback((vm, p) => vm.image(p), []),
getValue: useCallback(() => undefined, []), // Images don't have a readable value
defaultValue: null,
buildPropertyOperations: useCallback((safePropertyAccess) => ({
setValue: (newValue: RiveRenderImage | null) => {
safePropertyAccess(prop => { prop.value = newValue; });
}
}), [])
}
);
return {
setValue: result.setValue
};
}

View File

@@ -0,0 +1,75 @@
import { useCallback, useState } from 'react';
import { ViewModelInstance, ViewModelInstanceList } from '@rive-app/canvas';
import { UseViewModelInstanceListResult } from '../types';
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
/**
* Hook for interacting with list properties of a ViewModelInstance.
*
* @param path - Path to the property (e.g. "items" or "nested/items")
* @param viewModelInstance - The ViewModelInstance containing the list property
* @returns An object with the list length and manipulation functions
*/
export default function useViewModelInstanceList(
path: string,
viewModelInstance?: ViewModelInstance | null
): UseViewModelInstanceListResult {
// We track revision to trigger re-renders on list manipulation (e.g. addInstance, removeInstance, etc).
// This is mostly important for things like the swap function which wouldn't trigger a re-render otherwise because it doesn't change the length of the list.
// For example, if the user swaps two items in the list and we don't trigger a re-render, the user will see the old items if they were using the getInstanceAt function.
// It also accounts for changes that happen within the Rive file itself rather than through the hook.
const [, setRevision] = useState(0);
const result = useViewModelInstanceProperty<ViewModelInstanceList, number, Omit<UseViewModelInstanceListResult, 'length'>>(
path,
viewModelInstance,
{
getProperty: useCallback((vm, p) => vm.list(p), []),
getValue: useCallback((prop) => prop.length, []),
defaultValue: null,
onPropertyEvent: () => {
// This fires when the list changes in Rive
setRevision(prev => prev + 1);
},
buildPropertyOperations: useCallback((safePropertyAccess) => ({
addInstance: (instance: ViewModelInstance) => {
safePropertyAccess(prop => prop.addInstance(instance));
},
addInstanceAt: (instance: ViewModelInstance, index: number): boolean => {
let result = false;
safePropertyAccess(prop => {
result = prop.addInstanceAt(instance, index);
});
return result;
},
removeInstance: (instance: ViewModelInstance) => {
safePropertyAccess(prop => prop.removeInstance(instance));
},
removeInstanceAt: (index: number) => {
safePropertyAccess(prop => prop.removeInstanceAt(index));
},
getInstanceAt: (index: number): ViewModelInstance | null => {
let result: ViewModelInstance | null = null;
safePropertyAccess(prop => {
result = prop.instanceAt(index);
});
return result;
},
swap: (a: number, b: number) => {
safePropertyAccess(prop => prop.swap(a, b));
}
}), [])
}
);
return {
length: result.value ?? 0,
addInstance: result.addInstance,
addInstanceAt: result.addInstanceAt,
removeInstance: result.removeInstance,
removeInstanceAt: result.removeInstanceAt,
getInstanceAt: result.getInstanceAt,
swap: result.swap
};
}

View File

@@ -9,8 +9,11 @@ import useViewModelInstanceBoolean from './hooks/useViewModelInstanceBoolean';
import useViewModelInstanceColor from './hooks/useViewModelInstanceColor';
import useViewModelInstanceEnum from './hooks/useViewModelInstanceEnum';
import useViewModelInstanceTrigger from './hooks/useViewModelInstanceTrigger';
import useViewModelInstanceImage from './hooks/useViewModelInstanceImage';
import useViewModelInstanceList from './hooks/useViewModelInstanceList';
import useResizeCanvas from './hooks/useResizeCanvas';
import useRiveFile from './hooks/useRiveFile';
import useViewModelInstanceArtboard from './hooks/useViewModelInstanceArtboard';
export default Rive;
export {
@@ -26,6 +29,9 @@ export {
useViewModelInstanceColor,
useViewModelInstanceEnum,
useViewModelInstanceTrigger,
useViewModelInstanceImage,
useViewModelInstanceList,
useViewModelInstanceArtboard,
RiveProps,
};
export {

View File

@@ -1,12 +1,17 @@
import {
type decodeImage,
Rive,
RiveFile,
RiveFileParameters,
RiveParameters,
ViewModelInstance,
ViewModelInstanceArtboard,
} from '@rive-app/canvas';
import { ComponentProps, RefCallback } from 'react';
export type UseRiveParameters = Partial<Omit<RiveParameters, 'canvas'>> | null;
export type UseRiveParameters = Partial<Omit<RiveParameters, 'canvas'>> & {
onRiveReady?: (rive: Rive) => void;
} | null;
export type UseRiveOptions = {
useDevicePixelRatio: boolean;
@@ -183,4 +188,63 @@ export type UseViewModelInstanceTriggerResult = {
* Fires the property trigger.
*/
trigger: () => void;
};
export type RiveRenderImage = Awaited<ReturnType<typeof decodeImage>>;
export type UseViewModelInstanceImageResult = {
/**
* Set the value of the image.
* @param value - The image to set.
*/
setValue: (value: RiveRenderImage | null) => void;
};
export type UseViewModelInstanceListResult = {
/**
* The current length of the list.
*/
length: number;
/**
* Add an instance to the end of the list.
* @param instance - The ViewModelInstance to add.
*/
addInstance: (instance: ViewModelInstance) => void;
/**
* Add an instance at a specific index in the list.
* @param instance - The ViewModelInstance to add.
* @param index - The index to add the instance at.
* @returns True if the instance was successfully added, false otherwise.
*/
addInstanceAt: (instance: ViewModelInstance, index: number) => boolean;
/**
* Remove an instance from the list.
* @param instance - The ViewModelInstance to remove.
*/
removeInstance: (instance: ViewModelInstance) => void;
/**
* Remove an instance at a specific index from the list.
* @param index - The index to remove the instance from.
*/
removeInstanceAt: (index: number) => void;
/**
* Get an instance at a specific index from the list.
* @param index - The index to get the instance from.
* @returns The ViewModelInstance at the index, or null if not found.
*/
getInstanceAt: (index: number) => ViewModelInstance | null;
/**
* Swap two instances in the list.
* @param a - The first index.
* @param b - The second index.
*/
swap: (a: number, b: number) => void;
};
export type UseViewModelInstanceArtboardResult = {
/**
* Set the value of the artboard.
* @param value - The artboard to set.
*/
setValue: (value: ViewModelInstanceArtboard extends { value: infer T } ? T : never) => void;
};