mirror of
https://github.com/rive-app/rive-react.git
synced 2026-03-13 08:22:30 +08:00
Compare commits
544 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1055c2907 | ||
|
|
3c1459beb3 | ||
|
|
3c59c7a667 | ||
|
|
2dceda58bf | ||
|
|
1af00a38c7 | ||
|
|
6ab0496025 | ||
|
|
37ced47323 | ||
|
|
79f1a06df0 | ||
|
|
f0d4c3e6d2 | ||
|
|
0d3300eca4 | ||
|
|
7f6ab5ad10 | ||
|
|
5df93323ed | ||
|
|
e6bfec7c51 | ||
|
|
770858fb00 | ||
|
|
5145a6edb3 | ||
|
|
f74cfbc3d0 | ||
|
|
925de2fc94 | ||
|
|
c7d1d92b9e | ||
|
|
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 | ||
|
|
ff3976d9fe | ||
|
|
f2a6860292 | ||
|
|
8213c6ed7e | ||
|
|
a9a98fece2 | ||
|
|
4052b7492b | ||
|
|
8bb6f1dc58 | ||
|
|
6d6b68e868 | ||
|
|
d3305f8afe | ||
|
|
9eb292c2c5 | ||
|
|
7277ed2f0d | ||
|
|
5354d1f69b | ||
|
|
86e67d5f11 | ||
|
|
e5c751aa54 | ||
|
|
60b1dda98d | ||
|
|
4914770c70 | ||
|
|
21e457bda4 | ||
|
|
cc0e91689b | ||
|
|
8244982000 | ||
|
|
439b413782 | ||
|
|
9ab4e378fc | ||
|
|
9061a93978 | ||
|
|
88d1bc000c | ||
|
|
9b1d3825f8 | ||
|
|
277f4d7db0 | ||
|
|
e68ca963a9 | ||
|
|
d820cdf6f1 | ||
|
|
c9a4dc0b5c | ||
|
|
19b58f5eb6 | ||
|
|
6263449898 | ||
|
|
630f70a81d | ||
|
|
bbd0d08fed | ||
|
|
ef23793d26 | ||
|
|
ca10584ed4 | ||
|
|
6fe765df84 | ||
|
|
c62b80af09 | ||
|
|
777337c481 | ||
|
|
da4b5978e7 | ||
|
|
09c0d1f947 | ||
|
|
0ca5933c41 | ||
|
|
f272fd92f0 | ||
|
|
192a562764 | ||
|
|
224a768737 | ||
|
|
c085bd5ea3 | ||
|
|
bb13c71982 | ||
|
|
779e06583c | ||
|
|
e172e0f210 | ||
|
|
abbcf14b70 | ||
|
|
a0885648ce | ||
|
|
9f768bdd0c | ||
|
|
94c9a2a60b | ||
|
|
dfe0d10936 | ||
|
|
d27eb7254d | ||
|
|
c0b27bd018 | ||
|
|
739ac7c370 | ||
|
|
b1b8040b4f | ||
|
|
1f6ae83de3 | ||
|
|
e5ff233c34 | ||
|
|
64795564d8 | ||
|
|
f0b32056a4 | ||
|
|
b5dc2fa8da | ||
|
|
0108d425dd | ||
|
|
74a27143b4 | ||
|
|
2cfb8a4955 | ||
|
|
af6c3f84c6 | ||
|
|
351d9966b1 | ||
|
|
ba73cb7053 | ||
|
|
4a4f8a7151 | ||
|
|
b5baef5ce7 | ||
|
|
79f80d39cd | ||
|
|
e1ed8250e3 | ||
|
|
a40ba0dacc | ||
|
|
7ea9c8fbcb | ||
|
|
d4cef9a131 | ||
|
|
99489af287 | ||
|
|
d881bb18c8 | ||
|
|
2e461c66b3 | ||
|
|
bb078296d0 | ||
|
|
f9bad8b62d | ||
|
|
e42f5e32d7 | ||
|
|
83fa6635ec | ||
|
|
bdd4c84d8b | ||
|
|
414543d37d | ||
|
|
119f7eea2a | ||
|
|
92b0342882 | ||
|
|
38b0a1cc86 | ||
|
|
2fd87e45e8 | ||
|
|
dc99fe27cc | ||
|
|
2358c82626 | ||
|
|
0475d47535 | ||
|
|
f51e538479 | ||
|
|
29fa6e76fb | ||
|
|
2459c489bf | ||
|
|
927dbc6b32 | ||
|
|
8ebbfb164f | ||
|
|
388fd16b5b | ||
|
|
8c87bc0ab7 | ||
|
|
280b2ec4a4 | ||
|
|
55a40c7d9f | ||
|
|
d4e6243dbb | ||
|
|
dc738d00a6 | ||
|
|
2ae22d6c30 | ||
|
|
4a3fc22261 | ||
|
|
bdc734b90b | ||
|
|
eeda717178 | ||
|
|
178f04df61 | ||
|
|
ef11079784 | ||
|
|
11f548b21c | ||
|
|
d0ff23c124 | ||
|
|
a83f5a275a | ||
|
|
b437230063 | ||
|
|
10912052f2 | ||
|
|
fe8265cd58 | ||
|
|
5b458b76e5 | ||
|
|
1decbc7d85 | ||
|
|
87627125f5 | ||
|
|
e823e9519f | ||
|
|
ac88187118 | ||
|
|
1897e823b9 | ||
|
|
1a2e57a82c | ||
|
|
0d16feeec1 | ||
|
|
ca48907649 | ||
|
|
9b02c0f1bc | ||
|
|
b25c775994 | ||
|
|
17811adac4 | ||
|
|
e4fcef529e | ||
|
|
f51fe6ee2a | ||
|
|
0f558e66e1 | ||
|
|
7a67fb14f7 | ||
|
|
da3fc317ff | ||
|
|
9b40a1a02e | ||
|
|
aa92c01329 | ||
|
|
99a8a42a15 | ||
|
|
f0ea7add89 | ||
|
|
ac9f322ccb | ||
|
|
e1c2d108e9 | ||
|
|
3aaee0bcf8 | ||
|
|
e3739f1a1f | ||
|
|
0e0a2bd972 | ||
|
|
5c47a411f2 | ||
|
|
16dc257b2f | ||
|
|
4678ea9ecf | ||
|
|
d58963e29d | ||
|
|
c32433284a | ||
|
|
8bb5652df0 | ||
|
|
9ecacf37fe | ||
|
|
f837fbe0d4 | ||
|
|
8e181d6ae2 | ||
|
|
a3a41dca40 | ||
|
|
b78c9715d7 | ||
|
|
520e5bb51d | ||
|
|
586ee4c002 | ||
|
|
3d6c7ed499 | ||
|
|
c3900b7845 | ||
|
|
d369817113 | ||
|
|
d68302ccb3 | ||
|
|
bbd4cc7af6 | ||
|
|
3936277f65 | ||
|
|
aa2a783d1c | ||
|
|
d8d7d64749 | ||
|
|
af3edad2c2 | ||
|
|
5326f800f7 | ||
|
|
b44f9ad9e1 | ||
|
|
b2495300b7 | ||
|
|
eb436263d7 | ||
|
|
0e6385288e | ||
|
|
78f75434fc | ||
|
|
a9c2950419 | ||
|
|
e799f64554 | ||
|
|
95a1daa4ef | ||
|
|
8a5b88c591 | ||
|
|
d3b29cf7d7 | ||
|
|
611522b3e0 | ||
|
|
89d35976d4 | ||
|
|
a7875b26a4 | ||
|
|
4653b8bea1 | ||
|
|
0d0688feb6 | ||
|
|
7a97c14c93 | ||
|
|
cff787524d | ||
|
|
a34a588ee5 | ||
|
|
aa89f2a1ba | ||
|
|
39d275b3e0 | ||
|
|
e22acf98ba | ||
|
|
2e52e35ddc | ||
|
|
f67cb244d2 | ||
|
|
c2319b8dea | ||
|
|
877e43e354 | ||
|
|
6a033a99d3 | ||
|
|
88f7558ddf | ||
|
|
cfe3a298fb | ||
|
|
8e24185f3f | ||
|
|
5fad06d4bd | ||
|
|
df14bc383b | ||
|
|
b8dc1e5371 | ||
|
|
1196dd4c4e | ||
|
|
06ceb3cdb0 | ||
|
|
acaa12f7fc | ||
|
|
0acf384570 | ||
|
|
97a29acfdc | ||
|
|
5eb5ccfdaa | ||
|
|
edcc2c1320 | ||
|
|
68d8ca44fa | ||
|
|
ee9b83d764 | ||
|
|
187066c2cb | ||
|
|
427d74d14f | ||
|
|
061377432e | ||
|
|
53d7548b15 | ||
|
|
e1a4ca0058 | ||
|
|
83ebd6776d | ||
|
|
75dac6fe64 | ||
|
|
e13c023933 | ||
|
|
95183ff8d5 | ||
|
|
64416e0b4d | ||
|
|
1c6837bf92 | ||
|
|
15330ababe | ||
|
|
af70b8150d | ||
|
|
a984e007f4 | ||
|
|
04d8e01f87 | ||
|
|
44e46c5dee | ||
|
|
323e92e636 | ||
|
|
a07fa02ed6 | ||
|
|
d710e1e4b2 | ||
|
|
d35e9b2805 | ||
|
|
cf44463805 | ||
|
|
f59f44692a | ||
|
|
6ba68fab9e | ||
|
|
982addf163 | ||
|
|
dfd89c7691 | ||
|
|
0247a46c47 | ||
|
|
44626c7450 | ||
|
|
c0eb7d689d | ||
|
|
ff7a6aa676 | ||
|
|
dc89439d84 | ||
|
|
7ebc10a4da | ||
|
|
e8e1a5bc08 | ||
|
|
1bb8885f22 | ||
|
|
00d13badcf | ||
|
|
b9ab7b83d2 | ||
|
|
e0f00174c8 | ||
|
|
200a86a6ba | ||
|
|
26622596c8 | ||
|
|
ac789f0855 | ||
|
|
6ecdc05aea | ||
|
|
f8aa50fa49 | ||
|
|
b59bfdef84 | ||
|
|
948ded25f7 | ||
|
|
27f7f2b250 | ||
|
|
184ba2e110 | ||
|
|
f6d8db323b | ||
|
|
301e58f390 | ||
|
|
4cce3067a7 | ||
|
|
4496eff598 | ||
|
|
5e98586354 | ||
|
|
f19295f9a5 | ||
|
|
70546308b4 | ||
|
|
62a425f30e | ||
|
|
4a554dd1ad | ||
|
|
3e6262f21f | ||
|
|
e57a99b2d1 | ||
|
|
8a968ec266 | ||
|
|
b5f00e5c97 | ||
|
|
22e6dd3494 | ||
|
|
08b9f9a2aa | ||
|
|
a24b910096 | ||
|
|
33760042d1 | ||
|
|
2c82fa04e7 | ||
|
|
2b249494d5 | ||
|
|
b56c17d48c | ||
|
|
a6fe08ced9 | ||
|
|
cd1e1410f4 | ||
|
|
353fbf8e9d | ||
|
|
ae05ad4375 | ||
|
|
ff8bbb084c | ||
|
|
89a6802fa0 | ||
|
|
91025e6772 | ||
|
|
d3bc913bcf | ||
|
|
ce56321e1d | ||
|
|
4b6f5410fd | ||
|
|
6544874d3d | ||
|
|
ed90c7f7c9 | ||
|
|
e06fdd1c8b | ||
|
|
b8ffb6b53c | ||
|
|
2dc925ef70 | ||
|
|
639de79c9e | ||
|
|
a9961c821a | ||
|
|
fc082d1a03 | ||
|
|
87fa1ae2a5 | ||
|
|
62490a50f9 | ||
|
|
fcc1a16df4 | ||
|
|
29e0ceb797 | ||
|
|
3e6a951ca1 | ||
|
|
04910f78f1 | ||
|
|
c2977c705c | ||
|
|
4e2137422b | ||
|
|
0696417926 | ||
|
|
f11a433365 | ||
|
|
48a4726d1f | ||
|
|
c927f24b5a | ||
|
|
3cbbf99fec | ||
|
|
03f05f57b7 | ||
|
|
8b483b807b | ||
|
|
816fcfcdbc | ||
|
|
6c28a8795e | ||
|
|
6a57630ae6 | ||
|
|
acba24c4ed | ||
|
|
1e6eb5ec92 | ||
|
|
6871a81ebd | ||
|
|
33053b3b7f | ||
|
|
4040083d5a | ||
|
|
2153b81e6b | ||
|
|
6b1500e681 | ||
|
|
503702cf1f | ||
|
|
7be20b0a87 | ||
|
|
78491f5819 | ||
|
|
39edb88a19 | ||
|
|
fd1a1653b1 | ||
|
|
8d7f0ab28c | ||
|
|
2a2e532564 | ||
|
|
2b1aa01a87 | ||
|
|
06c4e2aea3 | ||
|
|
819bd51ea9 | ||
|
|
6b7f113296 | ||
|
|
a62e9b3a9a | ||
|
|
04685c0bcd | ||
|
|
48fd9f9d80 | ||
|
|
3c578b730f | ||
|
|
7a46886133 | ||
|
|
092049d20f | ||
|
|
f7aced03cd | ||
|
|
eb07281415 | ||
|
|
156b3bdfb5 | ||
|
|
24d8e0a907 | ||
|
|
a1a155849a | ||
|
|
59e67cec3d | ||
|
|
84b18cc3dd | ||
|
|
1092b44947 | ||
|
|
efe28aa5f3 | ||
|
|
16d836c959 | ||
|
|
838ed1abf8 | ||
|
|
d010a55cc0 | ||
|
|
fd1c00a995 | ||
|
|
45aec2db1c | ||
|
|
62b3a1d8dc | ||
|
|
5be9d2f874 | ||
|
|
49a6b1de11 | ||
|
|
ec61a6835d | ||
|
|
ac29fa30a7 | ||
|
|
e966316971 | ||
|
|
ae6efc14d4 | ||
|
|
8ce77f153c | ||
|
|
08638359bb | ||
|
|
7dbade4589 | ||
|
|
8175c4a4d4 | ||
|
|
795ee53340 | ||
|
|
04353db432 | ||
|
|
1f0e511f6e | ||
|
|
7aee5cfab4 | ||
|
|
3477afdef1 | ||
|
|
8b43a82c5f | ||
|
|
bd49e6a4ee | ||
|
|
1dbb9cd38d | ||
|
|
aee7407f79 | ||
|
|
b48de9db84 | ||
|
|
6772f166b7 | ||
|
|
3a0a959383 | ||
|
|
314d7c19cc | ||
|
|
802648eda8 | ||
|
|
5ad5a957a6 | ||
|
|
414d6f895a | ||
|
|
38625a00c3 | ||
|
|
bad688dfa3 | ||
|
|
ec230faa73 | ||
|
|
90c6d1edb1 | ||
|
|
0480dc92c8 | ||
|
|
de24fa5641 | ||
|
|
1a49e89c33 | ||
|
|
805afd5dff | ||
|
|
84d9730767 | ||
|
|
9abee34d12 | ||
|
|
12801b10cc | ||
|
|
da11387558 | ||
|
|
3902948a2e | ||
|
|
21a17ed40e | ||
|
|
f0e70924ec | ||
|
|
f4eccbe2ce | ||
|
|
83c81b49c5 | ||
|
|
5de40fad5b | ||
|
|
aab811b975 | ||
|
|
965507ac34 | ||
|
|
74b791ffa4 | ||
|
|
2853851d52 | ||
|
|
4f37bbdb76 | ||
|
|
39f1990eac | ||
|
|
bb7d3e4e6c | ||
|
|
7f2bd76eb3 | ||
|
|
5c0b9cd613 | ||
|
|
92dd811948 | ||
|
|
4429be44f2 | ||
|
|
9398e0d74e | ||
|
|
563dca3608 | ||
|
|
1da73aac05 | ||
|
|
5c4336b84f | ||
|
|
6e72ed5271 | ||
|
|
464fa97c20 | ||
|
|
ac01b7ec69 | ||
|
|
cd9baef250 | ||
|
|
3a2ed32856 | ||
|
|
d35f5b53c5 | ||
|
|
73b57d0b81 | ||
|
|
0a18b522cc | ||
|
|
5e34e13563 | ||
|
|
c3f138014d | ||
|
|
b13413ba8b | ||
|
|
881e38fe6a | ||
|
|
38c85624a7 | ||
|
|
a9732e141e | ||
|
|
59edf6f67e | ||
|
|
4be496cc15 | ||
|
|
4c9de18b1c | ||
|
|
c688b9f17a | ||
|
|
fcfe3cf6cf | ||
|
|
8ce856270f | ||
|
|
1e1cabd057 | ||
|
|
0be3bc7e93 | ||
|
|
48af4b00ad | ||
|
|
fe44977e47 | ||
|
|
24d9e871d6 | ||
|
|
8b8d6bc44b | ||
|
|
956b76d01c | ||
|
|
2517efaebd | ||
|
|
019cadf5f2 | ||
|
|
e599b1a38c | ||
|
|
78d3118dcc | ||
|
|
536c8325f2 | ||
|
|
9e65654274 | ||
|
|
75420d2f1d | ||
|
|
e6b036ee17 | ||
|
|
bfbb8a59ee | ||
|
|
5a19354ab3 | ||
|
|
c4dc027522 | ||
|
|
e54a0d42d3 | ||
|
|
1a597943cb | ||
|
|
026d097658 | ||
|
|
a4777ec7e3 | ||
|
|
416334ef44 | ||
|
|
f309481a43 | ||
|
|
06500e6dcf | ||
|
|
33e34a46e1 | ||
|
|
c8f57f5896 | ||
|
|
5d0822836c | ||
|
|
1c3cc69f40 | ||
|
|
44056eb565 | ||
|
|
a9c3233452 | ||
|
|
ce2c7065f5 | ||
|
|
203faf9ce2 | ||
|
|
fd0efb1b81 | ||
|
|
11b6f2e879 | ||
|
|
8a1adf147d |
22
.eslintrc.js
22
.eslintrc.js
@@ -3,7 +3,11 @@ module.exports = {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: ['plugin:react/recommended', 'prettier'],
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
'prettier',
|
||||
'plugin:storybook/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
@@ -12,11 +16,23 @@ module.exports = {
|
||||
ecmaVersion: 12,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'prettier'],
|
||||
plugins: ['@typescript-eslint', 'prettier', 'react-hooks'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'prefer-const': ['warn', { destructuring: 'all' }],
|
||||
'prefer-const': [
|
||||
'warn',
|
||||
{
|
||||
destructuring: 'all',
|
||||
},
|
||||
],
|
||||
'no-var': 'error',
|
||||
eqeqeq: ['error', 'smart'],
|
||||
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
|
||||
'react-hooks/exhaustive-deps': 'off', // Checks effect dependencies
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
26
.github/workflows/add_to_project.yml
vendored
Normal file
26
.github/workflows/add_to_project.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Adds all new issues to project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.5.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/rive-app/projects/12/views/1
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_ACTION }}
|
||||
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ["triage"]
|
||||
})
|
||||
59
.github/workflows/publish.yml
vendored
59
.github/workflows/publish.yml
vendored
@@ -1,35 +1,52 @@
|
||||
name: Publish to NPM
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
major:
|
||||
description: 'Major'
|
||||
type: boolean
|
||||
default: false
|
||||
minor:
|
||||
description: 'Minor'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
merge_job:
|
||||
publish_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
node-version: '12.x'
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT_GITHUB }}
|
||||
- name: Setup Git config
|
||||
run: |
|
||||
git config --local user.email 'hello@rive.app'
|
||||
git config --local user.name ${{ github.actor }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Upgrade npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
- name: Install Modules
|
||||
run: npm ci
|
||||
run: npm install
|
||||
- name: Run type check
|
||||
run: npm run types:check
|
||||
- name: Run Linter
|
||||
run: npm run lint
|
||||
- name: Run Tests
|
||||
run: npm test
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Git config
|
||||
run: |
|
||||
git config --local user.email 'hello@rive.app'
|
||||
git config --local user.name ${{ github.actor }}
|
||||
- name: Authenticate with registry
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
run: npm run release
|
||||
- if: ${{ inputs.major == true }}
|
||||
name: Major Release - Bump version number, update changelog, push and tag
|
||||
run: npm run release:major
|
||||
- if: ${{inputs.major == false && inputs.minor == true}}
|
||||
name: Minor release - Bump version number, update changelog, push and tag
|
||||
run: npm run release:minor
|
||||
- if: ${{inputs.major == false && inputs.minor == false}}
|
||||
name: Patch release - Bump version number, update changelog, push and tag
|
||||
run: npm run release:patch
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Modules
|
||||
run: npm ci
|
||||
run: npm install
|
||||
- name: Run type check
|
||||
run: npm run types:check
|
||||
- name: Run Linter
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ dist
|
||||
.idea
|
||||
.vscode
|
||||
examples/**/package-lock.json
|
||||
package-lock.json
|
||||
@@ -1,17 +1,25 @@
|
||||
{
|
||||
"git": {
|
||||
"addUntrackedFiles": true,
|
||||
"requireCleanWorkingDir": false,
|
||||
"commitMessage": "chore: release ${version}",
|
||||
"tagName": "v${version}",
|
||||
"changelog": "npx auto-changelog --stdout --commit-limit false --unreleased --template https://raw.githubusercontent.com/release-it/release-it/master/templates/changelog-compact.hbs"
|
||||
},
|
||||
"npm": {
|
||||
"publish": true
|
||||
"publish": false
|
||||
},
|
||||
"github": {
|
||||
"release": true,
|
||||
"releaseName": "${version}"
|
||||
},
|
||||
"hooks": {
|
||||
"after:bump": "npx auto-changelog -p"
|
||||
"after:version:bump": [
|
||||
"npm run build",
|
||||
"npm run setup-builds",
|
||||
"npm run setup-packages",
|
||||
"npx auto-changelog -p",
|
||||
"npm run publish:all"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1604
CHANGELOG.md
1604
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
78
CONTRIBUTING.md
Normal file
78
CONTRIBUTING.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Contributing
|
||||
|
||||
We love contributions! If you want to run the project locally to test out changes, run the examples, or just see how things work under the hood, read on below.
|
||||
|
||||
## Local development
|
||||
|
||||
This runtime consumes specific tied-down versions of the [JS/WASM runtime](https://github.com/rive-app/rive-wasm) to have better control over changes that occur in that downstream runtime.
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the project down
|
||||
2. Run `npm i` in the shell/terminal at the base of the project to install the dependencies needed for the project
|
||||
|
||||
### Local dev server
|
||||
|
||||
To start the local dev server to reflect any changes made to the core `src/` files, run the following in a terminal tab:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Running the example storybook locally
|
||||
|
||||
We use Storybook to deploy our examples out onto a public-facing page for folks to view and see code examples for. It also serves as the place we'll include any example suites. These story files are stored in `/examples`
|
||||
|
||||
To run Storybook, run the following command in the terminal:
|
||||
|
||||
```
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
To see changes made to the Rive React runtime reflected in your storyook, run the following command in a separate terminal window:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
We also have a suite of unit tests against the high-level component and various hooks exported in the `test/` folder. When adding new components, changing the API, or underlying functionality, make sure to add a test here!
|
||||
|
||||
To run the test suite:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
## Making changes
|
||||
|
||||
When you're ready to make changes, push up to a feature branch off of the `main` branch. Create a pull request to this repository in Github. When creating commit messages, please be as descriptive as possible to the changes being made.
|
||||
|
||||
For example, if the change is simply a bug fix or patch change:
|
||||
|
||||
```
|
||||
git commit -m "Fix: Fixing a return type from useRive"
|
||||
```
|
||||
|
||||
Or if it's simply a docs change:
|
||||
|
||||
```
|
||||
git commit -m "Docs: Adding a new link for another example page"
|
||||
```
|
||||
|
||||
For minor/major version releases, also ensure you preface commit messages with:
|
||||
|
||||
```
|
||||
git commit -m "Major: Restructuring the useRive API with new parameters"
|
||||
```
|
||||
|
||||
These messages help make the changelog clear as to what changes are made for future devs to see.
|
||||
|
||||
When pull requests are merged, the runtime will automatically deploy the next release version. By default, patch versions are published. If you want to set the next version as a minor/major version to be released, you have to manually update the `package.json` file at the root of the project to the verison you want it to.
|
||||
|
||||
You can find the deploy scripts in `.github/`
|
||||
|
||||
## Bumping the underlying JS/WASM runtime
|
||||
|
||||
Many times, fixes to the runtime and feature adds come from the underlying JS/WASM runtime. In these cases, just bump the `@rive-app/canvas` and `@rive-app/webgl` versions to the verison you need to incorporate the fix/feature. Run `npm i` and test out the change locally against the Storybook examples and run the test suite to make sure nothing breaks, and then submit a PR with just the `package.json` change if that's all that's needed.
|
||||
173
README.md
173
README.md
@@ -4,141 +4,80 @@
|
||||
|
||||
# Rive React
|
||||
|
||||
React Runtime for [Rive](https://rive.app).
|
||||

|
||||
|
||||
A wrapper around [Rive.js](https://github.com/rive-app/rive-wasm), providing full control over the js runtime while making it super simple to use in React applications.
|
||||
[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.
|
||||
|
||||
Detailed runtime documentation can be found in [Rive's help center](https://help.rive.app/runtimes).
|
||||
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.
|
||||
|
||||
## Create and ship interactive animations to any platform
|
||||
For more information, check out the following resources:
|
||||
|
||||
[Rive](https://rive.app) is a real-time interactive design and animation tool. Use our collaborative editor to create motion graphics that respond to different states and user inputs. Then load your animations into apps, games, and websites with our lightweight open-source runtimes.
|
||||
- [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/)
|
||||
|
||||
## Installation
|
||||
## Table of contents
|
||||
|
||||
```
|
||||
npm i --save rive-react
|
||||
```
|
||||
- :star: [Rive Overview](#rive-overview)
|
||||
- 🚀 [Getting Started & API docs](#getting-started)
|
||||
- :mag: [Supported Versions](#supported-versions)
|
||||
- :books: [Examples](#examples)
|
||||
- :runner: [Migration Guides](#migration-guides)
|
||||
- 👨💻 [Contributing](#contributing)
|
||||
- :question: [Issues](#issues)
|
||||
|
||||
_Note: This library is using React hooks so the minimum version required for both react and react-dom is 16.8.0._
|
||||
## Rive overview
|
||||
|
||||
## Usage
|
||||
[Rive](https://rive.app) is a real-time interactive design and animation tool that helps teams create and run interactive animations anywhere. Designers and developers use our collaborative editor to create motion graphics that respond to different states and user inputs. Our lightweight open-source runtime libraries allow them to load their animations into apps, games, and websites.
|
||||
|
||||
### Component
|
||||
:house_with_garden: [Homepage](https://rive.app/)
|
||||
|
||||
Rive React provides a basic component as it's default import for displaying simple animations.
|
||||
:blue_book: [General help docs](https://rive.app/community/doc/)
|
||||
|
||||
```js
|
||||
import Rive from 'rive-react';
|
||||
🛠 [Rive Forums](https://rive.app/community/forums/home)
|
||||
|
||||
function Example() {
|
||||
return <Rive src="loader.riv" />;
|
||||
}
|
||||
## Getting started
|
||||
|
||||
export default Example;
|
||||
```
|
||||
Follow along with the link below for a quick start in getting Rive React integrated into your React apps.
|
||||
|
||||
#### Props
|
||||
- [Getting Started with Rive in React](https://rive.app/community/doc/react/docRfaSQ0eaE)
|
||||
- [API documentation](https://rive.app/community/doc/parameters-and-return-values/docJlDMNulDh)
|
||||
|
||||
- `src`: File path or URL to the .riv file to display.
|
||||
- `artboard`: _(optional)_ Name to display.
|
||||
- `animations`: _(optional)_ Name or list of names of animtions to play.
|
||||
- `layout`: _(optional)_ Layout object to define how animations are displayed on the canvas. See [Rive.js](https://github.com/rive-app/rive-wasm#layout) for more details.
|
||||
- _All attributes and eventHandlers that can be passed to a `div` element can also be passed to the `Rive` component and used in the same manner._
|
||||
For more information, see the Runtime sections of the Rive help documentation:
|
||||
|
||||
### useRive Hook
|
||||
- [Animation Playback](https://rive.app/community/doc/animation-playback/docDKKxsr7ko)
|
||||
- [Layout](https://rive.app/community/doc/layout/docBl81zd1GB)
|
||||
- [State Machines](https://rive.app/community/doc/state-machines/docxeznG7iiK)
|
||||
- [Rive Text](https://rive.app/community/doc/text/docn2E6y1lXo)
|
||||
- [Rive Events](https://rive.app/community/doc/rive-events/docbOnaeffgr)
|
||||
- [Loading Assets](https://rive.app/community/doc/loading-assets/doct4wVHGPgC)
|
||||
|
||||
For more advanced usage, the `useRive` hook is provided. The hook will return a component and a [Rive.js](https://github.com/rive-app/rive-wasm) `Rive` object which gives you control of the current rive file.
|
||||
## Supported versions
|
||||
|
||||
```js
|
||||
import { useRive } from 'rive-react';
|
||||
|
||||
function Example() {
|
||||
const params = {
|
||||
src: 'loader.riv',
|
||||
autoplay: false,
|
||||
};
|
||||
const { RiveComponent, rive } = useRive(params);
|
||||
|
||||
return (
|
||||
<RiveComponent
|
||||
onMouseEnter={() => rive && rive.play()}
|
||||
onMouseLeave={() => rive && rive.pause()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Example;
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `riveParams`: Set of parameters that are passed to the Rive.js `Rive` class constructor. `null` and `undefined` can be passed to conditionally display the .rive file.
|
||||
- `opts`: Rive React specific options.
|
||||
|
||||
#### Return Values
|
||||
|
||||
- `RiveComponent`: A Component that can be used to display your .riv file. This component accepts the same attributes and event handlers as a `div` element.
|
||||
- `rive`: A Rive.js `Rive` object. This will return as null until the .riv file has fully loaded.
|
||||
- `canvas`: HTMLCanvasElement object, on which the .riv file is rendering.
|
||||
- `setCanvasRef`: A callback ref that can be passed to your own canvas element, if you wish to have control over the rendering of the Canvas element.
|
||||
- `setContainerRef`: A callback ref that can be passed to a container element that wraps the canvas element, if you which to have control over the rendering of the container element.
|
||||
_For the vast majority of use cases, you can just the returned `RiveComponent` and don't need to worry about `setCanvasRef` and `setContainerRef`._
|
||||
|
||||
#### riveParams
|
||||
|
||||
- `src?`: _(optional)_ File path or URL to the .riv file to use. One of `src` or `buffer` must be provided.
|
||||
- `buffer?`: _(optional)_ ArrayBuffer containing the raw bytes from a .riv file. One of `src` or `buffer` must be provided.
|
||||
- `artboard?`: _(optional)_ Name of the artboard to use.
|
||||
- `animations?`: _(optional)_ Name or list of names of animations to play.
|
||||
- `stateMachines?`: _(optional)_ Name of list of names of state machines to load.
|
||||
- `layout?`: _(optional)_ Layout object to define how animations are displayed on the canvas. See [Rive.js](https://github.com/rive-app/rive-wasm#layout) for more details.
|
||||
- `autoplay?`: _(optional)_ If `true`, the animation will automatically start playing when loaded. Defaults to false.
|
||||
- `onLoad?`: _(optional)_ Callback that get's fired when the .rive file loads .
|
||||
- `onLoadError?`: _(optional)_ Callback that get's fired when an error occurs loading the .riv file.
|
||||
- `onPlay?`: _(optional)_ Callback that get's fired when the animation starts playing.
|
||||
- `onPause?`: _(optional)_ Callback that get's fired when the animation pauses.
|
||||
- `onStop?`: _(optional)_ Callback that get's fired when the animation stops playing.
|
||||
- `onLoop?`: _(optional)_ Callback that get's fired when the animation completes a loop.
|
||||
- `onStateChange?`: _(optional)_ Callback that get's fired when a state change occurs.
|
||||
|
||||
#### opts
|
||||
|
||||
- `useDevicePixelRatio`: _(optional)_ If `true`, the hook will scale the resolution of the animation based the [devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio). Defaults to `true`. NOTE: Requires the `setContainerRef` ref callback to be passed to a element wrapping a canvas element. If you use the `RiveComponent`, then this will happen automatically.
|
||||
- `fitCanvasToArtboardHeight`: _(optional)_ If `true`, then the canvas will resize based on the height of the artboard. Defaults to `false`.
|
||||
|
||||
### useStateMachineInput Hook
|
||||
|
||||
The `useStateMachineInput` hook is provided to make it easier to interact with state machine inputs on a rive file.
|
||||
|
||||
```js
|
||||
import { useRive, useStateMachineInput } from 'rive-react';
|
||||
|
||||
function Example() {
|
||||
const { RiveComponent, rive } = useRive({
|
||||
src: 'button.riv',
|
||||
stateMachines: 'button',
|
||||
autoplay: true,
|
||||
});
|
||||
|
||||
const onClickInput = useStateMachineInput(rive, 'button', 'onClick');
|
||||
|
||||
return <RiveComponent onClick={() => onClickInput && onClickInput.fire())} />;
|
||||
}
|
||||
|
||||
export default Example;
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `rive`: A `Rive` object. This is returned by the `useRive` hook.
|
||||
- `stateMachineName`: Name of the state machine.
|
||||
- `inputName`: Name of the state machine input.
|
||||
|
||||
#### Return Value
|
||||
|
||||
A Rive.js `stateMachineInput` object.
|
||||
This library supports React versions `^16.8.0` through `^18.0.0`.
|
||||
|
||||
## Examples
|
||||
|
||||
The `examples` shows a number of different ways to use Rive React. See the instructions for each example to run locally.
|
||||
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.
|
||||
|
||||
- [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)
|
||||
|
||||
### Awesome Rive
|
||||
|
||||
For even more examples and resources on using Rive at runtime or in other tools, checkout the [awesome-rive](https://github.com/rive-app/awesome-rive) repo.
|
||||
|
||||
## Migration guides
|
||||
|
||||
Using an older version of the runtime and need to learn how to upgrade to the latest version? Check out the migration guides below in our help center that help guide you through version bumps; breaking changes and all!
|
||||
|
||||
[Migration guides](https://rive.app/community/doc/migrating-from-v3-to-v4/dociIPXVHKFF)
|
||||
|
||||
## Contributing
|
||||
|
||||
We love contributions! Check out our [contributing docs](./CONTRIBUTING.md) to get more details into how to run this project, the examples, and more all locally.
|
||||
|
||||
## Issues
|
||||
|
||||
Have an issue with using the runtime, or want to suggest a feature/API to help make your development life better? Log an issue in our [issues](https://github.com/rive-app/rive-react/issues) tab! You can also browse older issues and discussion threads there to see solutions that may have worked for common problems.
|
||||
|
||||
@@ -21,3 +21,5 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
*storybook.log
|
||||
74
examples/.storybook/main.ts
Normal file
74
examples/.storybook/main.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { StorybookConfig } from '@storybook/react-webpack5';
|
||||
import path from 'path';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/preset-create-react-app',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ['../public'],
|
||||
webpackFinal: async (config) => {
|
||||
if (!config.resolve) config.resolve = {};
|
||||
if (!config.resolve.alias) config.resolve.alias = {};
|
||||
|
||||
config.resolve.alias['react'] = path.resolve(
|
||||
__dirname,
|
||||
'../../node_modules/react'
|
||||
);
|
||||
config.resolve.alias['react-dom'] = path.resolve(
|
||||
__dirname,
|
||||
'../../node_modules/react-dom'
|
||||
);
|
||||
|
||||
config.resolve.alias['@rive-app/react-canvas'] = path.resolve(
|
||||
__dirname,
|
||||
'../../'
|
||||
);
|
||||
config.resolve.alias['@rive-app/react-canvas-lite'] = path.resolve(
|
||||
__dirname,
|
||||
'../../'
|
||||
);
|
||||
config.resolve.alias['@rive-app/react-webgl'] = path.resolve(
|
||||
__dirname,
|
||||
'../../'
|
||||
);
|
||||
config.resolve.alias['@rive-app/react-webgl2'] = path.resolve(
|
||||
__dirname,
|
||||
'../../'
|
||||
);
|
||||
|
||||
config.module?.rules?.push({
|
||||
test: /\.(ts|tsx|js|jsx)$/,
|
||||
include: [
|
||||
path.resolve(__dirname, '../src'),
|
||||
path.resolve(__dirname, '../../'),
|
||||
],
|
||||
use: {
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
presets: [
|
||||
require.resolve('@babel/preset-env'),
|
||||
require.resolve('@babel/preset-react'),
|
||||
require.resolve('@babel/preset-typescript'),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
config.watchOptions = {
|
||||
ignored: /node_modules/,
|
||||
poll: 1000,
|
||||
aggregateTimeout: 300,
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
16
examples/.storybook/preview.ts
Normal file
16
examples/.storybook/preview.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
|
||||
import './stories.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
7
examples/.storybook/stories.css
Normal file
7
examples/.storybook/stories.css
Normal file
@@ -0,0 +1,7 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#storybook-root {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
# Basic Typescript
|
||||
|
||||
This is an example app that uses the useRive hook with typescript.
|
||||
|
||||
## To Run
|
||||
|
||||
This example is created using [Create React App](https://reactjs.org/docs/create-a-new-react-app.html).
|
||||
|
||||
To install and run:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "basic-typescript",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/node": "^12.20.14",
|
||||
"@types/react": "^17.0.9",
|
||||
"@types/react-dom": "^17.0.6",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"rive-react": "0.0.1",
|
||||
"typescript": "^4.3.2",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "SKIP_PREFLIGHT_CHECK=true react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<title>React App - Basic Typescript</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -1,19 +0,0 @@
|
||||
import { useRive, UseRiveParameters } from "rive-react";
|
||||
|
||||
function App() {
|
||||
const params: UseRiveParameters = {
|
||||
src: "poison-loader.riv",
|
||||
autoplay: true,
|
||||
};
|
||||
|
||||
const { RiveComponent } = useRive(params);
|
||||
|
||||
return (
|
||||
// The animation will fit to the parent element.
|
||||
<div style={{ height: "500px", width: "500px" }}>
|
||||
<RiveComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -1,14 +0,0 @@
|
||||
# Basic with hook
|
||||
|
||||
This is a very basic example of using the useRive to autoplay a simple looping animation.
|
||||
|
||||
## To Run
|
||||
|
||||
This example is created using [Create React App](https://reactjs.org/docs/create-a-new-react-app.html).
|
||||
|
||||
To install and run:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "basic-with-hook",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"rive-react": "0.0.1",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "SKIP_PREFLIGHT_CHECK=true react-scripts start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<title>Rive React - Basic with Hook</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -1,19 +0,0 @@
|
||||
import { useRive } from "rive-react";
|
||||
|
||||
function App() {
|
||||
const params = {
|
||||
src: "poison-loader.riv",
|
||||
autoplay: true,
|
||||
};
|
||||
|
||||
const { RiveComponent } = useRive(params);
|
||||
|
||||
return (
|
||||
// The animation will fit to the parent element.
|
||||
<div style={{ height: "500px", width: "500px" }}>
|
||||
<RiveComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
@@ -1,14 +0,0 @@
|
||||
# Basic
|
||||
|
||||
This is a very basic example of using the Rive component to autoplay a simple looping animation.
|
||||
|
||||
## To Run
|
||||
|
||||
This example is created using [Create React App](https://reactjs.org/docs/create-a-new-react-app.html).
|
||||
|
||||
To install and run:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "basic-example",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"rive-react": "0.0.1",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "SKIP_PREFLIGHT_CHECK=true react-scripts start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<title>Rive React - Basic</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -1,12 +0,0 @@
|
||||
import Rive from "rive-react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
// The animation will fit to the parent element.
|
||||
<div style={{ height: "500px", width: "500px" }}>
|
||||
<Rive src="poison-loader.riv" autoplay />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
56
examples/package.json
Normal file
56
examples/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.126",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"test-storybook": "test-storybook"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"plugin:storybook/recommended"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^8.6.12",
|
||||
"@storybook/addon-interactions": "^8.6.12",
|
||||
"@storybook/addon-onboarding": "^8.6.12",
|
||||
"@storybook/blocks": "^8.6.12",
|
||||
"@storybook/preset-create-react-app": "^8.6.12",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
# Play on hover
|
||||
|
||||
This provides an example of how to play/pause a Rive animation when the mouse is hovered over it.
|
||||
|
||||
## To Run
|
||||
|
||||
This example is created using [Create React App](https://reactjs.org/docs/create-a-new-react-app.html).
|
||||
|
||||
To install and run:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "play-on-hover",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"rive-react": "0.0.1",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "SKIP_PREFLIGHT_CHECK=true react-scripts start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<title>Rive React - Play on hover</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -1,32 +0,0 @@
|
||||
import { useRive } from "rive-react";
|
||||
|
||||
function App() {
|
||||
const params = {
|
||||
src: "poison-loader.riv",
|
||||
autoplay: false,
|
||||
};
|
||||
|
||||
const { RiveComponent, rive } = useRive(params);
|
||||
|
||||
function onMouseEnter() {
|
||||
// `rive` will return as null until the file as fully loaded, so we include this
|
||||
// guard to prevent any unwanted errors.
|
||||
if (rive) {
|
||||
rive.play();
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
if (rive) {
|
||||
rive.pause();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: "600px", width: "600px" }}>
|
||||
<RiveComponent onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
@@ -1,14 +0,0 @@
|
||||
# Play/Pause Buttons
|
||||
|
||||
This example shows how we can play/pause the Rive animation on a button click and update the text of the button based on events happening in the runtime.
|
||||
|
||||
## To Run
|
||||
|
||||
This example is created using [Create React App](https://reactjs.org/docs/create-a-new-react-app.html).
|
||||
|
||||
To install and run:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "play-pause-button",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"rive-react": "0.0.1",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "SKIP_PREFLIGHT_CHECK=true react-scripts start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<title>Rive React - Basic with Hook</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -1,50 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRive } from "rive-react";
|
||||
|
||||
function App() {
|
||||
const [buttonText, setButtonText] = useState("Pause");
|
||||
const { RiveComponent, rive } = useRive({
|
||||
src: "poison-loader.riv",
|
||||
autoplay: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (rive) {
|
||||
// "play" event is fired when the animation starts to play, so we update
|
||||
// button text on this event.
|
||||
rive.on("play", () => {
|
||||
setButtonText("Pause");
|
||||
});
|
||||
|
||||
// As above, the "pause" event is fired when the animation pauses.
|
||||
rive.on("pause", () => {
|
||||
setButtonText("Play");
|
||||
});
|
||||
}
|
||||
// We listen for changes to the rive object. The rive object will be null
|
||||
// until the rive file has loaded.
|
||||
}, [rive]);
|
||||
|
||||
function onButtonClick() {
|
||||
// `rive` will return as null until the file as fully loaded, so we include this
|
||||
// guard to prevent any unwanted errors.
|
||||
if (rive) {
|
||||
if (rive.isPlaying) {
|
||||
rive.pause();
|
||||
} else {
|
||||
rive.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// The animation will fit to the parent element, so we set a large height
|
||||
// and width for this example.
|
||||
<div style={{ height: "500px", width: "500px" }}>
|
||||
<RiveComponent />
|
||||
<button onClick={onButtonClick}>{buttonText}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
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/avatars.riv
Normal file
BIN
examples/public/avatars.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/layout_test.riv
Normal file
BIN
examples/public/layout_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/rating.riv
Normal file
BIN
examples/public/rating.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>
|
||||
);
|
||||
};
|
||||
17
examples/src/components/Events.stories.ts
Normal file
17
examples/src/components/Events.stories.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import Events from './Events';
|
||||
|
||||
const meta = {
|
||||
title: 'Events',
|
||||
component: Events,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof Events>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
34
examples/src/components/Events.tsx
Normal file
34
examples/src/components/Events.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRive, EventType, RiveEventType } from '@rive-app/react-canvas';
|
||||
|
||||
const Events = () => {
|
||||
const { rive, RiveComponent } = useRive({
|
||||
src: 'rating.riv',
|
||||
stateMachines: 'State Machine 1',
|
||||
autoplay: true,
|
||||
automaticallyHandleEvents: true,
|
||||
});
|
||||
|
||||
const onRiveEventReceived = (riveEvent: any) => {
|
||||
console.log('Rive event received:', riveEvent);
|
||||
const eventData = riveEvent.data;
|
||||
const eventProperties = eventData.properties;
|
||||
if (eventData.type === RiveEventType.General) {
|
||||
console.log('Event name', eventData.name);
|
||||
console.log('Rating', eventProperties.rating);
|
||||
console.log('Message', eventProperties.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Wait until the rive object is instantiated before adding the Rive
|
||||
// event listener
|
||||
useEffect(() => {
|
||||
if (rive) {
|
||||
rive.on(EventType.RiveEvent, onRiveEventReceived);
|
||||
}
|
||||
}, [rive]);
|
||||
|
||||
return <RiveComponent />;
|
||||
};
|
||||
|
||||
export default Events;
|
||||
17
examples/src/components/Http.stories.ts
Normal file
17
examples/src/components/Http.stories.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import Http from './Http';
|
||||
|
||||
const meta = {
|
||||
title: 'Http',
|
||||
component: Http,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof Http>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
14
examples/src/components/Http.tsx
Normal file
14
examples/src/components/Http.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useRive } from '@rive-app/react-canvas';
|
||||
|
||||
const Http = () => {
|
||||
const { RiveComponent } = useRive({
|
||||
src: 'https://cdn.rive.app/animations/vehicles.riv',
|
||||
stateMachines: 'bumpy',
|
||||
autoplay: true,
|
||||
});
|
||||
|
||||
return <RiveComponent />;
|
||||
};
|
||||
|
||||
export default Http;
|
||||
17
examples/src/components/ResponsiveLayout.stories.ts
Normal file
17
examples/src/components/ResponsiveLayout.stories.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import ResponsiveLayout from './ResponsiveLayout';
|
||||
|
||||
const meta = {
|
||||
title: 'ResponsiveLayout',
|
||||
component: ResponsiveLayout,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof ResponsiveLayout>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
18
examples/src/components/ResponsiveLayout.tsx
Normal file
18
examples/src/components/ResponsiveLayout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Fit, useRive, Layout } from '@rive-app/react-canvas';
|
||||
|
||||
const ResponsiveLayout = () => {
|
||||
const { RiveComponent } = useRive({
|
||||
src: 'layout_test.riv',
|
||||
artboard: 'Artboard',
|
||||
stateMachines: 'State Machine 1',
|
||||
autoplay: true,
|
||||
layout: new Layout({
|
||||
fit: Fit.Layout,
|
||||
}),
|
||||
});
|
||||
|
||||
return <RiveComponent />;
|
||||
};
|
||||
|
||||
export default ResponsiveLayout;
|
||||
17
examples/src/components/Simple.stories.ts
Normal file
17
examples/src/components/Simple.stories.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import Simple from './Simple';
|
||||
|
||||
const meta = {
|
||||
title: 'Simple',
|
||||
component: Simple,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof Simple>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
14
examples/src/components/Simple.tsx
Normal file
14
examples/src/components/Simple.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useRive } from '@rive-app/react-canvas';
|
||||
|
||||
const Simple = () => {
|
||||
const { RiveComponent } = useRive({
|
||||
src: 'avatars.riv',
|
||||
artboard: 'Avatar 3',
|
||||
autoplay: true,
|
||||
});
|
||||
|
||||
return <RiveComponent />;
|
||||
};
|
||||
|
||||
export default Simple;
|
||||
@@ -1,14 +0,0 @@
|
||||
# State Machine Button
|
||||
|
||||
This example shows how to interact with a state machine, using various onMouse\* callbacks to trigger transations.
|
||||
|
||||
## To Run
|
||||
|
||||
This example is created using [Create React App](https://reactjs.org/docs/create-a-new-react-app.html).
|
||||
|
||||
To install and run:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "state-machine-button",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"rive-react": "0.0.1",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "SKIP_PREFLIGHT_CHECK=true react-scripts start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<title>Rive React - Basic with Hook</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -1,54 +0,0 @@
|
||||
import { useRive, useStateMachineInput } from "rive-react";
|
||||
|
||||
function App() {
|
||||
const { RiveComponent, rive } = useRive({
|
||||
src: "like.riv",
|
||||
stateMachines: "State Machine 1",
|
||||
artboard: "New Artboard",
|
||||
autoplay: true,
|
||||
});
|
||||
|
||||
const hoverInput = useStateMachineInput(rive, "State Machine 1", "Hover");
|
||||
const pressedInput = useStateMachineInput(rive, "State Machine 1", "Pressed");
|
||||
|
||||
function onMouseEnter() {
|
||||
// state machine inputs will be null until the rive file has loaded, so we
|
||||
// put these guards in place to avoid any errors.
|
||||
if (hoverInput) {
|
||||
hoverInput.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
if (hoverInput) {
|
||||
hoverInput.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseDown() {
|
||||
if (pressedInput) {
|
||||
pressedInput.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
if (pressedInput) {
|
||||
pressedInput.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// The animation will fit to the parent element, so we set a large height
|
||||
// and width for this example.
|
||||
<div style={{ height: "500px", width: "500px" }}>
|
||||
<RiveComponent
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
@@ -6,6 +6,12 @@
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"paths": {
|
||||
"@rive-app/react-canvas": ["../"],
|
||||
"@rive-app/react-webgl": ["../"],
|
||||
"@rive-app/react-webgl2": ["../"],
|
||||
"@rive-app/react-canvas-lite": ["../"]
|
||||
},
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
12307
examples/yarn.lock
Normal file
12307
examples/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
9
npm/react-canvas-lite/README.md
Normal file
9
npm/react-canvas-lite/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# @rive-app/react-canvas-lite
|
||||
|
||||
Output for `rive-react` using the backing `@rive-app/canvas-lite` JS runtime.
|
||||
|
||||
## Why Lite?
|
||||
|
||||
The current `@rive-app/react-canvas` dependency supports all Rive features and contains the necessary backing dependencies to render those graphics. This `lite` version has the same API, but does not compile and build with certain dependencies in order to keep the package size as small as possible.
|
||||
|
||||
At this time, this lite version of `@rive-app/react-canvas-lite` will not render Rive Text onto the canvas or play Rive Audio. Note however, that even if your Rive file may include Rive Text components, rendering the graphic should not cause any app errors, or cease to render. The same is true for playing audio.
|
||||
26
npm/react-canvas-lite/package.json
Normal file
26
npm/react-canvas-lite/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@rive-app/react-canvas-lite",
|
||||
"version": "4.26.2",
|
||||
"description": "React wrapper around the @rive-app/canvas-lite library",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/rive-app/rive-react.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/rive-app/rive-react/issues"
|
||||
},
|
||||
"homepage": "https://github.com/rive-app/rive-react#readme",
|
||||
"dependencies": {
|
||||
"@rive-app/canvas-lite": "2.34.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
}
|
||||
}
|
||||
3
npm/react-canvas/README.md
Normal file
3
npm/react-canvas/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @rive-app/react-canvas
|
||||
|
||||
Output for `rive-react` using the backing `@rive-app/canvas` JS runtime
|
||||
26
npm/react-canvas/package.json
Normal file
26
npm/react-canvas/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@rive-app/react-canvas",
|
||||
"version": "4.26.2",
|
||||
"description": "React wrapper around the @rive-app/canvas library",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/rive-app/rive-react.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/rive-app/rive-react/issues"
|
||||
},
|
||||
"homepage": "https://github.com/rive-app/rive-react#readme",
|
||||
"dependencies": {
|
||||
"@rive-app/canvas": "2.34.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
}
|
||||
}
|
||||
3
npm/react-webgl/README.md
Normal file
3
npm/react-webgl/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @rive-app/react-webgl
|
||||
|
||||
Output for `rive-react` using the backing `@rive-app/webgl` JS runtime
|
||||
26
npm/react-webgl/package.json
Normal file
26
npm/react-webgl/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@rive-app/react-webgl",
|
||||
"version": "4.26.2",
|
||||
"description": "React wrapper around the @rive-app/webgl library",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/rive-app/rive-react.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/rive-app/rive-react/issues"
|
||||
},
|
||||
"homepage": "https://github.com/rive-app/rive-react#readme",
|
||||
"dependencies": {
|
||||
"@rive-app/webgl": "2.34.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
}
|
||||
}
|
||||
0
npm/react-webgl2/README.md
Normal file
0
npm/react-webgl2/README.md
Normal file
26
npm/react-webgl2/package.json
Normal file
26
npm/react-webgl2/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@rive-app/react-webgl2",
|
||||
"version": "4.26.2",
|
||||
"description": "React wrapper around the @rive-app/webgl2 library",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/rive-app/rive-react.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/rive-app/rive-react/issues"
|
||||
},
|
||||
"homepage": "https://github.com/rive-app/rive-react#readme",
|
||||
"dependencies": {
|
||||
"@rive-app/webgl2": "2.34.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
}
|
||||
}
|
||||
8103
package-lock.json
generated
8103
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rive-react",
|
||||
"version": "0.0.16",
|
||||
"version": "4.26.2",
|
||||
"description": "React wrapper around the rive-js library",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/types/index.d.ts",
|
||||
@@ -10,10 +10,19 @@
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "bunchee src/index.ts -m --no-sourcemap",
|
||||
"dev": "watch 'npm run build' src",
|
||||
"lint": "eslint -c .eslintrc.js 'src/**/*{.ts,.tsx}'",
|
||||
"format": "prettier --write src",
|
||||
"types:check": "tsc --noEmit",
|
||||
"release": "release-it"
|
||||
"release": "release-it",
|
||||
"release:patch": "npm run release -- --ci",
|
||||
"release:minor": "npm run release -- minor --ci",
|
||||
"release:major": "npm run release -- major --ci",
|
||||
"setup-builds": "./scripts/build.sh",
|
||||
"setup-packages": "./scripts/setup_all_packages.sh",
|
||||
"bump-versions": "./scripts/bump_all_versions.sh $npm_package_version",
|
||||
"publish:all": "./scripts/publish_all.sh --access public",
|
||||
"storybook": "yarn --cwd examples storybook"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,22 +35,28 @@
|
||||
},
|
||||
"homepage": "https://github.com/rive-app/rive-react#readme",
|
||||
"dependencies": {
|
||||
"rive-js": "0.7.23"
|
||||
"@rive-app/canvas": "2.34.2",
|
||||
"@rive-app/canvas-lite": "2.34.2",
|
||||
"@rive-app/webgl": "2.34.2",
|
||||
"@rive-app/webgl2": "2.34.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0"
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.0",
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/react-hooks": "^7.0.0",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/react": "^17.0.9",
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
||||
"@typescript-eslint/parser": "^5.7.0",
|
||||
"auto-changelog": "^2.3.0",
|
||||
"bunchee": "^1.6.0",
|
||||
"babel-loader": "^8.2.5",
|
||||
"bunchee": "1.8.5",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
@@ -49,12 +64,16 @@
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react": "^7.27.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-storybook": "^0.5.12",
|
||||
"jest": "^27.0.4",
|
||||
"prettier": "^2.3.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"release-it": "^14.10.0",
|
||||
"ts-jest": "^27.0.2"
|
||||
"ts-jest": "^27.1.1",
|
||||
"typescript": "^4.5.4",
|
||||
"watch": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
42
scripts/build.sh
Executable file
42
scripts/build.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Copy the build to each react-variant build for npm release
|
||||
cp -r ./dist ./npm/react-webgl
|
||||
cp -r ./dist ./npm/react-canvas
|
||||
cp -r ./dist ./npm/react-canvas-lite
|
||||
cp -r ./dist ./npm/react-webgl2
|
||||
|
||||
echo "Replacing the canvas with webgl references in react-webgl"
|
||||
pushd ./npm/react-webgl/dist
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
find . -type f -name "*.ts" -print0 | xargs -0 sed -i '' -e 's/@rive-app\/canvas/@rive-app\/webgl/g'
|
||||
find . -type f -name "*.js" -print0 | xargs -0 sed -i '' -e 's/@rive-app\/canvas/@rive-app\/webgl/g'
|
||||
else
|
||||
find . -type f -name "*.ts" -print0 | xargs -0 sed -i -e 's/@rive-app\/canvas/@rive-app\/webgl/g'
|
||||
find . -type f -name "*.js" -print0 | xargs -0 sed -i -e 's/@rive-app\/canvas/@rive-app\/webgl/g'
|
||||
fi
|
||||
popd
|
||||
|
||||
echo "Replacing the canvas with webgl2 references in react-webgl2"
|
||||
pushd ./npm/react-webgl2/dist
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
find . -type f -name "*.ts" -print0 | xargs -0 sed -i '' -e 's/@rive-app\/canvas/@rive-app\/webgl2/g'
|
||||
find . -type f -name "*.js" -print0 | xargs -0 sed -i '' -e 's/@rive-app\/canvas/@rive-app\/webgl2/g'
|
||||
else
|
||||
find . -type f -name "*.ts" -print0 | xargs -0 sed -i -e 's/@rive-app\/canvas/@rive-app\/webgl2/g'
|
||||
find . -type f -name "*.js" -print0 | xargs -0 sed -i -e 's/@rive-app\/canvas/@rive-app\/webgl2/g'
|
||||
fi
|
||||
popd
|
||||
|
||||
echo "Replacing the canvas with canvas-lite references in react-canvas-lite"
|
||||
pushd ./npm/react-canvas-lite/dist
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
find . -type f -name "*.ts" -print0 | xargs -0 sed -i '' -e 's/@rive-app\/canvas/@rive-app\/canvas-lite/g'
|
||||
find . -type f -name "*.js" -print0 | xargs -0 sed -i '' -e 's/@rive-app\/canvas/@rive-app\/canvas-lite/g'
|
||||
else
|
||||
find . -type f -name "*.ts" -print0 | xargs -0 sed -i -e 's/@rive-app\/canvas/@rive-app\/canvas-lite/g'
|
||||
find . -type f -name "*.js" -print0 | xargs -0 sed -i -e 's/@rive-app\/canvas/@rive-app\/canvas-lite/g'
|
||||
fi
|
||||
popd
|
||||
10
scripts/publish_all.sh
Executable file
10
scripts/publish_all.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Bump the version number of every npm module in the npm folder.
|
||||
for dir in ./npm/*; do
|
||||
pushd $dir > /dev/null
|
||||
echo Publishing `echo $dir | sed 's:.*/::'`
|
||||
npm publish $@
|
||||
popd > /dev/null
|
||||
done
|
||||
24
scripts/setup_all_packages.sh
Executable file
24
scripts/setup_all_packages.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Copying package.json to rive-react npm package folders"
|
||||
|
||||
# Bump the version number of every npm module in the npm folder.
|
||||
for dir in ./npm/*; do
|
||||
echo $dir
|
||||
pushd $dir > /dev/null
|
||||
echo $dir
|
||||
if [ -f "./package.json" ]; then
|
||||
echo "Removing existing package.json..."
|
||||
rm "./package.json"
|
||||
echo "package.json deleted from $dir"
|
||||
fi
|
||||
cp ../../package.json ./
|
||||
repo_name=`echo $dir | sed 's:.*/::' | sed 's/_/-/g'`
|
||||
echo Setting package.json on npm packages
|
||||
echo $repo_name
|
||||
../../scripts/setup_package.sh $repo_name
|
||||
echo Finished setting up package.json
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
6
scripts/setup_package.sh
Executable file
6
scripts/setup_package.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Setup the package.json for a given npm module
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
node $SCRIPT_DIR/trimPackageJson.js `pwd` "$1"
|
||||
23
scripts/trimPackageJson.js
Normal file
23
scripts/trimPackageJson.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const fs = require('fs');
|
||||
const path = process.argv[2];
|
||||
const npmPackageSplit = process.argv[3].split('-');
|
||||
// extracts "webgl" or "canvas-lite" from the npm package name
|
||||
const renderer = npmPackageSplit.slice(1).join('-');
|
||||
const package = require(path + '/package.json');
|
||||
|
||||
function trimNpmPackage() {
|
||||
package.name = `@rive-app/react-${renderer}`;
|
||||
package.description = `React wrapper around the @rive-app/${renderer} library`;
|
||||
const webDependencyName = `@rive-app/${renderer}`;
|
||||
const canvasDep = package.dependencies[webDependencyName];
|
||||
package.dependencies = {
|
||||
[webDependencyName]: canvasDep,
|
||||
};
|
||||
delete package.devDependencies;
|
||||
delete package.scripts;
|
||||
fs.writeFileSync(path + '/package.json', JSON.stringify(package, null, 2));
|
||||
}
|
||||
|
||||
if (renderer) {
|
||||
trimNpmPackage();
|
||||
}
|
||||
@@ -24,7 +24,7 @@ window.IntersectionObserver = class IntersectionObserver {
|
||||
unobserve() {}
|
||||
};
|
||||
|
||||
jest.mock('rive-js', () => ({
|
||||
jest.mock('@rive-app/canvas', () => ({
|
||||
Rive: jest.fn().mockImplementation(() => ({
|
||||
on: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
|
||||
@@ -1,31 +1,86 @@
|
||||
import { Layout } from 'rive-js';
|
||||
import { Layout } from '@rive-app/canvas';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import useRive from '../hooks/useRive';
|
||||
|
||||
export type RiveProps = {
|
||||
export interface RiveProps {
|
||||
/**
|
||||
* URL of the Rive asset, or path to where the public asset is stored.
|
||||
*/
|
||||
src: string;
|
||||
/**
|
||||
* Artboard to render from the Rive asset.
|
||||
* Defaults to the first artboard created.
|
||||
*/
|
||||
artboard?: string;
|
||||
/**
|
||||
* Specify a starting animation to play.
|
||||
*/
|
||||
animations?: string | string[];
|
||||
/**
|
||||
* Specify a starting state machine to play.
|
||||
*/
|
||||
stateMachines?: string | string[];
|
||||
/**
|
||||
* Specify a starting Layout object to set Fill and Alignment for the drawing surface. See docs at https://rive.app/community/doc/layout/docBl81zd1GB for more on layout configuration.
|
||||
*/
|
||||
layout?: Layout;
|
||||
};
|
||||
/**
|
||||
* For `@rive-app/react-webgl`, sets this property to maintain a single WebGL context for multiple canvases. **We recommend to keep the default value** when rendering multiple Rive instances on a page.
|
||||
*/
|
||||
useOffscreenRenderer?: boolean;
|
||||
/**
|
||||
* Specify whether to disable Rive listeners on the canvas, thus preventing any event listeners to be attached to the canvas element
|
||||
*/
|
||||
shouldDisableRiveListeners?: boolean;
|
||||
/**
|
||||
* Specify whether to resize the canvas to its container automatically
|
||||
*/
|
||||
shouldResizeCanvasToContainer?: boolean;
|
||||
/**
|
||||
* Enable Rive Events to be handled by the runtime. This means any special Rive Event may have
|
||||
* functionality that can be invoked implicitly when detected.
|
||||
*
|
||||
* For example, if during the render loop an OpenUrlEvent is detected, the
|
||||
* browser may try to open the specified URL in the payload.
|
||||
*
|
||||
* This flag is false by default to prevent any unwanted behaviors from taking place.
|
||||
* This means any special Rive Event will have to be handled manually by subscribing to
|
||||
* EventType.RiveEvent
|
||||
*/
|
||||
automaticallyHandleEvents?: boolean;
|
||||
}
|
||||
|
||||
const Rive = ({
|
||||
src,
|
||||
artboard,
|
||||
animations,
|
||||
stateMachines,
|
||||
layout,
|
||||
useOffscreenRenderer = true,
|
||||
shouldDisableRiveListeners = false,
|
||||
shouldResizeCanvasToContainer = true,
|
||||
automaticallyHandleEvents = false,
|
||||
children,
|
||||
...rest
|
||||
}: RiveProps & ComponentProps<'div'>) => {
|
||||
}: RiveProps & ComponentProps<'canvas'>) => {
|
||||
const params = {
|
||||
src,
|
||||
artboard,
|
||||
animations,
|
||||
layout,
|
||||
stateMachines,
|
||||
autoplay: true,
|
||||
shouldDisableRiveListeners,
|
||||
automaticallyHandleEvents,
|
||||
};
|
||||
|
||||
const { RiveComponent } = useRive(params);
|
||||
return <RiveComponent {...rest} />;
|
||||
const options = {
|
||||
useOffscreenRenderer,
|
||||
shouldResizeCanvasToContainer,
|
||||
};
|
||||
|
||||
const { RiveComponent } = useRive(params, options);
|
||||
return <RiveComponent {...rest}>{children}</RiveComponent>;
|
||||
};
|
||||
|
||||
export default Rive;
|
||||
|
||||
38
src/hooks/elementObserver.ts
Normal file
38
src/hooks/elementObserver.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
class FakeIntersectionObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
const MyIntersectionObserver =
|
||||
globalThis.IntersectionObserver || FakeIntersectionObserver;
|
||||
|
||||
class ElementObserver {
|
||||
private observer;
|
||||
|
||||
private elementsMap: Map<Element, Function> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.observer = new MyIntersectionObserver(this.onObserved);
|
||||
}
|
||||
public onObserved = (entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry) => {
|
||||
const elementCallback = this.elementsMap.get(entry.target as Element);
|
||||
if (elementCallback) {
|
||||
elementCallback(entry);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
public registerCallback(element: Element, callback: Function) {
|
||||
this.observer.observe(element);
|
||||
this.elementsMap.set(element, callback);
|
||||
}
|
||||
|
||||
public removeCallback(element: Element) {
|
||||
this.observer.unobserve(element);
|
||||
this.elementsMap.delete(element);
|
||||
}
|
||||
}
|
||||
|
||||
export default ElementObserver;
|
||||
94
src/hooks/useContainerSize.ts
Normal file
94
src/hooks/useContainerSize.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Dimensions } from '../types';
|
||||
|
||||
// There are polyfills for this, but they add hundreds of lines of code
|
||||
class FakeResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
function throttle(f: Function, delay: number) {
|
||||
let timer = 0;
|
||||
return function (this: Function, ...args: any) {
|
||||
clearTimeout(timer);
|
||||
timer = window.setTimeout(() => f.apply(this, args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
const MyResizeObserver = globalThis.ResizeObserver || FakeResizeObserver;
|
||||
const hasResizeObserver = globalThis.ResizeObserver !== undefined;
|
||||
|
||||
const useResizeObserver = hasResizeObserver;
|
||||
const useWindowListener = !useResizeObserver;
|
||||
|
||||
/**
|
||||
* Hook to listen for a ref element's resize events being triggered. When resized,
|
||||
* it sets state to an object of {width: number, height: number} indicating the contentRect
|
||||
* size of the element at the new resize.
|
||||
*
|
||||
* @param containerRef - Ref element to listen for resize events on
|
||||
* @returns - Size object with width and height attributes
|
||||
*/
|
||||
export default function useSize(
|
||||
containerRef: React.MutableRefObject<HTMLElement | null>,
|
||||
shouldResizeCanvasToContainer = true
|
||||
) {
|
||||
const [size, setSize] = useState<Dimensions>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
// internet explorer does not support ResizeObservers.
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && shouldResizeCanvasToContainer) {
|
||||
const handleResize = () => {
|
||||
setSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
};
|
||||
|
||||
if (useWindowListener) {
|
||||
// only pay attention to window size changes when we do not have the resizeObserver (IE only)
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}
|
||||
}, []);
|
||||
const observer = useRef(
|
||||
new MyResizeObserver(
|
||||
throttle((entries: any) => {
|
||||
if (useResizeObserver) {
|
||||
setSize({
|
||||
width: entries[entries.length - 1].contentRect.width,
|
||||
height: entries[entries.length - 1].contentRect.height,
|
||||
});
|
||||
}
|
||||
}, 0)
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const currentObserver = observer.current;
|
||||
if (!shouldResizeCanvasToContainer) {
|
||||
currentObserver.disconnect();
|
||||
return;
|
||||
}
|
||||
const containerEl = containerRef.current;
|
||||
if (containerRef.current && useResizeObserver) {
|
||||
currentObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
currentObserver.disconnect();
|
||||
if (containerEl && useResizeObserver) {
|
||||
currentObserver.unobserve(containerEl);
|
||||
}
|
||||
};
|
||||
}, [containerRef, observer]);
|
||||
|
||||
return size;
|
||||
}
|
||||
51
src/hooks/useDevicePixelRatio.ts
Normal file
51
src/hooks/useDevicePixelRatio.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
/**
|
||||
* Listen for devicePixelRatio changes and set the new value accordingly. This could
|
||||
* happen for reasons such as:
|
||||
* - User moves window from retina screen display to a separate monitor
|
||||
* - User controls zoom settings on the browser
|
||||
*
|
||||
* Source: https://github.com/rexxars/use-device-pixel-ratio/blob/main/index.ts
|
||||
*
|
||||
* @param customDevicePixelRatio - Number to force a dpr to abide by, rather than using the window's
|
||||
*
|
||||
* @returns dpr: Number - Device pixel ratio; ratio of physical px to resolution in CSS pixels for current device
|
||||
*/
|
||||
export default function useDevicePixelRatio(customDevicePixelRatio?: number) {
|
||||
const dpr = customDevicePixelRatio || getDevicePixelRatio();
|
||||
const [currentDpr, setCurrentDpr] = useState(dpr);
|
||||
|
||||
useEffect(() => {
|
||||
const canListen = typeof window !== 'undefined' && 'matchMedia' in window;
|
||||
if (!canListen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateDpr = () => {
|
||||
const newDpr = customDevicePixelRatio || getDevicePixelRatio();
|
||||
setCurrentDpr(newDpr);
|
||||
};
|
||||
const mediaMatcher = window.matchMedia(
|
||||
`screen and (resolution: ${currentDpr}dppx)`
|
||||
);
|
||||
mediaMatcher.hasOwnProperty('addEventListener')
|
||||
? mediaMatcher.addEventListener('change', updateDpr)
|
||||
: mediaMatcher.addListener(updateDpr);
|
||||
|
||||
return () => {
|
||||
mediaMatcher.hasOwnProperty('removeEventListener')
|
||||
? mediaMatcher.removeEventListener('change', updateDpr)
|
||||
: mediaMatcher.removeListener(updateDpr);
|
||||
};
|
||||
}, [currentDpr, customDevicePixelRatio]);
|
||||
|
||||
return currentDpr;
|
||||
}
|
||||
|
||||
function getDevicePixelRatio(): number {
|
||||
const hasDprProp =
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.devicePixelRatio === 'number';
|
||||
const dpr = hasDprProp ? window.devicePixelRatio : 1;
|
||||
return Math.min(Math.max(1, dpr), 3);
|
||||
}
|
||||
32
src/hooks/useIntersectionObserver.ts
Normal file
32
src/hooks/useIntersectionObserver.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useCallback } from 'react';
|
||||
import ElementObserver from './elementObserver';
|
||||
|
||||
let observer: ElementObserver;
|
||||
const getObserver = () => {
|
||||
if(!observer) {
|
||||
observer = new ElementObserver();
|
||||
}
|
||||
return observer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to observe elements when they are intersecting with the viewport
|
||||
*
|
||||
* @returns - API to observer and unobserve elements
|
||||
*/
|
||||
export default function useIntersectionObserver() {
|
||||
const observe = useCallback((element: Element, callback: Function) => {
|
||||
const observer = getObserver();
|
||||
observer.registerCallback(element, callback);
|
||||
}, []);
|
||||
|
||||
const unobserve = useCallback((element: Element) => {
|
||||
const observer = getObserver();
|
||||
observer.removeCallback(element);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
observe,
|
||||
unobserve,
|
||||
};
|
||||
}
|
||||
195
src/hooks/useResizeCanvas.ts
Normal file
195
src/hooks/useResizeCanvas.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { useEffect, useState, MutableRefObject, useCallback } from 'react';
|
||||
import { Bounds } from '@rive-app/canvas';
|
||||
import { Dimensions, UseRiveOptions } from '../types';
|
||||
import useDevicePixelRatio from './useDevicePixelRatio';
|
||||
import useContainerSize from './useContainerSize';
|
||||
import { getOptions } from '../utils';
|
||||
|
||||
interface UseResizeCanvasProps {
|
||||
/**
|
||||
* Whether or not Rive is loaded and renderer is associated with the canvas
|
||||
*/
|
||||
riveLoaded: boolean;
|
||||
/**
|
||||
* Ref to the canvas element
|
||||
*/
|
||||
canvasElem: HTMLCanvasElement | null;
|
||||
/**
|
||||
* Ref to the container element of the canvas
|
||||
*/
|
||||
containerRef: MutableRefObject<HTMLElement | null>;
|
||||
/**
|
||||
* (Optional) Callback to be invoked after the canvas has been resized due to a resize
|
||||
* of its parent container. This is where you would want to reset the layout
|
||||
* dimensions for the Rive renderer to dictate the new min/max bounds of the
|
||||
* canvas.
|
||||
*
|
||||
* Using the high-level JS runtime, this might be a simple call to `rive.resizeToCanvas()`
|
||||
* Using the low-level JSruntime, this might be invoking the renderer's `.align()` method
|
||||
* with the Layout and min/max X/Y values of the canvas.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
onCanvasHasResized?: () => void;
|
||||
/**
|
||||
* (Optional) Options passed to the useRive hook, including the shouldResizeCanvasToContainer option
|
||||
* which prevents the canvas element from resizing to its parent container
|
||||
*/
|
||||
options?: Partial<UseRiveOptions>;
|
||||
/**
|
||||
* (Optional) AABB bounds of the artboard. If provided, the canvas will be sized to the artboard
|
||||
* height if the fitCanvasToArtboardHeight option is true.
|
||||
*/
|
||||
artboardBounds?: Bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper hook to listen for changes in the <canvas> parent container size and size the <canvas>
|
||||
* to match. If a resize event has occurred, a supplied callback (onCanvasHasResized)
|
||||
* will be inokved to allow for any re-calculation needed (i.e. Rive layout on the canvas).
|
||||
*
|
||||
* This hook is useful if you are not intending to use the `useRive` hook yourself, but still
|
||||
* want to use the auto-sizing logic on the canvas/container.
|
||||
*
|
||||
* @param props - Object to supply necessary props to the hook
|
||||
*/
|
||||
export default function useResizeCanvas({
|
||||
riveLoaded = false,
|
||||
canvasElem,
|
||||
containerRef,
|
||||
options = {},
|
||||
onCanvasHasResized,
|
||||
artboardBounds,
|
||||
}: UseResizeCanvasProps) {
|
||||
const presetOptions = getOptions(options);
|
||||
const [
|
||||
{ height: lastContainerHeight, width: lastContainerWidth },
|
||||
setLastContainerDimensions,
|
||||
] = useState<Dimensions>({
|
||||
height: 0,
|
||||
width: 0,
|
||||
});
|
||||
const [
|
||||
{ height: lastCanvasHeight, width: lastCanvasWidth },
|
||||
setLastCanvasSize,
|
||||
] = useState<Dimensions>({
|
||||
height: 0,
|
||||
width: 0,
|
||||
});
|
||||
|
||||
const [isFirstSizing, setIsFirstSizing] = useState(true);
|
||||
|
||||
const {
|
||||
fitCanvasToArtboardHeight,
|
||||
shouldResizeCanvasToContainer,
|
||||
useDevicePixelRatio: shouldUseDevicePixelRatio,
|
||||
customDevicePixelRatio,
|
||||
} = presetOptions;
|
||||
|
||||
const containerSize = useContainerSize(
|
||||
containerRef,
|
||||
shouldResizeCanvasToContainer
|
||||
);
|
||||
const currentDevicePixelRatio = useDevicePixelRatio(customDevicePixelRatio);
|
||||
|
||||
const { maxX, maxY } = artboardBounds ?? {};
|
||||
|
||||
const getContainerDimensions = useCallback(() => {
|
||||
const width = containerRef.current?.clientWidth ?? 0;
|
||||
const height = containerRef.current?.clientHeight ?? 0;
|
||||
if (fitCanvasToArtboardHeight && artboardBounds) {
|
||||
const { maxY, maxX } = artboardBounds;
|
||||
return { width, height: width * (maxY / maxX) };
|
||||
}
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}, [containerRef, fitCanvasToArtboardHeight, maxX, maxY]);
|
||||
|
||||
useEffect(() => {
|
||||
// If Rive is not ready, the container is not ready, or the user supplies a flag
|
||||
// to not resize the canvas to the container, then return early
|
||||
if (
|
||||
!shouldResizeCanvasToContainer ||
|
||||
!containerRef.current ||
|
||||
!riveLoaded
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height } = getContainerDimensions();
|
||||
let hasResized = false;
|
||||
if (canvasElem) {
|
||||
// Check if the canvas parent container bounds have changed and set
|
||||
// new values accordingly
|
||||
const boundsChanged =
|
||||
width !== lastContainerWidth || height !== lastContainerHeight;
|
||||
if (presetOptions.fitCanvasToArtboardHeight && boundsChanged) {
|
||||
containerRef.current.style.height = height + 'px';
|
||||
hasResized = true;
|
||||
}
|
||||
if (presetOptions.useDevicePixelRatio) {
|
||||
// Check if devicePixelRatio may have changed and get new canvas
|
||||
// width/height values to set the size
|
||||
const canvasSizeChanged =
|
||||
width * currentDevicePixelRatio !== lastCanvasWidth ||
|
||||
height * currentDevicePixelRatio !== lastCanvasHeight;
|
||||
if (boundsChanged || canvasSizeChanged) {
|
||||
const newCanvasWidthProp = currentDevicePixelRatio * width;
|
||||
const newCanvasHeightProp = currentDevicePixelRatio * height;
|
||||
canvasElem.width = newCanvasWidthProp;
|
||||
canvasElem.height = newCanvasHeightProp;
|
||||
canvasElem.style.width = width + 'px';
|
||||
canvasElem.style.height = height + 'px';
|
||||
setLastCanvasSize({
|
||||
width: newCanvasWidthProp,
|
||||
height: newCanvasHeightProp,
|
||||
});
|
||||
hasResized = true;
|
||||
}
|
||||
} else if (boundsChanged) {
|
||||
canvasElem.width = width;
|
||||
canvasElem.height = height;
|
||||
setLastCanvasSize({
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
hasResized = true;
|
||||
}
|
||||
setLastContainerDimensions({ width, height });
|
||||
}
|
||||
|
||||
// Callback to perform any Rive-related actions after resizing the canvas
|
||||
// (i.e., reset the Rive layout in the render loop)
|
||||
if (onCanvasHasResized && (isFirstSizing || hasResized)) {
|
||||
onCanvasHasResized && onCanvasHasResized();
|
||||
}
|
||||
isFirstSizing && setIsFirstSizing(false);
|
||||
}, [
|
||||
canvasElem,
|
||||
containerRef,
|
||||
containerSize,
|
||||
currentDevicePixelRatio,
|
||||
getContainerDimensions,
|
||||
isFirstSizing,
|
||||
setIsFirstSizing,
|
||||
lastCanvasHeight,
|
||||
lastCanvasWidth,
|
||||
lastContainerHeight,
|
||||
lastContainerWidth,
|
||||
onCanvasHasResized,
|
||||
shouldResizeCanvasToContainer,
|
||||
fitCanvasToArtboardHeight,
|
||||
shouldUseDevicePixelRatio,
|
||||
riveLoaded,
|
||||
]);
|
||||
|
||||
// Reset width and height values when the canvas changes
|
||||
useEffect(() => {
|
||||
setLastCanvasSize({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
}, [canvasElem]);
|
||||
}
|
||||
@@ -6,14 +6,12 @@ import React, {
|
||||
ComponentProps,
|
||||
RefCallback,
|
||||
} from 'react';
|
||||
import { Rive, EventType } from 'rive-js';
|
||||
import {
|
||||
UseRiveParameters,
|
||||
UseRiveOptions,
|
||||
RiveState,
|
||||
Dimensions,
|
||||
} from '../types';
|
||||
import { useWindowSize } from '../utils';
|
||||
import { Rive, EventType, Fit } from '@rive-app/canvas';
|
||||
import { UseRiveParameters, UseRiveOptions, RiveState } from '../types';
|
||||
import useResizeCanvas from './useResizeCanvas';
|
||||
import useDevicePixelRatio from './useDevicePixelRatio';
|
||||
import { getOptions } from '../utils';
|
||||
import useIntersectionObserver from './useIntersectionObserver';
|
||||
|
||||
type RiveComponentProps = {
|
||||
setContainerRef: RefCallback<HTMLElement>;
|
||||
@@ -23,39 +21,34 @@ type RiveComponentProps = {
|
||||
function RiveComponent({
|
||||
setContainerRef,
|
||||
setCanvasRef,
|
||||
className = '',
|
||||
style,
|
||||
children,
|
||||
...rest
|
||||
}: RiveComponentProps & ComponentProps<'div'>) {
|
||||
}: RiveComponentProps & ComponentProps<'canvas'>) {
|
||||
const containerStyle = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setContainerRef}
|
||||
style={'className' in rest ? undefined : containerStyle}
|
||||
{...rest}
|
||||
className={className}
|
||||
{...(!className && { style: containerStyle })}
|
||||
>
|
||||
<canvas ref={setCanvasRef} />
|
||||
<canvas
|
||||
ref={setCanvasRef}
|
||||
style={{ verticalAlign: 'top', width: 0, height: 0 }}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
useDevicePixelRatio: true,
|
||||
fitCanvasToArtboardHeight: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns options, with defaults set.
|
||||
*
|
||||
* @param opts
|
||||
* @returns
|
||||
*/
|
||||
function getOptions(opts: Partial<UseRiveOptions>) {
|
||||
return Object.assign({}, defaultOptions, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Hook for loading a Rive file.
|
||||
*
|
||||
@@ -74,108 +67,103 @@ export default function useRive(
|
||||
riveParams?: UseRiveParameters,
|
||||
opts: Partial<UseRiveOptions> = {}
|
||||
): RiveState {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const [canvasElem, setCanvasElem] = useState<HTMLCanvasElement | null>(null);
|
||||
const containerRef = useRef<HTMLElement | null>(null);
|
||||
const [rive, setRive] = useState<Rive | null>(null);
|
||||
const [dimensions, setDimensions] = useState<Dimensions>({
|
||||
height: 0,
|
||||
width: 0,
|
||||
});
|
||||
const riveRef = useRef<Rive | null>(null);
|
||||
|
||||
// Listen to changes in the window sizes and update the bounds when changes
|
||||
// occur.
|
||||
const windowSize = useWindowSize();
|
||||
const [rive, setRive] = useState<Rive | null>(null);
|
||||
|
||||
const isParamsLoaded = Boolean(riveParams);
|
||||
const options = getOptions(opts);
|
||||
|
||||
/**
|
||||
* Gets the intended dimensions of the canvas element.
|
||||
*
|
||||
* The intended dimensions are those of the container element, unless the
|
||||
* option `fitCanvasToArtboardHeight` is true, then they are adjusted to
|
||||
* the height of the artboard.
|
||||
*
|
||||
* @returns Dimensions object.
|
||||
*/
|
||||
function getCanvasDimensions() {
|
||||
const { width, height } =
|
||||
containerRef.current?.getBoundingClientRect() ?? new DOMRect(0, 0, 0, 0);
|
||||
|
||||
if (rive && options.fitCanvasToArtboardHeight) {
|
||||
const { maxY, maxX } = rive.bounds;
|
||||
return { width, height: width * (maxY / maxX) };
|
||||
}
|
||||
return { width, height };
|
||||
}
|
||||
const devicePixelRatio = useDevicePixelRatio();
|
||||
|
||||
/**
|
||||
* Updates the width and height of the canvas.
|
||||
* When the canvas/parent container resize, reset the Rive layout to match the
|
||||
* new (0, 0, canvas.width, canvas.height) bounds in the render loop
|
||||
*/
|
||||
function updateBounds() {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height } = getCanvasDimensions();
|
||||
const boundsChanged =
|
||||
width !== dimensions.width || height !== dimensions.height;
|
||||
if (canvasRef.current && rive && boundsChanged) {
|
||||
if (options.fitCanvasToArtboardHeight) {
|
||||
containerRef.current.style.height = height + 'px';
|
||||
}
|
||||
if (options.useDevicePixelRatio) {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvasRef.current.width = dpr * width;
|
||||
canvasRef.current.height = dpr * height;
|
||||
canvasRef.current.style.width = width + 'px';
|
||||
canvasRef.current.style.height = height + 'px';
|
||||
} else {
|
||||
canvasRef.current.width = width;
|
||||
canvasRef.current.height = height;
|
||||
}
|
||||
setDimensions({ width, height });
|
||||
|
||||
// Updating the canvas width or height will clear the canvas, so call
|
||||
// startRendering() to redraw the current frame as the animation might
|
||||
// be paused and not advancing.
|
||||
rive.startRendering();
|
||||
}
|
||||
|
||||
// Always resize to Canvas
|
||||
const onCanvasHasResized = useCallback(() => {
|
||||
if (rive) {
|
||||
if (rive.layout && rive.layout.fit === Fit.Layout) {
|
||||
if (canvasElem) {
|
||||
const resizeFactor = devicePixelRatio * rive.layout.layoutScaleFactor;
|
||||
rive.devicePixelRatioUsed = devicePixelRatio;
|
||||
rive.artboardWidth = canvasElem?.width / resizeFactor;
|
||||
rive.artboardHeight = canvasElem?.height / resizeFactor;
|
||||
}
|
||||
}
|
||||
|
||||
rive.startRendering();
|
||||
rive.resizeToCanvas();
|
||||
}
|
||||
}
|
||||
}, [rive, devicePixelRatio]);
|
||||
|
||||
/**
|
||||
* Listen to changes on the windowSize and the rive file being loaded
|
||||
* and update the canvas bounds as needed.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (rive) {
|
||||
updateBounds();
|
||||
}
|
||||
}, [rive, windowSize]);
|
||||
// Watch the canvas parent container resize and size the canvas to match
|
||||
useResizeCanvas({
|
||||
riveLoaded: !!rive,
|
||||
canvasElem,
|
||||
containerRef,
|
||||
options,
|
||||
onCanvasHasResized,
|
||||
artboardBounds: rive?.bounds,
|
||||
});
|
||||
|
||||
/**
|
||||
* Ref callback called when the canvas element mounts and unmounts.
|
||||
*/
|
||||
const setCanvasRef: RefCallback<HTMLCanvasElement> = useCallback(
|
||||
(canvas: HTMLCanvasElement | null) => {
|
||||
if (canvas && riveParams) {
|
||||
const r = new Rive({ ...riveParams, canvas });
|
||||
r.on(EventType.Load, () => setRive(r));
|
||||
} else if (canvas === null && canvasRef.current) {
|
||||
canvasRef.current.height = 0;
|
||||
canvasRef.current.width = 0;
|
||||
if (canvas === null && canvasElem) {
|
||||
canvasElem.height = 0;
|
||||
canvasElem.width = 0;
|
||||
}
|
||||
|
||||
canvasRef.current = canvas;
|
||||
setCanvasElem(canvas);
|
||||
},
|
||||
[isParamsLoaded]
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasElem || !riveParams) {
|
||||
return;
|
||||
}
|
||||
let isLoaded = rive != null;
|
||||
let r: Rive | null;
|
||||
if (rive == null) {
|
||||
const { useOffscreenRenderer } = options;
|
||||
const { onRiveReady, ...restRiveParams } = riveParams;
|
||||
r = new Rive({
|
||||
useOffscreenRenderer,
|
||||
...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) {
|
||||
setRive(r);
|
||||
} else {
|
||||
// If unmounted, cleanup the rive object immediately
|
||||
r!.cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
if (!isLoaded) {
|
||||
r?.cleanup();
|
||||
}
|
||||
};
|
||||
}, [canvasElem, isParamsLoaded, rive]);
|
||||
/**
|
||||
* Ref callback called when the container element mounts
|
||||
*/
|
||||
@@ -190,46 +178,116 @@ export default function useRive(
|
||||
* Set up IntersectionObserver to stop rendering if the animation is not in
|
||||
* view.
|
||||
*/
|
||||
const { observe, unobserve } = useIntersectionObserver();
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
let isPaused = false;
|
||||
// This is a workaround to retest whether an element is offscreen or not.
|
||||
// There seems to be a bug in Chrome that triggers an intersection change when an element
|
||||
// is moved within the DOM using insertBefore.
|
||||
// For some reason, when this is called whithin the context of a React application, the
|
||||
// intersection callback is called only once reporting isIntersecting as false but never
|
||||
// triggered back with isIntersecting as true.
|
||||
// For this reason we retest after 10 millisecond whether the element is actually off the
|
||||
// viewport or not.
|
||||
const retestIntersection = () => {
|
||||
if (canvasElem && isPaused) {
|
||||
const size = canvasElem.getBoundingClientRect();
|
||||
const isIntersecting =
|
||||
size.width > 0 &&
|
||||
size.height > 0 &&
|
||||
size.top <
|
||||
(window.innerHeight || document.documentElement.clientHeight) &&
|
||||
size.bottom > 0 &&
|
||||
size.left <
|
||||
(window.innerWidth || document.documentElement.clientWidth) &&
|
||||
size.right > 0;
|
||||
if (isIntersecting) {
|
||||
rive?.startRendering();
|
||||
isPaused = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
const onChange = (entry: IntersectionObserverEntry) => {
|
||||
entry.isIntersecting
|
||||
? rive && rive.startRendering()
|
||||
: rive && rive.stopRendering();
|
||||
});
|
||||
|
||||
if (canvasRef.current) {
|
||||
observer.observe(canvasRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
isPaused = !entry.isIntersecting;
|
||||
clearTimeout(timeoutId);
|
||||
if (!entry.isIntersecting && entry.boundingClientRect.width === 0) {
|
||||
timeoutId = setTimeout(retestIntersection, 10);
|
||||
}
|
||||
};
|
||||
}, [rive]);
|
||||
if (canvasElem && options.shouldUseIntersectionObserver !== false) {
|
||||
observe(canvasElem, onChange);
|
||||
}
|
||||
return () => {
|
||||
if (canvasElem) {
|
||||
unobserve(canvasElem);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
observe,
|
||||
unobserve,
|
||||
rive,
|
||||
canvasElem,
|
||||
options.shouldUseIntersectionObserver,
|
||||
]);
|
||||
|
||||
/**
|
||||
* On unmount, stop rive from rendering.
|
||||
* On unmount, call cleanup to cleanup any WASM generated objects that need
|
||||
* to be manually destroyed.
|
||||
*/
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (rive) {
|
||||
rive.stop();
|
||||
rive.cleanup();
|
||||
setRive(null);
|
||||
}
|
||||
};
|
||||
}, [rive]);
|
||||
}, [rive, canvasElem]);
|
||||
|
||||
const Component = useCallback((props: ComponentProps<'div'>): JSX.Element => {
|
||||
return (
|
||||
<RiveComponent
|
||||
setContainerRef={setContainerRef}
|
||||
setCanvasRef={setCanvasRef}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (riveRef.current != null) {
|
||||
riveRef.current!.cleanup();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Listen for changes in the animations params
|
||||
*/
|
||||
const animations = riveParams?.animations;
|
||||
useEffect(() => {
|
||||
if (rive && animations) {
|
||||
if (rive.isPlaying) {
|
||||
rive.stop(rive.animationNames);
|
||||
rive.play(animations);
|
||||
} else if (rive.isPaused) {
|
||||
rive.stop(rive.animationNames);
|
||||
rive.pause(animations);
|
||||
}
|
||||
}
|
||||
}, [animations, rive]);
|
||||
|
||||
const Component = useCallback(
|
||||
(props: ComponentProps<'canvas'>): JSX.Element => {
|
||||
return (
|
||||
<RiveComponent
|
||||
setContainerRef={setContainerRef}
|
||||
setCanvasRef={setCanvasRef}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[setCanvasRef, setContainerRef]
|
||||
);
|
||||
|
||||
return {
|
||||
canvas: canvasRef.current,
|
||||
canvas: canvasElem,
|
||||
container: containerRef.current,
|
||||
setCanvasRef,
|
||||
setContainerRef,
|
||||
rive,
|
||||
|
||||
57
src/hooks/useRiveFile.ts
Normal file
57
src/hooks/useRiveFile.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type {
|
||||
UseRiveFileParameters,
|
||||
RiveFileState,
|
||||
FileStatus,
|
||||
} from '../types';
|
||||
import { EventType, RiveFile } from '@rive-app/canvas';
|
||||
|
||||
/**
|
||||
* Custom hook for initializing and managing a RiveFile instance within a component.
|
||||
* It sets up a RiveFile based on provided source parameters (URL or ArrayBuffer) and ensures
|
||||
* proper cleanup to avoid memory leaks when the component unmounts or inputs change.
|
||||
*
|
||||
* @param params - Object containing parameters accepted by the Rive file in the @rive-app/canvas runtime,
|
||||
*
|
||||
* @returns {RiveFileState} Contains the active RiveFile instance (`riveFile`) and the loading status.
|
||||
*/
|
||||
function useRiveFile(params: UseRiveFileParameters): RiveFileState {
|
||||
const [riveFile, setRiveFile] = useState<RiveFile | null>(null);
|
||||
const [status, setStatus] = useState<FileStatus>('idle');
|
||||
|
||||
useEffect(() => {
|
||||
let file: RiveFile | null = null;
|
||||
|
||||
const loadRiveFile = async () => {
|
||||
try {
|
||||
setStatus('loading');
|
||||
file = new RiveFile(params);
|
||||
file.init();
|
||||
file.on(EventType.Load, () => {
|
||||
// We request an instance to add +1 to the referencesCount so it doesn't get destroyed
|
||||
// while this hook is active
|
||||
file?.getInstance();
|
||||
setRiveFile(file);
|
||||
setStatus('success');
|
||||
});
|
||||
file.on(EventType.LoadError, () => {
|
||||
setStatus('failed');
|
||||
});
|
||||
setRiveFile(file);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus('failed');
|
||||
}
|
||||
};
|
||||
|
||||
loadRiveFile();
|
||||
|
||||
return () => {
|
||||
file?.cleanup();
|
||||
};
|
||||
}, [params.src, params.buffer]);
|
||||
|
||||
return { riveFile, status };
|
||||
}
|
||||
|
||||
export default useRiveFile;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Rive, StateMachineInput } from 'rive-js';
|
||||
import { EventType, Rive, StateMachineInput } from '@rive-app/canvas';
|
||||
|
||||
/**
|
||||
* Custom hook for fetching a stateMachine input from a rive file.
|
||||
@@ -12,23 +12,39 @@ import { Rive, StateMachineInput } from 'rive-js';
|
||||
export default function useStateMachineInput(
|
||||
rive: Rive | null,
|
||||
stateMachineName?: string,
|
||||
inputName?: string
|
||||
inputName?: string,
|
||||
initialValue?: number | boolean
|
||||
) {
|
||||
const [input, setInput] = useState<StateMachineInput | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rive || !stateMachineName || !inputName) {
|
||||
setInput(null);
|
||||
}
|
||||
|
||||
if (rive && stateMachineName && inputName) {
|
||||
const inputs = rive.stateMachineInputs(stateMachineName);
|
||||
if (inputs) {
|
||||
const selectedInput = inputs.find((input) => input.name === inputName);
|
||||
setInput(selectedInput || null);
|
||||
function setStateMachineInput() {
|
||||
if (!rive || !stateMachineName || !inputName) {
|
||||
setInput(null);
|
||||
}
|
||||
} else {
|
||||
setInput(null);
|
||||
|
||||
if (rive && stateMachineName && inputName) {
|
||||
const inputs = rive.stateMachineInputs(stateMachineName);
|
||||
if (inputs) {
|
||||
const selectedInput = inputs.find(
|
||||
(input) => input.name === inputName
|
||||
);
|
||||
if (initialValue !== undefined && selectedInput) {
|
||||
selectedInput.value = initialValue;
|
||||
}
|
||||
setInput(selectedInput || null);
|
||||
}
|
||||
} else {
|
||||
setInput(null);
|
||||
}
|
||||
}
|
||||
setStateMachineInput();
|
||||
if (rive) {
|
||||
rive.on(EventType.Load, () => {
|
||||
// Check if the component/canvas is mounted before setting state to avoid setState
|
||||
// on an unmounted component in some rare cases
|
||||
setStateMachineInput();
|
||||
});
|
||||
}
|
||||
}, [rive]);
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user