diff --git a/src/hooks/useViewModel.ts b/src/hooks/useViewModel.ts new file mode 100644 index 00000000..5bfeb970 --- /dev/null +++ b/src/hooks/useViewModel.ts @@ -0,0 +1,74 @@ +import { useState, useEffect, useRef } from 'react'; +import { EventType, Rive, ViewModel } from '@rive-app/canvas'; +import { UseViewModelParameters } from '../types'; + +const defaultParams: UseViewModelParameters = { + useDefault: false, + name: '', +}; + +const equal = ( + params: UseViewModelParameters | null, + to: UseViewModelParameters | null +): boolean => { + if (!params || !to) { + return false; + } + if (params.useDefault !== to.useDefault || params.name !== to.name) { + return false; + } + return true; +}; + +/** + * Custom hook for fetching a view model. + * + * @param rive - Rive instance + * @param userParameters - Parameters to load view model + * @returns + */ +export default function useViewModel( + rive: Rive | null, + userParameters?: UseViewModelParameters +): ViewModel | null { + const [viewModel, setViewModel] = useState(null); + const currentParams = useRef(null); + + useEffect(() => { + const parameters = { + ...defaultParams, + ...userParameters, + }; + + function getViewModel(): ViewModel | null { + if (rive) { + if (parameters?.useDefault) { + return rive!.defaultViewModel(); + } else if (parameters?.name) { + return rive.viewModelByName(parameters?.name); + } + } + return null; + } + function setViewModelValue() { + if (!rive) { + setViewModel(null); + currentParams.current = null; + } else { + const viewModel = getViewModel(); + setViewModel(viewModel); + currentParams.current = parameters; + } + } + + if (!equal(parameters, currentParams.current)) { + rive?.on(EventType.Load, setViewModelValue); + setViewModelValue(); + } + return () => { + rive?.off(EventType.Load, setViewModelValue); + }; + }, [rive, userParameters]); + + return viewModel; +} diff --git a/src/hooks/useViewModelInstance.ts b/src/hooks/useViewModelInstance.ts new file mode 100644 index 00000000..e34c3161 --- /dev/null +++ b/src/hooks/useViewModelInstance.ts @@ -0,0 +1,91 @@ +import { useState, useEffect, useRef } from 'react'; +import { + EventType, + Rive, + ViewModel, + ViewModelInstance, +} from '@rive-app/canvas'; +import { UseViewModelInstanceParameters } from '../types'; + +const defaultParams: UseViewModelInstanceParameters = { + useDefault: false, + useNew: true, + name: '', +}; + +const equal = ( + params: UseViewModelInstanceParameters | null, + to: UseViewModelInstanceParameters | null +): boolean => { + if (!params || !to) { + return false; + } + if ( + params.useDefault !== to.useDefault || + params.useNew !== to.useNew || + params.name !== to.name + ) { + return false; + } + return true; +}; + +/** + * Custom hook for fetching a view model instance. + * + * @param rive - Rive instance + * @param userParameters - Parameters to load view model instance + * @returns + */ +export default function useViewModel( + rive: Rive | null, + viewModel: ViewModel | null, + userParameters?: UseViewModelInstanceParameters +) : ViewModelInstance | null { + const [viewModelInstance, setViewModelInstance] = + useState(null); + const currentParams = useRef(null); + + useEffect(() => { + const parameters = { + ...defaultParams, + ...userParameters, + }; + + function setInstance(instance: ViewModelInstance | null) { + setViewModelInstance(instance); + rive!.bindViewModelInstance(instance); + currentParams.current = parameters; + } + function getViewModelInstance(): ViewModelInstance | null { + if (viewModel) { + if (parameters.useDefault) { + return viewModel?.defaultInstance(); + } else if (parameters.name) { + return viewModel?.instanceByName(parameters.name); + } else if (parameters.useNew) { + return viewModel?.instance(); + } + } + return null; + } + function setViewModelValue() { + if (!rive || !viewModel) { + setViewModelInstance(null); + } else { + const instance = getViewModelInstance(); + setInstance(instance ?? null); + } + } + + if (!equal(parameters, currentParams.current)) { + rive?.on(EventType.Load, setViewModelValue); + setViewModelValue(); + } + return () => { + rive?.off(EventType.Load, setViewModelValue); + }; + }, [rive, userParameters]); + + return viewModelInstance; +} diff --git a/src/hooks/useViewModelInstanceProperty.ts b/src/hooks/useViewModelInstanceProperty.ts new file mode 100644 index 00000000..f38739b9 --- /dev/null +++ b/src/hooks/useViewModelInstanceProperty.ts @@ -0,0 +1,99 @@ +import { useState, useEffect, useRef } from 'react'; +import { + EventType, + ViewModelInstance, +} from '@rive-app/canvas'; +import { UseViewModelInstanceValueParameters } from '../types'; + +const defaultParams: UseViewModelInstanceValueParameters = { + viewModelInstance: null, +}; + +const equal = ( + path: string[], + params: UseViewModelInstanceValueParameters | null, + to: HookArguments | null +): boolean => { + if (!params || !to) { + return false; + } + if ( + params.rive !== to.parameters.rive || + params.viewModelInstance !== to.parameters.viewModelInstance || + path.join('') !== to.path.join('') + ) { + return false; + } + return true; +}; + +type HookArguments = { + path: string[], + parameters: UseViewModelInstanceValueParameters, +} + +/** + * 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 useViewModelInstanceProperty( + path: string[] = [], + userParameters?: UseViewModelInstanceValueParameters +): ViewModelInstance | null { + const [viewModelInstance, setViewModelValue] = + useState(null); + const currentArguments = useRef( + null + ); + + useEffect(() => { + const parameters = { + ...defaultParams, + ...userParameters, + }; + + function getInstanceValue(): ViewModelInstance | null { + let viewModelInstance: ViewModelInstance | null = null; + if (userParameters?.viewModelInstance) { + viewModelInstance = userParameters?.viewModelInstance; + } else if (userParameters?.rive) { + viewModelInstance = userParameters?.rive?.viewModelInstance; + } + if (viewModelInstance) { + let index = 0; + while (index < path?.length) { + if(!viewModelInstance) { + return null; + } + viewModelInstance = viewModelInstance?.viewModel(path[index]); + index++; + } + return viewModelInstance; + } + return null; + } + + function searchViewModelInstance() { + const instanceValue = getInstanceValue(); + setViewModelValue(instanceValue); + currentArguments.current = { + parameters, + path, + }; + } + + if (!equal(path, parameters, currentArguments.current)) { + parameters.rive?.on(EventType.Load, searchViewModelInstance); + searchViewModelInstance(); + } + return () => { + parameters.rive?.off(EventType.Load, searchViewModelInstance); + }; + }, [path, userParameters]); + + return viewModelInstance; +} diff --git a/src/hooks/useViewModelNumber.ts b/src/hooks/useViewModelNumber.ts new file mode 100644 index 00000000..cc3c961c --- /dev/null +++ b/src/hooks/useViewModelNumber.ts @@ -0,0 +1,90 @@ +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(null); + const currentArguments = useRef( + 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; +} diff --git a/src/hooks/useViewModelProperties.ts b/src/hooks/useViewModelProperties.ts new file mode 100644 index 00000000..2610d466 --- /dev/null +++ b/src/hooks/useViewModelProperties.ts @@ -0,0 +1,173 @@ +import { useState, useEffect, useRef } from 'react'; +import { + EventType, + ViewModelInstance, + ViewModelInstanceValue, +} from '@rive-app/canvas'; +import { UseViewModelInstanceValueParameters } from '../types'; + +const defaultParams: UseViewModelInstanceValueParameters = { + viewModelInstance: null, +}; + +const equal = ( + properties: string[], + params: UseViewModelInstanceValueParameters | null, + to: HookArguments | null +): boolean => { + if (!params || !to) { + return false; + } + if (properties.length !== to.properties.length) { + return false; + } + for (let i = 0; i < properties.length; i += 1) { + if (properties[i] !== to.properties[i]) { + return false; + } + } + if ( + params.rive !== to.parameters.rive || + params.viewModelInstance !== to.parameters.viewModelInstance + ) { + return false; + } + return true; +}; + +type HookArguments = { + properties: string[]; + parameters: UseViewModelInstanceValueParameters; +}; + +type PropertyResult = { + query: string; + property: ViewModelInstanceValue | null; +}; + +/** + * Custom hook for fetching a view model instance value. + * + * @param properties - list of queries properties + * @param path - Path to reach the required property + * @param userParameters - Parameters to load view model properties + * @returns + */ +export default function useViewModelProperties( + properties: string[], + userParameters?: UseViewModelInstanceValueParameters +): PropertyResult[] { + const [result, setResult] = useState([]); + const currentArguments = useRef(null); + + useEffect(() => { + const parameters = { + ...defaultParams, + ...userParameters, + }; + + function getViewModelInstance() { + if (parameters.viewModelInstance) { + return parameters.viewModelInstance; + } else if (parameters.rive) { + return parameters.rive.viewModelInstance; + } + return null; + } + + function getPropertyViewModelInstance( + path: string + ): ViewModelInstance | null { + const viewModelInstance: ViewModelInstance | null = getViewModelInstance(); + if(path === '') { + return viewModelInstance; + } + return viewModelInstance?.viewModel(path) || null; + } + + function getProperty( + viewModelInstance: ViewModelInstance | null, + name: string + ): ViewModelInstanceValue | null { + if (viewModelInstance) { + const viewModelProperties = viewModelInstance.properties; + const propertyData = viewModelProperties.find((candidate) => { + if (candidate.name === name) { + return candidate; + } + }); + if (propertyData !== null) { + switch (propertyData!.type.toString()) { + case 'number': + return viewModelInstance.number(name); + case 'string': + return viewModelInstance.string(name); + case 'boolean': + return viewModelInstance.boolean(name); + case 'enumType': + return viewModelInstance.enum(name); + case 'color': + return viewModelInstance.color(name); + case 'trigger': + return viewModelInstance.trigger(name); + } + } + } + return null; + } + + function searchViewModelValues() { + const viewModelInstance = getViewModelInstance(); + if (!viewModelInstance) { + setResult([]); + } else { + const result: PropertyResult[] = []; + properties.forEach((propertyQuery) => { + if (propertyQuery === '') { + result.push({ + query: propertyQuery, + property: null, + }); + } else { + const propertyParts = propertyQuery.split('/'); + const propertyName = propertyParts.pop(); + const propertyViewModelPath = propertyParts.join('/'); + const propertyViewModelInstance = getPropertyViewModelInstance( + propertyViewModelPath + ); + const property = getProperty( + propertyViewModelInstance, + propertyName! + ); + if (property) { + result.push({ + query: propertyQuery, + property: property, + }); + } else { + result.push({ + query: propertyQuery, + property: null, + }); + } + } + }); + setResult(result); + } + currentArguments.current = { + properties: properties, + parameters: parameters, + }; + } + + if (!equal(properties, parameters, currentArguments.current)) { + parameters.rive?.on(EventType.Load, searchViewModelValues); + searchViewModelValues(); + } + return () => { + parameters.rive?.off(EventType.Load, searchViewModelValues); + }; + }, [name, userParameters]); + + return result; +} diff --git a/src/hooks/useViewModelProperty.ts b/src/hooks/useViewModelProperty.ts new file mode 100644 index 00000000..ff02e3cd --- /dev/null +++ b/src/hooks/useViewModelProperty.ts @@ -0,0 +1,112 @@ +import { useState, useEffect, useRef } from 'react'; +import { + EventType, + ViewModelInstance, +} from '@rive-app/canvas'; +import { + UseViewModelInstancePropertyType, + AcceptedVieModelType, +} from '../types'; +import useViewModelInstanceProperty from './useViewModelInstanceProperty'; +import { DataType } from '@rive-app/canvas/rive_advanced.mjs'; + +const defaultParams: UseViewModelInstancePropertyType = { + viewModelInstance: null, +}; + +const equal = ( + name: string, + params: U | 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: UseViewModelInstancePropertyType; + 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 useViewModelProperty< + T extends UseViewModelInstancePropertyType, + U extends AcceptedVieModelType +>(name: string, path: string[] = [], userParameters?: T): U | null { + const [viewModel, setViewModelValue] = useState | null>(null); + const currentArguments = useRef(null); + + const viewModelInstance = useViewModelInstanceProperty(path, userParameters); + + useEffect(() => { + const parameters: T = { + ...defaultParams, + ...(userParameters as T), + }; + + function getVMI(name: string): U | null { + const properties = viewModelInstance!.properties; + const propData = properties.find((value) => value.name === name); + if (propData === null) { + return null; + } + if (propData!.type === DataType.number) { + return viewModelInstance!.number(name) as U; + } else if (propData!.type === DataType.string) { + return viewModelInstance!.string(name) as U; + } else if (propData!.type === DataType.boolean) { + return viewModelInstance!.boolean(name) as U; + } else if (propData!.type === DataType.color) { + return viewModelInstance!.color(name) as U; + } + return null; + } + + function setInitialValue(property: U, params: T) { + if (params.initialValue !== undefined) { + property.value = params.initialValue; + } + } + + function searchViewModelValue() { + const instanceValue = getVMI(name); + if (instanceValue !== null) { + setInitialValue(instanceValue, parameters); + } + 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 as U; +} diff --git a/src/index.ts b/src/index.ts index 6ba9119c..9173c425 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,29 @@ import Rive, { RiveProps } from './components/Rive'; import useRive from './hooks/useRive'; import useStateMachineInput from './hooks/useStateMachineInput'; +import useViewModel from './hooks/useViewModel'; +import useViewModelInstance from './hooks/useViewModelInstance'; +import useViewModelNumber from './hooks/useViewModelNumber'; +import useViewModelProperties from './hooks/useViewModelProperties'; import useResizeCanvas from './hooks/useResizeCanvas'; import useRiveFile from './hooks/useRiveFile'; export default Rive; -export { useRive, useStateMachineInput, useResizeCanvas, useRiveFile , RiveProps }; -export { RiveState, UseRiveParameters, UseRiveFileParameters, UseRiveOptions } from './types'; +export { + useRive, + useStateMachineInput, + useResizeCanvas, + useRiveFile, + useViewModel, + useViewModelInstance, + useViewModelNumber, + useViewModelProperties, + RiveProps, +}; +export { + RiveState, + UseRiveParameters, + UseRiveFileParameters, + UseRiveOptions, +} from './types'; export * from '@rive-app/canvas'; diff --git a/src/types.ts b/src/types.ts index 9ea138e1..5f2a1308 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,11 @@ import { RiveFile, RiveFileParameters, RiveParameters, + ViewModelInstance, + ViewModelInstanceBoolean, + ViewModelInstanceNumber, + ViewModelInstanceString, + ViewModelInstanceColor, } from '@rive-app/canvas'; import { ComponentProps, RefCallback } from 'react'; @@ -57,3 +62,56 @@ export type RiveFileState = { riveFile: RiveFile | null; status: FileStatus; }; + +export type UseViewModelParameters = { + useDefault?: boolean; + name?: string; +}; + +export type UseViewModelInstanceParameters = { + useNew?: boolean; + useDefault?: boolean; + name?: string; +}; + +export type UseViewModelInstanceValueParameters = { + viewModelInstance?: ViewModelInstance | null; + rive?: Rive | null; +}; + +export type UseViewModelInstanceNumberParameters = + UseViewModelInstanceValueParameters & { + initialValue?: number; + }; + +export type UseViewModelInstanceStringParameters = + UseViewModelInstanceValueParameters & { + initialValue?: string; + }; + +export type UseViewModelInstanceBooleanParameters = + UseViewModelInstanceValueParameters & { + initialValue?: boolean; + }; + +export type UseViewModelInstanceColorParameters = + UseViewModelInstanceValueParameters & { + initialValue?: number; + }; + +export type UseViewModelInstancePropertyType = + | UseViewModelInstanceNumberParameters + | UseViewModelInstanceStringParameters + | UseViewModelInstanceBooleanParameters + | UseViewModelInstanceColorParameters; + +export type AcceptedVieModelType = + T extends UseViewModelInstanceNumberParameters + ? ViewModelInstanceNumber + : T extends UseViewModelInstanceStringParameters + ? ViewModelInstanceString + : T extends UseViewModelInstanceBooleanParameters + ? ViewModelInstanceBoolean + : T extends UseViewModelInstanceColorParameters + ? ViewModelInstanceColor + : never;