1.1.7 • Published 3 years ago

tween-fn v1.1.7

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

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 units), 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:

GIF

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:

GIF

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 transforms

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
1.1.7

3 years ago

1.1.6

3 years ago

1.1.5

3 years ago

1.0.2

3 years ago

1.0.5

3 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago