Mastering Stepwise Easing: Chaining Multiple Functions for Unique Animation

The graph of 'Stepwise easing' with the various easing functions.

Easing—the technique of varying the rate of change in motion—can make your animations feel significantly more organic and professional.

In the example below, the top circle moves linearly, while the bottom one uses easing to create a more dynamic feel.



Let me show you the 'Stepwise easing' made with various easing functions. And the example code of animation using the 'Stepwise easing'.

[Japanese version / 日本語版はこちら]

 

The Fundamentals of Easing Functions

What is the 'easing'?

At its core, an easing function maps an input value (usually normalized from 0.0 to 1.0) to an output value along a specific curve, defining the acceleration and deceleration of an object.

For example, the code and the graph of the animation shown before are like these.


// no easing
function noEasing(x) {
  return x;
}

// easing
function easing(x) {
  return x * x * x;
}

The graph of no-easing and easing function return values.

The left shows no easing, while the right demonstrates cubic easing. The x-axis represents the input parameter, and the y-axis shows the function's output. The easing function's curve is simply x cubed.

 

How can I make an easing function?

Many easing functions exist, which are documented in the 'Easing Cheat Sheet'.

Easing Cheat Sheet from Robert Penner's Easing Functions
https://easings.net/

Clicking each graph reveals the function's description. The code is provided in the "Math function" section. The code is written in TypeScript, and can be adapted for p5.js with minor modifications.


// TypeScript
function easeInCubic(x: number): number {
  return x * x * x;
}

// p5.js
function easeInCubic(x) {
  return x * x * x;
}


 







Example: p5.js code using easeInCubic



// Circular motion with easing

const w = 720;
const h = w;
const cNum = 3;
const fRate = 30;
const cycle = fRate * 2;

function setup() {
  createCanvas(w, h);
  frameRate(fRate);
}

function draw() {

  const ease = easeInCubic((frameCount % cycle) / cycle)

  translate(w * 0.5, h * 0.5);
  background(240);

  noStroke();
  for (let c = 0; c < cNum; c++) {
    let r = 0.2 * (0.5 + sin(PI * ease));
    let t = TWO_PI * (ease + c / cNum);
    let x = w * r * cos(t);
    let y = h * r * sin(t);
    fill((c * 100) % 255);
    circle(x, y, w * 0.1);
  }

}

// Easing function : Standard easeInCubic implementation
function easeInCubic(x) {
  return x * x * x;
}

 

Combining Multiple Functions for Complex Motion

I experimented with the idea of chaining multiple easing functions to create a more complex, multi-stage motion. By sequencing different curves, the resulting graph resembles a set of stairs—so I call this technique "Stepwise Easing".

The graph of 'Stepwise easing' with the various easing functions.

Below is an example animations using a single easeInOutCubic function.


This example chains three functions: easeInOutCubic, easeOutQuad, and easeInCubic.

The graph of 'Stepwise easing' with three easing functions.

Using Stepwise Easing produces more varied motion than using a single easing function. Randomly changing the sequence creates fresh movement patterns each time.

 







 

Implementation in Processing using 'Stepwise Easing'


I explored the visual possibilities of blendMode(DIFFERENCE) with rotating rectangles. The overlapping patterns create striking, high-contrast forms during the motion, and Stepwise Easing effectively emphasizes these moments by adding pauses or stop-motion effects to the sequence.

An interesting shape of rectangles drawn with the 'blendMode(DIFFERENCE)'.

This code doesn't display any images on screen but instead generates image files in the frames directory. You can compile these files into an animation.

Please feel free to use this example code under the terms of the GPL.


/**
 * Life Goes Round.
 * An animation using stepwise easing.
 *
 * @author @deconbatch
 * @version 0.1
 * @license GPL Version 3 http://www.gnu.org/licenses/
 * Processing 3.5.3
 * 2022.01.23
 */


void setup() {
  size(720, 720);
  colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
  smooth();
  noLoop();
	rectMode(CENTER);

  int   frmRate  = 30;
  int   cycleSec = 3;
  int   frmMax   = frmRate * cycleSec;
  float rectSiz  = min(width, height) * random(0.5, 0.8);
  float phaseR   = random(PI);
  float phaseT   = random(PI);
  float hueOrg   = random(360.0);

  // easing functions
  ArrayList<Ease> easing = new ArrayList<Ease>();
  easing.add(new InOutQuart());
  easing.add(new OutQuart());
  easing.add(new InBack());
  easing.add(new InQuad());
  easing.add(new OutBack());
  int cycleMax = easing.size();
  int easingStart = floor(random(cycleMax));
  
	translate(width * 0.5, height * 0.5);
  noStroke();
  float easePrev  = 0.0;
  float easeRatio = 0.0;
  for (int cycleCnt = 0; cycleCnt < cycleMax; cycleCnt++) {
    for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {
      // Calculate stepwise easing ratio
      float frmRatio = map(frmCnt, 0, frmMax - 1, 0.0, 1.0);
      easeRatio = easePrev + easing.get((easingStart + cycleCnt) % cycleMax).ease(frmRatio) / cycleMax;
      float radii = abs(sin(phaseR + easeRatio * PI)) * rectSiz * 0.3;
      float hueBase = hueOrg + 360.0 * easeRatio;
  
      blendMode(BLEND);
      background(hueBase % 360.0, 30.0, 60.0, 100.0);

      blendMode(DIFFERENCE);
      fill((hueBase + 60.0) % 360.0, 30.0, 80.0, 100.0);
      for (int p = 0; p < 8; p++) {
        int sign = (p % 2 == 0) ? 1 : -1;
        for (float r = 0.2; r < 0.5; r += 0.1) {
          float t = PI * 0.25 * p + sign * (phaseT + TWO_PI * easeRatio) * r * 2.5;
          float x = r * width * cos(t);
          float y = r * height * sin(t);
          rect(x, y, rectSiz, rectSiz, radii);
        }
      }
  
      saveFrame("frames/" + String.format("%02d", cycleCnt) + ".00." + String.format("%04d", frmCnt) + ".png");

    }

    // Add stop-motion pause
    for (int i = 0; i < frmRate; i++) {
      saveFrame("frames/" + String.format("%02d", cycleCnt) + ".01." + String.format("%04d", i) + ".png");
    }
    
    easePrev = easeRatio;

  }
  exit();
}


/**
 * Ease : interface for easing function implementations.
 * Based on Robert Penner's Easing Functions (https://easings.net/)
 */
public interface Ease {
  public float ease(float _t);
}

public class InOutQuart implements Ease {
  public float ease(float _t) {
    return (_t < 0.5) ? 8.0 * _t * _t * _t * _t : 1 - pow(-2.0 * _t + 2.0, 4) / 2.0;
  }
}

public class OutQuart implements Ease {
  public float ease(float _t) {
    return 1.0 - pow(1.0 - _t, 4);
  }
}

public class InQuad implements Ease {
  public float ease(float _t) {
    return pow(_t, 2);
  }
}

public class InBack implements Ease {
  public float ease(float _t) {
    float c1 = 1.70158;
    float c3 = c1 + 1;
    return c3 * _t * _t * _t - c1 * _t * _t;
  }
}

public class OutBack implements Ease {
  public float ease(float _t) {
    float c1 = 1.70158;
    float c3 = c1 + 1;
    return 1.0 + c3 * pow(_t - 1.0, 3) + c1 * pow(_t - 1.0, 2);
  }
}

/*
Copyright (C) 2022- deconbatch

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/


 

Next Post Previous Post
No Comment
Add Comment
comment url