Skip to content

HTML5 Canvas Drawing & Animation Guide

DodaTech Updated 2026-06-24 8 min read

In this tutorial, you'll learn about HTML5 Canvas Drawing & Animation Guide. We cover key concepts, practical examples, and best practices.

HTML5 Canvas provides a pixel-level drawing surface for rendering shapes, text, images, and animations directly in the browser using JavaScript.

In this tutorial, you'll learn the HTML5 Canvas API — how to draw shapes, render text, manipulate pixels, and build smooth animations. Canvas is fundamental for data visualizations, games, image processing, and any application that needs pixel-level control. By the end, you'll build a real-time particle system for threat visualization.

Real-world use: Durga Antivirus Pro uses Canvas to render network traffic heatmaps and real-time scan progress visualizations. Doda Browser's built-in screenshot editor relies on Canvas for annotation tools.

flowchart TD
  A[Canvas API] --> B[Canvas Setup]
  B --> C[Canvas Context]
  C --> D[Shapes & Paths]
  C --> E[Text & Fonts]
  C --> F[Images & Video]
  C --> G[Pixel Data]
  D --> H[Animation Loop]
  H --> I[requestAnimationFrame]
  I --> J[Clear - Draw - Update]
  style A fill:#6366f1,color:#fff
  style C fill:#4af,color:#fff
  style J fill:#4a4,color:#fff

Canvas Setup

The <canvas> element creates a fixed-size drawing surface. You access its drawing context — either 2d for 2D graphics or webgl for 3D.

<canvas id="myCanvas" width="800" height="600">
  Your browser does not support the canvas element.
</canvas>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

if (!ctx) {
  throw new Error('Canvas 2D context not supported');
}

// High-DPI display handling
function setupCanvas(canvas, width, height) {
  const dpr = window.devicePixelRatio || 1;
  canvas.width = width * dpr;
  canvas.height = height * dpr;
  canvas.style.width = width + 'px';
  canvas.style.height = height + 'px';
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  return ctx;
}

setupCanvas(canvas, 800, 600);

Drawing Shapes

Canvas uses a path-based drawing model. You begin a path, define sub-paths, then stroke or fill.

// Rectangle
ctx.fillStyle = '#6366f1';
ctx.fillRect(10, 10, 100, 50);

ctx.strokeStyle = '#4f46e5';
ctx.lineWidth = 3;
ctx.strokeRect(130, 10, 100, 50);

// Circle via arc
ctx.beginPath();
ctx.arc(400, 60, 40, 0, Math.PI * 2);
ctx.fillStyle = '#f59e0b';
ctx.fill();

// Arc segment
ctx.beginPath();
ctx.arc(550, 60, 40, 0, Math.PI * 1.5);
ctx.lineTo(550, 60);
ctx.closePath();
ctx.fillStyle = '#ef4444';
ctx.fill();

// Line
ctx.beginPath();
ctx.moveTo(10, 150);
ctx.lineTo(200, 150);
ctx.strokeStyle = '#10b981';
ctx.lineWidth = 4;
ctx.stroke();

Expected output: A blue rectangle, a purple outlined rectangle, an orange circle, a red pie segment, and a green line rendered on the canvas.

Complex Paths

Use bezierCurveTo and quadraticCurveTo for smooth curves.

// Quadratic Bezier curve
ctx.beginPath();
ctx.moveTo(50, 250);
ctx.quadraticCurveTo(150, 100, 250, 250);
ctx.strokeStyle = '#6366f1';
ctx.lineWidth = 3;
ctx.stroke();

// Cubic Bezier curve
ctx.beginPath();
ctx.moveTo(300, 300);
ctx.bezierCurveTo(400, 100, 500, 400, 600, 250);
ctx.strokeStyle = '#ec4899';
ctx.lineWidth = 3;
ctx.stroke();

// Star shape
function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
  const step = Math.PI / spikes;
  ctx.beginPath();
  for (let i = 0; i < 2 * spikes; i++) {
    const radius = i % 2 === 0 ? outerRadius : innerRadius;
    const angle = i * step - Math.PI / 2;
    const x = cx + Math.cos(angle) * radius;
    const y = cy + Math.sin(angle) * radius;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  }
  ctx.closePath();
  ctx.fillStyle = '#f59e0b';
  ctx.fill();
  ctx.strokeStyle = '#d97706';
  ctx.lineWidth = 2;
  ctx.stroke();
}

drawStar(ctx, 400, 200, 5, 80, 40);

Expected output: A quadratic curve, a cubic bezier curve, and a filled five-pointed star shape.

Text Rendering

Canvas renders text with fillText and strokeText, supporting font styling and alignment.

ctx.font = 'bold 48px Inter, system-ui, sans-serif';
ctx.fillStyle = '#1e293b';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('DodaTech', 400, 150);

ctx.font = '24px Inter, sans-serif';
ctx.fillStyle = '#64748b';
ctx.fillText('Canvas Graphics Engine', 400, 200);

// Rotated text
ctx.save();
ctx.translate(400, 300);
ctx.rotate(-0.1);
ctx.font = '16px monospace';
ctx.fillStyle = '#475569';
ctx.fillText('Built by Doda Browser developers', 0, 0);
ctx.restore();

Expected output: "DodaTech" in bold 48px centered text, a subtitle, and rotated small text below.

Image Drawing

Load and draw images onto the canvas, with optional clipping and scaling.

const img = new Image();
img.src = '/images/logo.png';
img.onload = function() {
  // Draw image at original size
  ctx.drawImage(img, 50, 400);

  // Draw scaled
  ctx.drawImage(img, 300, 400, 150, 75);

  // Clip into circle
  ctx.beginPath();
  ctx.arc(550, 450, 50, 0, Math.PI * 2);
  ctx.closePath();
  ctx.clip();
  ctx.drawImage(img, 500, 400, 100, 100);
};

