Skip to content

9. Composition

So far all animations were set up once in setup(). But what if you need the same animation many times, starting at different moments, like a burst on every click? A Composition is a reusable, self-contained animation on its own timeline. Like any class, it can take parameters, so every instance can look a little different.

Click burst

Extend Composition, pass its duration to super(), and create its values with this.add() or this.keyed(). Key times are local: 0 is when the composition was created.

The constructor parameters customize each burst: size scales the keyed values, col colors the dots, and duration sets how long it plays. The key times are fractions of the duration, so the whole animation stretches with it:

class Burst extends Composition {
    constructor(x, y, size, col, duration) {
        super(duration);
        this.x = x;
        this.y = y;
        this.col = col;
        const d = duration;

        this.ring = this.add(Keyed.ofFloat()
            .key(Key.at(0).setEasing(Easing.CUBIC_OUT), 0)
            .key(d, 90 * size));

        this.fade = this.add(Keyed.ofFloat()
            .key(0.4 * d, 255)
            .key(d, 0));
    }
}

Each new Burst starts playing right away, here with a random size and color. Bigger bursts get a longer duration:

function mousePressed() {
    spawn(mouseX, mouseY);
}

function spawn(x, y) {
    const size = random(0.4, 1.6);
    const col = random(colors);
    const duration = 0.4 + 0.5 * size;
    bursts.push(new Burst(x, y, size, col, duration));
}

Once a burst isFinished(), dispose() stops its timeline so it can be garbage collected:

function draw() {
    background(255);
    for (let i = bursts.length - 1; i >= 0; i--) {
        const b = bursts[i];
        b.draw();
        if (b.isFinished()) {
            b.dispose();
            bursts.splice(i, 1);
        }
    }
}

Call play() to restart a composition from the beginning, and timeline() to control it like any other timeline.

The sketch also spawns a burst every 0.4 seconds with onLoop(), so there is something to see without clicking.

Full sketch: ClickBurst
const bursts = [];
let colors;

function setup() {
    createCanvas(400, 400);
    textFont('monospace');
    textSize(14);

    colors = [
        color(230, 60, 60),
        color(240, 190, 40),
        color(60, 120, 230),
        color(60, 180, 90),
    ];

    // Starts a burst every 0.4 seconds, so there is something to see.
    Keyed.init()
        .setDuration(0.4)
        .onLoop(() => spawn(random(60, 340), random(60, 320)));
}

function mousePressed() {
    spawn(mouseX, mouseY);
}

// The same Burst every time, but each one gets its own size, color and duration.
// Each one plays on its own, starting right away.
function spawn(x, y) {
    const size = random(0.4, 1.6);
    const col = random(colors);
    // Bigger bursts take longer.
    const duration = 0.4 + 0.5 * size;
    bursts.push(new Burst(x, y, size, col, duration));
}

function draw() {
    background(255);

    // Backwards, so finished bursts can be removed while looping.
    for (let i = bursts.length - 1; i >= 0; i--) {
        const b = bursts[i];
        b.draw();
        if (b.isFinished()) {
            // Stops its timeline so it can be garbage collected.
            b.dispose();
            bursts.splice(i, 1);
        }
    }

    // text() is outlined by the current stroke in p5.
    noStroke();
    fill(150);
    text('click anywhere   bursts: ' + bursts.length, 40, 380);
}
// A reusable animation: extend Composition and create its values with add().
// Constructor parameters make every instance a little different.
class Burst extends Composition {
    // size scales the burst, col colors its dots, and duration is how long it plays.
    constructor(x, y, size, col, duration) {
        // A timeline of its own that plays once.
        super(duration);
        this.x = x;
        this.y = y;
        this.col = col;

        // add() puts a value on this composition's timeline.
        // Key times are local: 0 is when this burst was created.
        // Here they are fractions of the duration, so the timing stretches with it.
        const d = duration;

        this.ring = this.add(Keyed.ofFloat()
            .key(Key.at(0).setEasing(Easing.CUBIC_OUT), 0)
            .key(d, 90 * size));

        this.fade = this.add(Keyed.ofFloat()
            .key(0.4 * d, 255)
            .key(d, 0));

        this.spread = this.add(Keyed.ofFloat()
            .key(Key.at(0).setEasing(Easing.QUART_OUT), 0)
            .key(d, 60 * size));

        this.dotSize = this.add(Keyed.ofFloat()
            .key(0, 14 * size)
            .key(d, 0));
    }

    draw() {
        noFill();
        stroke(0, this.fade.value());
        strokeWeight(4);
        circle(this.x, this.y, this.ring.value() * 2);
        strokeWeight(1);

        noStroke();
        fill(this.col);
        for (let i = 0; i < 8; i++) {
            const a = (i * TWO_PI) / 8;
            const r = this.spread.value();
            circle(this.x + cos(a) * r, this.y + sin(a) * r, this.dotSize.value());
        }
    }
}

Next: Export.