Easing (smooth transitions) with functions instead of two variables?

Edit: I think I'm pretty happy with this solution: https://forum.processing.org/two/discussion/comment/99885/#Comment_99885

Edit2: I also like the solution in my last post: https://forum.processing.org/two/discussion/comment/123690/#Comment_123690

x = 0;
y = 0;
hueSlider = 0;

function setup() {
    createCanvas(640, 480);
    colorMode(HSL);
    slider = createSlider(0, 360, 0);
}

function draw() {
    hueSlider = lerp(hueSlider, slider.value(), 0.01)
    background(hueSlider, 50, 70);

    fill(120, 50, 50);
    x = lerp(x, mouseX, 0.05);
    y = lerp(y, mouseY, 0.05);

    ellipse(x, y, 50, 50);
}

Original post:

Easing should be easier

Hi! I really miss having a simple easing function built into p5. I'm a beginner, and my attempts of implementing one myself have failed so far.

I've used vvvv quite a bit before, which has a simple to use "Damper" node/function, which is used like this:

damper(value, duration)

(Or maybe smoothTransition(value, duration) would be a better name?)

Explanation

Calling ellipse(damper(mouseX, 1.5), damper(mouseY, 1.5), 50, 50) and moving the mouse quickly would mean the ellipse would first go towards the new position rapidly, and slow down as it approaches, going from start to finish in 1.5 second, resulting in a nice and smooth transition.

The vvvv damper's outputs are the damped value, the acceleration and the velocity.

Standard p5 easing

In the p5 world, the most common solution I come across is something like this:

x += easingFactor * (mouseX - x)

It works fine, but it's not very elegant, and for each thing I want to apply easing to, I have to create a new variable:

Adding easing to a hue slider, the awkward way

function setup() {
    createCanvas(640, 480);
    colorMode(HSL);
    slider = createSlider(0, 360, 0);
    newhue = 0;
}

function draw() {
    background(newhue += 0.05 * (slider.value() - newhue), 50, 50);
}

I would like to use p5 in teaching to give kids a fun introduction to programming. However, this awkward and verbose way of achieving something seemingly simple seems off-putting, and contradictory to p5's general user-friendliness.

Adding easing to a hue slider, the easy (and imaginary) way

I would prefer not having to create any new variables, and rather enter something like this:

background(damper(slider.value(), 3), 50, 50);

Here's my failed attempt (without the time parameter for now):

function setup() {
    createCanvas(640, 480);
    colorMode(HSL);
    slider = createSlider(0, 360, 0);
}

function draw() {
    background(damper(slider.value()), 50, 50);
}

function damper(value) {
    newValue = value;
    newValue += 0.05 * (value - newValue);
    return newValue; // No easing happens
}

Easing curves that take t = 0.0 .. 1.0 as a parameter

Alternatively, there's the option of getting a better selection of curves (linear, easeInQuad, easeOutQuad, easeInOutQuad, etc.): https://gist.github.com/gre/1650294

This approach involves having a variable t going from 0.0 to 1.0 in x seconds. I can work with it when pressing a button, but my use case is for things that run continuously.

I guess I could store values in a fixed size array, using a.unshift() and a.push(val), and apply some kind of calculation to its numbers. However, it seems to me that this approach would also require new variables to be created.

Thanks for reading! - Kristian

