tween-fn v1.1.7
Tween-fn
A tiny (~1.7kb gzipped) library for coordinating complex animation sequences, it focuses on providing the means for writing a description of a sequence using a small but powerful set of function primitives which are fully composable. Unlike existing alternatives like GSAP
or animejs
, tween-fn
does not assume anything about your application, consequently, it doesn't know anything about the DOM or whichever JS framework you happen to prefer, this allows you to be very explicit about what you're trying to achieve
What do we mean by complex animation sequences?
It basically boils down to two concepts: parallel and sequencial execution, and some of the most popular libraries available today do offer some degree of support for these concepts, but they usually delegate the task of composition to the end-user in one way or another
Libraries like react-motion
although great in their own right, seem to be optimized for one-off animations (what we call unit
s), the API for composing sequences is weird and seems to have been included retroactively
This need can arise surprisingly often, here's an example from a simple trivia app:
Notice how we needed the background to begin a looping animation right away, then the logo fades in followed by a tap icon a few milliseconds later, which also includes a looping animation to emphasize the tapping gesture. This can not only happen on macro interactions like a page transition, but also on micro interactions like a button click:
Here we wanted to ..., you might call both separately with a delay, but its nice to have the idea of this being a single unit be captured in the code. You could argue that the difference between orchestrating the two separately and controlling both transitions in a single unit is barely noticeable, and you'd be right, but still, we'd like to have a choice to decide wether or not it makes sense to make that consession and not have the limitations of a library to make the decision for us
Enabling composition
One of the main pain points with existing solutions is how hard it is to organize a sequence of animations, we see this as being fundamentally an issue of expressiveness, and as it turns out, you can address most of these problems by making your primitives composable
// `anime.js`'s Timeline function forces you to
// "flatten" the sequence
// composable primitives allow you to group
// sequences which are semantically related
// in a natural way
Promoting abstraction
By abstracting out highly composable pieces into reusable units, you gain a good deal of readability as the intention behind the sequence is embeded into the code itself:
const master = parallel([
backgroundShapesLoop,
sequence([
bannerIntro,
parallel([
tapIndicatorIntro,
tapIndicatorLoop,
]),
]),
]);
This is the actual code for that intro sequence we showed earlier, the main takeaway from this snippet of code is that we've rescued the intention behind the animation and made it explicit. Taking advantage of these abstraction and composition abilities leads to code that is easy to reason about
Playground
Here's a remake of a button taken from Super Mario Party, unsurprisingly, there's a lot that goes into making those juicy Nintendo animations, check the code to see what's under the hood, or read this article which goes in-depth into the making of this button
Closing thoughts
The web as we know it is constantly changing, and as the pool of devices that access our webapps become more and more capable, new possibilites are unlocked. Check out the react integration, for bugfixes and suggestions you can use the appropiate channels on Github.
react
integration
There are X things we want from a React integration ...
redux
integration using a queue
Useful when you need to split a sequence in different places. This rescues the intention and makes it explicit
Installation
yarn add tween-fn
or
npm i -S tween-fn
Quickstart
We start by writing a sequence using the primitives provided by the library
const seq = sequence([
unit({
duration: 250,
change: (value) => {
el.style.width = `${interpolate(value, 100, 150)}px`;
},
}),
unit({
duration: 500,
ease: easings.SQUARED,
change: (value) => {
el.style.transform = `translateX(${interpolate(value, 0, 50)})`;
},
}),
]);
And then pass that to run
to play it
run(seq);
For cancelation, you can use the subscription object returned from run
const subscription = run(seq);
// somewhere else in your application...
subscription.unsubscribe();
Recipes
Animating multiple transform
s
To apply multiple transforms at the same time, use the meta
object supplied to your callback functions to rescue the original value of the transform, then compute the new transformation using the computeTransform
function
unit({
begin: (meta) => { meta.originalTransform = circle.style.transform; },
change: (value, { originalTransform }) => {
circle.style.transform = computeTransform(
originalTransform,
`scale(${interpolate(value, 1, 1.2)}) translateX(${interpolate(value, 0, 100)}px)`,
);
},
});
SVG path animations
Use the interpolatePath
utility to easily animate between two different paths
const path1 = '';
const path2 = '';
unit({
change: (value) => {
path.setAttribute('d', interpolatePath(value, path1, path2));
},
});
Staggering
Given a list of items, you can coordinate a staggering animation using mergeAll
and adding delay to each animation
mergeAll(nodeList.map((node, i) => unit({
delay: 100 * i, // 100 miliseconds of delay between each animation
change: (value) => {
// do something...
},
})));
API
unit
Used to create an animation, takes the following options
interface TweenOptions {
iterations?: number;
direction?: directions;
from?: number;
to?: number;
delay?: number;
duration?: number;
ease?: easingFn;
begin?: (meta?: object | null) => void;
update?: (y: number, meta?: object | null) => void;
complete?: (y: number, meta?: object | null) => void;
change?: (y?: number, meta?: object) => void;
loop?: (y?: number, meta?: object) => void;
meta?: object;
}
mergeAll
Used for parallel execution of multiple animations
mergeAll(ts: Array<Tween>): Tween
sequence
Describes a sequence of animations, where each animation supplied will run only after the previous one has completed (unless a negative value for delay
is used)
sequence(ts: Array<Tween>): Tween
run
Executes the given description
run(tween: Tween): Subscription
Utils
easings
A dictionary holding common easing functions, available functions are
easings.LINEAR
easings.SQUARED
easings.CUBIC
easings.QUART
easings.QUINT
easings.EASE_OUT_QUINT
easings.EASE_IN_OUT_QUINT
easings.EASE_OUT_ELASTIC
Check out easings.net for more information regarding these
interpolate
Linearly interpolates between two values
interpolate(progress: number, start: number, end: number): number
interpolatePath
Linearly interpolates between two paths, paths must have the same number of points
interpolatePath(progress: number, p1: string, p2: string): string
computeTransform
Replaces values defined by source
from target
. Returns the new transform string
computeTransform(target: string, source: string): string
// outputs `translate(-50%, -50%) scale(1) rotate(0)`
computeTransform(
'translate(-50%, -50%) scale(1.2) rotate(5deg)',
'scale(1) rotate(0)'
)
Roadmap
- playback controls
- more easing functions
- add examples