Learn how to create particles out of text on the HTML canvas element
Created on Thursday, 29th December at 21:00pm
TLDR; All the files, demos, any bug-fixes are also available on
Github:
https://github.com/disruptionlaboratory/canvas-text-particles
1 Let's create a HTML page
Here is the initial HTML markup. We're essentially normalizing the padding and margins across browsers, setting the page backgound colour to black and then adding canvas and script tags.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Make it snow</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: black;
}
</style>
</head>
<body>
<canvas></canvas>
<script></script>
</body>
</html>
2 Let's get the canvas element up and running
Inside the scripts tags add the following code:
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
We're getting a handle on the canvas HTML element and setting the context to 2d. The context is the object we'll be using later to draw text and particles on to the canvas. We're capturing the width and height from the window object and configuring the canvas to take up the full viewport.
3 Let's create a Particle class
We're going to create a class (a kind of template for creating objects) called Particle. This Particle class will have properties as well as methods. The properties are essentially its state whereas the methods are its behaviour. Let's stub out the methods for now - except the constructor (which is called when an object is created). Let's give the constructor the x, y and radius as well as the context to the canvas.
class Particle {
constructor(ctx, x, y, radius) {
this.ctx = ctx;
this.x = x;
this.y = y;
this.radius = radius;
}
render() {
}
update() {
}
}
Let's also flesh out the render method - drawing the particle on to the canvas. This will draw a white circle and fill it.
render() {
this.ctx.fillStyle = "#FFFFFF";
this.ctx.strokeStyle = "#FFFFFF";
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
this.ctx.stroke();
this.ctx.fill();
}
Let's finish this step by drawing random particles across the viewport.
let particles = [];
for (let i = 0; i < 1000; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
particles.push(new Particle(context, x, y, 1));
}
particles.forEach((particle) => { particle.render() } );
4 Let's track mouse movements
We create a simple object called mouse with the properties x, y and radius. We then add an anonymous function to the mousemove event listener which updates the mouse's x, y values when the mouse is moved.
//...
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const mouse = {
x: null,
y: null,
radius: 80,
};
document.addEventListener("mousemove", (evt) => {
mouse.x = evt.x;
mouse.y = evt.y;
});
//...
Let's revisit the Particle class. We need to git it two new properties - originX and originY - so it can remember it's original position. We also need to flesh out the update method so that it is aware of the mouse movements and knows how react to them.
constructor(ctx, x, y, radius) {
this.ctx = ctx;
this.x = x;
this.y = y;
this.radius = radius;
this.originX = x;
this.originY = y;
}
// ...
update() {
const dx = mouse.x - this.x;
const dy = mouse.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const forceDirectionX = dx / distance;
const forceDirectionY = dy / distance;
const maxDistance = mouse.radius;
const force = (maxDistance - distance) / maxDistance;
const directionX = forceDirectionX * force * this.density;
const directionY = forceDirectionY * force * this.density;
if (distance < mouse.radius) {
this.x -= directionX;
this.y -= directionY;
} else {
if (this.x !== this.originX) {
const dx = this.x - this.originX;
this.x -= dx / 10;
}
if (this.y !== this.originY) {
const dy = this.y - this.originY;
this.y -= dy / 10;
}
}
this.render();
}
The update method might look daunting. Don't worry, I'll explain what's going on. We're doing a little Maths to work out the distance between the particle and the mouse. We're then moving the particle out of the way - to the radius of the mouse. There's also some Maths to determine the speed at which the particle moves. If the particle is outside of the radius of the mouse and it isn't at it's original position, it will move back towards it. Finally, the update method calls the render method so that the particle can re-draw it's now position.
5 Let's add some text
// ...
context.fillStyle = "#FFFFFF";
context.font = "30px Arial, sans serif";
context.textAlign = "left";
context.fillText("Text Particles", 0, 40);
let particles = [];
for (let i = 0; i < 1000; i++) {
const x = Math.random() * canvas.width;
// ...
6 Let's capture the text pixel data and create particle text
// ...
context.fillStyle = "#FFFFFF";
context.font = "30px Arial, sans serif";
context.textAlign = "left";
context.fillText("Text Particles", 0, 40);
const textCoordinates = context.getImageData(0, 0, 250, 100);
for (let y = 0, y2 = textCoordinates.height; y < y2; y++) {
for (let x = 0, x2 = textCoordinates.width; x < x2; x++) {
if (
textCoordinates.data[y * 4 * textCoordinates.width + x * 4 + 3] >
0
) {
particles.push(new Particle(context, x * 8 + 40, y * 8 + 50, 3));
}
}
}
let particles = [];
for (let i = 0; i < 1000; i++) {
const x = Math.random() * canvas.width;
// ...
7 Let's tidy up things...
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let particles = [];
let animationFrameId = null;
const mouse = {
x: null,
y: null,
radius: 80,
};
class Particle {
constructor(ctx, x, y, radius) {
this.ctx = ctx;
this.x = x;
this.y = y;
this.radius = radius;
this.density = Math.random() * 80 + 1;
this.originX = x;
this.originY = y;
this.color = Particle.pickSpriteColor();
}
static pickSpriteColor() {
const colors = [
"#DC7633",
"#F5B041",
"#2ECC71",
"#16A085",
"#3498DB",
"#BB8FCE",
"#EC7063",
"#BFC9CA",
];
const color = colors[Math.floor(Math.random() * colors.length)];
return color;
}
render() {
this.ctx.fillStyle = this.color;
this.ctx.strokeStyle = this.color;
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
this.ctx.stroke();
this.ctx.fill();
}
update() {
const dx = mouse.x - this.x;
const dy = mouse.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const forceDirectionX = dx / distance;
const forceDirectionY = dy / distance;
const maxDistance = mouse.radius;
const force = (maxDistance - distance) / maxDistance;
const directionX = forceDirectionX * force * this.density;
const directionY = forceDirectionY * force * this.density;
if (distance < mouse.radius) {
this.x -= directionX;
this.y -= directionY;
} else {
if (this.x !== this.originX) {
const dx = this.x - this.originX;
this.x -= dx / 10;
}
if (this.y !== this.originY) {
const dy = this.y - this.originY;
this.y -= dy / 10;
}
}
this.render();
}
}
const createParticlesFromText = () => {
context.fillStyle = "#FFFFFF";
context.font = "30px Arial, sans serif";
context.textAlign = "left";
context.fillText("Text Particles", 0, 40);
const textCoordinates = context.getImageData(0, 0, 250, 100);
for (let y = 0, y2 = textCoordinates.height; y < y2; y++) {
for (let x = 0, x2 = textCoordinates.width; x < x2; x++) {
if (
textCoordinates.data[y * 4 * textCoordinates.width + x * 4 + 3] >
0
) {
particles.push(new Particle(context, x * 8 + 40, y * 8 + 50, 3));
}
}
}
};
createParticlesFromText();
mouse.x = 0;
mouse.y = 0;
document.addEventListener("mousemove", (evt) => {
mouse.x = evt.x;
mouse.y = evt.y;
});
window.addEventListener("resize", (evt) => {
cancelAnimationFrame(animationFrameId);
particles = [];
createParticlesFromText();
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
animate();
});
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
context.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((p) => {
p.update();
});
};
animate();
And here's the finished product! Move your cursor around the text to see the particle animations.