inital work for data binding hooks

This commit is contained in:
Hernan Torrisi
2025-01-07 16:40:24 -08:00
parent c9a4dc0b5c
commit f11d17bf3e
7 changed files with 570 additions and 2 deletions

74
src/hooks/useViewModel.ts Normal file
View File

@@ -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<ViewModel | null>(null);
const currentParams = useRef<UseViewModelParameters | null>(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;
}

View File

@@ -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<ViewModelInstance | null>(null);
const currentParams = useRef<UseViewModelInstanceParameters | null>(null);
useEffect(() => {
const parameters = {
...defaultParams,
...userParameters,
};
function setInstance(instance: ViewModelInstance | null) {
setViewModelInstance(instance);
rive!.setDataContextFromInstance(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;
}

View File

@@ -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<ViewModelInstance | null>(null);
const currentArguments = useRef<HookArguments | null>(
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;
}

View File

@@ -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<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

@@ -0,0 +1,174 @@
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
) {
console.log('paso 3');
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<PropertyResult[]>([]);
const currentArguments = useRef<HookArguments | null>(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;
}

View File

@@ -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';

View File

@@ -3,6 +3,7 @@ import {
RiveFile,
RiveFileParameters,
RiveParameters,
ViewModelInstance,
} from '@rive-app/canvas';
import { ComponentProps, RefCallback } from 'react';
@@ -57,3 +58,23 @@ 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;
};