Skip to content

5. Binding

Calling value() everywhere gets tedious, especially with objects that draw themselves. Binding lets Keyed write the animated value into a property or setter before every draw(), so your code just uses plain variables.

Field binding

Keyed.bind(object, 'property') animates a property by name. The property's current value becomes the default. For top-level globals in p5's global mode, that means using var, because let and const are not properties of window.

var x = 60;
var diameter = 20;

function setup() {
    Keyed.init().setDuration(3);

    Keyed.bind(window, 'x')
        .key(Key.at(0).setEasing(1 / 3), 60)
        .key(Key.at(1.5).setEasing(1 / 3), 340)
        .key(Key.at(3).setEasing(1 / 3), 60);

    Keyed.bind(window, 'diameter')
        .key(0, 20)
        .key(1.5, 80)
        .key(3, 20);
}

function draw() {
    background(255);
    circle(x, 180, diameter);
}

No value() calls: x and diameter are plain numbers, already up to date.

Full sketch: FieldBinding
// Plain variables. Their values here become the defaults.
// Top-level var variables are properties of window, so they can be bound by name; let and const are not.
var x = 60;
var diameter = 20;

function setup() {
    createCanvas(400, 400);
    Keyed.init().setDuration(3);
    textFont('monospace');
    textSize(14);

    // bind(object, "property") writes the animated value into a property
    // before every draw(). It works for properties of any object,
    // e.g. Keyed.bind(ball, "x"); here the object is window.
    Keyed.bind(window, 'x')
        .key(Key.at(0).setEasing(1 / 3), 60)
        .key(Key.at(1.5).setEasing(1 / 3), 340)
        .key(Key.at(3).setEasing(1 / 3), 60);

    Keyed.bind(window, 'diameter')
        .key(0, 20)
        .key(1.5, 80)
        .key(3, 20);
}

function draw() {
    background(255);

    // No value() calls: x and diameter are plain numbers, already up to date.
    noStroke();
    fill(0);
    circle(x, 180, diameter);

    fill(150);
    text(`x = ${nf(x, 1, 1)}`, 40, 330);
    text(`diameter = ${nf(diameter, 1, 1)}`, 40, 355);
}

Setter binding

For any other type, bind a setter: a function that receives the animated value. Pass a Lerp (how to blend, see Types), a default value and the setter:

Keyed.bind(new ColorLerp(), color(255), (c) => (bg = c))
    .key(0, color(255, 235, 215))
    .key(2, color(215, 230, 255))
    .key(4, color(255, 235, 215));

Or call bind() on an existing Keyed. The setter can convert the value, here from an integer to a line of text:

score = Keyed.ofInt()
    .key(0, 0)
    .key(4, 100)
    .bind(showScore);

function showScore(v) {
    label = `score ${v}`;
}
  • unbind() stops the updates, so the target keeps its last value. In the sketch the score freezes for a moment.
  • apply() writes the value right away, without waiting for draw(), e.g. to use it in setup().
Full sketch: SetterBinding
let bg;
let label = '';
let score;
let bound = true;

function setup() {
    createCanvas(400, 400);
    Keyed.init().setDuration(4);
    textFont('monospace');
    textAlign(CENTER, CENTER);

    // A Lerp, a default value and a setter, which receives
    // the animated value before every draw().
    Keyed.bind(new ColorLerp(), color(255), (c) => (bg = c))
        .key(0, color(255, 235, 215))
        .key(2, color(215, 230, 255))
        .key(4, color(255, 235, 215));

    // Or bind an existing Keyed. The setter can convert the value,
    // here from a number into a line of text.
    score = Keyed.ofInt()
        .key(0, 0)
        .key(4, 100)
        .bind(showScore);

    // apply() writes the value right away, without waiting for draw(),
    // e.g. to use label in setup().
    score.apply();
    console.log(label);
}

function showScore(v) {
    label = `score ${v}`;
}

function draw() {
    // No value() calls: bg and label are already up to date.
    background(bg);

    // text() is outlined by the current stroke in p5.
    noStroke();
    fill(0);
    textSize(32);
    text(label, width / 2, 180);

    fill(150);
    textSize(14);
    text(bound ? 'click to unbind' : 'unbound, click to bind', width / 2, 350);
}

function mousePressed() {
    // unbind() stops the updates, so label keeps its last value.
    // bind() starts them again.
    bound = !bound;
    if (bound) {
        score.bind(showScore);
    } else {
        score.unbind();
    }
}

Binding objects

Binding shines with objects. Each Ball gets its y property bound by name, and its color through a setter method. Delaying the keys of each ball a little creates a wave:

for (let i = 0; i < 7; i++) {
    const b = new Ball(60 + i * 47, 300);
    balls.push(b);
    const delay = i * 0.1;

    Keyed.bind(b, 'y')
        .key(Key.at(delay).setEasing(1 / 3), 300)
        .key(Key.at(delay + 0.5).setEasing(1 / 3), 120)
        .key(Key.at(delay + 1).setEasing(1 / 3), 300);

    Keyed.bind(new ColorLerp(), color(0), (c) => b.setColor(c))
        .key(delay, color(0))
        .key(delay + 0.5, color(230, 60, 60))
        .key(delay + 1, color(0));
}

The balls are always up to date and simply draw themselves.

Full sketch: ObjectBinding
class Ball {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.col = color(0);
    }

    setColor(c) {
        this.col = c;
    }

    draw() {
        noStroke();
        fill(this.col);
        circle(this.x, this.y, 30);
    }
}

const balls = [];

function setup() {
    createCanvas(400, 400);
    Keyed.init().setDuration(2);

    for (let i = 0; i < 7; i++) {
        const b = new Ball(60 + i * 47, 300);
        balls.push(b);

        // Each ball starts a little later than the one before.
        const delay = i * 0.1;

        // Binds the y property of this ball, by name.
        Keyed.bind(b, 'y')
            .key(Key.at(delay).setEasing(1 / 3), 300)
            .key(Key.at(delay + 0.5).setEasing(1 / 3), 120)
            .key(Key.at(delay + 1).setEasing(1 / 3), 300);

        // Colors are objects, so a setter of the ball receives them.
        // (Binding the color by name works too: Keyed.bind(b, "col").)
        Keyed.bind(new ColorLerp(), color(0), (c) => b.setColor(c))
            .key(delay, color(0))
            .key(delay + 0.5, color(230, 60, 60))
            .key(delay + 1, color(0));
    }
}

function draw() {
    background(255);
    // The balls are already up to date; they just draw themselves.
    for (const b of balls) {
        b.draw();
    }
}

Next: Paths.