Skip to content

4. Events

Animations often need to trigger something: a sound, a particle burst, the next scene. Timelines can call you back at specific times, when they loop, and when they finish.

Markers

A marker calls back whenever playback crosses its time. It can be placed at a time or on a pin, and optionally named:

hit = Pin.at(1.5);

x = Keyed.ofFloat()
    .key(0, 50)
    .key(hit, 350)
    .key(3, 50);

tm.addMarker('left', 0, (m) => fired(m));
tm.addMarker('right', hit, (m) => fired(m));

The callback receives the Marker, with its name() and t():

function fired(m) {
    last = `${m.name()} at ${nf(m.t(), 1, 2)}`;
}

Placing a marker on the key's pin means it fires exactly when the ball arrives, even after the pin is moved.

Markers fire during playback in either direction, also when a loop wraps around. Jumping with to(t) fires nothing, but to(t, true) fires every marker passed on the way. That's what the sketch uses while scrubbing:

tm.to(t, true);

Markers can be looked up with marker(name) or markers(), and removed with removeMarker(). Press M in the sketch to toggle the right one.

Full sketch: Markers
let tm;
let hit;
let x;

let leftFlash = 0;
let rightFlash = 0;
let last = '';

const barX0 = 40;
const barX1 = 360;
const barY = 300;

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

    hit = Pin.at(1.5);

    x = Keyed.ofFloat()
        .key(0, 50)
        .key(hit, 350)
        .key(3, 50);

    // A marker calls back whenever playback crosses its time.
    tm.addMarker('left', 0, (m) => fired(m));
    // On the key's pin, it fires exactly when the ball arrives,
    // even after the pin is moved.
    tm.addMarker('right', hit, (m) => fired(m));
}

function fired(m) {
    if (m.name() === 'left') {
        leftFlash = 255;
    } else {
        rightFlash = 255;
    }
    last = `${m.name()} at ${nf(m.t(), 1, 2)}`;
}

function draw() {
    background(255);

    if (mouseIsPressed) {
        // to(t, true) fires the markers passed on the way, in either direction.
        // A plain to(t) jumps silently.
        const t = map(constrain(mouseX, barX0, barX1), barX0, barX1, 0, tm.duration());
        tm.to(t, true);
    }

    // Walls that flash when their marker fires.
    noStroke();
    fill(230, 60, 60, leftFlash);
    rect(20, 60, 10, 160);
    fill(230, 60, 60, rightFlash);
    rect(370, 60, 10, 160);
    leftFlash *= 0.9;
    rightFlash *= 0.9;

    fill(0);
    circle(x.value(), 140, 40);

    // The bar, with every marker and the playhead.
    stroke(220);
    line(barX0, barY, barX1, barY);
    for (const m of tm.markers()) {
        const mx = map(m.t(), 0, tm.duration(), barX0, barX1);
        noStroke();
        fill(230, 60, 60);
        triangle(mx - 6, barY - 16, mx + 6, barY - 16, mx, barY - 6);
        fill(150);
        text(m.name(), mx, barY - 30);
    }
    stroke(0);
    const head = map(tm.t(), 0, tm.duration(), barX0, barX1);
    line(head, barY - 10, head, barY + 10);

    noStroke();
    fill(0);
    text(last, width / 2, 250);
    fill(150);
    text('drag to scrub   M toggle right', width / 2, barY + 40);
}

function mousePressed() {
    tm.play(false);
}

function mouseReleased() {
    tm.play(true);
}

function keyPressed() {
    if (key.toLowerCase() === 'm') {
        // Markers can be looked up by name, removed and added again.
        const m = tm.marker('right');
        if (m !== null) {
            tm.removeMarker(m);
        } else {
            tm.addMarker('right', hit, (marker) => fired(marker));
        }
    }
}

Loop and finish

onLoop() is called every time a looping timeline wraps around. onFinish() is called once when a non-looping timeline reaches its end:

tm.onLoop(() => loops++);

once = new Timeline().setDuration(2, false);
once.onFinish(() => {
    finishes++;
    doneFlash = 255;
});

After restarting with once.to(0), onFinish() fires again at the end. To stop listening, keep the callback in a variable and pass it to removeListener().

Full sketch: LoopFinish
let once;
let x;
let progress;

let loops = 0;
let finishes = 0;
let doneFlash = 0;

function setup() {
    createCanvas(400, 400);
    const tm = Keyed.init().setDuration(1);
    textFont('monospace');

    x = Keyed.ofFloat()
        .key(Key.at(0).setEasing(1 / 3), 60)
        .key(Key.at(0.5).setEasing(1 / 3), 340)
        .key(Key.at(1).setEasing(1 / 3), 60);

    // Called every time a looping timeline wraps around.
    // To stop it later, keep the lambda in a variable and pass it to removeListener().
    tm.onLoop(() => loops++);

    // Plays once, in 2 seconds.
    once = new Timeline().setDuration(2, false);
    progress = Keyed.ofFloat()
        .setTimeline(once)
        .key(0, 0)
        .key(2, 1);

    // Called once when a non-looping timeline reaches its end.
    once.onFinish(() => {
        finishes++;
        doneFlash = 255;
    });
}

function draw() {
    background(255);
    noStroke();

    fill(150);
    text(`onLoop    loops: ${loops}`, 40, 70);
    fill(0);
    circle(x.value(), 120, 30);

    fill(150);
    text(`onFinish  finishes: ${finishes}`, 40, 230);
    fill(235);
    rect(40, 260, 320, 20);
    fill(0);
    rect(40, 260, 320 * progress.value(), 20);

    fill(230, 60, 60, doneFlash);
    text('done!', 40, 310);
    doneFlash *= 0.95;

    fill(150);
    text('click to restart the bar', 40, 370);
}

function mousePressed() {
    // After restarting, onFinish fires again at the end.
    once.to(0);
}

Next: Binding.