Back to Projects Expert

Drawing App

Build a full-featured drawing application using the HTML5 Canvas API - with freehand drawing, shape tools, color picker, adjustable brush size, and undo support.

HTML, CSS, JavaScript ~2-3 hours to build Free source code

What You'll Learn

  • How to use the HTML5 Canvas API and its 2D drawing context
  • How to handle mouse and touch events for freehand drawing
  • How to draw geometric shapes - lines, rectangles, and ellipses - on a canvas
  • How to implement undo functionality using getImageData() and putImageData()
  • How to scale a canvas for high-DPI (Retina) displays using devicePixelRatio

How It Works

Canvas is your digital sketchpad

The <canvas> element gives you a blank bitmap surface. Using its 2D context, you draw paths, shapes, and strokes with JavaScript. Unlike DOM elements, everything on a canvas is just pixels - so it's blazing fast for real-time drawing.

Mouse and touch events drive every stroke

We listen for mousedown, mousemove, and mouseup (plus their touch equivalents). On mousedown we start a new path; on mousemove we extend it; on mouseup we finalize and save a snapshot for undo.

Shape tools use a snapshot-restore technique

When you drag to draw a line, rectangle, or circle, we save a snapshot of the canvas before you start. On every mousemove, we restore that snapshot and redraw the shape at the new size - giving you a live preview without leaving trails.

Undo is powered by image data history

After every stroke, we push a full copy of the canvas pixels into a history array using getImageData(). Pressing undo pops the latest entry and restores the previous state with putImageData().

Source Code

HTML Structure

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Drawing App</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="app">
    <h1>?? Drawing App</h1>
    <div class="toolbar">
      <div class="tool-group">
        <button class="tool-btn active" data-tool="pen" title="Pen">??</button>
        <button class="tool-btn" data-tool="eraser" title="Eraser">??</button>
        <button class="tool-btn" data-tool="line" title="Line">??</button>
        <button class="tool-btn" data-tool="rect" title="Rectangle">?</button>
        <button class="tool-btn" data-tool="circle" title="Circle">?</button>
      </div>
      <div class="color-group">
        <input type="color" class="color-pick" id="colorPick" value="#1a1a2e" title="Pick color">
      </div>
      <div class="size-group">
        <span class="size-label" id="sizeVal">3px</span>
        <input type="range" class="size-slider" id="sizeSlider" min="1" max="30" value="3">
      </div>
      <button class="action-btn" id="undoBtn" title="Undo">? Undo</button>
      <button class="action-btn clear" id="clearBtn" title="Clear canvas">Clear</button>
    </div>
    <div class="canvas-wrap">
      <canvas id="canvas"></canvas>
    </div>
    <div class="status">
      <span id="toolInfo">Tool: Pen</span>
      <span id="coordInfo">0, 0</span>
    </div>
  </div>
  <script src="script.js"></script>
</body>
</html>

CSS Styling

