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

  1. Use requestAnimationFrame for animations

    function animate() {
      // Update and draw
      requestAnimationFrame(animate);
    }
    
  2. Clear only what's necessary

    // Clear specific region instead of entire canvas
    ctx.clearRect(x, y, width, height);
    
  3. Cache complex drawings

    // Draw to offscreen canvas once
    offscreenCtx.drawComplexShape();
    // Use cached result
    ctx.drawImage(offscreenCanvas, 0, 0);
    
  4. 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!