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():
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:
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.