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.
What You'll Learn
- How to use the HTML5
Canvas APIand 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()andputImageData() - 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
<!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
* { 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
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 ProjectSingle HTML file · All code included