mirror of
https://github.com/rive-app/rive-react.git
synced 2026-03-13 08:22:30 +08:00
Compare commits
69 Commits
feature/in
...
v4.24.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19f20268ae | ||
|
|
e6e43564b1 | ||
|
|
efeee472e6 | ||
|
|
42d502f452 | ||
|
|
bd483e0ab7 | ||
|
|
7c361e4c53 | ||
|
|
d80df170aa | ||
|
|
a3118d5984 | ||
|
|
f732a3b044 | ||
|
|
57ebc37e3f | ||
|
|
69a356894d | ||
|
|
23d9d7f48b | ||
|
|
788b7ef68e | ||
|
|
69658c204a | ||
|
|
7249fa36e7 | ||
|
|
52dd934e43 | ||
|
|
c151ee37b5 | ||
|
|
c660a675c2 | ||
|
|
74e1d5a5f2 | ||
|
|
963ecc43b8 | ||
|
|
85807f2166 | ||
|
|
9a33504d3a | ||
|
|
1a4d7e7168 | ||
|
|
b3d0fd4339 | ||
|
|
c4239ab6b2 | ||
|
|
22f8d5a945 | ||
|
|
721ed786dc | ||
|
|
eef56fb641 | ||
|
|
4bc0f496f8 | ||
|
|
10bb4c69ea | ||
|
|
c5b6826996 | ||
|
|
ec4875933c | ||
|
|
d808a8bdea | ||
|
|
7b174f7f51 | ||
|
|
eecd0d3c5b | ||
|
|
6c00364e60 | ||
|
|
d310f1c96d | ||
|
|
68e8fbe46d | ||
|
|
8ff9a844fe | ||
|
|
a565795452 | ||
|
|
b26280ae12 | ||
|
|
a62e89de94 | ||
|
|
31255f9746 | ||
|
|
3e768533df | ||
|
|
c790e66723 | ||
|
|
ab89793032 | ||
|
|
2088c0bf9b | ||
|
|
81fdf57736 | ||
|
|
3a59585207 | ||
|
|
46e19874a2 | ||
|
|
d4de776a01 | ||
|
|
fb4f543077 | ||
|
|
345905f810 | ||
|
|
0d9dabb135 | ||
|
|
ad12fe20d1 | ||
|
|
4e45f74a47 | ||
|
|
efd6c4ce82 | ||
|
|
37e379091c | ||
|
|
6d76e9f85d | ||
|
|
e7b64201ca | ||
|
|
5dc8362107 | ||
|
|
56bc96fa76 | ||
|
|
01ab78db97 | ||
|
|
7800cc041f | ||
|
|
1f9fc84629 | ||
|
|
452eb89e72 | ||
|
|
3109e45724 | ||
|
|
d303e8c96f | ||
|
|
479d5340e8 |
9
.github/workflows/publish.yml
vendored
9
.github/workflows/publish.yml
vendored
@@ -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 }}
|
||||
|
||||
30
.github/workflows/storybook.yml
vendored
30
.github/workflows/storybook.yml
vendored
@@ -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
|
||||
138
CHANGELOG.md
138
CHANGELOG.md
@@ -4,8 +4,146 @@ 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,5 +1,4 @@
|
||||

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

|
||||

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

