Easing

Easing functions help to give a dynamic to transition. In Cartesian terms, they give a y value for x (where x is 0 .. 1). Or in temporal terms, you can think of them as giving a value at time t (where t is 0 .. 1).

Normally, a way of getting from 0 to 1 would be count upwards by some fixed amount. And if that's all you need, ixfx's count and numericRange functions might do the job.

For example, count by 0.1 from 0 to 1:

This is a linear function, with the same 'speed' throughout the progression towards the end point.

In contrast, easing functions give some dynamics to the journey. For example, maybe it starts slowly, but gets faster as it nears the end, as in this cubicIn function:

Or perhaps gathering speed quickly, but then slowing down toward the end (cubicOut):

Jump to the Defined Easings to see a list of pre-defined easing functions.

Usage

Easings can be driven by time or ticks, created by Easings.time or Easings.tick, respectively.

Each of these returns an Easings.Easing, which has this type:

type Easing = {
  // Returns value at this point
  // Usually 0-1, but some functions can overshoot
  compute():number
  // Reset easing to beginning
  reset():void
  // Returns true if easing is finished
  get isDone():boolean
}

Time-based

Example: 'sineIn' easing that takes one second to complete:

import { Easings } from "https://unpkg.com/ixfx/dist/modulation.js";

// Initialise
const e = Easings.time(`sineIn`, 1000);

// Returns value at this point in time.
// Usually 0-1, but some functions overshoot bounds
const v = e.compute();
// ..call e.compute() whenever the 'latest' value is needed

// Can check if easing has completed
if (e.isDone) ...

Time starts being counted from when the easing is initialised (ie. calling Easings.time()). For this reason, you likely want to initialise just before it's needed, or values you will lose part of the range. Calling reset() can also be useful for resetting the timer.

You'll probably call compute() inside an existing draw/update loop that is running. If you don't have a loop already, here is a snippet that returns an easing over time:

import { Generators, Flow } from "https://unpkg.com/ixfx/dist/bundle.js";
import { Easings } from "https://unpkg.com/ixfx/dist/modulation.js";

// Define an easing that takes 1 second to reach end
const e = Easings.time(`sineIn`, 1000);

// Increment by 10% from 0 to 1
const range = Generators.numericPercent(0.1); 
// Iterate over range at 100ms intervals
for await (const v of Flow.interval(range, 100)) {
  console.log(e.compute());
}

Tick-based

Example: a sineOut easing that takes 100 ticks to complete:

import { Easings } from "https://unpkg.com/ixfx/dist/modulation.js";

// Set up
const e = Easings.tick(`sineOut`, 100);

// Get the value of the easing function at this point
const v = e.compute();
// ...each call to compute() will 'advance' the easing
// function by one tick. So after 100 calls to compute()
// the function will be done.

// Can check if easing has completed
if (e.isDone) ...

compute typically returns value between 0..1, but some functions purposefully overshoot this range (such as the back series of easings, shown below).

Once you have this value, it can be applied as necessary. For example, positioning an element:

el.style.transform = `translate(${e.compute() * width}px, 0px)`;

See the source of the demos below for more ideas, or try it out on fn-vis

It's a function

It's good to remember that easing functions are not magic. They just return a number based on an input within the range of 0..1.

An easing-function-from-scratch example is an exponential function:

const fn = (x) => Math.pow(x,2);
fn(0);    // 0
fn(0.5);  // 0.25
fn(0.75); // 0.5625
fn(1);    // 1

You can see this in action:

Try the following functions:

  • x: Linear, returns the same input value
  • x/2: Halves input value
  • Math.sqrt(x): Square root of value
  • Math.random() * x: Reduces value by some random amount
  • x + (0.1 - Math.random()* 0.2): Jitters value by up to 10%

Pre-defined easing functions can be used directly to avoid the time/tick mechanism. In this case, you need to provide an input value on a scale of 0..1.

import { Easings } from "https://unpkg.com/ixfx/dist/modulation.js";

// Get the function
const fn = Easings.functions.cubicIn;

// Use it to transform an input value (0..1)
// and set to variable 'v'
const v = fn(0.5);

Demos

Here, the easing function advances on each call (tapping the circle), rather than by time.

Below is the common usage of time-based easing

In this demo, a target value is reached over time by using an easing function.

Custom curves

Simple cubic beziers

You can make your own easing curve using a simplified cubic bezier. See this curve, for example. It's defined by points 0, 1.24, 1, -1.15. The key points we need here are 1.24 (point b), and -1.15 (point d).

This can be used as follows:

import { Easings } from "https://unpkg.com/ixfx/dist/modulation.js";
const e = Easings.time(Easings.fromCubicBezier(1.24, -1.15), 1000);
e.compute();

Use the cubic bezier editor to shape a curve, but make sure the first point remains 0 and the third point remains 1.

Defined easings

There are several well known easing functions which are pre-defined in ixfx.

Credits

Most easing functions by Andrey Sitnik and Ivan Solovev