Skip to content

Commit eef56fb

Browse files
committed
feat: add useViewModelInstanceImage hook
1 parent 4bc0f49 commit eef56fb

6 files changed

Lines changed: 179 additions & 2 deletions

File tree

examples/public/image_db_test.riv

677 Bytes
Binary file not shown.

examples/src/components/DataBindingTests.stories.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
22
import type { Meta, StoryObj } from '@storybook/react';
33
import { within, expect, waitFor, userEvent } from '@storybook/test';
44

5-
import { StringPropertyTest, NumberPropertyTest, BooleanPropertyTest, ColorPropertyTest, EnumPropertyTest, NestedViewModelTest, TriggerPropertyTest, PersonForm, PersonInstances } from './DataBindingTests';
5+
import { StringPropertyTest, NumberPropertyTest, BooleanPropertyTest, ColorPropertyTest, EnumPropertyTest, NestedViewModelTest, TriggerPropertyTest, PersonForm, PersonInstances, ImagePropertyTest } from './DataBindingTests';
66

77
const meta: Meta = {
88
title: 'Tests/DataBinding',
@@ -345,4 +345,44 @@ export const PersonFormStory: StoryObj = {
345345
};
346346

347347

348+
export const ImagePropertyStory: StoryObj = {
349+
name: 'Image Property',
350+
render: () => <ImagePropertyTest src="image_db_test.riv" />,
351+
play: async ({ canvasElement }) => {
352+
const canvas = within(canvasElement);
353+
354+
// Wait for the Rive file to load
355+
await waitFor(() => {
356+
expect(canvas.getByTestId('load-random-image')).toBeTruthy();
357+
expect(canvas.getByTestId('clear-image')).toBeTruthy();
358+
}, { timeout: 3000 });
359+
360+
const loadImageButton = canvas.getByTestId('load-random-image');
361+
const clearImageButton = canvas.getByTestId('clear-image');
362+
363+
expect(canvas.queryByTestId('current-image-url')).toBeNull();
364+
365+
// Load a random image
366+
await userEvent.click(loadImageButton);
348367

368+
// Wait for the image to load and URL to appear
369+
await waitFor(() => {
370+
expect(canvas.getByTestId('current-image-url')).toBeTruthy();
371+
}, { timeout: 5000 });
372+
373+
// Verify the image URL is displayed
374+
const imageUrlElement = canvas.getByTestId('current-image-url');
375+
expect(imageUrlElement.textContent).toContain('Current image: https://picsum.photos');
376+
377+
// Clear the image
378+
await userEvent.click(clearImageButton);
379+
380+
// Load another image to test it works multiple times
381+
await userEvent.click(loadImageButton);
382+
383+
// Wait for the new image to load
384+
await waitFor(() => {
385+
expect(canvas.getByTestId('current-image-url')).toBeTruthy();
386+
}, { timeout: 5000 });
387+
}
388+
};

examples/src/components/DataBindingTests.tsx

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import Rive, {
99
useViewModelInstanceNumber,
1010
useViewModelInstanceEnum,
1111
useViewModelInstanceColor,
12-
useViewModelInstanceTrigger
12+
useViewModelInstanceTrigger,
13+
useViewModelInstanceImage,
14+
decodeImage
1315
} from '@rive-app/react-webgl2';
1416

1517

@@ -522,3 +524,90 @@ export const PersonInstances = ({ src }: { src: string }) => {
522524
</div>
523525
);
524526
};
527+
528+
export const ImagePropertyTest = ({ src }: { src: string }) => {
529+
const [currentImageUrl, setCurrentImageUrl] = useState<string>('');
530+
const [isLoading, setIsLoading] = useState<boolean>(false);
531+
532+
const { rive, RiveComponent } = useRive({
533+
src,
534+
artboard: "Artboard",
535+
stateMachines: "State Machine 1",
536+
autoplay: true,
537+
autoBind: false,
538+
});
539+
540+
const viewModel = useViewModel(rive, { name: 'Post' });
541+
const viewModelInstance = useViewModelInstance(viewModel, { rive });
542+
543+
const { setValue: setImage } = useViewModelInstanceImage(
544+
'image',
545+
viewModelInstance
546+
);
547+
548+
const loadRandomImage = async () => {
549+
if (!setImage) return;
550+
551+
setIsLoading(true);
552+
try {
553+
const imageUrl = `https://picsum.photos/400/300?random=${Date.now()}`;
554+
setCurrentImageUrl(imageUrl);
555+
556+
const response = await fetch(imageUrl);
557+
const imageBuffer = await response.arrayBuffer();
558+
const decodedImage = await decodeImage(new Uint8Array(imageBuffer));
559+
560+
setImage(decodedImage);
561+
562+
decodedImage.unref();
563+
} catch (error) {
564+
console.error('Failed to load image:', error);
565+
} finally {
566+
setIsLoading(false);
567+
}
568+
};
569+
570+
const clearImage = () => {
571+
if (setImage) {
572+
setImage(null);
573+
setCurrentImageUrl('');
574+
}
575+
};
576+
577+
578+
return (
579+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px' }}>
580+
<div style={{ width: '400px', height: '300px', border: '1px solid #ccc' }}>
581+
<RiveComponent />
582+
</div>
583+
584+
{rive === null ? (
585+
<div data-testid="loading-text">Loading…</div>
586+
) : (
587+
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
588+
<button
589+
onClick={loadRandomImage}
590+
disabled={isLoading}
591+
data-testid="load-random-image"
592+
>
593+
{isLoading ? 'Loading...' : 'Load Random Image'}
594+
</button>
595+
596+
<button
597+
onClick={clearImage}
598+
disabled={isLoading}
599+
data-testid="clear-image"
600+
>
601+
Clear Image
602+
</button>
603+
</div>
604+
)}
605+
606+
{currentImageUrl && (
607+
<div style={{ fontSize: '12px', color: '#666' }}>
608+
<span data-testid="current-image-url">Current image: {currentImageUrl}</span>
609+
</div>
610+
)}
611+
</div>
612+
);
613+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useCallback } from 'react';
2+
import { ViewModelInstance, ViewModelInstanceAssetImage } from '@rive-app/canvas';
3+
import { UseViewModelInstanceImageResult, RiveRenderImage } from '../types';
4+
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
5+
6+
/**
7+
* Hook for interacting with image properties of a ViewModelInstance.
8+
*
9+
* @param path - Path to the image property (e.g. "profileImage" or "group/avatar")
10+
* @param viewModelInstance - The ViewModelInstance containing the image property
11+
* @returns An object with a setter function
12+
*/
13+
export default function useViewModelInstanceImage(
14+
path: string,
15+
viewModelInstance?: ViewModelInstance | null
16+
): UseViewModelInstanceImageResult {
17+
const result = useViewModelInstanceProperty<ViewModelInstanceAssetImage, undefined, UseViewModelInstanceImageResult>(
18+
path,
19+
viewModelInstance,
20+
{
21+
getProperty: useCallback((vm, p) => vm.image(p), []),
22+
getValue: useCallback(() => undefined, []), // Images don't have a readable value
23+
defaultValue: null,
24+
buildPropertyOperations: useCallback((safePropertyAccess) => ({
25+
setValue: (newValue: RiveRenderImage | null) => {
26+
safePropertyAccess(prop => { prop.value = newValue; });
27+
}
28+
}), [])
29+
}
30+
);
31+
32+
return {
33+
setValue: result.setValue
34+
};
35+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import useViewModelInstanceBoolean from './hooks/useViewModelInstanceBoolean';
99
import useViewModelInstanceColor from './hooks/useViewModelInstanceColor';
1010
import useViewModelInstanceEnum from './hooks/useViewModelInstanceEnum';
1111
import useViewModelInstanceTrigger from './hooks/useViewModelInstanceTrigger';
12+
import useViewModelInstanceImage from './hooks/useViewModelInstanceImage';
1213
import useResizeCanvas from './hooks/useResizeCanvas';
1314
import useRiveFile from './hooks/useRiveFile';
1415

@@ -26,6 +27,7 @@ export {
2627
useViewModelInstanceColor,
2728
useViewModelInstanceEnum,
2829
useViewModelInstanceTrigger,
30+
useViewModelInstanceImage,
2931
RiveProps,
3032
};
3133
export {

src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type decodeImage,
23
Rive,
34
RiveFile,
45
RiveFileParameters,
@@ -185,4 +186,14 @@ export type UseViewModelInstanceTriggerResult = {
185186
* Fires the property trigger.
186187
*/
187188
trigger: () => void;
189+
};
190+
191+
export type RiveRenderImage = Awaited<ReturnType<typeof decodeImage>>;
192+
193+
export type UseViewModelInstanceImageResult = {
194+
/**
195+
* Set the value of the image.
196+
* @param value - The image to set.
197+
*/
198+
setValue: (value: RiveRenderImage | null) => void;
188199
};

0 commit comments

Comments
 (0)