Mastering Stepwise Easing: Chaining Multiple Functions for Unique Animation
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.
イージング有り/なしの動き 🙂
— deconbatch (@deconbatch) January 22, 2022
上がなしで、下が easeInCubic のイージング。#p5js #creativecoding pic.twitter.com/xGbyDXYp5i
Let me show you the 'Stepwise easing' made with various easing functions. And the example code of animation using the 'Stepwise easing'.
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 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
easeInCubic の作例 😀
— deconbatch (@deconbatch) January 22, 2022
function easeInCubic(x) {
return x * x * x;
}#p5js #creativecoding pic.twitter.com/XaKBZG7N3n
// 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".
Below is an example animations using a single easeInOutCubic function.
easeInOutCubic イージングの繰り返し 😌#p5js #creativecoding pic.twitter.com/eBQkm1XQOX
— deconbatch (@deconbatch) January 22, 2022
This example chains three functions: easeInOutCubic, easeOutQuad, and easeInCubic.
easeInOutCubic, easeOutQuad, easeInCubic の一連のイージングで一回転のパターン。🙂#p5js #creativecoding pic.twitter.com/d54IRZIycX
— deconbatch (@deconbatch) January 22, 2022
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.
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/>
*/