|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
"build-storybook": "storybook build",
|
||||
"test-storybook": "test-storybook"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -47,8 +48,9 @@
|
||||
"@storybook/react": "^8.6.12",
|
||||
"@storybook/react-webpack5": "^8.6.12",
|
||||
"@storybook/test": "^8.6.12",
|
||||
"@storybook/test-runner": "^0.22.0",
|
||||
"eslint-plugin-storybook": "^0.12.0",
|
||||
"storybook": "^8.6.12",
|
||||
"webpack": "^5.99.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
examples/public/artboard_db_test.riv
Normal file
BIN
examples/public/artboard_db_test.riv
Normal file
Binary file not shown.
BIN
examples/public/db_list_test.riv
Normal file
BIN
examples/public/db_list_test.riv
Normal file
Binary file not shown.
BIN
examples/public/image_db_test.riv
Normal file
BIN
examples/public/image_db_test.riv
Normal file
Binary file not shown.
BIN
examples/public/person_databinding_test.riv
Normal file
BIN
examples/public/person_databinding_test.riv
Normal file
Binary file not shown.
BIN
examples/public/stocks.riv
Normal file
BIN
examples/public/stocks.riv
Normal file
Binary file not shown.
17
examples/src/components/DataBinding.stories.ts
Normal file
17
examples/src/components/DataBinding.stories.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import DataBinding from './DataBinding';
|
||||
|
||||
const meta = {
|
||||
title: 'DataBinding',
|
||||
component: DataBinding,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof DataBinding>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
147
examples/src/components/DataBinding.tsx
Normal file
147
examples/src/components/DataBinding.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
useRive,
|
||||
useViewModel,
|
||||
useViewModelInstance,
|
||||
useViewModelInstanceColor,
|
||||
useViewModelInstanceNumber,
|
||||
useViewModelInstanceString,
|
||||
useViewModelInstanceEnum,
|
||||
useViewModelInstanceTrigger,
|
||||
} from '@rive-app/react-webgl2';
|
||||
|
||||
const randomValue = () => Math.random() * 200 - 100;
|
||||
|
||||
const DataBinding = () => {
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src: 'stocks.riv',
|
||||
artboard: 'Main',
|
||||
stateMachines: 'State Machine 1',
|
||||
autoplay: true,
|
||||
autoBind: false,
|
||||
});
|
||||
|
||||
// Get the default instance of the view model
|
||||
const viewModel = useViewModel(rive, { name: 'Dashboard' });
|
||||
const viewModelInstance = useViewModelInstance(viewModel, { rive });
|
||||
|
||||
// Get the view model instance properties
|
||||
|
||||
const { setValue: setTitle } = useViewModelInstanceString(
|
||||
'title',
|
||||
viewModelInstance
|
||||
);
|
||||
|
||||
const { setValue: setLogoShape } = useViewModelInstanceEnum(
|
||||
'logoShape',
|
||||
viewModelInstance
|
||||
);
|
||||
|
||||
const { setValue: setRootColor } = useViewModelInstanceColor(
|
||||
'rootColor',
|
||||
viewModelInstance
|
||||
);
|
||||
|
||||
const { trigger: triggerSpinLogo } = useViewModelInstanceTrigger(
|
||||
'triggerSpinLogo',
|
||||
viewModelInstance
|
||||
);
|
||||
|
||||
useViewModelInstanceTrigger('triggerButton', viewModelInstance, {
|
||||
onTrigger: () => console.log('Button Triggered!'),
|
||||
});
|
||||
|
||||
// Apple Values
|
||||
const { setValue: setAppleName } = useViewModelInstanceString(
|
||||
'apple/name',
|
||||
viewModelInstance
|
||||
);
|
||||
const { setValue: setAppleStockChange } = useViewModelInstanceNumber(
|
||||
'apple/stockChange',
|
||||
viewModelInstance
|
||||
);
|
||||
const { value: appleColor } = useViewModelInstanceColor(
|
||||
'apple/currentColor',
|
||||
viewModelInstance
|
||||
);
|
||||
// Apple Values
|
||||
const { setValue: setMicrosoftName } = useViewModelInstanceString(
|
||||
'microsoft/name',
|
||||
viewModelInstance
|
||||
);
|
||||
const { setValue: setMicrosoftStockChange } = useViewModelInstanceNumber(
|
||||
'microsoft/stockChange',
|
||||
viewModelInstance
|
||||
);
|
||||
// Tesla Values
|
||||
const { setValue: setTeslaName } = useViewModelInstanceString(
|
||||
'tesla/name',
|
||||
viewModelInstance
|
||||
);
|
||||
const { setValue: setTeslaStockChange } = useViewModelInstanceNumber(
|
||||
'tesla/stockChange',
|
||||
viewModelInstance
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Set initial values for the view model
|
||||
if (
|
||||
setTitle &&
|
||||
setLogoShape &&
|
||||
setRootColor &&
|
||||
setAppleName &&
|
||||
setMicrosoftName &&
|
||||
setTeslaName
|
||||
) {
|
||||
setTitle('Rive Stocks Dashboard');
|
||||
setLogoShape('triangle');
|
||||
setRootColor(parseInt('ffc0ffee', 16));
|
||||
setAppleName('AAPL');
|
||||
setMicrosoftName('MSFT');
|
||||
setTeslaName('TSLA');
|
||||
}
|
||||
|
||||
// randomly generate stock values every 2 seconds
|
||||
const interval = setInterval(() => {
|
||||
const appleValue = randomValue();
|
||||
const microsoftValue = randomValue();
|
||||
const teslaValue = randomValue();
|
||||
|
||||
setAppleStockChange(appleValue);
|
||||
setMicrosoftStockChange(microsoftValue);
|
||||
setTeslaStockChange(teslaValue);
|
||||
|
||||
// If all the stock values are either all positive or all negative, spin the logo
|
||||
if (
|
||||
(appleValue > 0 && microsoftValue > 0 && teslaValue > 0) ||
|
||||
(appleValue < 0 && microsoftValue < 0 && teslaValue < 0)
|
||||
) {
|
||||
triggerSpinLogo();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [
|
||||
setTitle,
|
||||
setLogoShape,
|
||||
setRootColor,
|
||||
setAppleName,
|
||||
setMicrosoftName,
|
||||
setTeslaName,
|
||||
setAppleStockChange,
|
||||
setMicrosoftStockChange,
|
||||
setTeslaStockChange,
|
||||
triggerSpinLogo,
|
||||
]);
|
||||
|
||||
// listen for changes to the AAPL color and log them
|
||||
useEffect(() => {
|
||||
if (appleColor) {
|
||||
console.log('Apple color changed:', appleColor);
|
||||
}
|
||||
}, [appleColor]);
|
||||
|
||||
return <RiveComponent />;
|
||||
};
|
||||
|
||||
export default DataBinding;
|
||||
527
examples/src/components/DataBindingTests.stories.tsx
Normal file
527
examples/src/components/DataBindingTests.stories.tsx
Normal file
@@ -0,0 +1,527 @@
|
||||
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';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Tests/DataBinding',
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
|
||||
export const StringPropertyStory: StoryObj = {
|
||||
name: 'String Property',
|
||||
render: () => <StringPropertyTest src="person_databinding_test.riv" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Wait for the Rive file to load
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('name-value')).toBeTruthy();
|
||||
}, { timeout: 3000 });
|
||||
|
||||
const nameInput = canvas.getByTestId<HTMLInputElement>('name-input');
|
||||
await userEvent.clear(nameInput);
|
||||
|
||||
// Wait for the input to be cleared
|
||||
await waitFor(() => {
|
||||
expect(nameInput.value).toBe('');
|
||||
}, { timeout: 1000 });
|
||||
|
||||
await userEvent.click(nameInput);
|
||||
await userEvent.paste('Test User');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(nameInput.value).toBe('Test User');
|
||||
}, { timeout: 2000 });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('name-value').textContent).toBe('Test User');
|
||||
}, { timeout: 2000 });
|
||||
}
|
||||
};
|
||||
|
||||
export const NumberPropertyStory: StoryObj = {
|
||||
name: 'Number Property',
|
||||
render: () => <NumberPropertyTest src="person_databinding_test.riv" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Wait for the Rive file to load
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('age-value')).toBeTruthy();
|
||||
}, { timeout: 2000 });
|
||||
|
||||
const ageInput = canvas.getByTestId<HTMLInputElement>('age-input');
|
||||
|
||||
const currentValue = ageInput.value;
|
||||
expect(currentValue).toBe('23');
|
||||
|
||||
await userEvent.click(ageInput);
|
||||
await userEvent.clear(ageInput);
|
||||
await waitFor(() => {
|
||||
expect(ageInput.value).toBe('0'); // This is a hack to wait for the input to be cleared
|
||||
}, { timeout: 1000 });
|
||||
|
||||
await userEvent.paste('42');
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('age-value').textContent).toBe('42');
|
||||
}, { timeout: 2000 });
|
||||
}
|
||||
};
|
||||
|
||||
export const BooleanPropertyStory: StoryObj = {
|
||||
name: 'Boolean Property',
|
||||
render: () => <BooleanPropertyTest src="person_databinding_test.riv" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Wait for the Rive file to load
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('terms-value')).toBeTruthy();
|
||||
}, { timeout: 2000 });
|
||||
|
||||
const termsCheckbox = canvas.getByTestId<HTMLInputElement>('terms-checkbox');
|
||||
|
||||
expect(termsCheckbox.checked).toBe(false);
|
||||
|
||||
expect(canvas.getByTestId('terms-value').textContent).toBe('false');
|
||||
|
||||
await userEvent.click(termsCheckbox);
|
||||
|
||||
// Verify terms update
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('terms-value').textContent).toBe('true');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const ColorPropertyStory: StoryObj = {
|
||||
name: 'Color Property',
|
||||
render: () => <ColorPropertyTest src="person_databinding_test.riv" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Wait for the Rive file to load and the component to render
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('color-value')).toBeTruthy();
|
||||
expect(canvas.getByTestId('set-color-red')).toBeTruthy();
|
||||
expect(canvas.getByTestId('set-color-blue')).toBeTruthy();
|
||||
}, { timeout: 5000 });
|
||||
|
||||
const numberValueDiv = canvas.getByTestId('number-value');
|
||||
const hexValueDiv = canvas.getByTestId('hex-value');
|
||||
|
||||
// Verify initial state is red
|
||||
await waitFor(() => {
|
||||
expect(hexValueDiv.textContent).toContain('Hex value: #ce2323');
|
||||
expect(numberValueDiv.textContent).toContain('Number value: -3267805');
|
||||
});
|
||||
|
||||
|
||||
// Change color to Blue ---
|
||||
const blueButton = canvas.getByTestId('set-color-blue');
|
||||
await userEvent.click(blueButton);
|
||||
|
||||
// Verify Blue State
|
||||
await waitFor(() => {
|
||||
expect(numberValueDiv.textContent).toContain('Number value: -16776961');
|
||||
expect(hexValueDiv.textContent).toContain('Hex value: #0000ff');
|
||||
});
|
||||
|
||||
|
||||
// Change color back to Red ---
|
||||
const redButton = canvas.getByTestId('set-color-red');
|
||||
await userEvent.click(redButton);
|
||||
|
||||
// Verify Red State
|
||||
await waitFor(() => {
|
||||
expect(numberValueDiv.textContent).toContain('Number value: -65536');
|
||||
expect(hexValueDiv.textContent).toContain('Hex value: #ff0000');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const EnumPropertyStory: StoryObj = {
|
||||
name: 'Enum Property',
|
||||
render: () => <EnumPropertyTest src="person_databinding_test.riv" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Wait for the Rive file to load
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('country-value')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Wait for options to be loaded
|
||||
await waitFor(() => {
|
||||
const countrySelect = canvas.getByTestId<HTMLSelectElement>('country-select');
|
||||
return countrySelect.options.length > 0;
|
||||
});
|
||||
|
||||
const countrySelect = canvas.getByTestId<HTMLSelectElement>('country-select');
|
||||
|
||||
// Verify that the dropdown contains usa, japan, and canada
|
||||
const optionValues = Array.from(countrySelect.options).map(option => option.value);
|
||||
expect(optionValues).toContain('usa');
|
||||
expect(optionValues).toContain('japan');
|
||||
expect(optionValues).toContain('canada');
|
||||
|
||||
const currentValue = countrySelect.value;
|
||||
|
||||
expect(currentValue).toBe('usa');
|
||||
|
||||
let optionToSelect = 'japan';
|
||||
|
||||
await userEvent.selectOptions(countrySelect, optionToSelect);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('country-value').textContent).toBe(optionToSelect);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const NestedViewModelStory: StoryObj = {
|
||||
name: 'Nested ViewModel Property',
|
||||
render: () => <NestedViewModelTest src="person_databinding_test.riv" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
|
||||
// Wait for the Rive file to load
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('drink-type-value')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Wait for options to be loaded
|
||||
await waitFor(() => {
|
||||
const drinkTypeSelect = canvas.getByTestId<HTMLSelectElement>('drink-type-select');
|
||||
return drinkTypeSelect.options.length > 0;
|
||||
}, { timeout: 2000 });
|
||||
|
||||
const drinkTypeSelect = canvas.getByTestId<HTMLSelectElement>('drink-type-select');
|
||||
const optionValues = Array.from(drinkTypeSelect.options).map(option => option.value);
|
||||
expect(optionValues).toContain('Coffee');
|
||||
expect(optionValues).toContain('Tea');
|
||||
|
||||
|
||||
expect(drinkTypeSelect.value).toBe('Tea');
|
||||
|
||||
let optionToSelect = 'Coffee';
|
||||
|
||||
await userEvent.selectOptions(drinkTypeSelect, optionToSelect);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('drink-type-value').textContent).toBe(optionToSelect);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const TriggerPropertyStory: StoryObj = {
|
||||
name: 'Trigger Property',
|
||||
render: () => <TriggerPropertyTest src="person_databinding_test.riv" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Wait for the Rive file to load
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('submit-button')).toBeTruthy();
|
||||
}, { timeout: 2000 });
|
||||
|
||||
expect(canvas.getByTestId('callback-triggered').textContent).toContain('none');
|
||||
|
||||
// Trigger submit action
|
||||
await userEvent.click(canvas.getByTestId('submit-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('callback-triggered').textContent).toContain('submit-callback');
|
||||
});
|
||||
|
||||
await userEvent.click(canvas.getByTestId('reset-button'));
|
||||
|
||||
// Verify onTrigger callback works for reset
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('callback-triggered').textContent).toContain('reset-callback');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const PersonInstancesStory: StoryObj = {
|
||||
name: 'Person Instances',
|
||||
render: () => <PersonInstances src="person_databinding_test.riv" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Wait for the Rive file to load
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('instance-name')).toBeTruthy();
|
||||
expect(canvas.getByTestId('select-jane')).toBeTruthy();
|
||||
expect(canvas.getByTestId('select-default')).toBeTruthy();
|
||||
}, { timeout: 2000 });
|
||||
|
||||
// Initially should show Steve
|
||||
expect(canvas.getByTestId('instance-name').textContent).toContain('Steve');
|
||||
|
||||
// Switch to Jane
|
||||
const janeButton = canvas.getByTestId('select-jane');
|
||||
await userEvent.click(janeButton);
|
||||
|
||||
// Verify instance changed to Jane
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('instance-name').textContent).toContain('Jane');
|
||||
});
|
||||
|
||||
// Switch to Default instance
|
||||
const defaultButton = canvas.getByTestId('select-default');
|
||||
await userEvent.click(defaultButton);
|
||||
|
||||
// Verify instance changed to Default
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('instance-name').textContent).toContain('Default');
|
||||
});
|
||||
|
||||
// Switch back to Steve
|
||||
const steveButton = canvas.getByTestId('select-steve');
|
||||
await userEvent.click(steveButton);
|
||||
|
||||
// Verify instance changed back to Steve
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('instance-name').textContent).toContain('Steve');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// A configurable form story, so we can test all the properties at once
|
||||
export const PersonFormStory: StoryObj = {
|
||||
name: 'Complete Person Form',
|
||||
render: () => <PersonForm src="person_databinding_test.riv" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
|
||||
// Wait for the Rive file to load
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId('name-value')).toBeTruthy();
|
||||
}, { timeout: 2000 });
|
||||
|
||||
// Update name
|
||||
const nameInput = canvas.getByTestId('name-input');
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, 'Test User');
|
||||
|
||||
// Update age
|
||||
const ageInput = canvas.getByTestId('age-input');
|
||||
await userEvent.clear(ageInput);
|
||||
await userEvent.type(ageInput, '42');
|
||||
|
||||
// Toggle terms agreement
|
||||
const termsCheckbox = canvas.getByTestId('terms-checkbox');
|
||||
await userEvent.click(termsCheckbox);
|
||||
|
||||
// Change color
|
||||
const colorButton = canvas.getByTestId('set-color-red');
|
||||
await userEvent.click(colorButton);
|
||||
|
||||
// Change country
|
||||
const countrySelect = canvas.getByTestId<HTMLSelectElement>('country-select');
|
||||
await userEvent.selectOptions(countrySelect, 'japan');
|
||||
|
||||
// Change drink type
|
||||
const drinkTypeSelect = canvas.getByTestId<HTMLSelectElement>('drink-type-select');
|
||||
await userEvent.selectOptions(drinkTypeSelect, 'Coffee');
|
||||
|
||||
// Submit the form
|
||||
const submitButton = canvas.getByTestId('submit-button');
|
||||
await userEvent.click(submitButton);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
};
|
||||
881
examples/src/components/DataBindingTests.tsx
Normal file
881
examples/src/components/DataBindingTests.tsx
Normal file
@@ -0,0 +1,881 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Rive, {
|
||||
useRive,
|
||||
useViewModel,
|
||||
useViewModelInstance,
|
||||
useViewModelInstanceBoolean,
|
||||
useViewModelInstanceString,
|
||||
useViewModelInstanceNumber,
|
||||
useViewModelInstanceEnum,
|
||||
useViewModelInstanceColor,
|
||||
useViewModelInstanceTrigger,
|
||||
useViewModelInstanceImage,
|
||||
decodeImage,
|
||||
ViewModelInstance,
|
||||
useViewModelInstanceList,
|
||||
useViewModelInstanceArtboard
|
||||
} from '@rive-app/react-webgl2';
|
||||
|
||||
|
||||
export const StringPropertyTest = ({ src }: { src: string }) => {
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src,
|
||||
autoplay: true,
|
||||
artboard: "Artboard",
|
||||
autoBind: true,
|
||||
stateMachines: "State Machine 1",
|
||||
});
|
||||
|
||||
const { value: name, setValue: setName } = useViewModelInstanceString('name', rive?.viewModelInstance);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RiveComponent style={{ width: '400px', height: '400px' }} />
|
||||
{(rive === null) ? <div data-testid="loading-text">Loading…</div> : (
|
||||
<div>
|
||||
<label>
|
||||
Name:
|
||||
<input
|
||||
data-testid="name-input"
|
||||
type="text"
|
||||
value={name || ''}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</label>
|
||||
<div data-testid="name-value">{name}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NumberPropertyTest = ({ src }: { src: string }) => {
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src,
|
||||
autoplay: true,
|
||||
artboard: "Artboard",
|
||||
autoBind: true,
|
||||
stateMachines: "State Machine 1",
|
||||
});
|
||||
|
||||
const { value: age, setValue: setAge } = useViewModelInstanceNumber('age', rive?.viewModelInstance);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RiveComponent style={{ width: '400px', height: '400px' }} />
|
||||
{(rive === null) ? <div data-testid="loading-text">Loading…</div> : (
|
||||
<div>
|
||||
<label>
|
||||
Age:
|
||||
<input
|
||||
data-testid="age-input"
|
||||
type="number"
|
||||
value={age ?? 0}
|
||||
onChange={(e) => setAge(Number(e.target.value))}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</label>
|
||||
<div data-testid="age-value">{age}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BooleanPropertyTest = ({ src }: { src: string }) => {
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src,
|
||||
autoplay: true,
|
||||
artboard: "Artboard",
|
||||
autoBind: true,
|
||||
stateMachines: "State Machine 1",
|
||||
});
|
||||
|
||||
const { value: agreedToTerms, setValue: setAgreedToTerms } = useViewModelInstanceBoolean('agreedToTerms', rive?.viewModelInstance);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RiveComponent style={{ width: '400px', height: '400px' }} />
|
||||
{(rive === null) ? <div data-testid="loading-text">Loading…</div> : (
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
data-testid="terms-checkbox"
|
||||
type="checkbox"
|
||||
checked={agreedToTerms ?? false}
|
||||
onChange={(e) => setAgreedToTerms(e.target.checked)}
|
||||
/>
|
||||
Agree to Terms
|
||||
</label>
|
||||
<div data-testid="terms-value">{agreedToTerms ? 'true' : 'false'}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const colorNumberToHexString = (colorNum: number | null) => {
|
||||
if (colorNum === null) {
|
||||
return 'N/A';
|
||||
}
|
||||
const unsignedInt = colorNum >>> 0;
|
||||
const r = (unsignedInt >> 16) & 0xff;
|
||||
const g = (unsignedInt >> 8) & 0xff;
|
||||
const b = unsignedInt & 0xff;
|
||||
|
||||
const toHex = (c: number) => c.toString(16).padStart(2, '0');
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
};
|
||||
|
||||
export const ColorPropertyTest = ({ src }: { src: string }) => {
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src,
|
||||
autoplay: true,
|
||||
artboard: "Artboard",
|
||||
autoBind: true,
|
||||
stateMachines: "State Machine 1",
|
||||
});
|
||||
|
||||
|
||||
const { value: colorNum, setValue: setColor, setRgb } = useViewModelInstanceColor('favColor', rive?.viewModelInstance);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RiveComponent style={{ width: '400px', height: '400px' }} />
|
||||
{(rive === null) ? <div data-testid="loading-text">Loading…</div> : (
|
||||
<div>
|
||||
<label>
|
||||
Favorite Color:
|
||||
<div data-testid="color-value" style={{
|
||||
backgroundColor: typeof colorNum === 'string' ? colorNum : colorNumberToHexString(colorNum),
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
display: 'inline-block',
|
||||
marginLeft: '10px'
|
||||
}}></div>
|
||||
<div data-testid="number-value">
|
||||
Number value: {typeof colorNum === 'number' ? colorNum : 'N/A'}
|
||||
</div>
|
||||
<div data-testid="hex-value">
|
||||
Hex value: {colorNumberToHexString(colorNum)}
|
||||
</div>
|
||||
</label>
|
||||
<button
|
||||
data-testid="set-color-red"
|
||||
type="button"
|
||||
onClick={() => setRgb(255, 0, 0)}
|
||||
>
|
||||
Red
|
||||
</button>
|
||||
<button
|
||||
data-testid="set-color-blue"
|
||||
type="button"
|
||||
onClick={() => setRgb(0, 0, 255)}
|
||||
>
|
||||
Blue
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EnumPropertyTest = ({ src }: { src: string }) => {
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src,
|
||||
autoplay: true,
|
||||
artboard: "Artboard",
|
||||
autoBind: true,
|
||||
stateMachines: "State Machine 1",
|
||||
});
|
||||
|
||||
const { value: country, setValue: setCountry, values: countries } = useViewModelInstanceEnum('country', rive?.viewModelInstance);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RiveComponent style={{ width: '400px', height: '400px' }} />
|
||||
{(rive === null) ? <div data-testid="loading-text">Loading…</div> : (
|
||||
<div>
|
||||
<label>
|
||||
Country:
|
||||
<select
|
||||
data-testid="country-select"
|
||||
value={country || ''}
|
||||
onChange={(e) => setCountry(e.target.value)}
|
||||
>
|
||||
{countries.map(c => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div data-testid="country-value">{country}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NestedViewModelTest = ({ src }: { src: string }) => {
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src,
|
||||
autoplay: true,
|
||||
artboard: "Artboard",
|
||||
autoBind: true,
|
||||
stateMachines: "State Machine 1",
|
||||
});
|
||||
|
||||
const { value: drinkType, setValue: setDrinkType, values: drinkTypes } = useViewModelInstanceEnum('favDrink/type', rive?.viewModelInstance);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RiveComponent style={{ width: '400px', height: '400px' }} />
|
||||
{(rive === null) ? <div data-testid="loading-text">Loading…</div> : (
|
||||
<div>
|
||||
<label>
|
||||
Favorite Drink Type:
|
||||
<select
|
||||
data-testid="drink-type-select"
|
||||
value={drinkType || ''}
|
||||
onChange={(e) => setDrinkType(e.target.value)}
|
||||
>
|
||||
{drinkTypes.map(dt => (
|
||||
<option key={dt} value={dt}>{dt}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div data-testid="drink-type-value">{drinkType}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TriggerPropertyTest = ({ src }: { src: string }) => {
|
||||
const [callbackTriggered, setCallbackTriggered] = useState('');
|
||||
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src,
|
||||
autoplay: true,
|
||||
autoBind: true,
|
||||
artboard: "Artboard",
|
||||
stateMachines: "State Machine 1",
|
||||
});
|
||||
|
||||
|
||||
|
||||
const { trigger: onFormSubmit } = useViewModelInstanceTrigger('onFormSubmit', rive?.viewModelInstance,
|
||||
{
|
||||
onTrigger: () => {
|
||||
setCallbackTriggered('submit-callback');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { trigger: onFormReset } = useViewModelInstanceTrigger('onFormReset', rive?.viewModelInstance,
|
||||
{
|
||||
onTrigger: () => {
|
||||
setCallbackTriggered('reset-callback');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
onFormSubmit();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
onFormReset();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RiveComponent style={{ width: '400px', height: '400px' }} />
|
||||
{(rive === null) ? <div data-testid="loading-text">Loading…</div> : (
|
||||
<div>
|
||||
<button data-testid="submit-button" type="button" onClick={handleSubmit}>Submit</button>
|
||||
<button data-testid="reset-button" type="button" onClick={handleReset}>Reset</button>
|
||||
|
||||
<div data-testid="callback-triggered">
|
||||
Last callback triggered: {callbackTriggered || 'none'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PersonForm = ({ src }: { src: string }) => {
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src,
|
||||
autoplay: true,
|
||||
autoBind: true,
|
||||
artboard: "Artboard",
|
||||
stateMachines: "State Machine 1",
|
||||
});
|
||||
|
||||
const { value: name, setValue: setName } = useViewModelInstanceString('name', rive?.viewModelInstance);
|
||||
const { value: age, setValue: setAge } = useViewModelInstanceNumber('age', rive?.viewModelInstance);
|
||||
const { value: agreedToTerms, setValue: setAgreedToTerms } = useViewModelInstanceBoolean('agreedToTerms', rive?.viewModelInstance);
|
||||
const { value: colorNum, setRgb } = useViewModelInstanceColor('favColor', rive?.viewModelInstance);
|
||||
const { value: country, setValue: setCountry, values: countries } = useViewModelInstanceEnum('country', rive?.viewModelInstance);
|
||||
const { trigger: onFormSubmit } = useViewModelInstanceTrigger('onFormSubmit', rive?.viewModelInstance);
|
||||
const { trigger: onFormReset } = useViewModelInstanceTrigger('onFormReset', rive?.viewModelInstance);
|
||||
|
||||
|
||||
// Drink properties (nested viewmodel)
|
||||
const { value: drinkType, setValue: setDrinkType, values: drinkTypes } = useViewModelInstanceEnum('favDrink/type', rive?.viewModelInstance);
|
||||
|
||||
const handleReset = () => {
|
||||
setName('');
|
||||
setAge(0);
|
||||
setAgreedToTerms(false);
|
||||
setRgb(0, 0, 0);
|
||||
setCountry(countries[0]);
|
||||
setDrinkType(drinkTypes[0]);
|
||||
onFormReset();
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
onFormSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RiveComponent style={{ width: '400px', height: '400px' }} />
|
||||
|
||||
{(rive === null) ? <div data-testid="loading-text">Loading…</div> : (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label>
|
||||
Name:
|
||||
<input
|
||||
data-testid="name-input"
|
||||
type="text"
|
||||
value={name || ''}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<div data-testid="name-value">{name}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Age:
|
||||
<input
|
||||
data-testid="age-input"
|
||||
type="number"
|
||||
value={age || 0}
|
||||
onChange={(e) => setAge(Number(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<div data-testid="age-value">{age}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
data-testid="terms-checkbox"
|
||||
type="checkbox"
|
||||
checked={agreedToTerms || false}
|
||||
onChange={(e) => setAgreedToTerms(e.target.checked)}
|
||||
/>
|
||||
Agree to Terms
|
||||
</label>
|
||||
<div data-testid="terms-value">{agreedToTerms ? 'true' : 'false'}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Favorite Color:
|
||||
<div data-testid="color-value" style={{
|
||||
backgroundColor: colorNumberToHexString(colorNum),
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
display: 'inline-block',
|
||||
marginLeft: '10px'
|
||||
}}></div>
|
||||
</label>
|
||||
<button
|
||||
data-testid="set-color-red"
|
||||
type="button"
|
||||
onClick={() => setRgb(255, 0, 0)}
|
||||
>
|
||||
Red
|
||||
</button>
|
||||
<button
|
||||
data-testid="set-color-blue"
|
||||
type="button"
|
||||
onClick={() => setRgb(0, 0, 255)}
|
||||
>
|
||||
Blue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Country:
|
||||
<select
|
||||
data-testid="country-select"
|
||||
value={country || ''}
|
||||
onChange={(e) => setCountry(e.target.value)}
|
||||
>
|
||||
{countries.map(c => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div data-testid="country-value">{country}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Favorite Drink Type:
|
||||
<select
|
||||
data-testid="drink-type-select"
|
||||
value={drinkType || ''}
|
||||
onChange={(e) => setDrinkType(e.target.value)}
|
||||
>
|
||||
{drinkTypes.map(dt => (
|
||||
<option key={dt} value={dt}>{dt}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div data-testid="drink-type-value">{drinkType}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button data-testid="submit-button" type="submit">Submit</button>
|
||||
<button data-testid="reset-button" type="button" onClick={handleReset}>Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Component to demonstrate different viewmodel instances
|
||||
export const PersonInstances = ({ src }: { src: string }) => {
|
||||
const [activeInstance, setActiveInstance] = useState('Steve');
|
||||
const [useDefaultInstance, setUseDefaultInstance] = useState(false);
|
||||
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src,
|
||||
autoplay: true,
|
||||
artboard: "Artboard",
|
||||
stateMachines: "State Machine 1",
|
||||
});
|
||||
|
||||
const viewModel = useViewModel(rive, { name: 'PersonViewModel' });
|
||||
const params = useDefaultInstance ? { useDefault: true, rive } : { name: activeInstance, rive }
|
||||
const viewModelInstance = useViewModelInstance(viewModel, params);
|
||||
|
||||
|
||||
const { value: name } = useViewModelInstanceString('name', viewModelInstance);
|
||||
const { value: age } = useViewModelInstanceNumber('age', viewModelInstance);
|
||||
const { value: country } = useViewModelInstanceEnum('country', viewModelInstance);
|
||||
|
||||
const switchToNamedInstance = (instanceName: string) => {
|
||||
setActiveInstance(instanceName);
|
||||
setUseDefaultInstance(false);
|
||||
};
|
||||
|
||||
const switchToDefaultInstance = () => {
|
||||
setUseDefaultInstance(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RiveComponent style={{ width: '400px', height: '400px' }} />
|
||||
|
||||
{(rive === null) ? <div data-testid="loading-text">Loading…</div> : (
|
||||
<div>
|
||||
<button
|
||||
data-testid="select-steve"
|
||||
onClick={() => switchToNamedInstance('Steve')}
|
||||
disabled={!useDefaultInstance && activeInstance === 'Steve'}
|
||||
>
|
||||
Steve
|
||||
</button>
|
||||
<button
|
||||
data-testid="select-jane"
|
||||
onClick={() => switchToNamedInstance('Jane')}
|
||||
disabled={!useDefaultInstance && activeInstance === 'Jane'}
|
||||
>
|
||||
Jane
|
||||
</button>
|
||||
<button
|
||||
data-testid="select-default"
|
||||
onClick={switchToDefaultInstance}
|
||||
disabled={useDefaultInstance}
|
||||
>
|
||||
Default
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 data-testid="instance-name">Instance: {useDefaultInstance ? 'Default' : activeInstance}</h3>
|
||||
<p data-testid="person-name">Name: {name}</p>
|
||||
<p data-testid="person-age">Age: {age}</p>
|
||||
<p data-testid="person-country">Country: {country}</p>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
5002
examples/yarn.lock
5002
examples/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rive-app/react-canvas-lite",
|
||||
"version": "4.19.0",
|
||||
"version": "4.24.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.27.1"
|
||||
"@rive-app/canvas-lite": "2.32.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rive-app/react-canvas",
|
||||
"version": "4.19.0",
|
||||
"version": "4.24.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.27.1"
|
||||
"@rive-app/canvas": "2.32.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rive-app/react-webgl",
|
||||
"version": "4.19.0",
|
||||
"version": "4.24.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.27.1"
|
||||
"@rive-app/webgl": "2.32.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rive-app/react-webgl2",
|
||||
"version": "4.19.0",
|
||||
"version": "4.24.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.27.1"
|
||||
"@rive-app/webgl2": "2.32.0"
|
||||
},
|
||||
"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.19.0",
|
||||
"version": "4.24.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.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.32.0",
|
||||
"@rive-app/canvas-lite": "2.32.0",
|
||||
"@rive-app/webgl": "2.32.0",
|
||||
"@rive-app/webgl2": "2.32.0"
|
||||
},
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
55
src/hooks/useViewModel.ts
Normal file
55
src/hooks/useViewModel.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Rive, ViewModel, EventType } from '@rive-app/canvas';
|
||||
import { UseViewModelParameters } from '../types';
|
||||
|
||||
/**
|
||||
* Hook for fetching a ViewModel from a Rive instance.
|
||||
*
|
||||
* @param rive - The Rive instance to retrieve the ViewModel from
|
||||
* @param params - Options for retrieving a ViewModel
|
||||
* @param params.name - When provided, specifies the name of the ViewModel to retrieve
|
||||
* @param params.useDefault - When true, uses the default ViewModel from the Rive instance
|
||||
* @returns The ViewModel or null if not found
|
||||
*/
|
||||
export default function useViewModel(
|
||||
rive: Rive | null,
|
||||
params?: UseViewModelParameters
|
||||
): ViewModel | null {
|
||||
const { name, useDefault = false } = params ?? {};
|
||||
const [viewModel, setViewModel] = useState<ViewModel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function fetchViewModel() {
|
||||
if (!rive) {
|
||||
setViewModel(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let model: ViewModel | null = null;
|
||||
|
||||
if (name != null) {
|
||||
model = rive.viewModelByName?.(name) || null;
|
||||
} else if (useDefault) {
|
||||
model = rive.defaultViewModel() || null;
|
||||
} else {
|
||||
model = rive.defaultViewModel() || null;
|
||||
}
|
||||
|
||||
setViewModel(model);
|
||||
}
|
||||
|
||||
fetchViewModel();
|
||||
|
||||
if (rive) {
|
||||
rive.on(EventType.Load, fetchViewModel);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (rive) {
|
||||
rive.off(EventType.Load, fetchViewModel);
|
||||
}
|
||||
};
|
||||
}, [rive, name, useDefault]);
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
49
src/hooks/useViewModelInstance.ts
Normal file
49
src/hooks/useViewModelInstance.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ViewModel, ViewModelInstance } from '@rive-app/canvas';
|
||||
import { UseViewModelInstanceParameters } from '../types';
|
||||
|
||||
/**
|
||||
* Hook for fetching a ViewModelInstance from a ViewModel.
|
||||
*
|
||||
* @param viewModel - The ViewModel to get an instance from
|
||||
* @param params - Options for retrieving a ViewModelInstance
|
||||
* @param params.name - When provided, specifies the name of the instance to retrieve
|
||||
* @param params.useDefault - When true, uses the default instance from the ViewModel
|
||||
* @param params.useNew - When true, creates a new instance of the ViewModel
|
||||
* @param params.rive - If provided, automatically binds the instance to this Rive instance
|
||||
* @returns The ViewModelInstance or null if not found
|
||||
*/
|
||||
export default function useViewModelInstance(
|
||||
viewModel: ViewModel | null,
|
||||
params?: UseViewModelInstanceParameters
|
||||
): ViewModelInstance | null {
|
||||
const { name, useDefault = false, useNew = false, rive } = params ?? {};
|
||||
const [instance, setInstance] = useState<ViewModelInstance | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewModel) {
|
||||
setInstance(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;
|
||||
} else {
|
||||
result = viewModel.defaultInstance?.() || null;
|
||||
}
|
||||
|
||||
setInstance(result);
|
||||
|
||||
if (rive && result && rive.viewModelInstance !== result) {
|
||||
rive.bindViewModelInstance(result);
|
||||
}
|
||||
}, [viewModel, name, useDefault, useNew, rive]);
|
||||
|
||||
return instance;
|
||||
}
|
||||
35
src/hooks/useViewModelInstanceArtboard.ts
Normal file
35
src/hooks/useViewModelInstanceArtboard.ts
Normal 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
|
||||
};
|
||||
}
|
||||
36
src/hooks/useViewModelInstanceBoolean.ts
Normal file
36
src/hooks/useViewModelInstanceBoolean.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ViewModelInstanceBoolean, ViewModelInstance } from '@rive-app/canvas';
|
||||
import { UseViewModelInstanceBooleanResult } from '../types';
|
||||
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
|
||||
|
||||
/**
|
||||
* Hook for interacting with boolean ViewModel instance properties.
|
||||
*
|
||||
* @param path - The path to the boolean property
|
||||
* @param viewModelInstance - The ViewModelInstance containing the boolean property to operate on
|
||||
* @returns An object with the boolean value and a setter function
|
||||
*/
|
||||
export default function useViewModelInstanceBoolean(
|
||||
path: string,
|
||||
viewModelInstance?: ViewModelInstance | null
|
||||
): UseViewModelInstanceBooleanResult {
|
||||
const result = useViewModelInstanceProperty<ViewModelInstanceBoolean, boolean, Omit<UseViewModelInstanceBooleanResult, 'value'>>(
|
||||
path,
|
||||
viewModelInstance,
|
||||
{
|
||||
getProperty: useCallback((vm, p) => vm.boolean(p), []),
|
||||
getValue: useCallback((prop) => prop.value, []),
|
||||
defaultValue: null,
|
||||
buildPropertyOperations: useCallback((safePropertyAccess) => ({
|
||||
setValue: (newValue: boolean) => {
|
||||
safePropertyAccess(prop => { prop.value = newValue; });
|
||||
}
|
||||
}), [])
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
value: result.value,
|
||||
setValue: result.setValue
|
||||
};
|
||||
}
|
||||
56
src/hooks/useViewModelInstanceColor.ts
Normal file
56
src/hooks/useViewModelInstanceColor.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ViewModelInstanceColor, ViewModelInstance } from '@rive-app/canvas';
|
||||
import { UseViewModelInstanceColorResult } from '../types';
|
||||
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
|
||||
|
||||
/**
|
||||
* Hook for interacting with color properties of a ViewModelInstance.
|
||||
*
|
||||
* @param path - Path to the color property
|
||||
* @param viewModelInstance - The ViewModelInstance containing the color property
|
||||
* @returns An object with the color value and setter functions for different color formats
|
||||
*/
|
||||
export default function useViewModelInstanceColor(
|
||||
path: string,
|
||||
viewModelInstance?: ViewModelInstance | null
|
||||
): UseViewModelInstanceColorResult {
|
||||
const result = useViewModelInstanceProperty<ViewModelInstanceColor, number, Omit<UseViewModelInstanceColorResult, 'value'>>(
|
||||
path,
|
||||
viewModelInstance,
|
||||
{
|
||||
getProperty: useCallback((vm, p) => vm.color(p), []),
|
||||
getValue: useCallback((prop) => prop.value, []),
|
||||
defaultValue: null,
|
||||
buildPropertyOperations: useCallback((safePropertyAccess) => ({
|
||||
setValue: (newValue: number) => {
|
||||
safePropertyAccess(prop => { prop.value = newValue; });
|
||||
},
|
||||
|
||||
setRgb: (r: number, g: number, b: number) => {
|
||||
safePropertyAccess(prop => { prop.rgb(r, g, b); });
|
||||
},
|
||||
|
||||
setRgba: (r: number, g: number, b: number, a: number) => {
|
||||
safePropertyAccess(prop => { prop.rgba(r, g, b, a); });
|
||||
},
|
||||
|
||||
setAlpha: (a: number) => {
|
||||
safePropertyAccess(prop => { prop.alpha(a); });
|
||||
},
|
||||
|
||||
setOpacity: (o: number) => {
|
||||
safePropertyAccess(prop => { prop.opacity(o); });
|
||||
}
|
||||
}), [])
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
value: result.value,
|
||||
setValue: result.setValue,
|
||||
setRgb: result.setRgb,
|
||||
setRgba: result.setRgba,
|
||||
setAlpha: result.setAlpha,
|
||||
setOpacity: result.setOpacity
|
||||
};
|
||||
}
|
||||
45
src/hooks/useViewModelInstanceEnum.ts
Normal file
45
src/hooks/useViewModelInstanceEnum.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ViewModelInstance, ViewModelInstanceEnum } from '@rive-app/canvas';
|
||||
import { UseViewModelInstanceEnumResult } from '../types';
|
||||
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
|
||||
|
||||
/**
|
||||
* Hook for interacting with enum properties of a ViewModelInstance.
|
||||
*
|
||||
* @param params - Parameters for interacting with enum properties
|
||||
* @param params.path - Path to the enum property (e.g. "state" or "group/state")
|
||||
* @param params.viewModelInstance - The ViewModelInstance containing the enum property
|
||||
* @returns An object with the enum value, available values, and a setter function
|
||||
*/
|
||||
export default function useViewModelInstanceEnum(
|
||||
path: string,
|
||||
viewModelInstance?: ViewModelInstance | null
|
||||
): UseViewModelInstanceEnumResult {
|
||||
const result = useViewModelInstanceProperty<
|
||||
ViewModelInstanceEnum,
|
||||
string,
|
||||
Omit<UseViewModelInstanceEnumResult, 'value' | 'values'>,
|
||||
string[]
|
||||
>(path, viewModelInstance, {
|
||||
getProperty: useCallback((vm, p) => vm.enum(p), []),
|
||||
getValue: useCallback((prop) => prop.value, []),
|
||||
defaultValue: null,
|
||||
getExtendedData: useCallback((prop: any) => prop.values, []),
|
||||
buildPropertyOperations: useCallback(
|
||||
(safePropertyAccess) => ({
|
||||
setValue: (newValue: string) => {
|
||||
safePropertyAccess((prop) => {
|
||||
prop.value = newValue;
|
||||
});
|
||||
},
|
||||
}),
|
||||
[]
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
value: result.value,
|
||||
values: result.extendedData || [],
|
||||
setValue: result.setValue,
|
||||
};
|
||||
}
|
||||
35
src/hooks/useViewModelInstanceImage.ts
Normal file
35
src/hooks/useViewModelInstanceImage.ts
Normal 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
|
||||
};
|
||||
}
|
||||
75
src/hooks/useViewModelInstanceList.ts
Normal file
75
src/hooks/useViewModelInstanceList.ts
Normal 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
|
||||
};
|
||||
}
|
||||
37
src/hooks/useViewModelInstanceNumber.ts
Normal file
37
src/hooks/useViewModelInstanceNumber.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ViewModelInstance, ViewModelInstanceNumber } from '@rive-app/canvas';
|
||||
import { UseViewModelInstanceNumberResult } from '../types';
|
||||
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
|
||||
|
||||
/**
|
||||
* Hook for interacting with number properties of a ViewModelInstance.
|
||||
*
|
||||
* @param params - Parameters for interacting with number properties
|
||||
* @param params.path - Path to the number property (e.g. "speed" or "group/speed")
|
||||
* @param params.viewModelInstance - The ViewModelInstance containing the number property
|
||||
* @returns An object with the number value and a setter function
|
||||
*/
|
||||
export default function useViewModelInstanceNumber(
|
||||
path: string,
|
||||
viewModelInstance?: ViewModelInstance | null
|
||||
): UseViewModelInstanceNumberResult {
|
||||
const result = useViewModelInstanceProperty<ViewModelInstanceNumber, number, Omit<UseViewModelInstanceNumberResult, 'value'>>(
|
||||
path,
|
||||
viewModelInstance,
|
||||
{
|
||||
getProperty: useCallback((vm, p) => vm.number(p), []),
|
||||
getValue: useCallback((prop) => prop.value, []),
|
||||
defaultValue: null,
|
||||
buildPropertyOperations: useCallback((safePropertyAccess) => ({
|
||||
setValue: (newValue: number) => {
|
||||
safePropertyAccess(prop => { prop.value = newValue; });
|
||||
}
|
||||
}), [])
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
value: result.value,
|
||||
setValue: result.setValue
|
||||
};
|
||||
}
|
||||
175
src/hooks/useViewModelInstanceProperty.ts
Normal file
175
src/hooks/useViewModelInstanceProperty.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { ViewModelInstance, ViewModelInstanceValue } from '@rive-app/canvas';
|
||||
|
||||
/**
|
||||
* Base hook for all ViewModelInstance property interactions.
|
||||
*
|
||||
* This hook handles the common tasks needed when working with Rive properties:
|
||||
* 1. Safely accessing properties (even during hot-reload)
|
||||
* 2. Keeping React state in sync with property changes
|
||||
* 3. Providing type safety for all operations
|
||||
*
|
||||
* @param path - Property path in the ViewModelInstance
|
||||
* @param viewModelInstance - The source ViewModelInstance
|
||||
* @param options - Configuration for working with the property
|
||||
* @returns Object with the value and operations
|
||||
*/
|
||||
export function useViewModelInstanceProperty<P extends ViewModelInstanceValue, V, R, E = undefined>(
|
||||
path: string,
|
||||
viewModelInstance: ViewModelInstance | null | undefined,
|
||||
options: {
|
||||
/** Function to get the property from a ViewModelInstance */
|
||||
getProperty: (vm: ViewModelInstance, path: string) => P | null;
|
||||
|
||||
/** Function to get the current value from the property */
|
||||
getValue: (prop: P) => V;
|
||||
|
||||
/** Default value to use when property is unavailable */
|
||||
defaultValue: V | null;
|
||||
|
||||
/**
|
||||
* Function to create the property-specific operations
|
||||
*
|
||||
* @param safePropertyAccess - Helper function for safely working with properties. Handles stale property references.
|
||||
* @returns Object with operations like setValue, trigger, etc.
|
||||
*/
|
||||
buildPropertyOperations: (safePropertyAccess: (callback: (prop: P) => void) => void) => R;
|
||||
|
||||
/** Optional callback for property events (mainly used by triggers) */
|
||||
onPropertyEvent?: () => void;
|
||||
|
||||
/**
|
||||
* Optional function to extract additional property data (like enum values)
|
||||
* Returns undefined if not provided
|
||||
*/
|
||||
getExtendedData?: (prop: P) => E;
|
||||
}
|
||||
): R & { value: V | null } & (E extends undefined ? {} : { extendedData: E | null }) {
|
||||
const [property, setProperty] = useState<P | null>(null);
|
||||
const [value, setValue] = useState<V | null>(options.defaultValue);
|
||||
const [extendedData, setExtendedData] = useState<E | null>(null);
|
||||
|
||||
const instanceRef = useRef<ViewModelInstance | null | undefined>(null);
|
||||
const pathRef = useRef<string>(path);
|
||||
const optionsRef = useRef(options);
|
||||
|
||||
useEffect(() => {
|
||||
optionsRef.current = options;
|
||||
}, [options]);
|
||||
|
||||
const updateProperty = useCallback(() => {
|
||||
const currentInstance = instanceRef.current;
|
||||
const currentPath = pathRef.current;
|
||||
const currentOptions = optionsRef.current;
|
||||
|
||||
if (!currentInstance || !currentPath) {
|
||||
setProperty(null);
|
||||
setValue(currentOptions.defaultValue);
|
||||
setExtendedData(null);
|
||||
return () => { };
|
||||
}
|
||||
|
||||
const prop = currentOptions.getProperty(currentInstance, currentPath);
|
||||
if (prop) {
|
||||
setProperty(prop);
|
||||
setValue(currentOptions.getValue(prop));
|
||||
|
||||
if (currentOptions.getExtendedData) {
|
||||
setExtendedData(currentOptions.getExtendedData(prop));
|
||||
}
|
||||
|
||||
const handleChange = () => {
|
||||
setValue(currentOptions.getValue(prop));
|
||||
|
||||
if (currentOptions.getExtendedData) {
|
||||
setExtendedData(currentOptions.getExtendedData(prop));
|
||||
}
|
||||
|
||||
if (currentOptions.onPropertyEvent) {
|
||||
currentOptions.onPropertyEvent();
|
||||
}
|
||||
};
|
||||
|
||||
prop.on(handleChange);
|
||||
|
||||
return () => {
|
||||
prop.off(handleChange);
|
||||
};
|
||||
}
|
||||
|
||||
return () => { };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
instanceRef.current = viewModelInstance;
|
||||
pathRef.current = path;
|
||||
|
||||
// subscribe & get our unsubscribe function
|
||||
const cleanup = updateProperty();
|
||||
return cleanup;
|
||||
}, [viewModelInstance, path, updateProperty]);
|
||||
|
||||
/**
|
||||
* Helper function that safely accesses properties, even during hot-reload.
|
||||
*
|
||||
* It tries to:
|
||||
* 1. Use the existing property reference when possible
|
||||
* 2. Fetch a fresh reference when needed
|
||||
* 3. Apply the callback to whichever reference works
|
||||
*/
|
||||
const safePropertyAccess = useCallback(
|
||||
(callback: (prop: P) => void) => {
|
||||
// Try the fast path first
|
||||
if (property && instanceRef.current === viewModelInstance) {
|
||||
try {
|
||||
callback(property);
|
||||
|
||||
// Update extended data after callback if available
|
||||
if (optionsRef.current.getExtendedData) {
|
||||
setExtendedData(optionsRef.current.getExtendedData(property));
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
// Property might be stale - so we silently catch and try alternative
|
||||
// This commonly happens during hot module replacement
|
||||
}
|
||||
}
|
||||
|
||||
// Get a fresh property if needed
|
||||
if (instanceRef.current) {
|
||||
try {
|
||||
const freshProp = optionsRef.current.getProperty(instanceRef.current, pathRef.current);
|
||||
if (freshProp) {
|
||||
setProperty(freshProp);
|
||||
callback(freshProp);
|
||||
|
||||
// Update extended data after callback if available
|
||||
if (optionsRef.current.getExtendedData) {
|
||||
setExtendedData(optionsRef.current.getExtendedData(freshProp));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail during hot-reload - this is expected behavior
|
||||
// We don't want to crash the app during development
|
||||
}
|
||||
}
|
||||
},
|
||||
[property, viewModelInstance]
|
||||
);
|
||||
|
||||
const operations = useMemo(
|
||||
() => optionsRef.current.buildPropertyOperations(safePropertyAccess),
|
||||
[safePropertyAccess]
|
||||
);
|
||||
|
||||
const result = {
|
||||
value,
|
||||
...operations
|
||||
} as R & { value: V | null } & (E extends undefined ? {} : { extendedData: E | null });
|
||||
|
||||
if (options.getExtendedData) {
|
||||
(result as any).extendedData = extendedData;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
38
src/hooks/useViewModelInstanceString.ts
Normal file
38
src/hooks/useViewModelInstanceString.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ViewModelInstance, ViewModelInstanceString } from '@rive-app/canvas';
|
||||
import { UseViewModelInstanceStringResult } from '../types';
|
||||
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
|
||||
|
||||
/**
|
||||
* Hook for interacting with string properties of a ViewModelInstance.
|
||||
*
|
||||
* @param params - Parameters for interacting with string properties
|
||||
* @param params.path - Path to the property (e.g. "text" or "nested/text")
|
||||
* @param params.viewModelInstance - The ViewModelInstance containing the string property
|
||||
* @returns An object with the string value and a setter function
|
||||
*/
|
||||
export default function useViewModelInstanceString(
|
||||
path: string,
|
||||
viewModelInstance?: ViewModelInstance | null
|
||||
): UseViewModelInstanceStringResult {
|
||||
|
||||
const result = useViewModelInstanceProperty<ViewModelInstanceString, string, Omit<UseViewModelInstanceStringResult, 'value'>>(
|
||||
path,
|
||||
viewModelInstance,
|
||||
{
|
||||
getProperty: useCallback((vm, p) => vm.string(p), []),
|
||||
getValue: useCallback((prop) => prop.value, []),
|
||||
defaultValue: null,
|
||||
buildPropertyOperations: useCallback((safePropertyAccess) => ({
|
||||
setValue: (newValue: string) => {
|
||||
safePropertyAccess(prop => { prop.value = newValue; });
|
||||
}
|
||||
}), [])
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
value: result.value,
|
||||
setValue: result.setValue
|
||||
};
|
||||
}
|
||||
42
src/hooks/useViewModelInstanceTrigger.ts
Normal file
42
src/hooks/useViewModelInstanceTrigger.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ViewModelInstance, ViewModelInstanceTrigger } from '@rive-app/canvas';
|
||||
import { UseViewModelInstanceTriggerParameters, UseViewModelInstanceTriggerResult } from '../types';
|
||||
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
|
||||
|
||||
/**
|
||||
* Hook for interacting with trigger properties of a ViewModelInstance.
|
||||
*
|
||||
* @param params - Parameters for interacting with trigger properties
|
||||
* @param params.path - Path to the trigger property (e.g. "onTap" or "group/onTap")
|
||||
* @param params.viewModelInstance - The ViewModelInstance containing the trigger property
|
||||
* @param params.onTrigger - Callback that runs when the trigger is fired
|
||||
* @returns An object with a trigger function
|
||||
*/
|
||||
export default function useViewModelInstanceTrigger(
|
||||
path: string,
|
||||
viewModelInstance?: ViewModelInstance | null,
|
||||
params?: UseViewModelInstanceTriggerParameters
|
||||
): UseViewModelInstanceTriggerResult {
|
||||
const { onTrigger } = params ?? {};
|
||||
|
||||
const { trigger } = useViewModelInstanceProperty<ViewModelInstanceTrigger, undefined, UseViewModelInstanceTriggerResult>(
|
||||
path,
|
||||
viewModelInstance,
|
||||
{
|
||||
getProperty: useCallback((vm, p) => vm.trigger(p), []),
|
||||
getValue: useCallback(() => undefined, []), // Triggers don't have a 'value'
|
||||
defaultValue: null,
|
||||
onPropertyEvent: onTrigger,
|
||||
buildPropertyOperations: useCallback((safePropertyAccess) => ({
|
||||
trigger: () => {
|
||||
|
||||
safePropertyAccess(prop => {
|
||||
prop.trigger();
|
||||
});
|
||||
}
|
||||
}), [])
|
||||
}
|
||||
);
|
||||
|
||||
return { trigger };
|
||||
}
|
||||
39
src/index.ts
39
src/index.ts
@@ -1,10 +1,43 @@
|
||||
import Rive, { RiveProps } from './components/Rive';
|
||||
import useRive from './hooks/useRive';
|
||||
import useStateMachineInput from './hooks/useStateMachineInput';
|
||||
import useViewModel from './hooks/useViewModel';
|
||||
import useViewModelInstance from './hooks/useViewModelInstance';
|
||||
import useViewModelInstanceNumber from './hooks/useViewModelInstanceNumber';
|
||||
import useViewModelInstanceString from './hooks/useViewModelInstanceString';
|
||||
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 { useRive, useStateMachineInput, useResizeCanvas, useRiveFile , RiveProps };
|
||||
export { RiveState, UseRiveParameters, UseRiveFileParameters, UseRiveOptions } from './types';
|
||||
export * from '@rive-app/canvas';
|
||||
export {
|
||||
useRive,
|
||||
useStateMachineInput,
|
||||
useResizeCanvas,
|
||||
useRiveFile,
|
||||
useViewModel,
|
||||
useViewModelInstance,
|
||||
useViewModelInstanceNumber,
|
||||
useViewModelInstanceString,
|
||||
useViewModelInstanceBoolean,
|
||||
useViewModelInstanceColor,
|
||||
useViewModelInstanceEnum,
|
||||
useViewModelInstanceTrigger,
|
||||
useViewModelInstanceImage,
|
||||
useViewModelInstanceList,
|
||||
useViewModelInstanceArtboard,
|
||||
RiveProps,
|
||||
};
|
||||
export {
|
||||
RiveState,
|
||||
UseRiveParameters,
|
||||
UseRiveFileParameters,
|
||||
UseRiveOptions,
|
||||
} from './types';
|
||||
export * from '@rive-app/canvas';
|
||||
193
src/types.ts
193
src/types.ts
@@ -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;
|
||||
@@ -57,3 +62,189 @@ export type RiveFileState = {
|
||||
riveFile: RiveFile | null;
|
||||
status: FileStatus;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parameters for useViewModel hook.
|
||||
*
|
||||
* @property name - When provided, specifies the name of the ViewModel to retrieve.
|
||||
* @property useDefault - When true, uses the default ViewModel from the Rive instance.
|
||||
*/
|
||||
export type UseViewModelParameters =
|
||||
| { name: string; useDefault?: never }
|
||||
| { useDefault?: boolean; name?: never };
|
||||
|
||||
/**
|
||||
* Parameters for useViewModelInstance hook.
|
||||
*
|
||||
* @property name - When provided, specifies the name of the instance to retrieve.
|
||||
* @property useDefault - When true, uses the default instance from the ViewModel.
|
||||
* @property useNew - When true, creates a new instance of the ViewModel.
|
||||
* @property rive - If provided, automatically binds the instance to this Rive instance.
|
||||
*/
|
||||
export type UseViewModelInstanceParameters =
|
||||
| { name: string; useDefault?: never; useNew?: never; rive?: Rive | null }
|
||||
| { useDefault?: boolean; name?: never; useNew?: never; rive?: Rive | null }
|
||||
| { useNew?: boolean; name?: never; useDefault?: never; rive?: Rive | null };
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parameters for interacting with trigger properties of a ViewModelInstance
|
||||
* @property onTrigger - Callback that runs when the trigger fires
|
||||
*/
|
||||
export type UseViewModelInstanceTriggerParameters = {
|
||||
onTrigger?: () => void;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export type UseViewModelInstanceNumberResult = {
|
||||
/**
|
||||
* The current value of the number.
|
||||
*/
|
||||
value: number | null;
|
||||
/**
|
||||
* Set the value of the number.
|
||||
* @param value - The value to set the number to.
|
||||
*/
|
||||
setValue: (value: number) => void;
|
||||
};
|
||||
export type UseViewModelInstanceStringResult = {
|
||||
/**
|
||||
* The current value of the string.
|
||||
*/
|
||||
value: string | null;
|
||||
/**
|
||||
* Set the value of the string.
|
||||
* @param value - The value to set the string to.
|
||||
*/
|
||||
setValue: (value: string) => void;
|
||||
};
|
||||
export type UseViewModelInstanceBooleanResult = {
|
||||
/**
|
||||
* The current value of the boolean.
|
||||
*/
|
||||
value: boolean | null;
|
||||
/**
|
||||
* Set the value of the boolean.
|
||||
* @param value - The value to set the boolean to.
|
||||
*/
|
||||
setValue: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export type UseViewModelInstanceColorResult = {
|
||||
/**
|
||||
* The current value of the color.
|
||||
*/
|
||||
value: number | null;
|
||||
/**
|
||||
* Set the value of the color.
|
||||
* @param value - The value to set the color to.
|
||||
*/
|
||||
setValue: (value: number) => void;
|
||||
/**
|
||||
* Set the red value of the color.
|
||||
* @param r - The red value to set the color to.
|
||||
*/
|
||||
setRgb: (r: number, g: number, b: number) => void;
|
||||
/**
|
||||
* Set the red, green, blue, and alpha values of the color.
|
||||
* @param r - The red value to set the color to.
|
||||
* @param g - The green value to set the color to.
|
||||
* @param b - The blue value to set the color to.
|
||||
* @param a - The alpha value to set the color to.
|
||||
*/
|
||||
setRgba: (r: number, g: number, b: number, a: number) => void;
|
||||
/**
|
||||
* Set the alpha value of the color.
|
||||
* @param a - The alpha value to set the color to.
|
||||
*/
|
||||
setAlpha: (a: number) => void;
|
||||
/**
|
||||
* Set the opacity value of the color.
|
||||
* @param o - The opacity value to set the color to.
|
||||
*/
|
||||
setOpacity: (o: number) => void;
|
||||
};
|
||||
|
||||
export type UseViewModelInstanceEnumResult = {
|
||||
/**
|
||||
* The current value of the enum.
|
||||
*/
|
||||
value: string | null;
|
||||
/**
|
||||
* Set the value of the enum.
|
||||
* @param value - The value to set the enum to.
|
||||
*/
|
||||
setValue: (value: string) => void;
|
||||
/**
|
||||
* The values of the enum.
|
||||
*/
|
||||
values: string[];
|
||||
};
|
||||
|
||||
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