pose-core
Version:
Factory for Pose animation state machines
351 lines (304 loc) • 9.61 kB
text/typescript
import {
Props,
Pose,
PoserState,
PoseMap,
ChildPosers,
GetTransitionProps,
GetInstantTransition,
StartAction,
StopAction,
ResolveTarget,
TransformPose,
AddTransitionDelay,
ConvertTransitionDefinition,
TransitionFactory,
TransitionMapFactory,
TransitionMap,
SetValue,
SetValueNative,
ApplyMap
} from '../types';
import { getPoseValues } from '../inc/selectors';
import { invariant } from 'hey-listen';
type AnimationsPromiseList = Array<Promise<any>>;
type SetterFactoryProps<V, A, C, P, TD> = {
state: PoserState<V, A, C, P>;
poses: PoseMap<A, TD>;
getInstantTransition: GetInstantTransition<V, A>;
startAction: StartAction<V, A, C>;
stopAction: StopAction<C>;
setValue: SetValue<V>;
setValueNative: SetValueNative;
addActionDelay: AddTransitionDelay<A>;
getTransitionProps: GetTransitionProps<V>;
resolveTarget: ResolveTarget<V>;
convertTransitionDefinition: ConvertTransitionDefinition<V, A, TD>;
transformPose?: TransformPose<V, A, C, P, TD>;
posePriority?: string[];
forceRender?: (props: Props) => any;
};
export const resolveProp = (target: any, props: Props) =>
typeof target === 'function' ? target(props) : target;
const poseDefault = <A, TD>(
pose: Pose<A, TD>,
prop: string,
defaultValue: any,
resolveProps: Props
) =>
pose && pose[prop] !== undefined
? resolveProp(pose[prop], resolveProps)
: defaultValue;
const startChildAnimations = <V, A, C, P, TD>(
children: ChildPosers<V, A, C, P>,
next: string,
pose: Pose<A, TD>,
props: Props
) => {
const animations: Array<Promise<any>> = [];
const delay = poseDefault<A, TD>(pose, 'delayChildren', 0, props);
const stagger = poseDefault<A, TD>(pose, 'staggerChildren', 0, props);
const staggerDirection = poseDefault<A, TD>(
pose,
'staggerDirection',
1,
props
);
const maxStaggerDuration = (children.size - 1) * stagger;
const generateStaggerDuration =
staggerDirection === 1
? (i: number) => i * stagger
: (i: number) => maxStaggerDuration - i * stagger;
Array.from(children).forEach((child, i) => {
animations.push(
child.set(next, {
delay: delay + generateStaggerDuration(i)
})
);
});
return animations;
};
const resolveTransition = <V, A, TD>(
transition: TransitionMap<A, TD> | TransitionMapFactory<A, TD>,
key: string,
value: V,
props: Props,
convertTransitionDefinition: ConvertTransitionDefinition<V, A, TD>,
getInstantTransition: GetInstantTransition<V, A>
): A => {
let resolvedTransition: ReturnType<TransitionFactory<A, TD>>;
/**
* transition: () => {}
*/
if (typeof transition === 'function') {
const resolvedTransitionMap = transition(props);
/**
* transition: () => { d: () => tweenFn }
*/
resolvedTransition = resolveTransition(
resolvedTransitionMap,
key,
value,
props,
convertTransitionDefinition,
getInstantTransition
);
// Or if it's a keyed object
} else if (transition[key] || transition.default) {
const keyTransition = transition[key] || transition.default;
/**
* transition: {
* x: () => {}
* }
*/
if (typeof keyTransition === 'function') {
resolvedTransition = (keyTransition as TransitionFactory<A, TD>)(props);
/**
* transition: {
* x: { type: 'tween' } || false
* }
*/
} else {
resolvedTransition = keyTransition;
}
/**
* transition: { type: 'tween' } || false
*/
} else {
resolvedTransition = (transition as any) as typeof resolvedTransition;
}
return resolvedTransition === false
? getInstantTransition(value, props)
: convertTransitionDefinition(value, resolvedTransition, props);
};
const findInsertionIndex = (
poseList: string[],
priorityList: string[],
priorityIndex: number
) => {
let insertionIndex = 0;
for (let i = priorityIndex - 1; i >= 0; i--) {
const nextHighestPriorityIndex = poseList.indexOf(priorityList[i]);
if (nextHighestPriorityIndex !== -1) {
insertionIndex = nextHighestPriorityIndex + 1;
break;
}
}
return insertionIndex;
};
const applyValues = <V>(
toApply: ApplyMap,
values: Map<string, V>,
props: Props,
setValue: SetValue<V>,
setValueNative: SetValueNative
) => {
invariant(
typeof toApply === 'object',
'applyAtStart and applyAtEnd must be of type object'
);
return Object.keys(toApply).forEach(key => {
const valueToSet = resolveProp(toApply[key], props);
values.has(key)
? setValue(values.get(key), valueToSet)
: setValueNative(key, valueToSet, props);
});
};
const createPoseSetter = <V, A, C, P, TD>(
setterProps: SetterFactoryProps<V, A, C, P, TD>
) => {
const {
state,
poses,
startAction,
stopAction,
getInstantTransition,
addActionDelay,
getTransitionProps,
resolveTarget,
transformPose,
posePriority,
convertTransitionDefinition,
setValue,
setValueNative,
forceRender
} = setterProps;
return (next: string, nextProps: Props = {}, propagate: boolean = true) => {
const { children, values, props, activeActions, activePoses } = state;
const { delay = 0 } = nextProps;
const hasChildren = children.size;
const baseTransitionProps = {
...props,
...nextProps
};
let nextPose = poses[next];
const getChildAnimations = (): AnimationsPromiseList =>
hasChildren && propagate
? startChildAnimations(children, next, nextPose, baseTransitionProps)
: [];
const getParentAnimations = (): AnimationsPromiseList => {
if (!nextPose) return [];
const { applyAtStart } = nextPose;
// Apply initial values, if set
if (applyAtStart) {
applyValues(
applyAtStart,
values,
baseTransitionProps,
setValue,
setValueNative
);
if (forceRender) forceRender(baseTransitionProps);
}
if (transformPose) nextPose = transformPose(nextPose, next, state);
const { preTransition, transition: getTransition, applyAtEnd } = nextPose;
// Run pre-transition prep, if set
if (preTransition) preTransition(baseTransitionProps);
const animations = Object.keys(getPoseValues(nextPose)).map(key => {
// Add pose to the pose order
const valuePoses = activePoses.has(key)
? activePoses.get(key)
: (activePoses.set(key, []), activePoses.get(key));
// Remove pose if it already exists
const existingIndex = valuePoses.indexOf(next);
if (existingIndex !== -1) valuePoses.splice(existingIndex, 1);
// Insert pose in its priority order
const priority = posePriority ? posePriority.indexOf(next) : 0;
const insertionIndex =
priority <= 0
? 0
: findInsertionIndex(valuePoses, posePriority, priority);
valuePoses.splice(insertionIndex, 0, next);
return insertionIndex === 0
? new Promise(complete => {
const value = values.get(key);
const transitionProps = {
...baseTransitionProps,
key,
value
};
// Resolve target from pose
const target = resolveTarget(
value,
resolveProp(nextPose[key], transitionProps)
);
// Stop the current action
if (activeActions.has(key)) stopAction(activeActions.get(key));
// Get the transition
const resolveTransitionProps = {
to: target,
...transitionProps,
...getTransitionProps(value, target, transitionProps)
};
let transition = resolveTransition<V, A, TD>(
getTransition,
key,
value,
resolveTransitionProps,
convertTransitionDefinition,
getInstantTransition
);
// Add delay if defined on pose
const poseDelay =
delay || resolveProp(nextPose.delay, transitionProps);
if (poseDelay) {
transition = addActionDelay(poseDelay, transition);
}
// Start transition
activeActions.set(key, startAction(value, transition, complete));
})
: Promise.resolve();
});
return applyAtEnd
? [
Promise.all(animations).then(() => {
applyValues(
applyAtEnd,
values,
baseTransitionProps,
setValue,
setValueNative
);
})
]
: animations;
};
// Check before and afterChildren props to check if we need to reorder these animations
if (nextPose && hasChildren) {
// parent before children
if (resolveProp(nextPose.beforeChildren, baseTransitionProps)) {
return Promise.all(getParentAnimations()).then(() =>
Promise.all(getChildAnimations())
);
// children before parent
} else if (resolveProp(nextPose.afterChildren, baseTransitionProps)) {
return Promise.all(getChildAnimations()).then(() =>
Promise.all(getParentAnimations())
);
}
}
// Otherwise, run all animations in parallel
return Promise.all([...getParentAnimations(), ...getChildAnimations()]);
};
};
export default createPoseSetter;