popmotion-pose
Version:
A declarative animation library for HTML and SVG
203 lines (166 loc) • 5.21 kB
text/typescript
import {
value,
chain,
delay as delayAction,
Action,
ColdSubscription
} from 'popmotion';
import poseFactory, { Poser } from 'pose-core';
import {
Value,
TransitionProps,
Transformer,
PopmotionPoserFactoryConfig,
TransitionDefinition,
CubicBezierArgs
} from '../types';
import defaultTransitions, { just } from '../inc/default-transitions';
import { animationLookup, easingLookup } from '../inc/lookups';
import { getValueType } from '../inc/value-types';
import { invariant } from 'hey-listen';
import * as easing from '@popmotion/easing';
import { Easing } from '@popmotion/easing';
export { Poser };
const createPassiveValue = (
init: any,
parent: Value,
transform: Transformer
) => {
const raw = value(transform(init));
parent.raw.subscribe((v: any) => raw.update(transform(v)));
return { raw };
};
const createValue = (init: any) => {
const type = getValueType(init);
const raw = value(init);
return { raw, type };
};
const addActionDelay = (delay = 0, transition: Action) =>
chain(delayAction(delay), transition);
const isCubicBezierArgs = (
args: Easing[] | CubicBezierArgs
): args is CubicBezierArgs => typeof args[0] === 'number';
// At the moment this function just uses `type` as a key - in the future
// we could infer the animation type based on the properties being provided
const getAction = (
v: Value,
{ type = 'tween', ease: definedEase, ...def }: TransitionDefinition,
{ from, to, velocity }: TransitionProps
) => {
invariant(
animationLookup[type] !== undefined,
`Invalid transition type '${type}'. Valid transition types are: tween, spring, decay, physics and keyframes.`
);
let ease: Exclude<
typeof definedEase,
keyof typeof easingLookup | CubicBezierArgs
>;
// Convert ease definition into easing function
if (type === 'tween') {
if (typeof definedEase !== 'function') {
if (typeof definedEase === 'string') {
invariant(
easingLookup[definedEase] !== undefined,
`Invalid easing type '${definedEase}'. popmotion.io/pose/api/config`
);
ease = easingLookup[definedEase];
} else if (Array.isArray(definedEase) && isCubicBezierArgs(definedEase)) {
invariant(
definedEase.length === 4,
`Cubic bezier arrays must contain four numerical values.`
);
const [x1, y1, x2, y2] = definedEase;
ease = easing.cubicBezier(x1, y1, x2, y2);
}
}
}
ease = ease || (definedEase as typeof ease);
const baseProps =
type !== 'keyframes'
? {
from,
to,
velocity,
ease
}
: { ease };
return animationLookup[type]({ ...baseProps, ...def });
};
const isAction = (action: any): action is Action =>
typeof action.start !== 'undefined';
const pose = <P>({
transformPose,
addListenerToValue,
extendAPI,
readValueFromSource,
posePriority,
setValueNative
}: PopmotionPoserFactoryConfig<P, TransitionDefinition>) =>
poseFactory<Value, Action, ColdSubscription, P, TransitionDefinition>({
bindOnChange: (values, onChange) => key => {
if (!values.has(key)) return;
const { raw } = values.get(key);
raw.subscribe(onChange[key]);
},
readValue: ({ raw }) => raw.get(),
setValue: ({ raw }, to) => raw.update(to),
createValue: (
init,
key,
{ elementStyler },
{ passiveParent, passiveProps } = {}
) => {
const val = passiveParent
? createPassiveValue(init, passiveParent, passiveProps)
: createValue(init);
val.raw.subscribe(addListenerToValue(key, elementStyler));
return val;
},
convertValue: (raw, key, { elementStyler }) => {
raw.subscribe(addListenerToValue(key, elementStyler));
return {
raw,
type: getValueType(raw.get())
};
},
getTransitionProps: ({ raw, type }, to) => ({
from: raw.get(),
velocity: raw.getVelocity(),
to,
type
}),
resolveTarget: (_, to) => to,
selectValueToRead: ({ raw }) => raw,
startAction: ({ raw }, action, complete) => {
const reaction = {
update: (v: any) => raw.update(v),
complete
};
return action.start(reaction);
},
stopAction: action => action.stop(),
getInstantTransition: (_, { to }) => just(to),
convertTransitionDefinition: (
val: Value,
def: TransitionDefinition | Action,
props: TransitionProps
): Action => {
if (isAction(def)) return def;
const { delay, min, max, round, ...remainingDef } = def;
let action = getAction(val, remainingDef, props);
const outputPipe: Function[] = [];
if (delay) action = addActionDelay(delay, action);
if (min !== undefined) outputPipe.push((v: number) => Math.max(v, min));
if (max !== undefined) outputPipe.push((v: number) => Math.min(v, max));
if (round) outputPipe.push(Math.round);
return outputPipe.length ? action.pipe(...outputPipe) : action;
},
setValueNative,
addActionDelay,
defaultTransitions,
transformPose,
readValueFromSource,
posePriority,
extendAPI
});
export default pose;