1. Getting Started¶
In this chapter you animate your first value, move things around with vectors, meet the other value types, and learn that an animation can be read at any point in time.
All sketches on these pages are live: they run right in the page, and the code below each one is the full sketch.
Your first key¶
Every Keyed sketch starts by connecting the library to the sketch in setup():
init() hooks Keyed into p5, so animations follow real time without you having to advance them. It returns the default timeline, which we tell to loop every 2 seconds.
Next, create an animated value. Keyed.ofFloat() makes an animated number. Then add keys: a time in seconds and the value at that time.
At 0 seconds x is 50, at 1 second it's 350, and at 2 seconds it's back at 50. In between, Keyed blends the values. In draw(), value() gives you the value at the current time:
That's all there is to it. The rest of this tutorial adds to this pattern.
Full sketch: FirstKey
// An animated float: its value changes over time, blending from key to key.
let x;
function setup() {
createCanvas(400, 400);
// Connects Keyed to the sketch so animations follow real time.
// It returns the default timeline, which we loop every 2 seconds.
Keyed.init().setDuration(2);
// Each key is a time in seconds and a value.
x = Keyed.ofFloat()
.key(0, 50)
.key(1, 350)
.key(2, 50);
}
function draw() {
background(255);
noStroke();
fill(0);
// value() is the value at the current time.
circle(x.value(), height / 2, 40);
}
Vectors¶
Keyed.ofVector() animates a p5.Vector, blending x, y and z together. Here the ball visits one corner per second. The last key returns to the first corner, so the loop closes without a jump:
pos = Keyed.ofVector();
for (let i = 0; i <= corners.length; i++) {
pos.key(i, corners[i % corners.length]);
}
Keys store a copy of the vector you pass, so changing corners later won't move them.
Full sketch: Vectors
// An animated p5.Vector blends x, y and z together.
let pos;
let corners;
function setup() {
createCanvas(400, 400);
Keyed.init().setDuration(4);
corners = [
createVector(100, 100),
createVector(300, 100),
createVector(300, 300),
createVector(100, 300),
];
pos = Keyed.ofVector();
// One corner per second. The last key returns to the first corner,
// so the loop closes without a jump.
for (let i = 0; i <= corners.length; i++) {
pos.key(i, corners[i % corners.length]);
}
// Keys store a copy, so changing corners now would not move them.
}
function draw() {
background(255);
noStroke();
fill(200);
for (const c of corners) {
circle(c.x, c.y, 10);
}
const p = pos.value();
fill(0);
circle(p.x, p.y, 40);
}
Value types¶
Keyed knows how to blend more than numbers:
| Factory | Type | How it blends |
|---|---|---|
Keyed.ofFloat() |
number | linearly |
Keyed.ofInt() |
number | linearly, rounded to the nearest whole number |
Keyed.ofColor() |
p5.Color |
in the color space of the colors, like lerpColor() |
Keyed.ofVector() |
p5.Vector |
x, y and z |
Keyed.ofString() |
string | morphs one character edit at a time |
Keyed.ofBoolean() |
boolean | switches when the next key is reached |
Each factory also takes a default value, e.g. Keyed.ofFloat(50), used while there are no keys. Without one it's 0, black, false, an empty string or (0, 0). Keyed.of(value) is a shorthand that picks the type from the value: a number, string, boolean, p5.Vector or p5.Color. Whole numbers always need ofInt(), because a number alone could be either.
col = Keyed.ofColor()
.key(0, color(230, 60, 60))
.key(2, color(60, 120, 230))
.key(4, color(230, 60, 60));
word = Keyed.ofString()
.key(0, 'keyed')
.key(2, 'animation')
.key(4, 'keyed');
Note
Colors blend in the color mode they were created in. Colors made while colorMode(HSB) is active blend in HSB, passing through the hues.
Want to blend something else? See Types.
Full sketch: ValueTypes
let col;
let count;
let word;
let visible;
let hiddenColor;
function setup() {
createCanvas(400, 400);
Keyed.init().setDuration(4);
textFont('monospace');
textSize(18);
textAlign(LEFT, CENTER);
// Colors are p5.Color objects, so they have their own factory.
col = Keyed.ofColor()
.key(0, color(230, 60, 60))
.key(2, color(60, 120, 230))
.key(4, color(230, 60, 60));
// Whole numbers, rounded to the nearest int.
count = Keyed.ofInt()
.key(0, 0)
.key(2, 100)
.key(4, 0);
// Text morphs one character edit at a time.
word = Keyed.ofString()
.key(0, 'keyed')
.key(2, 'animation')
.key(4, 'keyed');
// Booleans can't blend, so they switch when the next key is reached.
visible = Keyed.ofBoolean()
.key(0, true)
.key(2, false);
hiddenColor = color(0, 0, 180);
}
function draw() {
background(255);
noStroke();
label('ofColor', 80);
fill(col.value());
rect(180, 60, 160, 40);
label('ofInt', 160);
fill(0);
text(count.value(), 180, 160);
rect(230, 150, count.value() * 1.1, 20);
label('of(String)', 240);
fill(0);
text(word.value(), 180, 240);
label('of(boolean)', 320);
fill(visible.value() ? 0 : hiddenColor);
circle(200, 320, 40);
}
function label(s, y) {
fill(150);
text(s, 40, y);
}
Any time¶
value() is the value now. value(t) is the value at any time t, past or future. Here it's used to draw the whole animation as a graph, one point per pixel:
beginShape();
for (let x = gx0; x <= gx1; x++) {
const t = map(x, gx0, gx1, 0, duration);
vertex(x, ballY.value(t));
}
endShape();
keys() lists the keys, and each key's t() is its time, which is handy for drawing them:
The current time of the default timeline is Keyed.defaultTimeline().t(). Hover over the graph in the sketch to read the value at any other time.
Full sketch: AnyTime
let duration = 4;
let ballY;
// The graph maps time 0..duration to x.
let gx0 = 40;
let gx1 = 300;
function setup() {
createCanvas(400, 400);
Keyed.init().setDuration(duration);
textFont('monospace');
textSize(14);
// The ball's height over time.
ballY = Keyed.ofFloat()
.key(Key.at(0).setEasing(1 / 3), 300)
.key(Key.at(1).setEasing(1 / 3), 100)
.key(Key.at(2.5).setEasing(1 / 3), 220)
.key(Key.at(4).setEasing(1 / 3), 300);
}
function draw() {
background(255);
// value(t) gives the value at any time, not just the current one.
// Here it draws the whole animation as a graph, one point per pixel.
noFill();
stroke(220);
beginShape();
for (let x = int(gx0); x <= gx1; x++) {
const t = map(x, gx0, gx1, 0, duration);
vertex(x, ballY.value(t));
}
endShape();
// The keys, on the curve.
noStroke();
fill(150);
for (const k of ballY.keys()) {
circle(tx(k.t()), ballY.value(k.t()), 8);
}
// value() is the value at the timeline's current time, so the playhead
// always sits on the curve, at the same height as the ball.
const now = Keyed.defaultTimeline().t();
const y = ballY.value();
stroke(220);
line(tx(now), 60, tx(now), 340);
line(tx(now), y, 350, y);
noStroke();
fill(0);
circle(tx(now), y, 10);
circle(350, y, 40);
text('value() = ' + nf(y, 1, 1), 40, 360);
// Hover over the graph to read the value at any other time.
if (mouseX >= gx0 && mouseX <= gx1) {
const t = map(mouseX, gx0, gx1, 0, duration);
const v = ballY.value(t);
noFill();
stroke(230, 60, 60);
circle(mouseX, v, 14);
noStroke();
fill(230, 60, 60);
text('value(' + nf(t, 1, 2) + ') = ' + nf(v, 1, 1), 40, 380);
} else {
fill(150);
text('hover over the graph', 40, 380);
}
}
function tx(t) {
return map(t, 0, duration, gx0, gx1);
}
Text and stroke
p5 outlines text() with the current stroke, and the stroke stays set from one frame to the next. If a line you drew earlier (like a timeline bar) leaves a stroke on, your text looks doubled or blurry. Call noStroke() before drawing text:
Processing's text() ignores the stroke, so this is a difference to keep in mind when you port a sketch.
Next: Easing.