🎨 Canvas & Animation
The HTML5 Canvas API allows you to draw graphics, create animations, and build interactive games directly in the browser using JavaScript. It provides a powerful 2D drawing surface that's essential for creating visual effects, charts, and games.
💡 Key Concept
Canvas is a bitmap-based drawing surface where you use JavaScript to draw shapes, images, and text. Unlike SVG, canvas is raster-based (pixels), making it better for animations and games.
🚀 Getting Started with Canvas
Setting Up Canvas
HTML Canvas Element
<!-- HTML -->
<canvas id="myCanvas" width="800" height="600"></canvas>
<!-- Canvas dimensions should be set in HTML or JavaScript, not CSS -->
<canvas id="myCanvas" width="800" height="600" style="border: 1px solid black;">
Your browser does not support canvas.
</canvas>
<script>
// JavaScript: Get canvas and context
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d'); // 2D rendering context
// Set dimensions dynamically
canvas.width = 800;
canvas.height = 600;
// Make canvas responsive
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
</script>
⚠️ Canvas vs CSS Sizing
Set canvas dimensions using the width and height attributes, not CSS. CSS will stretch the canvas, causing blurry graphics.
📐 Drawing Basic Shapes
Rectangles
Drawing Rectangles
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// fillRect(x, y, width, height) - filled rectangle
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 200, 100);
// strokeRect(x, y, width, height) - outlined rectangle
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.strokeRect(300, 50, 200, 100);
// clearRect(x, y, width, height) - clear rectangle area
ctx.clearRect(75, 75, 50, 50); // Cuts a hole in the red rectangle
// Multiple rectangles
ctx.fillStyle = 'green';
ctx.fillRect(50, 200, 100, 100);
ctx.fillStyle = 'orange';
ctx.fillRect(200, 200, 100, 100);
ctx.fillStyle = 'purple';
ctx.fillRect(350, 200, 100, 100);
Paths (Lines, Triangles, Custom Shapes)
Drawing with Paths
// Drawing a line
ctx.beginPath(); // Start new path
ctx.moveTo(50, 50); // Move to starting point
ctx.lineTo(200, 100); // Draw line to this point
ctx.lineTo(300, 50); // Draw another line
ctx.strokeStyle = 'red';
ctx.lineWidth = 3;
ctx.stroke(); // Actually draw the path
// Drawing a triangle
ctx.beginPath();
ctx.moveTo(100, 200); // Top point
ctx.lineTo(50, 300); // Bottom left
ctx.lineTo(150, 300); // Bottom right
ctx.closePath(); // Close the path back to start
ctx.fillStyle = 'blue';
ctx.fill(); // Fill the triangle
// Drawing a custom shape
ctx.beginPath();
ctx.moveTo(250, 200);
ctx.lineTo(300, 250);
ctx.lineTo(350, 200);
ctx.lineTo(400, 250);
ctx.lineTo(350, 300);
ctx.lineTo(250, 300);
ctx.closePath();
ctx.strokeStyle = 'green';
ctx.lineWidth = 2;
ctx.stroke();
// Line properties
ctx.lineCap = 'round'; // 'butt', 'round', 'square'
ctx.lineJoin = 'round'; // 'miter', 'round', 'bevel'
Circles and Arcs
Drawing Circles and Arcs
// arc(x, y, radius, startAngle, endAngle, anticlockwise)
// Angles are in radians (0 to 2π)
// Full circle
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2); // Full circle
ctx.fillStyle = 'red';
ctx.fill();
// Outlined circle
ctx.beginPath();
ctx.arc(250, 100, 50, 0, Math.PI * 2);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.stroke();
// Half circle (arc)
ctx.beginPath();
ctx.arc(400, 100, 50, 0, Math.PI); // 0 to π (half circle)
ctx.fillStyle = 'green';
ctx.fill();
// Quarter circle (pie slice)
ctx.beginPath();
ctx.moveTo(100, 250); // Start at center
ctx.arc(100, 250, 50, 0, Math.PI / 2); // 0 to π/2 (quarter)
ctx.closePath();
ctx.fillStyle = 'orange';
ctx.fill();
// Pac-Man shape
ctx.beginPath();
ctx.arc(250, 250, 50, 0.2 * Math.PI, 1.8 * Math.PI);
ctx.lineTo(250, 250); // Line back to center
ctx.closePath();
ctx.fillStyle = 'yellow';
ctx.fill();
// Helper: Degrees to Radians
function toRadians(degrees) {
return degrees * (Math.PI / 180);
}
ctx.arc(400, 250, 50, toRadians(0), toRadians(90));
🎨 Colors, Gradients, and Patterns
Colors and Transparency
Working with Colors
// Solid colors
ctx.fillStyle = 'red';
ctx.fillStyle = '#ff0000';
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; // Semi-transparent
// Global transparency
ctx.globalAlpha = 0.5; // All drawings 50% transparent
ctx.fillRect(50, 50, 100, 100);
ctx.globalAlpha = 1.0; // Reset to opaque
// Overlapping with transparency
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(100, 100, 150, 150);
ctx.fillStyle = 'rgba(0, 0, 255, 0.5)';
ctx.fillRect(150, 150, 150, 150);
Gradients
Linear and Radial Gradients
// Linear gradient
const linearGradient = ctx.createLinearGradient(0, 0, 200, 0); // x0, y0, x1, y1
linearGradient.addColorStop(0, 'red'); // Start color
linearGradient.addColorStop(0.5, 'yellow'); // Middle color
linearGradient.addColorStop(1, 'blue'); // End color
ctx.fillStyle = linearGradient;
ctx.fillRect(50, 50, 200, 100);
// Radial gradient (circular)
const radialGradient = ctx.createRadialGradient(350, 100, 10, 350, 100, 50);
radialGradient.addColorStop(0, 'white');
radialGradient.addColorStop(1, 'blue');
ctx.fillStyle = radialGradient;
ctx.fillRect(300, 50, 100, 100);
// Gradient for circle
const circleGradient = ctx.createRadialGradient(100, 250, 0, 100, 250, 50);
circleGradient.addColorStop(0, 'yellow');
circleGradient.addColorStop(1, 'orange');
ctx.beginPath();
ctx.arc(100, 250, 50, 0, Math.PI * 2);
ctx.fillStyle = circleGradient;
ctx.fill();
📝 Drawing Text
Text Rendering
// fillText(text, x, y, [maxWidth])
ctx.font = '30px Arial';
ctx.fillStyle = 'black';
ctx.fillText('Hello Canvas!', 50, 50);
// strokeText(text, x, y, [maxWidth])
ctx.font = '40px Arial';
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
ctx.strokeText('Outlined Text', 50, 120);
// Font properties
ctx.font = 'bold 24px Georgia';
ctx.font = 'italic 20px Times New Roman';
ctx.font = '18px monospace';
// Text alignment
ctx.textAlign = 'left'; // 'left', 'center', 'right', 'start', 'end'
ctx.textAlign = 'center';
ctx.fillText('Centered', 400, 200);
// Baseline alignment
ctx.textBaseline = 'top'; // 'top', 'middle', 'bottom', 'alphabetic'
ctx.textBaseline = 'middle';
ctx.fillText('Middle aligned', 400, 250);
// Measure text
const text = 'Measure me';
const metrics = ctx.measureText(text);
console.log('Text width:', metrics.width);
// Shadow effect
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillText('Shadow Text', 50, 300);
// Reset shadow
ctx.shadowColor = 'transparent';
🖼️ Drawing Images
Working with Images
// Load and draw image
const img = new Image();
img.src = 'path/to/image.png';
img.onload = function() {
// drawImage(image, x, y)
ctx.drawImage(img, 50, 50);
// drawImage(image, x, y, width, height) - scaled
ctx.drawImage(img, 200, 50, 100, 100);
// drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) - cropped
ctx.drawImage(
img,
0, 0, 50, 50, // Source: crop from (0,0) 50x50
350, 50, 100, 100 // Destination: draw at (350,50) 100x100
);
};
// Canvas to image
const dataURL = canvas.toDataURL('image/png');
const imgElement = document.createElement('img');
imgElement.src = dataURL;
document.body.appendChild(imgElement);
// Image from another canvas
const canvas2 = document.getElementById('canvas2');
ctx.drawImage(canvas2, 0, 0);
🔄 Transformations
Translate, Rotate, Scale
// Save current state
ctx.save();
// Translate (move origin)
ctx.translate(100, 100);
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 50, 50); // Draws at (100, 100)
// Restore previous state
ctx.restore();
// Rotate (in radians)
ctx.save();
ctx.translate(250, 100); // Move to rotation point
ctx.rotate(Math.PI / 4); // Rotate 45 degrees
ctx.fillStyle = 'blue';
ctx.fillRect(-25, -25, 50, 50); // Draw centered at rotation point
ctx.restore();
// Scale
ctx.save();
ctx.translate(400, 100);
ctx.scale(2, 1); // Double width, normal height
ctx.fillStyle = 'green';
ctx.fillRect(-25, -25, 50, 50);
ctx.restore();
// Combined transformations
ctx.save();
ctx.translate(100, 300);
ctx.rotate(Math.PI / 6);
ctx.scale(1.5, 1.5);
ctx.fillStyle = 'purple';
ctx.fillRect(0, 0, 50, 50);
ctx.restore();
// Always use save() and restore() to isolate transformations!
🎬 Animation Basics
Animation Loop with requestAnimationFrame
Basic Animation Loop
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
let x = 0;
let y = 100;
let speed = 2;
function animate() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Update position
x += speed;
// Bounce at edges
if (x + 50 > canvas.width || x < 0) {
speed = -speed;
}
// Draw
ctx.fillStyle = 'red';
ctx.fillRect(x, y, 50, 50);
// Continue animation
requestAnimationFrame(animate);
}
// Start animation
animate();
// Stop animation
let animationId;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(x, y, 50, 50);
x += speed;
animationId = requestAnimationFrame(animate);
}
// Stop
cancelAnimationFrame(animationId);
Moving Circle Animation
Bouncing Circle
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const ball = {
x: 100,
y: 100,
radius: 20,
dx: 3, // Velocity X
dy: 2, // Velocity Y
color: 'blue'
};
function drawBall() {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
ctx.closePath();
}
function updateBall() {
// Update position
ball.x += ball.dx;
ball.y += ball.dy;
// Bounce off walls
if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
ball.dx = -ball.dx;
}
if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
ball.dy = -ball.dy;
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
updateBall();
requestAnimationFrame(animate);
}
animate();
🎮 Game Loop Pattern
Complete Game Loop
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// Game state
const gameState = {
player: { x: 100, y: 100, width: 30, height: 30, speed: 5 },
enemies: [],
score: 0,
gameOver: false
};
// Input handling
const keys = {};
document.addEventListener('keydown', (e) => keys[e.key] = true);
document.addEventListener('keyup', (e) => keys[e.key] = false);
// Update game state
function update() {
if (gameState.gameOver) return;
// Move player
if (keys['ArrowLeft']) gameState.player.x -= gameState.player.speed;
if (keys['ArrowRight']) gameState.player.x += gameState.player.speed;
if (keys['ArrowUp']) gameState.player.y -= gameState.player.speed;
if (keys['ArrowDown']) gameState.player.y += gameState.player.speed;
// Keep player in bounds
gameState.player.x = Math.max(0, Math.min(canvas.width - gameState.player.width, gameState.player.x));
gameState.player.y = Math.max(0, Math.min(canvas.height - gameState.player.height, gameState.player.y));
// Update enemies
gameState.enemies.forEach((enemy) => {
enemy.y += enemy.speed;
// Remove off-screen enemies
if (enemy.y > canvas.height) {
const index = gameState.enemies.indexOf(enemy);
gameState.enemies.splice(index, 1);
gameState.score++;
}
});
// Spawn enemies
if (Math.random() < 0.02) {
gameState.enemies.push({
x: Math.random() * (canvas.width - 30),
y: -30,
width: 30,
height: 30,
speed: 2
});
}
// Collision detection
gameState.enemies.forEach((enemy) => {
if (checkCollision(gameState.player, enemy)) {
gameState.gameOver = true;
}
});
}
// Draw everything
function draw() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw player
ctx.fillStyle = 'blue';
ctx.fillRect(
gameState.player.x,
gameState.player.y,
gameState.player.width,
gameState.player.height
);
// Draw enemies
ctx.fillStyle = 'red';
gameState.enemies.forEach((enemy) => {
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
});
// Draw score
ctx.fillStyle = 'black';
ctx.font = '20px Arial';
ctx.fillText(`Score: ${gameState.score}`, 10, 30);
// Draw game over
if (gameState.gameOver) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = '48px Arial';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2);
ctx.font = '24px Arial';
ctx.fillText(`Score: ${gameState.score}`, canvas.width / 2, canvas.height / 2 + 40);
}
}
// Collision detection
function checkCollision(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
// Main game loop
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// Start game
gameLoop();
✨ Particle System
Simple Particle Effect
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = Math.random() * 5 + 2;
this.speedX = Math.random() * 3 - 1.5;
this.speedY = Math.random() * 3 - 1.5;
this.color = `hsl(${Math.random() * 360}, 50%, 50%)`;
this.life = 100;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.life -= 1;
this.size *= 0.98; // Shrink over time
}
draw() {
ctx.fillStyle = this.color;
ctx.globalAlpha = this.life / 100;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
const particles = [];
// Create particles on click
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
for (let i = 0; i < 30; i++) {
particles.push(new Particle(x, y));
}
});
function animate() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Update and draw particles
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
particles[i].draw();
// Remove dead particles
if (particles[i].life <= 0) {
particles.splice(i, 1);
}
}
requestAnimationFrame(animate);
}
animate();
🖱️ Mouse Interaction
Drawing with Mouse
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
let isDrawing = false;
let lastX = 0;
let lastY = 0;
canvas.addEventListener('mousedown', (e) => {
isDrawing = true;
const rect = canvas.getBoundingClientRect();
lastX = e.clientX - rect.left;
lastY = e.clientY - rect.top;
});
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.stroke();
lastX = x;
lastY = y;
});
canvas.addEventListener('mouseup', () => {
isDrawing = false;
});
canvas.addEventListener('mouseleave', () => {
isDrawing = false;
});
// Clear button
document.getElementById('clearBtn').addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
⚡ Performance Optimization
💡 Canvas Performance Tips
- Use
requestAnimationFrame()instead ofsetInterval() - Clear only the necessary parts instead of the entire canvas
- Batch drawing operations to minimize state changes
- Use integer coordinates when possible (avoid decimals)
- Pre-render complex scenes to off-screen canvas
- Limit particle count and remove off-screen objects
- Use
ctx.save()andctx.restore()sparingly - Avoid unnecessary
beginPath()calls - Cache repeated calculations
Performance Optimizations
// ❌ Bad: Drawing everything every frame
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw static background every frame
drawBackground();
drawStaticElements();
drawMovingElements();
requestAnimationFrame(animate);
}
// ✅ Good: Off-screen canvas for static elements
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d');
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
// Draw static elements once
drawBackgroundOnce(offscreenCtx);
drawStaticElementsOnce(offscreenCtx);
function animate() {
// Draw pre-rendered static elements
ctx.drawImage(offscreenCanvas, 0, 0);
// Only draw moving elements
drawMovingElements();
requestAnimationFrame(animate);
}
// Efficient clearing for small objects
function clearArea(x, y, width, height) {
ctx.clearRect(x - 1, y - 1, width + 2, height + 2);
}
🎯 Practical Examples
Example 1: Clock Animation
Animated Analog Clock
const canvas = document.getElementById('clockCanvas');
const ctx = canvas.getContext('2d');
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 150;
function drawClock() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw clock face
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.fill();
ctx.strokeStyle = 'black';
ctx.lineWidth = 5;
ctx.stroke();
// Draw numbers
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
for (let i = 1; i <= 12; i++) {
const angle = (i * 30 - 90) * Math.PI / 180;
const x = centerX + Math.cos(angle) * (radius - 30);
const y = centerY + Math.sin(angle) * (radius - 30);
ctx.fillText(i.toString(), x, y);
}
// Get current time
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
// Draw hour hand
const hourAngle = ((hours * 30 + minutes * 0.5) - 90) * Math.PI / 180;
drawHand(hourAngle, radius * 0.5, 6, 'black');
// Draw minute hand
const minuteAngle = ((minutes * 6 + seconds * 0.1) - 90) * Math.PI / 180;
drawHand(minuteAngle, radius * 0.7, 4, 'black');
// Draw second hand
const secondAngle = ((seconds * 6) - 90) * Math.PI / 180;
drawHand(secondAngle, radius * 0.9, 2, 'red');
// Center dot
ctx.beginPath();
ctx.arc(centerX, centerY, 8, 0, Math.PI * 2);
ctx.fillStyle = 'black';
ctx.fill();
}
function drawHand(angle, length, width, color) {
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(
centerX + Math.cos(angle) * length,
centerY + Math.sin(angle) * length
);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = 'round';
ctx.stroke();
}
function animate() {
drawClock();
requestAnimationFrame(animate);
}
animate();
Example 2: Starfield Animation
Moving Starfield
const canvas = document.getElementById('starCanvas');
const ctx = canvas.getContext('2d');
class Star {
constructor() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.z = Math.random() * canvas.width;
}
update() {
this.z -= 5;
if (this.z <= 0) {
this.z = canvas.width;
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
}
}
draw() {
const sx = (this.x - canvas.width / 2) * (canvas.width / this.z);
const sy = (this.y - canvas.height / 2) * (canvas.width / this.z);
const x = sx + canvas.width / 2;
const y = sy + canvas.height / 2;
const size = (1 - this.z / canvas.width) * 3;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
}
const stars = Array.from({ length: 200 }, () => new Star());
function animate() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
stars.forEach((star) => {
star.update();
star.draw();
});
requestAnimationFrame(animate);
}
animate();
✅ Best Practices
💡 Canvas Best Practices
- Set canvas size with width/height attributes, not CSS
- Always use
save()andrestore()for transformations - Clear canvas before each frame with
clearRect() - Use
requestAnimationFrame()for smooth animations - Store
getContext()result, don't call it repeatedly - Close paths with
closePath()for filled shapes - Use radians for angles (degrees × π / 180)
- Optimize by reducing unnecessary redraws
- Test performance on different devices
📝 Summary
- Canvas provides a powerful 2D drawing API for graphics and games
- getContext('2d') returns the drawing context
- Shapes can be drawn using rectangles, paths, circles, and arcs
- Styles include colors, gradients, patterns, and text
- Transformations allow rotation, scaling, and translation
- requestAnimationFrame() creates smooth animations
- Game loops follow update-draw pattern
- Collision detection enables interactivity
- Performance matters for smooth animations