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

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.5f);

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

tm.addMarker("left", 0, this::fired);
tm.addMarker("right", hit, this::fired);

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

void fired(Marker 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
import ch.domizai.keyed.*;

Timeline tm;
Pin hit;
Keyed<Float> x;

float leftFlash, rightFlash;
String last = "";

float barX0 = 40, barX1 = 360, barY = 300;

void settings() {
    size(400, 400);
}

void setup() {
    tm = Keyed.init(this).setDuration(3);
    textFont(createFont("Courier", 14));
    textAlign(CENTER, CENTER);

    hit = Pin.at(1.5f);

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

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

void fired(Marker m) {
    if (m.name().equals("left")) {
        leftFlash = 255;
    } else {
        rightFlash = 255;
    }
    last = m.name() + " at " + nf(m.t(), 1, 2);
}

void draw() {
    background(255);

    if (mousePressed) {
        // to(t, true) fires the markers passed on the way, in either direction.
        // A plain to(t) jumps silently.
        float 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.9f;
    rightFlash *= 0.9f;

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

    // The bar, with every marker and the playhead.
    stroke(220);
    line(barX0, barY, barX1, barY);
    for (Marker m : tm.markers()) {
        float 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);
    float 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);
}

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

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

void keyPressed() {
    if (key == 'm' || key == 'M') {
        // Markers can be looked up by name, removed and added again.
        Marker m = tm.marker("right");
        if (m != null) {
            tm.removeMarker(m);
        } else {
            tm.addMarker("right", hit, this::fired);
        }
    }
}

Loop and finish

LoopFinish

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

tm.onLoop(t -> loops++);

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

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

Full sketch: LoopFinish
import ch.domizai.keyed.*;

Timeline once;
Keyed<Float> x, progress;

int loops = 0;
int finishes = 0;
float doneFlash = 0;

void settings() {
    size(400, 400);
}

void setup() {
    Timeline tm = Keyed.init(this).setDuration(1);
    textFont(createFont("Courier", 14));

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

    // 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(t -> loops++);

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

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

void 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.95f;

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

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

Next: Binding.