CSS
* { margin:0; padding:0; box-sizing:border-box; }
body { min-height:100vh; background:linear-gradient(135deg,#1a1a2e,#16213e);
  display:flex; align-items:flex-start; justify-content:center;
  padding:20px 10px; font-family:'Segoe UI',sans-serif; }
.app { background:#fff; border-radius:20px; padding:24px; width:100%;
  max-width:600px; box-shadow:0 20px 60px rgba(0,0,0,0.4); }
h1 { text-align:center; font-size:1.4rem; color:#1a1a2e; margin-bottom:16px; }
.toolbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center;
  margin-bottom:14px; padding:12px; background:#f4f6fb; border-radius:12px; }
.tool-group { display:flex; gap:4px; }
.tool-btn { width:36px; height:36px; border:2px solid #ddd; border-radius:8px;
  background:#fff; cursor:pointer; display:flex; align-items:center;
  justify-content:center; font-size:1rem; transition:all .2s; }
.tool-btn:hover { border-color:#5c6bc0; }
.tool-btn.active { border-color:#5c6bc0; background:#e8eaf6; color:#5c6bc0; }
.color-pick { width:36px; height:36px; border:2px solid #ddd;
  border-radius:8px; cursor:pointer; padding:2px; background:#fff; }
.size-label { font-size:.75rem; color:#666; font-weight:600; }
.size-slider { width:80px; accent-color:#5c6bc0; }
.action-btn { height:36px; padding:0 14px; border:2px solid #ddd;
  border-radius:8px; background:#fff; cursor:pointer; font-size:.78rem;
  font-weight:600; color:#555; transition:all .2s; }
.action-btn:hover { border-color:#5c6bc0; color:#5c6bc0; }
.action-btn.clear { border-color:#ef5350; color:#ef5350; }
.action-btn.clear:hover { background:#ef5350; color:#fff; }
.canvas-wrap { border:2px solid #e0e0e0; border-radius:12px;
  overflow:hidden; cursor:crosshair; background:#fff; touch-action:none; }
canvas { display:block; width:100%; height:400px; }
.status { display:flex; justify-content:space-between;
  margin-top:10px; font-size:.75rem; color:#999; }

JavaScript Logic

JavaScript
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const colorPick = document.getElementById('colorPick');
const sizeSlider = document.getElementById('sizeSlider');
const sizeVal = document.getElementById('sizeVal');
const toolInfo = document.getElementById('toolInfo');
const coordInfo = document.getElementById('coordInfo');
const toolBtns = document.querySelectorAll('.tool-btn');

let tool = 'pen', drawing = false, color = '#1a1a2e', size = 3;
let startX, startY, snapshot, history = [];

function resize() {
  const wrap = canvas.parentElement;
  const w = wrap.clientWidth;
  const h = 400;
  const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
  canvas.width = w * devicePixelRatio;
  canvas.height = h * devicePixelRatio;
  canvas.style.height = h + 'px';
  ctx.scale(devicePixelRatio, devicePixelRatio);
  ctx.putImageData(data, 0, 0);
}

function saveState() {
  if (history.length > 40) history.shift();
  history.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
}

function undo() {
  if (history.length < 2) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    history = [];
    return;
  }
  history.pop();
  ctx.putImageData(history[history.length - 1], 0, 0);
}

function getPos(e) {
  const r = canvas.getBoundingClientRect();
  const t = e.touches ? e.touches[0] : e;
  return [(t.clientX - r.left), (t.clientY - r.top)];
}

function startDraw(e) {
  e.preventDefault();
  drawing = true;
  [startX, startY] = getPos(e);
  snapshot = ctx.getImageData(0, 0, canvas.width, canvas.height);
  if (tool === 'pen' || tool === 'eraser') {
    ctx.beginPath();
    ctx.moveTo(startX, startY);
  }
}

function draw(e) {
  e.preventDefault();
  const [x, y] = getPos(e);
  coordInfo.textContent = Math.round(x) + ', ' + Math.round(y);
  if (!drawing) return;
  ctx.lineWidth = size;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';

  if (tool === 'pen') {
    ctx.strokeStyle = color;
    ctx.globalCompositeOperation = 'source-over';
    ctx.lineTo(x, y);
    ctx.stroke();
  } else if (tool === 'eraser') {
    ctx.strokeStyle = '#ffffff';
    ctx.globalCompositeOperation = 'source-over';
    ctx.lineTo(x, y);
    ctx.stroke();
  } else {
    ctx.putImageData(snapshot, 0, 0);
    ctx.strokeStyle = color;
    ctx.globalCompositeOperation = 'source-over';
    ctx.beginPath();
    if (tool === 'line') {
      ctx.moveTo(startX, startY);
      ctx.lineTo(x, y);
    } else if (tool === 'rect') {
      ctx.rect(startX, startY, x - startX, y - startY);
    } else if (tool === 'circle') {
      const rx = Math.abs(x - startX) / 2;
      const ry = Math.abs(y - startY) / 2;
      const cx = startX + (x - startX) / 2;
      const cy = startY + (y - startY) / 2;
      ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
    }
    ctx.stroke();
  }
}

function endDraw() {
  if (!drawing) return;
  drawing = false;
  ctx.closePath();
  saveState();
}

canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDraw);
canvas.addEventListener('mouseleave', endDraw);
canvas.addEventListener('touchstart', startDraw, { passive: false });
canvas.addEventListener('touchmove', draw, { passive: false });
canvas.addEventListener('touchend', endDraw);

toolBtns.forEach(btn => btn.addEventListener('click', () => {
  toolBtns.forEach(b => b.classList.remove('active'));
  btn.classList.add('active');
  tool = btn.dataset.tool;
  toolInfo.textContent = 'Tool: ' + btn.title;
}));

colorPick.addEventListener('input', e => { color = e.target.value; });
sizeSlider.addEventListener('input', e => {
  size = +e.target.value;
  sizeVal.textContent = size + 'px';
});
document.getElementById('undoBtn').addEventListener('click', undo);
document.getElementById('clearBtn').addEventListener('click', () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  history = [];
  saveState();
});

window.addEventListener('resize', resize);
resize();
saveState();

Download Source Code

Single ready-to-run HTML file. Open it in any browser!

Download Project

Single HTML file · All code included