To practice lerp and vertex(not rlly):
This code creates an animation effect that includes:
A cannon (firing towards the top-left), decorated with patterns.
Five ribbons that shoot out from the cannon’s direction, swaying as time progresses. Their width and color change over time.
A mouse drag feature that influences the movement direction of the ribbons.
1. Motion
ribbons: An array that stores all the ribbons, each ribbon is made up of pointsPerRibbon points. (Initially, all points are stacked at the leader position).
leaders: An array that stores the "leader points" for each ribbon, which determine the motion path of the ribbons.
for (let r = 0; r < ribbons.length; r++) {
let ribbon = ribbons[r];
let leader = leaders[r];
To make the leader point rotate around a small radius, we make the ribbon's motion more natural:
leader.angle += leader.rotateSpeed;
let rotationRadius = sin(frameCount * 0.02) * 4;
The speeds speedX and speedY control the general movement of the ribbon, while cos and sin introduce slight oscillations:
leader.x += leader.speedX + cos(leader.angle) * rotationRadius;
leader.y += leader.speedY + sin(leader.angle) * rotationRadius;
2. Using lerp
Let the ribbon follow the leader:
ribbon[0].x = leader.x;
ribbon[0].y = leader.y;
for (let i = 1; i < ribbon.length; i++) {
let p = ribbon[i];
let prev = ribbon[i - 1];
The lerp() function allows each point to gradually move closer to its predecessor, creating a flowing effect for the ribbons.
p.x = lerp(p.x, prev.x, 0.1);
p.y = lerp(p.y, prev.y, 0.1);
}
I couldn’t use lerp() to make the color change smooth:
let r = map(sin(frameCount * 0.02 + progress * PI), -1, 1, 50, 250);
let g = map(sin(frameCount * 0.02 + progress * PI + TWO_PI / 3), -1, 1, 50, 250);
let b = map(sin(frameCount * 0.02 + progress * PI + 2 * TWO_PI / 3), -1, 1, 50, 250);
Here, I used the sin function and frameCount to control the color changes. This allowed for multiple gradient colors to appear on a single ribbon, but if I used lerp(), I could only achieve a result like this:❓
3. Using vertex
vertex(p.x - thickness + wave, p.y + wave * 0.5);
vertex(p.x + thickness + wave, p.y + wave * 0.5);
Before using vertex, I applied a wave effect to each point with the wave variable. This wave effect is calculated with sin(time + p.phase + i * 0.05), causing the x and y coordinates of each point to fluctuate over time, creating a dynamic wave effect.
🔑:
I learned an advanced concept from a reference: TRIANGLE_STRIP.
beginShape(TRIANGLE_STRIP);
This draws a strip of triangles. Every two consecutive vertex points form a triangle, and multiple consecutive vertex points form a series of adjacent triangles, thus drawing a ribbon-like effect.
v1 -- v3 -- v5 -- v7
| / | / | / |
v2 -- v4 -- v6 -- v8
Every four vertices form two triangles:
Triangle 1: v1-v2-v3
Triangle 2: v2-v3-v4
Each time vertex(x, y) is called, it marks a point on the canvas, and connecting these points creates a path. If we use only LINE_STRIP, each pair of adjacent vertices would be connected into a line. However, TRIANGLE_STRIP uses two adjacent vertices along with a previous vertex to form a triangle, and multiple vertices will form multiple adjacent triangles.
4. Some Small Issues
Due to the following lines:
leader.speedX += dx * 0.001;
leader.speedY += dy * 0.001;
I needed to limit the speed:
let maxSpeed = 2;
if (leader.speedX > maxSpeed) leader.speedX = maxSpeed;
if (leader.speedX < -maxSpeed) leader.speedX = -maxSpeed;
When I added the rotation movement, the update of the leader point depends not only on speedX and speedY, but also on cos(leader.angle) and sin(leader.angle).
This means the leader’s actual position will be influenced by the rotation radius and may move outside the canvas boundaries. To ensure the leader point bounces back at the boundaries, we need to calculate the new position before updating it:
let newX = leader.x + leader.speedX + cos(leader.angle) * rotationRadius;
let newY = leader.y + leader.speedY + sin(leader.angle) * rotationRadius;
Code
let ribbons = [];
let numRibbons = 5;
let pointsPerRibbon =80;
let leaders = [];
function setup() {
createCanvas(600, 600);
// create leader point for each
for (let r = 0; r < numRibbons; r++) {
let leader = {
x: width-100,
y: height-100,
speedX: random(-2, 2),
speedY: random(-2, 2),
angle: random(TWO_PI),
rotateSpeed: random(0.02, 0.05)
};
leaders.push(leader);
// create ribbon point
let ribbon = [];
for (let i = 0; i < pointsPerRibbon; i++) {
ribbon.push({
x: leader.x,
y: leader.y,
phase: random(PI)
});
}
ribbons.push(ribbon);
}
}
function draw() {
background(0, 50);
push();
translate(width-40, height-40);
scale(2)
rotate(-PI / 4);
fill(200, 50, 50);
rect(-10, -30, 20, 50, 10);
rect(-10, -35, 20, 20);
stroke(255, 200, 0);
for (let i = -25; i < 20; i += 10) {
line(-10, i, 10, i + 10);
}
pop();
for (let r = 0; r < ribbons.length; r++) {
let ribbon = ribbons[r];
let leader = leaders[r];
// update leader point
leader.angle += leader.rotateSpeed;
//rotate
let rotationRadius = sin(frameCount * 0.02) * 4;
// new pos
leader.x += leader.speedX + cos(leader.angle) * rotationRadius;
leader.y += leader.speedY + sin(leader.angle) * rotationRadius;
// boundary check
if (leader.x < 0) {
leader.x = 0;
leader.speedX *= -1;
} else if (leader.x > width) {
leader.x = width;
leader.speedX *= -1;
}
if (leader.y < 0) {
leader.y = 0;
leader.speedY *= -1;
} else if (leader.y > height) {
leader.y = height;
leader.speedY *= -1;
}
//update ribbon point
ribbon[0].x = leader.x;
ribbon[0].y = leader.y;
// other points,follow
for (let i = 1; i < ribbon.length; i++) {
let p = ribbon[i];
let prev = ribbon[i - 1];
p.x = lerp(p.x, prev.x, 0.1);
p.y = lerp(p.y, prev.y, 0.1);
}
// draw ribbon
beginShape(TRIANGLE_STRIP);
for (let i = 0; i < ribbon.length; i++) {
let p = ribbon[i];
let timee = frameCount * 0.015;
let wave = sin(timee + p.phase + i * 0.05) * 15;
let progress = i / (ribbon.length - 1);
let r = map(sin(frameCount * 0.02 + progress * PI), -1, 1, 50, 250);
let g = map(sin(frameCount * 0.02 + progress * PI + TWO_PI / 3), -1, 1, 50, 250);
let b = map(sin(frameCount * 0.02 + progress * PI + 2 * TWO_PI / 3), -1, 1, 50, 250);
let alpha = map(progress, 0, 1, 100, 25);
fill(r, g, b, alpha);
noStroke();
let thickness = map(progress, 0, 1, 20, 1);
vertex(p.x - thickness + wave, p.y + wave * 0.5);
vertex(p.x + thickness + wave, p.y + wave * 0.5);
}
endShape();
}
}
//drag
function mouseDragged() {
for (let leader of leaders) {
let dx = mouseX - leader.x;
let dy = mouseY - leader.y;
leader.speedX += dx * 0.001;
leader.speedY += dy * 0.001;
let maxSpeed = 2;
if (leader.speedX > maxSpeed) leader.speedX = maxSpeed;
if (leader.speedX < -maxSpeed) leader.speedX = -maxSpeed;
if (leader.speedY > maxSpeed) leader.speedY = maxSpeed;
if (leader.speedY < -maxSpeed) leader.speedY = -maxSpeed;
}
}