Browser APIsFeatured
JavaScript Canvas API: Complete Graphics Programming Guide
Master the Canvas API in JavaScript for 2D graphics, animations, and games. Learn drawing, transformations, animations, and image manipulation.
By JavaScriptDoc Team•
canvasgraphicsanimationjavascript2d
JavaScript Canvas API: Complete Graphics Programming Guide
The Canvas API provides a powerful way to draw graphics, create animations, and build interactive visualizations directly in the browser using JavaScript.
Getting Started with Canvas
The Canvas API allows you to draw 2D graphics using JavaScript.
// Basic canvas setup
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// Set canvas size
canvas.width = 800;
canvas.height = 600;
// Handle high DPI displays
function setupCanvas(canvas) {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
// Set actual size in memory
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// Scale coordinate system
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
// Set CSS size
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
return ctx;
}
// Drawing basics
ctx.fillStyle = 'blue';
ctx.fillRect(10, 10, 100, 100); // x, y, width, height
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.strokeRect(120, 10, 100, 100);
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
Drawing Shapes
Basic Shapes
class ShapeDrawer {
constructor(ctx) {
this.ctx = ctx;
}
// Rectangle
drawRectangle(x, y, width, height, options = {}) {
const {
fillColor = null,
strokeColor = null,
lineWidth = 1,
borderRadius = 0,
} = options;
this.ctx.save();
if (borderRadius > 0) {
this.drawRoundedRect(x, y, width, height, borderRadius);
} else {
this.ctx.beginPath();
this.ctx.rect(x, y, width, height);
}
if (fillColor) {
this.ctx.fillStyle = fillColor;
this.ctx.fill();
}
if (strokeColor) {
this.ctx.strokeStyle = strokeColor;
this.ctx.lineWidth = lineWidth;
this.ctx.stroke();
}
this.ctx.restore();
}
// Rounded rectangle
drawRoundedRect(x, y, width, height, radius) {
this.ctx.beginPath();
this.ctx.moveTo(x + radius, y);
this.ctx.lineTo(x + width - radius, y);
this.ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
this.ctx.lineTo(x + width, y + height - radius);
this.ctx.quadraticCurveTo(
x + width,
y + height,
x + width - radius,
y + height
);
this.ctx.lineTo(x + radius, y + height);
this.ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
this.ctx.lineTo(x, y + radius);
this.ctx.quadraticCurveTo(x, y, x + radius, y);
this.ctx.closePath();
}
// Circle
drawCircle(x, y, radius, options = {}) {
const {
fillColor = null,
strokeColor = null,
lineWidth = 1,
startAngle = 0,
endAngle = Math.PI * 2,
} = options;
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(x, y, radius, startAngle, endAngle);
if (fillColor) {
this.ctx.fillStyle = fillColor;
this.ctx.fill();
}
if (strokeColor) {
this.ctx.strokeStyle = strokeColor;
this.ctx.lineWidth = lineWidth;
this.ctx.stroke();
}
this.ctx.restore();
}
// Polygon
drawPolygon(points, options = {}) {
if (points.length < 3) return;
const {
fillColor = null,
strokeColor = null,
lineWidth = 1,
closePath = true,
} = options;
this.ctx.save();
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
this.ctx.lineTo(points[i].x, points[i].y);
}
if (closePath) {
this.ctx.closePath();
}
if (fillColor) {
this.ctx.fillStyle = fillColor;
this.ctx.fill();
}
if (strokeColor) {
this.ctx.strokeStyle = strokeColor;
this.ctx.lineWidth = lineWidth;
this.ctx.stroke();
}
this.ctx.restore();
}
// Star
drawStar(cx, cy, outerRadius, innerRadius, points, options = {}) {
const angle = Math.PI / points;
const vertices = [];
for (let i = 0; i < points * 2; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const x = cx + radius * Math.cos(i * angle - Math.PI / 2);
const y = cy + radius * Math.sin(i * angle - Math.PI / 2);
vertices.push({ x, y });
}
this.drawPolygon(vertices, options);
}
// Heart
drawHeart(x, y, size, options = {}) {
this.ctx.save();
this.ctx.beginPath();
const topCurveHeight = size * 0.3;
this.ctx.moveTo(x, y + topCurveHeight);
// Top left curve
this.ctx.bezierCurveTo(
x,
y,
x - size / 2,
y,
x - size / 2,
y + topCurveHeight
);
// Bottom left curve
this.ctx.bezierCurveTo(
x - size / 2,
y + (size + topCurveHeight) / 2,
x,
y + (size + topCurveHeight) / 2,
x,
y + size
);
// Bottom right curve
this.ctx.bezierCurveTo(
x,
y + (size + topCurveHeight) / 2,
x + size / 2,
y + (size + topCurveHeight) / 2,
x + size / 2,
y + topCurveHeight
);
// Top right curve
this.ctx.bezierCurveTo(x + size / 2, y, x, y, x, y + topCurveHeight);
if (options.fillColor) {
this.ctx.fillStyle = options.fillColor;
this.ctx.fill();
}
if (options.strokeColor) {
this.ctx.strokeStyle = options.strokeColor;
this.ctx.lineWidth = options.lineWidth || 1;
this.ctx.stroke();
}
this.ctx.restore();
}
}
// Usage
const drawer = new ShapeDrawer(ctx);
drawer.drawCircle(100, 100, 50, {
fillColor: 'rgba(255, 0, 0, 0.5)',
strokeColor: 'darkred',
lineWidth: 3,
});
drawer.drawStar(300, 100, 50, 25, 5, {
fillColor: 'gold',
strokeColor: 'orange',
lineWidth: 2,
});
Paths and Lines
Complex Path Drawing
class PathDrawer {
constructor(ctx) {
this.ctx = ctx;
}
// Bezier curves
drawBezierCurve(points, options = {}) {
const {
strokeColor = 'black',
lineWidth = 1,
showControlPoints = false,
} = options;
this.ctx.save();
this.ctx.strokeStyle = strokeColor;
this.ctx.lineWidth = lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, points[0].y);
if (points.length === 3) {
// Quadratic bezier
this.ctx.quadraticCurveTo(
points[1].x,
points[1].y,
points[2].x,
points[2].y
);
} else if (points.length === 4) {
// Cubic bezier
this.ctx.bezierCurveTo(
points[1].x,
points[1].y,
points[2].x,
points[2].y,
points[3].x,
points[3].y
);
}
this.ctx.stroke();
// Show control points
if (showControlPoints) {
this.ctx.fillStyle = 'red';
points.forEach((point, index) => {
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, 4, 0, Math.PI * 2);
this.ctx.fill();
if (index > 0 && index < points.length - 1) {
this.ctx.strokeStyle = 'rgba(255, 0, 0, 0.3)';
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, points[0].y);
this.ctx.lineTo(point.x, point.y);
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(
points[points.length - 1].x,
points[points.length - 1].y
);
this.ctx.lineTo(point.x, point.y);
this.ctx.stroke();
}
});
}
this.ctx.restore();
}
// Smooth curve through points
drawSmoothCurve(points, options = {}) {
if (points.length < 2) return;
const { strokeColor = 'black', lineWidth = 1, tension = 0.5 } = options;
this.ctx.save();
this.ctx.strokeStyle = strokeColor;
this.ctx.lineWidth = lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, points[0].y);
for (let i = 0; i < points.length - 1; i++) {
const p0 = points[i === 0 ? i : i - 1];
const p1 = points[i];
const p2 = points[i + 1];
const p3 = points[i + 2 < points.length ? i + 2 : i + 1];
const cp1x = p1.x + ((p2.x - p0.x) / 6) * tension;
const cp1y = p1.y + ((p2.y - p0.y) / 6) * tension;
const cp2x = p2.x - ((p3.x - p1.x) / 6) * tension;
const cp2y = p2.y - ((p3.y - p1.y) / 6) * tension;
this.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
}
this.ctx.stroke();
this.ctx.restore();
}
// Dashed line
drawDashedLine(x1, y1, x2, y2, pattern = [5, 5]) {
this.ctx.save();
this.ctx.setLineDash(pattern);
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
this.ctx.restore();
}
// Arrow
drawArrow(x1, y1, x2, y2, options = {}) {
const {
headLength = 10,
headAngle = Math.PI / 6,
strokeColor = 'black',
lineWidth = 1,
} = options;
const angle = Math.atan2(y2 - y1, x2 - x1);
this.ctx.save();
this.ctx.strokeStyle = strokeColor;
this.ctx.lineWidth = lineWidth;
// Draw line
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
// Draw arrowhead
this.ctx.beginPath();
this.ctx.moveTo(x2, y2);
this.ctx.lineTo(
x2 - headLength * Math.cos(angle - headAngle),
y2 - headLength * Math.sin(angle - headAngle)
);
this.ctx.moveTo(x2, y2);
this.ctx.lineTo(
x2 - headLength * Math.cos(angle + headAngle),
y2 - headLength * Math.sin(angle + headAngle)
);
this.ctx.stroke();
this.ctx.restore();
}
}
Text and Typography
Advanced Text Rendering
class TextRenderer {
constructor(ctx) {
this.ctx = ctx;
}
// Basic text drawing
drawText(text, x, y, options = {}) {
const {
font = '16px Arial',
fillColor = 'black',
strokeColor = null,
lineWidth = 1,
align = 'left',
baseline = 'alphabetic',
maxWidth = null,
} = options;
this.ctx.save();
this.ctx.font = font;
this.ctx.textAlign = align;
this.ctx.textBaseline = baseline;
if (fillColor) {
this.ctx.fillStyle = fillColor;
this.ctx.fillText(text, x, y, maxWidth);
}
if (strokeColor) {
this.ctx.strokeStyle = strokeColor;
this.ctx.lineWidth = lineWidth;
this.ctx.strokeText(text, x, y, maxWidth);
}
this.ctx.restore();
}
// Multi-line text
drawMultilineText(text, x, y, lineHeight, maxWidth) {
const words = text.split(' ');
let line = '';
let currentY = y;
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i] + ' ';
const metrics = this.ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && i > 0) {
this.ctx.fillText(line, x, currentY);
line = words[i] + ' ';
currentY += lineHeight;
} else {
line = testLine;
}
}
this.ctx.fillText(line, x, currentY);
}
// Text along path
drawTextAlongPath(text, path, options = {}) {
const {
font = '16px Arial',
fillColor = 'black',
letterSpacing = 0,
} = options;
this.ctx.save();
this.ctx.font = font;
this.ctx.fillStyle = fillColor;
let distance = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i];
const width = this.ctx.measureText(char).width;
const point = this.getPointOnPath(path, distance + width / 2);
if (!point) break;
this.ctx.save();
this.ctx.translate(point.x, point.y);
this.ctx.rotate(point.angle);
this.ctx.fillText(char, -width / 2, 0);
this.ctx.restore();
distance += width + letterSpacing;
}
this.ctx.restore();
}
getPointOnPath(path, distance) {
// Simplified - assumes path is array of points
let accumulatedDistance = 0;
for (let i = 0; i < path.length - 1; i++) {
const p1 = path[i];
const p2 = path[i + 1];
const segmentLength = Math.sqrt(
Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)
);
if (accumulatedDistance + segmentLength >= distance) {
const t = (distance - accumulatedDistance) / segmentLength;
const x = p1.x + (p2.x - p1.x) * t;
const y = p1.y + (p2.y - p1.y) * t;
const angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
return { x, y, angle };
}
accumulatedDistance += segmentLength;
}
return null;
}
// Gradient text
drawGradientText(text, x, y, gradientColors, options = {}) {
const { font = '48px Arial', direction = 'horizontal' } = options;
this.ctx.save();
this.ctx.font = font;
const metrics = this.ctx.measureText(text);
const gradient =
direction === 'horizontal'
? this.ctx.createLinearGradient(x, y, x + metrics.width, y)
: this.ctx.createLinearGradient(x, y - 24, x, y + 24);
gradientColors.forEach((color, index) => {
gradient.addColorStop(index / (gradientColors.length - 1), color);
});
this.ctx.fillStyle = gradient;
this.ctx.fillText(text, x, y);
this.ctx.restore();
}
}
Image Manipulation
Working with Images
class ImageProcessor {
constructor(ctx) {
this.ctx = ctx;
}
// Load and draw image
async loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
// Draw image with options
drawImage(img, x, y, options = {}) {
const {
width = img.width,
height = img.height,
sourceX = 0,
sourceY = 0,
sourceWidth = img.width,
sourceHeight = img.height,
rotation = 0,
opacity = 1,
flipX = false,
flipY = false,
} = options;
this.ctx.save();
this.ctx.globalAlpha = opacity;
// Apply transformations
this.ctx.translate(x + width / 2, y + height / 2);
if (rotation !== 0) {
this.ctx.rotate(rotation);
}
if (flipX || flipY) {
this.ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1);
}
this.ctx.drawImage(
img,
sourceX,
sourceY,
sourceWidth,
sourceHeight,
-width / 2,
-height / 2,
width,
height
);
this.ctx.restore();
}
// Apply filters to image data
applyFilter(imageData, filterType, options = {}) {
const data = imageData.data;
const width = imageData.width;
const height = imageData.height;
switch (filterType) {
case 'grayscale':
this.grayscaleFilter(data);
break;
case 'brightness':
this.brightnessFilter(data, options.value || 0);
break;
case 'contrast':
this.contrastFilter(data, options.value || 1);
break;
case 'blur':
return this.blurFilter(imageData, options.radius || 5);
case 'sepia':
this.sepiaFilter(data);
break;
case 'invert':
this.invertFilter(data);
break;
}
return imageData;
}
grayscaleFilter(data) {
for (let i = 0; i < data.length; i += 4) {
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
data[i] = gray; // red
data[i + 1] = gray; // green
data[i + 2] = gray; // blue
}
}
brightnessFilter(data, brightness) {
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, Math.max(0, data[i] + brightness));
data[i + 1] = Math.min(255, Math.max(0, data[i + 1] + brightness));
data[i + 2] = Math.min(255, Math.max(0, data[i + 2] + brightness));
}
}
contrastFilter(data, contrast) {
const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, Math.max(0, factor * (data[i] - 128) + 128));
data[i + 1] = Math.min(
255,
Math.max(0, factor * (data[i + 1] - 128) + 128)
);
data[i + 2] = Math.min(
255,
Math.max(0, factor * (data[i + 2] - 128) + 128)
);
}
}
sepiaFilter(data) {
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
data[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
}
}
invertFilter(data) {
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
}
blurFilter(imageData, radius) {
// Simple box blur
const width = imageData.width;
const height = imageData.height;
const output = new ImageData(width, height);
const outputData = output.data;
const data = imageData.data;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0,
g = 0,
b = 0,
a = 0;
let count = 0;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
const idx = (ny * width + nx) * 4;
r += data[idx];
g += data[idx + 1];
b += data[idx + 2];
a += data[idx + 3];
count++;
}
}
}
const idx = (y * width + x) * 4;
outputData[idx] = r / count;
outputData[idx + 1] = g / count;
outputData[idx + 2] = b / count;
outputData[idx + 3] = a / count;
}
}
return output;
}
}
Animation
Animation Framework
class AnimationController {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.animations = new Map();
this.running = false;
this.lastTime = 0;
}
addAnimation(id, animation) {
this.animations.set(id, animation);
}
removeAnimation(id) {
this.animations.delete(id);
}
start() {
if (!this.running) {
this.running = true;
this.lastTime = performance.now();
this.animate();
}
}
stop() {
this.running = false;
}
animate(currentTime = 0) {
if (!this.running) return;
const deltaTime = currentTime - this.lastTime;
this.lastTime = currentTime;
// Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Update and draw animations
for (const [id, animation] of this.animations) {
animation.update(deltaTime);
animation.draw(this.ctx);
if (animation.isComplete && animation.isComplete()) {
this.animations.delete(id);
}
}
requestAnimationFrame((time) => this.animate(time));
}
}
// Particle system
class ParticleSystem {
constructor(x, y, options = {}) {
this.x = x;
this.y = y;
this.particles = [];
this.options = {
particleCount: 100,
particleLife: 2000,
emissionRate: 10,
spread: Math.PI * 2,
speed: { min: 50, max: 200 },
size: { min: 2, max: 5 },
colors: ['#ff0000', '#ff7700', '#ffff00'],
gravity: 100,
...options,
};
this.emissionTimer = 0;
}
update(deltaTime) {
// Update existing particles
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
particle.life -= deltaTime;
if (particle.life <= 0) {
this.particles.splice(i, 1);
continue;
}
// Update position
particle.x += (particle.vx * deltaTime) / 1000;
particle.y += (particle.vy * deltaTime) / 1000;
// Apply gravity
particle.vy += (this.options.gravity * deltaTime) / 1000;
// Update opacity based on life
particle.opacity = particle.life / this.options.particleLife;
}
// Emit new particles
this.emissionTimer += deltaTime;
const particlesToEmit = Math.floor(
(this.emissionTimer / 1000) * this.options.emissionRate
);
if (particlesToEmit > 0) {
this.emissionTimer = 0;
for (let i = 0; i < particlesToEmit; i++) {
if (this.particles.length < this.options.particleCount) {
this.emitParticle();
}
}
}
}
emitParticle() {
const angle = Math.random() * this.options.spread - this.options.spread / 2;
const speed =
this.options.speed.min +
Math.random() * (this.options.speed.max - this.options.speed.min);
this.particles.push({
x: this.x,
y: this.y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: this.options.particleLife,
size:
this.options.size.min +
Math.random() * (this.options.size.max - this.options.size.min),
color:
this.options.colors[
Math.floor(Math.random() * this.options.colors.length)
],
opacity: 1,
});
}
draw(ctx) {
ctx.save();
for (const particle of this.particles) {
ctx.globalAlpha = particle.opacity;
ctx.fillStyle = particle.color;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
}
// Sprite animation
class SpriteAnimation {
constructor(image, frameWidth, frameHeight, frameCount, fps = 30) {
this.image = image;
this.frameWidth = frameWidth;
this.frameHeight = frameHeight;
this.frameCount = frameCount;
this.frameDuration = 1000 / fps;
this.currentFrame = 0;
this.elapsedTime = 0;
this.x = 0;
this.y = 0;
this.scale = 1;
this.rotation = 0;
}
update(deltaTime) {
this.elapsedTime += deltaTime;
if (this.elapsedTime >= this.frameDuration) {
this.elapsedTime = 0;
this.currentFrame = (this.currentFrame + 1) % this.frameCount;
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.scale(this.scale, this.scale);
const sourceX = this.currentFrame * this.frameWidth;
ctx.drawImage(
this.image,
sourceX,
0,
this.frameWidth,
this.frameHeight,
-this.frameWidth / 2,
-this.frameHeight / 2,
this.frameWidth,
this.frameHeight
);
ctx.restore();
}
}
Interactive Graphics
Mouse and Touch Interaction
class InteractiveCanvas {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.objects = [];
this.selectedObject = null;
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.setupEventListeners();
}
setupEventListeners() {
// Mouse events
this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
this.canvas.addEventListener('mouseup', (e) => this.handleMouseUp(e));
// Touch events
this.canvas.addEventListener('touchstart', (e) => this.handleTouchStart(e));
this.canvas.addEventListener('touchmove', (e) => this.handleTouchMove(e));
this.canvas.addEventListener('touchend', (e) => this.handleTouchEnd(e));
}
getMousePos(e) {
const rect = this.canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
}
getTouchPos(e) {
const rect = this.canvas.getBoundingClientRect();
return {
x: e.touches[0].clientX - rect.left,
y: e.touches[0].clientY - rect.top,
};
}
handleMouseDown(e) {
const pos = this.getMousePos(e);
this.handleStart(pos);
}
handleTouchStart(e) {
e.preventDefault();
const pos = this.getTouchPos(e);
this.handleStart(pos);
}
handleStart(pos) {
// Check if clicking on an object
for (let i = this.objects.length - 1; i >= 0; i--) {
const obj = this.objects[i];
if (obj.contains(pos.x, pos.y)) {
this.selectedObject = obj;
this.isDragging = true;
this.dragOffset = {
x: pos.x - obj.x,
y: pos.y - obj.y,
};
// Move to front
this.objects.splice(i, 1);
this.objects.push(obj);
this.redraw();
break;
}
}
}
handleMouseMove(e) {
const pos = this.getMousePos(e);
this.handleMove(pos);
}
handleTouchMove(e) {
e.preventDefault();
const pos = this.getTouchPos(e);
this.handleMove(pos);
}
handleMove(pos) {
if (this.isDragging && this.selectedObject) {
this.selectedObject.x = pos.x - this.dragOffset.x;
this.selectedObject.y = pos.y - this.dragOffset.y;
this.redraw();
} else {
// Check hover
let hovered = false;
for (const obj of this.objects) {
if (obj.contains(pos.x, pos.y)) {
if (!obj.isHovered) {
obj.isHovered = true;
this.redraw();
}
hovered = true;
} else if (obj.isHovered) {
obj.isHovered = false;
this.redraw();
}
}
this.canvas.style.cursor = hovered ? 'pointer' : 'default';
}
}
handleMouseUp(e) {
this.handleEnd();
}
handleTouchEnd(e) {
e.preventDefault();
this.handleEnd();
}
handleEnd() {
this.isDragging = false;
this.selectedObject = null;
}
addObject(obj) {
this.objects.push(obj);
this.redraw();
}
redraw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (const obj of this.objects) {
obj.draw(this.ctx);
}
}
}
// Interactive object base class
class InteractiveObject {
constructor(x, y) {
this.x = x;
this.y = y;
this.isHovered = false;
}
contains(x, y) {
// Override in subclasses
return false;
}
draw(ctx) {
// Override in subclasses
}
}
// Interactive circle
class InteractiveCircle extends InteractiveObject {
constructor(x, y, radius, color) {
super(x, y);
this.radius = radius;
this.color = color;
}
contains(x, y) {
const dx = x - this.x;
const dy = y - this.y;
return dx * dx + dy * dy <= this.radius * this.radius;
}
draw(ctx) {
ctx.save();
ctx.fillStyle = this.color;
if (this.isHovered) {
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
}
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
Performance Optimization
Canvas Performance Tips
class CanvasOptimizer {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.offscreenCanvas = null;
this.offscreenCtx = null;
}
// Create offscreen canvas for caching
createOffscreenCanvas(width, height) {
this.offscreenCanvas = document.createElement('canvas');
this.offscreenCanvas.width = width;
this.offscreenCanvas.height = height;
this.offscreenCtx = this.offscreenCanvas.getContext('2d');
return {
canvas: this.offscreenCanvas,
ctx: this.offscreenCtx,
};
}
// Batch drawing operations
batchDraw(operations) {
this.ctx.save();
// Group by drawing type
const grouped = this.groupOperations(operations);
// Execute batched operations
for (const [type, ops] of grouped) {
this.executeBatch(type, ops);
}
this.ctx.restore();
}
groupOperations(operations) {
const grouped = new Map();
for (const op of operations) {
if (!grouped.has(op.type)) {
grouped.set(op.type, []);
}
grouped.get(op.type).push(op);
}
return grouped;
}
executeBatch(type, operations) {
switch (type) {
case 'rect':
this.batchRectangles(operations);
break;
case 'circle':
this.batchCircles(operations);
break;
case 'line':
this.batchLines(operations);
break;
}
}
batchRectangles(rects) {
// Group by style
const byStyle = new Map();
for (const rect of rects) {
const key = `${rect.fillStyle}:${rect.strokeStyle}`;
if (!byStyle.has(key)) {
byStyle.set(key, []);
}
byStyle.get(key).push(rect);
}
// Draw each style group
for (const [style, group] of byStyle) {
const [fillStyle, strokeStyle] = style.split(':');
if (fillStyle !== 'null') {
this.ctx.fillStyle = fillStyle;
for (const rect of group) {
this.ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
}
}
if (strokeStyle !== 'null') {
this.ctx.strokeStyle = strokeStyle;
for (const rect of group) {
this.ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
}
}
}
}
// Dirty rectangle optimization
createDirtyRectManager() {
return {
dirtyRects: [],
markDirty(x, y, width, height) {
this.dirtyRects.push({ x, y, width, height });
},
getDirtyRegion() {
if (this.dirtyRects.length === 0) return null;
let minX = Infinity,
minY = Infinity;
let maxX = -Infinity,
maxY = -Infinity;
for (const rect of this.dirtyRects) {
minX = Math.min(minX, rect.x);
minY = Math.min(minY, rect.y);
maxX = Math.max(maxX, rect.x + rect.width);
maxY = Math.max(maxY, rect.y + rect.height);
}
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
},
clear() {
this.dirtyRects = [];
},
};
}
// Image data caching
createImageDataCache() {
const cache = new Map();
return {
get(key) {
return cache.get(key);
},
set(key, imageData) {
cache.set(key, imageData);
},
clear() {
cache.clear();
},
};
}
}
// Request animation frame optimization
class FrameRateController {
constructor(targetFPS = 60) {
this.targetFPS = targetFPS;
this.frameTime = 1000 / targetFPS;
this.lastTime = 0;
this.deltaTime = 0;
this.fps = 0;
this.frames = 0;
this.lastFPSUpdate = 0;
}
shouldUpdate(currentTime) {
this.deltaTime = currentTime - this.lastTime;
if (this.deltaTime >= this.frameTime) {
// Update FPS counter
this.frames++;
if (currentTime - this.lastFPSUpdate >= 1000) {
this.fps = this.frames;
this.frames = 0;
this.lastFPSUpdate = currentTime;
}
this.lastTime = currentTime - (this.deltaTime % this.frameTime);
return true;
}
return false;
}
getFPS() {
return this.fps;
}
}
Best Practices
-
Use requestAnimationFrame for animations
function animate() { // Update and draw requestAnimationFrame(animate); }
-
Clear only what's necessary
// Clear specific region instead of entire canvas ctx.clearRect(x, y, width, height);
-
Cache complex drawings
// Draw to offscreen canvas once offscreenCtx.drawComplexShape(); // Use cached result ctx.drawImage(offscreenCanvas, 0, 0);
-
Batch similar operations
// Set style once for multiple shapes ctx.fillStyle = 'blue'; shapes.forEach((shape) => ctx.fill(shape));
Conclusion
The Canvas API enables powerful graphics capabilities:
- 2D drawing with shapes, paths, and text
- Image manipulation and filtering
- Smooth animations with requestAnimationFrame
- Interactive graphics with mouse/touch events
- Performance optimization techniques
- Creative possibilities for games and visualizations
Key takeaways:
- Understand the coordinate system
- Master paths and transformations
- Use appropriate optimization techniques
- Handle different screen densities
- Test performance across devices
- Consider WebGL for complex 3D graphics
Master the Canvas API to create engaging visual experiences on the web!