Answers

  • @kristianp89 --

    It looks like you are on the right track. I believe that a vvvv damper isn't a function -- it is an object, which is to say that it does contain an envelope (array) of values. There is no way for a computer to simulate a damper without saving those values.

    So you probably want to define a Damper class which contains such an array, then create a new Damper, as per the objects tutorial.

    https://p5js.org/examples/objects-objects.html

    If your students cut and paste the damper class, then they will be able to use it with a simplified syntax, and you won't need to fiddle with global variables.

  • edited June 2017 Answer ✓

    Okay, so I think I'm getting there, but my solution only works with one variable. What am I missing?

    const dampingFactor = 0.05;
    
    function setup() {
        createCanvas(640, 480);
        colorMode(HSL);
        slider = createSlider(0, 360, 0);
    
        // x is used for regular damping
        x = 0;
    
        // Here's the new way, although it doesn't work with more than one variable
        damper = Damper();
    }
    
    function draw() {
        background(100, 50, 90);
    
        // Red: standard damping with global variables
        fill(0, 50, 50);
        ellipse(x += dampingFactor * (mouseX - x), height/2, 50, 50);
    
        // Green: simpler damping
        // Follows the red ellipse nicely, until I uncomment any of the ellipses below:
        fill(120, 50, 50);
        ellipse(damper(mouseX) + 10, height/2, 50, 50);
        // ellipse(damper(mouseX) + 20, height/2, 50, 50);
        // ellipse(damper(mouseX) + 10, damper(mouseY), 50, 50);
    
        fill(240, 50, 50);
        // ellipse(damper(mouseX) + 20, height/2, 50, 50);
    }
    
    function Damper(val) {
        this.val = 0;
    
        function f(val) {
            return this.val += dampingFactor * (val - this.val);
        }
    
        return f;
    }
    
  • edited June 2017

    http://Bl.ocks.org/GoSubRoutine/fa085945d45152786698f44a9523ccac

    /**
     * Damper Balls (v1.0.2)
     * KristianP89 (2017-Jun-05)
     * Modded GoToLoop
     *
     * forum.Processing.org/two/discussion/22925/
     * p5-easing-how-can-i-do-easing-with-a-function-value-duration-
     * instead-of-creating-new-variables#Item_3
     *
     * Bl.ocks.org/GoSubRoutine/fa085945d45152786698f44a9523ccac
     */
    
    "use strict";
    
    const DAMPERS = 3, BOLD = 1.5, HEIGHT = 150, CY = HEIGHT >> 1,
          dampers = Array(DAMPERS);
    
    let bg;
    
    function setup() {
      windowResized();
    
      colorMode(HSL).ellipseMode(CENTER);
      strokeWeight(BOLD).stroke(0).frameRate(60);
    
      bg = color(100, 50, 90);
    
      dampers[0] = new Damper(0, CY, color(0, 50, 50));
      dampers[1] = new Damper(width, CY, color(120, 50, 50));
      dampers[2] = new Damper(width >> 1, CY, color(240, 50, 50));
    }
    
    function draw() {
      background(bg);
      let x = mouseX;
      for (const d of dampers)  ( { x } = d.update(x).display() );
    }
    
    function windowResized() {
      const { marginLeft, marginRight } = getComputedStyle(canvas.parentElement),
            w = windowWidth - round(float(marginLeft) + float(marginRight));
    
      resizeCanvas(w, HEIGHT, true);
    }
    
    class Damper {
      static get FACTOR() { return .05; }
      static get DIAM() { return CY; }
    
      constructor(x = 0, y = 0, col = 0) { this.x = x, this.y = y, this.c = col; }
    
      update(x = 0) { this.x += Damper.FACTOR * (x - this.x); return this; }
      display() { fill(this.c).ellipse(this.x, this.y, Damper.DIAM); return this; }
    }
    
  • Answer ✓

    That's a cool example, but it requires the creation of variables (dampers[0]), going through a for loop, and it's not very readable.

    I've been looking around some more, and I'm actually quite happy with this solution:

    x = 0;
    y = 0;
    hueSlider = 0;
    
    function setup() {
        createCanvas(640, 480);
        colorMode(HSL);
        slider = createSlider(0, 360, 0);
    }
    
    function draw() {
        hueSlider = lerp(hueSlider, slider.value(), 0.01)
        background(hueSlider, 50, 70);
    
        fill(120, 50, 50);
        x = lerp(x, mouseX, 0.05);
        y = lerp(y, mouseY, 0.05);
    
        ellipse(x, y, 50, 50);
    }
    

    I wish I didn't have to create an extra variable for everything I want to apply easing to, but overall I think this is pretty good.

    Here's the video I got it from:

  • edited June 2017

    http://Bl.ocks.org/GoSubRoutine/fa085945d45152786698f44a9523ccac

    /**
     * Damper Balls (v1.1)
     * KristianP89 (2017-Jun-05)
     * Modded GoToLoop
     *
     * forum.Processing.org/two/discussion/22925/
     * p5-easing-how-can-i-do-easing-with-a-function-value-duration-
     * instead-of-creating-new-variables#Item_5
     *
     * Bl.ocks.org/GoSubRoutine/fa085945d45152786698f44a9523ccac
     */
    
    "use strict";
    
    const DAMPERS = 6, BOLD = 1.5, HEIGHT = 150, CY = HEIGHT >> 1,
          dampers = new Set;
    
    let bg;
    
    function setup() {
      windowResized();
    
      colorMode(HSL).ellipseMode(CENTER);
      strokeWeight(BOLD).stroke(0).frameRate(60);
    
      bg = color(100, 50, 90);
    
      for (let x = width/DAMPERS, c = 360/DAMPERS | 0, i = 0; i < DAMPERS; ++i)
        dampers.add(new Damper(x * (i + 1), CY, color(c*i, 50, 50)));
    }
    
    function draw() {
      background(bg);
      let x = mouseX;
      for (const d of dampers)  ( { x } = d.update(x).display() );
    }
    
    function windowResized() {
      const { marginLeft, marginRight } = getComputedStyle(canvas.parentElement),
            w = windowWidth - round(float(marginLeft) + float(marginRight));
    
      resizeCanvas(w, HEIGHT, true);
    }
    
    class Damper {
      static get FACTOR() { return .05; }
      static get DIAM() { return CY; }
    
      constructor(x = 0, y = 0, col = 0) { this.x = x, this.y = y, this.c = col; }
    
      update(x = 0) { this.x += Damper.FACTOR * (x - this.x); return this; }
      display() { fill(this.c).ellipse(this.x, this.y, Damper.DIAM); return this; }
    }
    
  • edited May 11

    [Code below] Further down is my preferred solution, written in native JS, but applicable to p5. It's not the solution I wanted, but I think it's the most readable one so far.

    For those who haven't seen this before, this is called "easing"(?), and it's is a way of achieving smooth animations. (I just call it "damping", because that's what I know from vvvv.)

    For most cases, this technique looks far better than instant movements or linearly increasing variables.

    Basically, each time the damping function is called, x.damped gets 5% closer to x.target.

    In this example, x.target is changed from 0 to 100. x.damped then gets incremented by 5% of what's remaining to the target, each frame:

    (0): 0.000 (+5.000)
    (1): 5.000 (+4.750)
    (2): 9.750 (+4.513)
    (3): 14.262 (+4.287)
    (4): 18.549 (+4.073)
    (5): 22.622 (+3.869)
    (6): 26.491 (+3.675)
    (7): 30.166 (+3.492)
    (8): 33.658 (+3.317)
    (9): 36.975 (+3.151)
    (10): 40.126 (+2.994)
    (11): 43.120 (+2.844)
    (12): 45.964 (+2.702)
    (13): 48.666 (+2.567)
    (14): 51.233 (+2.438)
    (15): 53.671 (+2.316)
    (16): 55.987 (+2.201)
    (17): 58.188 (+2.091)
    (18): 60.279 (+1.986)
    (19): 62.265 (+1.887)
    (20): 64.151 (+1.792)
    (21): 65.944 (+1.703)
    (22): 67.647 (+1.618)
    (23): 69.264 (+1.537)
    (24): 70.801 (+1.460)
    (25): 72.261 (+1.387)
    (26): 73.648 (+1.318)
    (27): 74.966 (+1.252)
    (28): 76.217 (+1.189)
    (29): 77.406 (+1.130)
    (30): 78.536 (+1.073)
    (31): 79.609 (+1.020)
    (32): 80.629 (+0.969)
    (33): 81.597 (+0.920)
    (34): 82.518 (+0.874)
    (35): 83.392 (+0.830)
    (36): 84.222 (+0.789)
    (37): 85.011 (+0.749)
    (38): 85.760 (+0.712)
    (39): 86.472 (+0.676)
    (40): 87.149 (+0.643)
    (41): 87.791 (+0.610)
    (42): 88.402 (+0.580)
    (43): 88.982 (+0.551)
    (44): 89.533 (+0.523)
    (45): 90.056 (+0.497)
    (46): 90.553 (+0.472)
    (47): 91.026 (+0.449)
    (48): 91.474 (+0.426)
    (49): 91.901 (+0.405)
    (50): 92.306 (+0.385)
    (51): 92.690 (+0.365)
    (52): 93.056 (+0.347)
    (53): 93.403 (+0.330)
    (54): 93.733 (+0.313)
    (55): 94.046 (+0.298)
    (56): 94.344 (+0.283)
    (57): 94.627 (+0.269)
    (58): 94.895 (+0.255)
    (59): 95.151 (+0.242)
    (60): 95.393 (+0.230)
    etc.
    

    Paste into .html file:

    <!DOCTYPE html>
    <html>
    
    <head>
        <meta charset="utf-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>Page Title</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            * {
                margin: 0;
                overflow: hidden;
            }
        </style>
    </head>
    
    <body>
        <canvas></canvas>
        <script>
            const canvas = document.querySelector("canvas");
            const c = canvas.getContext("2d");
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
    
            let mouseX = {target: 0, damped: 0};
            let mouseY = {target: 0, damped: 0};
            let radius = {target: 20, damped: 20};
            let hue = {target: 0, damped: 0};
    
            let frameCount = 0;
            let bigCircle = false;
            let circleSize = 100;
    
            canvas.addEventListener("mousemove", event => {
                mouseX.target = event.layerX;
                mouseY.target = event.layerY;
            });
    
            addEventListener("click", () => {
                bigCircle = !bigCircle;
                if (bigCircle) {
                    radius.target = 100;
                } else {
                    radius.target = 20;
                }
            });
    
            (function draw() {
                damping();
                changeHueEverySecond();
                background();
                cursor();
                requestAnimationFrame(draw);
            })();
    
            function damping() {
                mouseX.damped += 0.05 * (mouseX.target - mouseX.damped);
                mouseY.damped += 0.05 * (mouseY.target - mouseY.damped);
                radius.damped += 0.05 * (radius.target - radius.damped);
                hue.damped += 0.05 * (hue.target - hue.damped);
            }
    
            function changeHueEverySecond() {
                frameCount++;
                if (frameCount % 60 === 0) {
                    hue.target = frameCount;
                }
            }
    
            function background() {
                c.fillStyle = "#ffa";
                c.fillRect(0, 0, canvas.width, canvas.height);
                c.fill();
            }
    
            function cursor() {
                c.fillStyle = `hsl(${hue.damped}, 50%, 50%)`;
                c.beginPath();
                c.arc(mouseX.damped, mouseY.damped, radius.damped, 0, Math.PI * 2);
                c.closePath();
                c.fill();
            }
        </script>
    </body>
    
    </html>
    

    A possible improvement would be to loop over an array or an object in the damping function, but I like being able to use x.damped instead of dampArray[2].damped or dampObject.x.damped for example.

  • edited May 11

    Here's one that I really like. I can just add variables to the string!

    It's a little less flexible than the manual approach, and it doesn't read as nicely, but there's less repetition.

    const canvas = document.querySelector("canvas");
    const c = canvas.getContext("2d");
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    
    let damper = {};
    let dampedVariables = "x, y, backgroundHue";
    
    dampedVariables = dampedVariables.split(", ");
    dampedVariables.forEach(variable => {
        damper[variable] = {
            target: 0,
            damp: 0
        };
    });
    
    function damp() {
        for (variable in damper) {
            damper[variable].damp += 0.05 * (damper[variable].target - damper[variable].damp);
        }
    }
    
    canvas.addEventListener("mousemove", event => {
        damper.x.target = event.layerX;
        damper.y.target = event.layerY;
    });
    
    canvas.addEventListener("click", () => {
        damper.backgroundHue.target = Math.random() * 360;
    });
    
    (function draw() {
        background(`hsl(${damper.backgroundHue.damp}, 40%, 80%)`);
        damp();
        cursor();
        requestAnimationFrame(draw);
    })();
    
    
    function background(color) {
        c.fillStyle = color;
        c.fillRect(0, 0, canvas.width, canvas.height);
    }
    
    function cursor() {
        c.fillStyle = "#e0a240";
        c.fillRect(damper.x.damp - 25, damper.y.damp - 25, 50, 50)
    }
    
Sign In or Register to comment.