pose-core
Version:
Factory for Pose animation state machines
236 lines (203 loc) • 6.11 kB
text/typescript
import {
AncestorValueList,
Props,
ValueMap,
PoseMap,
PassiveValueMap,
ReadValue,
ReadValueFromSource,
CreateValue,
ConvertValue,
ActivePoses,
SetValueNative
} from '../types';
import { getPoseValues } from '../inc/selectors';
import { resolveProp } from './setter';
import { Pose } from '..';
type ValueFactoryProps<V, A, TD> = {
poses: PoseMap<A, TD>;
passive?: PassiveValueMap;
ancestorValues: AncestorValueList<V>;
readValue: ReadValue<V>;
createValue: CreateValue<V>;
convertValue: ConvertValue<V>;
userSetValues: { [key: string]: V };
initialPose?: string | string[];
readValueFromSource?: ReadValueFromSource;
setValueNative: SetValueNative;
activePoses: ActivePoses;
props: Props;
};
export const DEFAULT_INITIAL_POSE = 'init';
const isScale = (key: string) => key.includes('scale');
const defaultReadValueFromSource = (key: string) => (isScale(key) ? 1 : 0);
/**
* This is a prime candidate for refactoring, possibly in combination
* with the `posesToSearch.find()` function.
*/
const readValueFromPose = <A, TD>(
pose: Pose<A, TD>,
key: string,
props: Props
) => {
const valueToResolve =
pose.applyAtEnd && pose.applyAtEnd[key] !== undefined
? pose.applyAtEnd[key]
: pose[key] !== undefined
? pose[key]
: pose.applyAtStart && pose.applyAtStart[key] !== undefined
? pose.applyAtStart[key]
: 0;
return resolveProp(valueToResolve, props);
};
const getPosesToSearch = (pose: string | string[]) => {
const posesToSearch = Array.isArray(pose) ? pose : [pose];
posesToSearch.push(DEFAULT_INITIAL_POSE);
return posesToSearch;
};
const getInitialValue = <A, TD>(
poses: PoseMap<A, TD>,
key: string,
initialPose: string | string[],
props: Props,
readValueFromSource: ReadValueFromSource = defaultReadValueFromSource,
activePoses: ActivePoses
) => {
const posesToSearch = getPosesToSearch(initialPose);
const pose: string = posesToSearch.filter(Boolean).find((name: string) => {
const thisPose: Pose<A, TD> = poses[name];
return (
thisPose &&
(thisPose[key] !== undefined ||
(thisPose.applyAtStart && thisPose.applyAtStart[key] !== undefined) ||
(thisPose.applyAtEnd && thisPose.applyAtEnd[key] !== undefined))
);
});
// Prime active values array with found pose as first item
// TODO: Instead of priming with 'init' if no pose found, create
// a unique pose with the readValueFromSource return value
activePoses.set(key, [pose || DEFAULT_INITIAL_POSE]);
return pose
? readValueFromPose(poses[pose], key, props)
: readValueFromSource(key, props);
};
const createValues = <V, A, TD>(
values: ValueMap<V>,
{
userSetValues,
createValue,
convertValue,
readValueFromSource,
initialPose,
poses,
activePoses,
props
}: ValueFactoryProps<V, A, TD>
) => (key: string) => {
if (values.has(key)) return;
let value: V;
// If this user has explicitly created a value, use that
if (userSetValues && userSetValues[key] !== undefined) {
value = convertValue(userSetValues[key], key, props);
// Or create a new value
} else {
const initValue = getInitialValue(
poses,
key,
initialPose,
props,
readValueFromSource,
activePoses
);
value = createValue(initValue, key, props);
}
values.set(key, value);
};
const scrapeValuesFromPose = <V, A, TD>(
values: ValueMap<V>,
props: ValueFactoryProps<V, A, TD>
) => (key: string) => {
const pose = props.poses[key];
Object.keys(getPoseValues<A, TD>(pose)).forEach(
createValues<V, A, TD>(values, props)
);
};
const getAncestorValue = <V>(
key: string,
fromParent: boolean | string,
ancestors: AncestorValueList<V>
): V => {
if (fromParent === true) {
return ancestors[0] && ancestors[0].values.get(key);
} else {
const foundAncestor = ancestors.find(({ label }) => label === fromParent);
return foundAncestor && foundAncestor.values.get(key);
}
};
const bindPassiveValues = <V, A, TD>(
values: ValueMap<V>,
{
passive,
ancestorValues,
createValue,
readValue,
props
}: ValueFactoryProps<V, A, TD>
) => (key: string) => {
const [valueKey, passiveProps, fromParent] = passive[key];
const valueToBind =
fromParent && ancestorValues.length
? getAncestorValue<V>(valueKey, fromParent, ancestorValues)
: values.has(valueKey)
? values.get(valueKey)
: false;
if (!valueToBind) return;
const newValue = createValue(readValue(valueToBind), key, props, {
passiveParentKey: valueKey,
passiveParent: valueToBind,
passiveProps
});
values.set(key, newValue);
};
const setNativeValues = <V, A, TD>({
setValueNative,
initialPose,
props,
poses
}: ValueFactoryProps<V, A, TD>) => {
const valuesHaveSet = new Set();
const setValues = (pose: Pose<A, TD>, propKey: string) => {
if (pose[propKey]) {
for (const key in pose[propKey]) {
if (!valuesHaveSet.has(key)) {
valuesHaveSet.add(key);
setValueNative(key, resolveProp(pose[propKey][key], props), props);
}
}
}
};
getPosesToSearch(initialPose).forEach(poseKey => {
const pose = poses[poseKey];
if (pose) {
setValues(pose, 'applyAtEnd');
setValues(pose, 'applyAtStart');
}
});
};
const createValueMap = <V, A, TD>(
props: ValueFactoryProps<V, A, TD>
): ValueMap<V> => {
const { poses, passive } = props;
const values: ValueMap<V> = new Map();
// Scrape values from poses
Object.keys(poses).forEach(scrapeValuesFromPose<V, A, TD>(values, props));
// Set any remaining values that won't be animated but need to be set initially
// An optimisation here might be to have a mutative value in props or some other flag
// so we know if we've found `applyAtStart` or `applyAtEnd` during value scraping
setNativeValues(props);
// Initiate passive values
if (passive)
Object.keys(passive).forEach(bindPassiveValues<V, A, TD>(values, props));
return values;
};
export default createValueMap;