Add some style to your header.
This is the first tutorial for pbritton.dev so it makes sense to start with the main project for this WordPress theme: the header image on the front page! These are basic instructions on how I created it with HTML, JavaScipt, and CSS. You can find the github repo here.
Alright, let’s walk through this code step-by-step in a more conversational way.
Step 1: Setting Up the HTML Structure
First, we start with the basic HTML structure. This includes the doctype declaration to specify HTML5 and the <html>
tag with a language attribute set to English. Inside the <head>
, we have meta tags to set the character encoding to UTF-8 and make the web page responsive by adjusting the viewport settings.
We also include a <title>
for the web page and some basic styles within a <style>
tag. These styles ensure that the page has no margin, fills the entire browser window, and prevents scrollbars from appearing. The <canvas>
element is set to fill the entire screen with no background color. Finally, the body of the HTML contains a single <canvas>
element where we’ll draw our bouncing balls.
Here’s what that looks like:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Bouncing Balls with Flinging and Header Text</title>
<style>
body, html {
margin: 0;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
background-color: transparent;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
JavaScriptStep 2: Adding Google Fonts
To make our header text look nice, we add a link to Google Fonts in the <head>
section. This link preloads the “Bebas Neue” font, which we’ll use for our header text.
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" rel="stylesheet">
Step 3: Writing the JavaScript
Now, we get to the fun part: the JavaScript. This script will handle everything from setting up the canvas to animating the balls and detecting interactions.
First, we add a <script>
tag and ensure the script runs after the DOM has fully loaded by using DOMContentLoaded
. Inside this, we get a reference to the canvas element and its 2D drawing context, which we’ll use to draw our balls.
<script>
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
We define some variables to hold the balls, set the number of balls, the friction coefficient to slow down the balls, and a text string for our header.
let balls = [];
const numSmallBalls = 10;
const friction = 0.99;
const text = "pbritton.dev";
const colors = ['#003865', '#78BE21', '#5D295F', '#000', '#002469', '#007336'];
Step 4: Setting Up an Off-Screen Canvas for Text
We create an off-screen canvas specifically for drawing the text. This helps us separate the text rendering from the main animation canvas. We also set its dimensions to match the main canvas and define the initial position for our text.
const textCanvas = document.createElement('canvas');
const textCtx = textCanvas.getContext('2d', { willReadFrequently: true });
textCanvas.width = canvas.width;
textCanvas.height = canvas.height;
let textPosition = { x: 10, y: canvas.height - 50 };
Step 5: Drawing the Text
We create a function to draw the text on our text canvas. This function clears any previous drawings, sets the alignment and font, and draws the text.
function drawText() {
textCtx.clearRect(0, 0, textCanvas.width, textCanvas.height);
textCtx.textAlign = 'left';
textCtx.textBaseline = 'bottom';
textCtx.fillStyle = '#003865';
textCtx.font = '6.25rem "Bebas Neue"';
textCtx.fillText(text, textPosition.x, textPosition.y);
}
Step 6: Resizing the Canvas
Next, we define a function to resize the canvas whenever the window is resized. This ensures that our canvas always fits the screen. Inside this function, we also adjust the size of the text canvas and recalculate the position and size of the text.
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
textCanvas.width = canvas.width;
textCanvas.height = canvas.height;
textPosition.x = 10;
textPosition.y = textCanvas.height - 50;
adjustFontSize();
drawText();
}
window.addEventListener('resize', resizeCanvas);
Step 7: Adjusting the Font Size
To ensure our text fits nicely within the canvas, we define a function to adjust the font size. This function starts with a maximum font size and reduces it until the text fits within the canvas width.
function adjustFontSize() {
const maxFontSize = 100;
let fontSize = maxFontSize;
textCtx.font = `${fontSize}px "Bebas Neue", sans-serif`;
let textWidth = textCtx.measureText(text).width;
while (textWidth > textCanvas.width - 20 && fontSize > 10) {
fontSize -= 1;
textCtx.font = `${fontSize}px "Bebas Neue"`;
textWidth = textCtx.measureText(text).width;
}
}
Step 8: Initializing the Balls
We create the balls with random positions, velocities, colors, and radii. Each ball is pushed into the balls
array.
for (let i = 0; i < numSmallBalls; i++) {
const color = colors[i % colors.length];
balls.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: 10 + Math.random() * 10,
color: color,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
isMoving: true
});
}
Step 9: Handling Mouse and Touch Events
We add functions to handle mouse and touch events, allowing us to drag the balls around. These functions check if a ball is clicked and update its position as it is dragged.
let isDragging = false;
let dragBall = null;
let lastMouseX, lastMouseY;
function handleMouseDown(event) {
event.preventDefault();
const mouseX = event.clientX || event.touches[0].clientX;
const mouseY = event.clientY || event.touches[0].clientY;
balls.forEach(ball => {
if (Math.hypot(ball.x - mouseX, ball.y - mouseY) <= ball.radius) {
isDragging = true;
dragBall = ball;
lastMouseX = mouseX;
lastMouseY = mouseY;
}
});
}
function handleMouseMove(event) {
event.preventDefault();
const mouseX = event.clientX || event.touches[0].clientX;
const mouseY = event.clientY || event.touches[0].clientY;
if (isDragging) {
dragBall.x = mouseX;
dragBall.y = mouseY;
dragBall.vx = mouseX - lastMouseX;
dragBall.vy = mouseY - lastMouseY;
lastMouseX = mouseX;
lastMouseY = mouseY;
}
}
function handleMouseUp(event) {
event.preventDefault();
isDragging = false;
dragBall = null;
}
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('touchstart', handleMouseDown, { passive: false });
canvas.addEventListener('touchmove', handleMouseMove, { passive: false });
canvas.addEventListener('touchend', handleMouseUp);
Step 10: Drawing and Updating the Balls
We create a function to draw the balls on the canvas and another to update their positions and handle collisions. The update function is called continuously using requestAnimationFrame
.
function drawBalls() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(textCanvas, 0, 0);
balls.forEach(ball => {
ctx.fillStyle = ball.color;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fill();
});
}
function detectCollisions() {
for (let i = 0; i < balls.length; i++) {
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[i];
const ball2 = balls[j];
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y
;
const distance = Math.hypot(dx, dy);
const minDistance = ball1.radius + ball2.radius;
if (distance < minDistance) {
const angle = Math.atan2(dy, dx);
const totalMass = ball1.radius + ball2.radius;
const v1 = Math.sqrt(ball1.vx * ball1.vx + ball1.vy * ball1.vy);
const v2 = Math.sqrt(ball2.vx * ball2.vx + ball2.vy * ball2.vy);
ball1.vx = (v1 * Math.cos(angle) * (ball1.radius - ball2.radius) + (2 * ball2.radius * v2 * Math.cos(angle))) / totalMass;
ball1.vy = (v1 * Math.sin(angle) * (ball1.radius - ball2.radius) + (2 * ball2.radius * v2 * Math.sin(angle))) / totalMass;
ball2.vx = (v2 * Math.cos(angle) * (ball2.radius - ball1.radius) + (2 * ball1.radius * v1 * Math.cos(angle))) / totalMass;
ball2.vy = (v2 * Math.sin(angle) * (ball2.radius - ball1.radius) + (2 * ball1.radius * v1 * Math.sin(angle))) / totalMass;
const overlap = minDistance - distance;
const correction = overlap / 2;
ball1.x -= correction * Math.cos(angle);
ball1.y -= correction * Math.sin(angle);
ball2.x += correction * Math.cos(angle);
ball2.y += correction * Math.sin(angle);
}
}
}
}
function textCollision(ball) {
const radius = ball.radius;
const imageData = textCtx.getImageData(ball.x - radius, ball.y - radius, radius * 2, radius * 2);
const pixels = imageData.data;
for (let y = 0; y < radius * 2; y++) {
for (let x = 0; x < radius * 2; x++) {
const index = (y * radius * 2 + x) * 4;
if (pixels[index + 3] > 128) {
const dx = x - radius;
const dy = y - radius;
const distance = Math.hypot(dx, dy);
if (distance < radius) {
ball.vx = -ball.vx * 0.7;
ball.vy = -ball.vy * 0.7;
ball.x += ball.vx;
ball.y += ball.vy;
return;
}
}
}
}
}
function update() {
balls.forEach(ball => {
if (!isDragging || ball !== dragBall) {
if (ball.isMoving) {
ball.vy += 0.2;
ball.vx += 0;
ball.vy *= friction;
ball.vx *= friction;
ball.y += ball.vy;
ball.x += ball.vx;
if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
ball.vy *= -0.7;
ball.y = ball.y > canvas.height / 2 ? canvas.height - ball.radius : ball.radius;
}
if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
ball.vx *= -0.7;
ball.x = ball.x > canvas.width / 2 ? canvas.width - ball.radius : ball.radius;
}
textCollision(ball);
if (Math.abs(ball.vx) < 0.01 && Math.abs(ball.vy) < 0.01) {
ball.vx = 0;
ball.vy = 0;
ball.isMoving = false;
}
}
}
});
detectCollisions();
drawBalls();
requestAnimationFrame(update);
}
update();
});
</script>
And that’s it! The JavaScript code handles everything from setting up the canvas and drawing the balls to animating them and detecting collisions. With this code, you’ll have an interactive canvas with bouncing balls that you can drag around and fling.