mirror of
https://github.com/rive-app/rive-react.git
synced 2026-03-13 08:22:30 +08:00
Compare commits
34 Commits
v4.24.0
...
feature/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dab7a8d6f0 | ||
|
|
d2ccee1220 | ||
|
|
5bf53fc238 | ||
|
|
e49b9ae606 | ||
|
|
fc78d38a0b | ||
|
|
04397e658c | ||
|
|
52519885c6 | ||
|
|
2ad2df085b | ||
|
|
c9308db9dc | ||
|
|
082b9a6323 | ||
|
|
6aaee36ff0 | ||
|
|
b7581e5b25 | ||
|
|
4879b95245 | ||
|
|
4407dec4fc | ||
|
|
b73c3f58b2 | ||
|
|
cee7ca5a43 | ||
|
|
cf1d7c4c1c | ||
|
|
c414f143fd | ||
|
|
1f8afe4635 | ||
|
|
469e9073ce | ||
|
|
43290d1cfd | ||
|
|
37763e11c7 | ||
|
|
384b62c1b7 | ||
|
|
d9e61373b3 | ||
|
|
66f1ae021c | ||
|
|
1fc4fa44e2 | ||
|
|
3866cb9e06 | ||
|
|
bef03fc403 | ||
|
|
c4da27c7b6 | ||
|
|
c2ec32350d | ||
|
|
ead1d8e0e3 | ||
|
|
26cb0b5dec | ||
|
|
0e40840e6f | ||
|
|
480037c399 |
9
.github/workflows/publish.yml
vendored
9
.github/workflows/publish.yml
vendored
@@ -14,10 +14,9 @@ jobs:
|
||||
publish_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT_GITHUB }}
|
||||
- name: Setup Git config
|
||||
run: |
|
||||
git config --local user.email 'hello@rive.app'
|
||||
@@ -40,14 +39,20 @@ 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 }}
|
||||
|
||||
30
.github/workflows/storybook.yml
vendored
Normal file
30
.github/workflows/storybook.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
138
CHANGELOG.md
138
CHANGELOG.md
@@ -4,146 +4,8 @@ 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.24.0](https://github.com/rive-app/rive-react/compare/v4.23.4...v4.24.0)
|
||||
|
||||
- chore: bump rive web 2.32.0 [`e6e4356`](https://github.com/rive-app/rive-react/commit/e6e43564b1af8da608c146a2a76795e29063daf8)
|
||||
|
||||
#### [v4.23.4](https://github.com/rive-app/rive-react/compare/v4.23.3...v4.23.4)
|
||||
|
||||
> 23 September 2025
|
||||
|
||||
- chore: release 4.23.4 [`efeee47`](https://github.com/rive-app/rive-react/commit/efeee472e60946d293f6287e11350fdaf4afe03b)
|
||||
- bump rive to 2.31.6 [`42d502f`](https://github.com/rive-app/rive-react/commit/42d502f452b6a76cafc332c2cd84bd97e315be77)
|
||||
|
||||
#### [v4.23.3](https://github.com/rive-app/rive-react/compare/v4.23.2...v4.23.3)
|
||||
|
||||
> 4 September 2025
|
||||
|
||||
- chore: release 4.23.3 [`bd483e0`](https://github.com/rive-app/rive-react/commit/bd483e0ab72a99a9c55c7e9fb80bd95827ca54cb)
|
||||
- chore: bump rive wasm to 2.31.5 [`7c361e4`](https://github.com/rive-app/rive-react/commit/7c361e4c538cf813fdc94f572fac1e4a82258ae8)
|
||||
|
||||
#### [v4.23.2](https://github.com/rive-app/rive-react/compare/v4.23.1...v4.23.2)
|
||||
|
||||
> 4 September 2025
|
||||
|
||||
- chore: release 4.23.2 [`d80df17`](https://github.com/rive-app/rive-react/commit/d80df170aa0d1c575101c8cd1bb76968173c439f)
|
||||
- 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)
|
||||
|
||||
14
README.md
14
README.md
@@ -1,4 +1,5 @@
|
||||

|
||||
[](https://rive-app.github.io/rive-react)
|
||||

|
||||

|
||||
|
||||
@@ -6,16 +7,9 @@
|
||||
|
||||

|
||||
|
||||
[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.
|
||||
A React runtime library for [Rive](https://rive.app).
|
||||
|
||||
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/)
|
||||
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.
|
||||
|
||||
## Table of contents
|
||||
|
||||
@@ -61,6 +55,8 @@ 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)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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, ImagePropertyTest, TodoListTest, ArtboardPropertyTest } from './DataBindingTests';
|
||||
import { StringPropertyTest, NumberPropertyTest, BooleanPropertyTest, ColorPropertyTest, EnumPropertyTest, NestedViewModelTest, TriggerPropertyTest, PersonForm, PersonInstances } from './DataBindingTests';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Tests/DataBinding',
|
||||
@@ -345,183 +345,4 @@ 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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -9,12 +9,7 @@ import Rive, {
|
||||
useViewModelInstanceNumber,
|
||||
useViewModelInstanceEnum,
|
||||
useViewModelInstanceColor,
|
||||
useViewModelInstanceTrigger,
|
||||
useViewModelInstanceImage,
|
||||
decodeImage,
|
||||
ViewModelInstance,
|
||||
useViewModelInstanceList,
|
||||
useViewModelInstanceArtboard
|
||||
useViewModelInstanceTrigger
|
||||
} from '@rive-app/react-webgl2';
|
||||
|
||||
|
||||
@@ -527,355 +522,3 @@ 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>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rive-app/react-canvas-lite",
|
||||
"version": "4.24.0",
|
||||
"version": "4.19.0",
|
||||
"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.32.0"
|
||||
"@rive-app/canvas-lite": "2.27.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rive-app/react-canvas",
|
||||
"version": "4.24.0",
|
||||
"version": "4.19.0",
|
||||
"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.32.0"
|
||||
"@rive-app/canvas": "2.27.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rive-app/react-webgl",
|
||||
"version": "4.24.0",
|
||||
"version": "4.19.0",
|
||||
"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.32.0"
|
||||
"@rive-app/webgl": "2.27.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rive-app/react-webgl2",
|
||||
"version": "4.24.0",
|
||||
"version": "4.19.0",
|
||||
"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.32.0"
|
||||
"@rive-app/webgl2": "2.27.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
|
||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rive-react",
|
||||
"version": "4.24.0",
|
||||
"version": "4.19.0",
|
||||
"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.32.0",
|
||||
"@rive-app/canvas-lite": "2.32.0",
|
||||
"@rive-app/webgl": "2.32.0",
|
||||
"@rive-app/webgl2": "2.32.0"
|
||||
"@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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
@@ -48,7 +48,6 @@
|
||||
"@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",
|
||||
|
||||
@@ -69,7 +69,6 @@ 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);
|
||||
|
||||
@@ -131,23 +130,13 @@ export default function useRive(
|
||||
let r: Rive | null;
|
||||
if (rive == null) {
|
||||
const { useOffscreenRenderer } = options;
|
||||
const { onRiveReady, ...restRiveParams } = riveParams;
|
||||
r = new Rive({
|
||||
useOffscreenRenderer,
|
||||
...restRiveParams,
|
||||
...riveParams,
|
||||
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) {
|
||||
@@ -248,14 +237,6 @@ export default function useRive(
|
||||
};
|
||||
}, [rive, canvasElem]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (riveRef.current != null) {
|
||||
riveRef.current!.cleanup();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Listen for changes in the animations params
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } 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.
|
||||
*
|
||||
@@ -16,37 +35,57 @@ 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() {
|
||||
if (!rive) {
|
||||
const currentRive = riveRef.current;
|
||||
const currentParams = paramsRef.current;
|
||||
|
||||
if (!currentRive) {
|
||||
setViewModel(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let model: ViewModel | null = null;
|
||||
|
||||
if (name != null) {
|
||||
model = rive.viewModelByName?.(name) || null;
|
||||
} else if (useDefault) {
|
||||
model = rive.defaultViewModel() || null;
|
||||
if (currentParams?.name != null) {
|
||||
model = currentRive.viewModelByName?.(currentParams.name) || null;
|
||||
} else if (currentParams?.useDefault) {
|
||||
model = currentRive.defaultViewModel() || null;
|
||||
} else {
|
||||
model = rive.defaultViewModel() || null;
|
||||
model = currentRive.defaultViewModel() || null;
|
||||
}
|
||||
|
||||
setViewModel(model);
|
||||
shouldUpdate.current = false;
|
||||
}
|
||||
|
||||
fetchViewModel();
|
||||
|
||||
if (rive) {
|
||||
rive.on(EventType.Load, fetchViewModel);
|
||||
const currentRive = riveRef.current;
|
||||
if (currentRive) {
|
||||
currentRive.on(EventType.Load, fetchViewModel);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (rive) {
|
||||
rive.off(EventType.Load, fetchViewModel);
|
||||
if (currentRive) {
|
||||
currentRive.off(EventType.Load, fetchViewModel);
|
||||
}
|
||||
};
|
||||
}, [rive, name, useDefault]);
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } 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.
|
||||
*
|
||||
@@ -20,26 +43,50 @@ 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(() => {
|
||||
if (!viewModel) {
|
||||
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) {
|
||||
setInstance(null);
|
||||
instanceRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let result: ViewModelInstance | null = null;
|
||||
|
||||
if (name != null) {
|
||||
result = viewModel.instanceByName(name) || null;
|
||||
} else if (useDefault) {
|
||||
result = viewModel.defaultInstance?.() || null;
|
||||
} else if (useNew) {
|
||||
result = viewModel.instance?.() || 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;
|
||||
} else {
|
||||
result = viewModel.defaultInstance?.() || null;
|
||||
result = currentViewModel.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);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -9,11 +9,8 @@ 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 {
|
||||
@@ -29,9 +26,6 @@ export {
|
||||
useViewModelInstanceColor,
|
||||
useViewModelInstanceEnum,
|
||||
useViewModelInstanceTrigger,
|
||||
useViewModelInstanceImage,
|
||||
useViewModelInstanceList,
|
||||
useViewModelInstanceArtboard,
|
||||
RiveProps,
|
||||
};
|
||||
export {
|
||||
|
||||
66
src/types.ts
66
src/types.ts
@@ -1,17 +1,12 @@
|
||||
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'>> & {
|
||||
onRiveReady?: (rive: Rive) => void;
|
||||
} | null;
|
||||
export type UseRiveParameters = Partial<Omit<RiveParameters, 'canvas'>> | null;
|
||||
|
||||
export type UseRiveOptions = {
|
||||
useDevicePixelRatio: boolean;
|
||||
@@ -188,63 +183,4 @@ 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;
|
||||
};
|
||||
Reference in New Issue
Block a user