feat: add instance property hooks

This commit is contained in:
Adam
2025-04-13 12:38:27 -07:00
parent 0e40840e6f
commit 26cb0b5dec
11 changed files with 462 additions and 102 deletions

View File

@@ -0,0 +1,32 @@
import { ViewModelInstanceBoolean } from '@rive-app/canvas';
import { UseViewModelInstanceBooleanParameters, UseViewModelInstanceBooleanResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
/**
* Hook for interacting with boolean ViewModel instance properties.
*
* @param path Path to the property (e.g. "isVisible" or "nested/isVisible")
* @param userParameters Optional parameters including initial value
* @returns Object with value and setter function
*/
export default function useViewModelInstanceBoolean(
path: string,
userParameters?: UseViewModelInstanceBooleanParameters
): UseViewModelInstanceBooleanResult {
return useViewModelInstancePropertyValues<
boolean,
UseViewModelInstanceBooleanParameters,
ViewModelInstanceBoolean,
{ value: boolean; setValue: (value: boolean) => void }
>(
path,
userParameters,
false,
(instance, name) => instance.boolean(name),
(instance) => instance.value,
(_instance, value, setValue) => ({
value,
setValue
})
);
}

View File

@@ -0,0 +1,43 @@
import { ViewModelInstanceColor } from '@rive-app/canvas';
import { UseViewModelInstanceColorResult, UseViewModelInstanceColorParameters } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
/**
* Hook for interacting with color ViewModel instance properties.
*
* @param path Path to the property (e.g. "color" or "nested/color")
* @param userParameters Optional parameters including initial value
* @returns Object with value, setter function, and color utilities
*/
export default function useViewModelInstanceColor(
path: string,
userParameters?: UseViewModelInstanceColorParameters
): UseViewModelInstanceColorResult {
return useViewModelInstancePropertyValues<
number,
UseViewModelInstanceColorParameters,
ViewModelInstanceColor,
{
value: number;
setValue: (value: number) => void;
rgb: (r: number, g: number, b: number) => void;
rgba: (r: number, g: number, b: number, a: number) => void;
alpha: (a: number) => void;
opacity: (o: number) => void;
}
>(
path,
userParameters,
0,
(instance, name) => instance.color(name),
(instance) => instance.value,
(instance, value, setValue) => ({
value,
setValue,
rgb: (r, g, b) => instance?.rgb(r, g, b),
rgba: (r, g, b, a) => instance?.rgba(r, g, b, a),
alpha: (a) => instance?.alpha(a),
opacity: (o) => instance?.opacity(o),
})
);
}

View File

@@ -0,0 +1,37 @@
import { ViewModelInstanceEnum } from '@rive-app/canvas';
import { UseViewModelInstanceEnumParameters, UseViewModelInstanceEnumResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
/**
* Hook for interacting with enum ViewModel instance properties.
*
* @param path Path to the property (e.g. "state" or "nested/state")
* @param userParameters Optional parameters including initial value
* @returns Object with value, values array, and setter function
*/
export default function useViewModelInstanceEnum(
path: string,
userParameters?: UseViewModelInstanceEnumParameters
): UseViewModelInstanceEnumResult {
return useViewModelInstancePropertyValues<
string,
UseViewModelInstanceEnumParameters,
ViewModelInstanceEnum,
{
value: string;
setValue: (value: string) => void;
values: string[];
}
>(
path,
userParameters,
'',
(instance, name) => instance.enum(name),
(instance) => instance.value,
(instance, value, setValue) => ({
value,
setValue,
values: instance?.values || []
})
);
}

View File

@@ -0,0 +1,32 @@
import { ViewModelInstanceNumber } from '@rive-app/canvas';
import { UseViewModelInstanceNumberParameters, UseViewModelInstanceNumberResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
/**
* Hook for interacting with numeric ViewModel instance properties.
*
* @param path Path to the property (e.g. "itemCount" or "nested/itemCount")
* @param userParameters Optional parameters including initial value
* @returns Object with value and setter function
*/
export default function useViewModelInstanceNumber(
path: string,
userParameters?: UseViewModelInstanceNumberParameters
): UseViewModelInstanceNumberResult {
return useViewModelInstancePropertyValues<
number,
UseViewModelInstanceNumberParameters,
ViewModelInstanceNumber,
{ value: number; setValue: (value: number) => void }
>(
path,
userParameters,
0,
(instance, name) => instance.number(name),
(instance) => instance.value,
(_instance, value, setValue) => ({
value,
setValue
})
);
}

View File

@@ -66,7 +66,7 @@ export default function useViewModelInstanceProperty(
if (viewModelInstance) {
let index = 0;
while (index < path?.length) {
if(!viewModelInstance) {
if (!viewModelInstance) {
return null;
}
viewModelInstance = viewModelInstance?.viewModel(path[index]);

View File

@@ -0,0 +1,110 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { EventCallback, EventType, ViewModelInstance, ViewModelInstanceValue } from '@rive-app/canvas';
import { UseViewModelInstancePropertyType } from '../types';
import useViewModelInstanceProperty from './useViewModelInstanceProperty';
export function useViewModelInstancePropertyValues<
T extends unknown,
P extends UseViewModelInstancePropertyType,
V extends ViewModelInstanceValue,
R extends Record<string, any>
>(
path: string,
userParameters: P | undefined,
defaultValue: T,
propertyGetter: (instance: ViewModelInstance, name: string) => V | null,
valueGetter: (propertyInstance: V) => T,
resultBuilder: (
propertyInstance: V | null,
value: T,
setValue: (value: T) => void
) => R
): R {
const [propertyInstance, setPropertyInstance] = useState<V | null>(null);
const [value, setValueState] = useState<T>(
(userParameters as any)?.initialValue ?? defaultValue
);
const pathSegments = path.includes('/') ? path.split('/') : [];
const propertyName = path.includes('/') ? path.split('/').pop() || path : path;
const basePath = pathSegments.length > 0 ? pathSegments.slice(0, -1) : [];
const viewModelInstance = useViewModelInstanceProperty(basePath, userParameters);
// Track current arguments to prevent unnecessary updates
const currentArgs = useRef<{
path: string,
parameters: P | undefined,
viewModelInstance: ViewModelInstance | null
} | null>(null);
useEffect(() => {
function searchProperty() {
if (!viewModelInstance) {
setPropertyInstance(null);
return;
}
const instance = propertyGetter(viewModelInstance, propertyName);
if (instance !== null) {
if ((userParameters as any)?.initialValue !== undefined) {
(instance as any).value = (userParameters as any).initialValue;
}
setValueState(valueGetter(instance));
setPropertyInstance(instance);
currentArgs.current = {
parameters: userParameters,
path,
viewModelInstance,
};
}
}
const argsChanged = !currentArgs.current ||
currentArgs.current.path !== path ||
currentArgs.current.viewModelInstance !== viewModelInstance;
if (argsChanged) {
userParameters?.rive?.on(EventType.Load, searchProperty);
searchProperty();
}
return () => {
userParameters?.rive?.off(EventType.Load, searchProperty);
};
}, [path, userParameters, viewModelInstance, propertyName, valueGetter]);
// We subscribe to value changes by default with the property hooks.
useEffect(() => {
if (!propertyInstance) return;
const handleChange: EventCallback = (event) => {
setValueState(event as unknown as T);
};
propertyInstance.on(handleChange);
return () => {
propertyInstance.off(handleChange);
};
}, [propertyInstance]);
const setValue = useCallback((newValue: T) => {
if (propertyInstance) {
(propertyInstance as any).value = newValue;
} else {
// If no instance yet, just update React state
setValueState(newValue);
}
}, [propertyInstance]);
return resultBuilder(propertyInstance, value, setValue);
}

View File

@@ -0,0 +1,32 @@
import { ViewModelInstanceString } from '@rive-app/canvas';
import { UseViewModelInstanceStringParameters, UseViewModelInstanceStringResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
/**
* Hook for interacting with string ViewModel instance properties.
*
* @param path Path to the property (e.g. "text" or "nested/text")
* @param userParameters Optional parameters including initial value
* @returns Object with value and setter function
*/
export default function useViewModelInstanceString(
path: string,
userParameters?: UseViewModelInstanceStringParameters
): UseViewModelInstanceStringResult {
return useViewModelInstancePropertyValues<
string,
UseViewModelInstanceStringParameters,
ViewModelInstanceString,
{ value: string; setValue: (value: string) => void }
>(
path,
userParameters,
'',
(instance, name) => instance.string(name),
(instance) => instance.value,
(_instance, value, setValue) => ({
value,
setValue
})
);
}

View File

@@ -0,0 +1,48 @@
import { useEffect, useCallback } from 'react';
import { ViewModelInstanceTrigger } from '@rive-app/canvas';
import { UseViewModelInstanceTriggerParameters, UseViewModelInstanceTriggerResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
/**
* Hook for interacting with trigger ViewModel instance properties.
*
* @param path Path to the property (e.g. "buttonPress" or "nested/buttonPress")
* @param userParameters Optional parameters including onTrigger callback
* @returns Object with trigger function
*/
export default function useViewModelInstanceTrigger(
path: string,
userParameters?: UseViewModelInstanceTriggerParameters
): UseViewModelInstanceTriggerResult {
return useViewModelInstancePropertyValues<
void,
UseViewModelInstanceTriggerParameters,
ViewModelInstanceTrigger,
{
trigger: () => void;
}
>(
path,
userParameters,
undefined,
(instance, name) => instance.trigger(name),
() => undefined,
(instance, _, __) => {
useEffect(() => {
if (instance && userParameters?.onTrigger) {
instance.on(userParameters.onTrigger);
return () => {
instance.off(userParameters.onTrigger);
};
}
}, [instance, userParameters?.onTrigger]);
return {
trigger: useCallback(() => {
instance?.trigger();
}, [instance])
};
}
);
}

View File

@@ -1,90 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import {
EventType,
ViewModelInstance,
ViewModelInstanceNumber,
} from '@rive-app/canvas';
import { UseViewModelInstanceNumberParameters } from '../types';
import useViewModelInstanceProperty from './useViewModelInstanceProperty';
const defaultParams: UseViewModelInstanceNumberParameters = {
viewModelInstance: null,
initialValue: 0,
};
const equal = (
name: string,
params: UseViewModelInstanceNumberParameters | null,
viewModelInstance: ViewModelInstance | null,
to: HookArguments | null
): boolean => {
if (!params || !to) {
return false;
}
if (
params.initialValue !== to.parameters.initialValue ||
name !== to.name ||
viewModelInstance !== to.viewModelInstance
) {
return false;
}
return true;
};
type HookArguments = {
name: string,
parameters: UseViewModelInstanceNumberParameters,
viewModelInstance: ViewModelInstance | null,
}
/**
* Custom hook for fetching a view model instance value.
*
* @param name - name of the propery
* @param path - Path to reach the required property
* @param userParameters - Parameters to load view model instance number
* @returns
*/
export default function useViewModelNumber(
name: string,
path: string[] = [],
userParameters?: UseViewModelInstanceNumberParameters
): ViewModelInstanceNumber | null {
const [viewModel, setViewModelValue] =
useState<ViewModelInstanceNumber | null>(null);
const currentArguments = useRef<HookArguments | null>(
null
);
const viewModelInstance = useViewModelInstanceProperty(path, userParameters);
useEffect(() => {
const parameters = {
...defaultParams,
...userParameters,
};
function searchViewModelValue() {
const instanceValue = viewModelInstance?.number(name) || null;
if(instanceValue !== null && parameters.initialValue !== undefined) {
instanceValue.value = parameters.initialValue;
}
setViewModelValue(instanceValue);
currentArguments.current = {
parameters,
name,
viewModelInstance,
};
}
if (!equal(name, parameters, viewModelInstance, currentArguments.current)) {
parameters.rive?.on(EventType.Load, searchViewModelValue);
searchViewModelValue();
}
return () => {
parameters.rive?.off(EventType.Load, searchViewModelValue);
};
}, [name, userParameters, viewModelInstance]);
return viewModel;
}

View File

@@ -3,7 +3,12 @@ import useRive from './hooks/useRive';
import useStateMachineInput from './hooks/useStateMachineInput';
import useViewModel from './hooks/useViewModel';
import useViewModelInstance from './hooks/useViewModelInstance';
import useViewModelNumber from './hooks/useViewModelNumber';
import useViewModelInstanceNumber from './hooks/useViewModelInstanceNumber';
import useViewModelInstanceString from './hooks/useViewModelInstanceString';
import useViewModelInstanceBoolean from './hooks/useViewModelInstanceBoolean';
import useViewModelInstanceColor from './hooks/useViewModelInstanceColor';
import useViewModelInstanceEnum from './hooks/useViewModelInstanceEnum';
import useViewModelInstanceTrigger from './hooks/useViewModelInstanceTrigger';
import useViewModelProperties from './hooks/useViewModelProperties';
import useResizeCanvas from './hooks/useResizeCanvas';
import useRiveFile from './hooks/useRiveFile';
@@ -16,7 +21,12 @@ export {
useRiveFile,
useViewModel,
useViewModelInstance,
useViewModelNumber,
useViewModelInstanceNumber,
useViewModelInstanceString,
useViewModelInstanceBoolean,
useViewModelInstanceColor,
useViewModelInstanceEnum,
useViewModelInstanceTrigger,
useViewModelProperties,
RiveProps,
};

View File

@@ -99,19 +99,125 @@ export type UseViewModelInstanceColorParameters =
initialValue?: number;
};
export type UseViewModelInstanceEnumParameters =
UseViewModelInstanceValueParameters & {
initialValue?: string;
};
export type UseViewModelInstanceTriggerParameters = UseViewModelInstanceValueParameters & {
/**
* Callback that runs when the trigger is fired.
*/
onTrigger?: () => void;
};
export type UseViewModelInstancePropertyType =
| UseViewModelInstanceNumberParameters
| UseViewModelInstanceStringParameters
| UseViewModelInstanceBooleanParameters
| UseViewModelInstanceColorParameters;
| UseViewModelInstanceColorParameters
| UseViewModelInstanceEnumParameters;
export type AcceptedVieModelType<T> =
T extends UseViewModelInstanceNumberParameters
? ViewModelInstanceNumber
: T extends UseViewModelInstanceStringParameters
? ViewModelInstanceString
: T extends UseViewModelInstanceBooleanParameters
? ViewModelInstanceBoolean
: T extends UseViewModelInstanceColorParameters
? ViewModelInstanceColor
: never;
? ViewModelInstanceNumber
: T extends UseViewModelInstanceStringParameters
? ViewModelInstanceString
: T extends UseViewModelInstanceBooleanParameters
? ViewModelInstanceBoolean
: T extends UseViewModelInstanceColorParameters
? ViewModelInstanceColor
: never;
export type UseViewModelInstanceNumberResult = {
/**
* The current value of the number.
*/
value: number;
/**
* Set the value of the number.
* @param value - The value to set the number to.
*/
setValue: (value: number) => void;
};
export type UseViewModelInstanceStringResult = {
/**
* The current value of the string.
*/
value: string;
/**
* Set the value of the string.
* @param value - The value to set the string to.
*/
setValue: (value: string) => void;
};
export type UseViewModelInstanceBooleanResult = {
/**
* The current value of the boolean.
*/
value: boolean;
/**
* Set the value of the boolean.
* @param value - The value to set the boolean to.
*/
setValue: (value: boolean) => void;
};
export type UseViewModelInstanceColorResult = {
/**
* The current value of the color.
*/
value: number;
/**
* Set the value of the color.
* @param value - The value to set the color to.
*/
setValue: (value: number) => void;
/**
* Set the red value of the color.
* @param r - The red value to set the color to.
*/
rgb: (r: number, g: number, b: number) => void;
/**
* Set the red, green, blue, and alpha values of the color.
* @param r - The red value to set the color to.
* @param g - The green value to set the color to.
* @param b - The blue value to set the color to.
* @param a - The alpha value to set the color to.
*/
rgba: (r: number, g: number, b: number, a: number) => void;
/**
* Set the alpha value of the color.
* @param a - The alpha value to set the color to.
*/
alpha: (a: number) => void;
/**
* Set the opacity value of the color.
* @param o - The opacity value to set the color to.
*/
opacity: (o: number) => void;
};
export type UseViewModelInstanceEnumResult = {
/**
* The current value of the enum.
*/
value: string;
/**
* Set the value of the enum.
* @param value - The value to set the enum to.
*/
setValue: (value: string) => void;
/**
* The values of the enum.
*/
values: string[];
};
export type UseViewModelInstanceTriggerResult = {
/**
* Fires the property trigger.
*/
trigger: () => void;
};