Pixel Manipulation

Access and modify individual pixel data using getImageData and putImageData.

function applyGrayscale(canvas) {
  const ctx = canvas.getContext('2d');
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;

  for (let i = 0; i < data.length; i += 4) {
    const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
    data[i] = gray;     // Red
    data[i + 1] = gray; // Green
    data[i + 2] = gray; // Blue
    // data[i + 3] is alpha - unchanged
  }

  ctx.putImageData(imageData, 0, 0);
}

function applySepia(canvas) {
  const ctx = canvas.getContext('2d');
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.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);
  }

  ctx.putImageData(imageData, 0, 0);
}

Animation Loop

Animations use requestAnimationFrame to sync with the display refresh rate.

const canvas = document.getElementById('animation');
const animationCtx = canvas.getContext('2d');
let frame = 0;

function animate() {
  animationCtx.clearRect(0, 0, canvas.width, canvas.height);

  // Bouncing ball
  const x = 400 + Math.sin(frame * 0.02) * 300;
  const y = 300 + Math.cos(frame * 0.03) * 200;

  animationCtx.beginPath();
  animationCtx.arc(x, y, 25, 0, Math.PI * 2);
  animationCtx.fillStyle = '#6366f1';
  animationCtx.fill();

  // Trail effect (semi-transparent clear instead of full clear)
  animationCtx.fillStyle = 'rgba(255, 255, 255, 0.1)';
  animationCtx.fillRect(0, 0, canvas.width, canvas.height);

  frame++;
  requestAnimationFrame(animate);
}

animate();

Particle System

A real-world particle system for visualizing network threats.

class Particle {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.vx = (Math.random() - 0.5) * 4;
    this.vy = (Math.random() - 0.5) * 4 - 2;
    this.size = Math.random() * 4 + 1;
    this.life = 1;
    this.decay = 0.005 + Math.random() * 0.01;
    this.hue = 200 + Math.random() * 60; // Blue-green range for safe traffic
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.vy += 0.05; // gravity
    this.life -= this.decay;
  }

  draw(ctx) {
    ctx.globalAlpha = this.life;
    ctx.fillStyle = `hsl(${this.hue}, 70%, 50%)`;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
    ctx.fill();
  }
}

class NetworkVisualizer {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.particles = [];
    this.running = false;
  }

  emit(count = 3) {
    for (let i = 0; i < count; i++) {
      this.particles.push(
        new Particle(
          this.canvas.width / 2,
          this.canvas.height - 20
        )
      );
    }
  }

  update() {
    this.ctx.fillStyle = 'rgba(15, 23, 42, 0.15)';
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);

    this.particles.forEach(p => {
      p.update();
      p.draw(this.ctx);
    });

    this.particles = this.particles.filter(p => p.life > 0);

    if (this.running) {
      requestAnimationFrame(() => this.update());
    }
  }

  start() {
    this.running = true;
    // Emit particles periodically
    setInterval(() => this.emit(5), 200);
    this.update();
  }
}

const viz = new NetworkVisualizer(document.getElementById('network-viz'));
viz.start();

Expected output: A dark canvas with glowing particles rising from the bottom center, fading out as they drift, simulating network traffic flow.

Common Errors

  1. Canvas size mismatch — CSS dimensions differ from canvas attribute dimensions. Always match canvas.width/height attributes with display size, especially on HiDPI displays.
  2. Forgetting ctx.beginPath() — Without a new path, subsequent draw calls accumulate on the previous path. Always start a new path before each shape.
  3. Missing onload for imagesdrawImage before the image loads draws nothing. Always use the load event or check img.complete.
  4. getImageData CORS error — Drawing images from external domains without crossOrigin="anonymous" taints the canvas, blocking getImageData.
  5. Not clearing the canvas — Animations accumulate previous frames. Use clearRect or a semi-transparent fill to create trails without smearing.

Practice Questions

How do you get the 2D drawing context from a canvas element?

Call canvas.getContext('2d'). It returns a CanvasRenderingContext2D object with all drawing methods. Returns null if the context type is not supported.

What is the difference between fillRect and strokeRect?

fillRect draws a filled rectangle using the current fillStyle. strokeRect draws only the outline using the current strokeStyle and lineWidth. Both take (x, y, width, height) parameters.

Why does requestAnimationFrame give better performance than setInterval?

requestAnimationFrame synchronizes with the browser's display refresh cycle, running only when the tab is visible. It avoids unnecessary work when the tab is hidden and prevents frame drops from missed vsync intervals.

How do you handle high-DPI (Retina) displays with Canvas?

Multiply the canvas width and height attributes by window.devicePixelRatio, then ctx.scale() by the same factor. Set CSS width/height to the logical dimensions. This ensures crisp rendering on Retina screens.

What is image tainting and how do you avoid it?

Drawing an image from a different origin without CORS headers taints the canvas, making getImageData and toDataURL throw security errors. Set img.crossOrigin = 'anonymous' and ensure the server sends Access-Control-Allow-Origin headers

Challenge

Build a canvas-based real-time packet visualizer. Emit colored particles based on packet type (green for safe, yellow for suspicious, red for threats). Add mouse interaction so clicking emits a burst of particles. Display a live particle count.

Real-World Task

You are building the threat detection dashboard for Durga Antivirus Pro. Use Canvas to render a heatmap showing which file system regions are being scanned. Overlay a particle system that flows from the scan origin outward. Connected nodes represent detected threats, with color intensity indicating severity. The animation must maintain 60fps using requestAnimationFrame and support HiDPI displays.


Related: Web Animations Guide | Previous: JavaScript Fundamentals | Next: Web Components

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro