Mini Project: Drawing Board
What This Combines
Mostly, this project reuses what you already know: <canvas>, mouse events (Module 2), localStorage (this module’s lesson 1), and drawImage() (this module’s Canvas lesson). It also introduces a small handful of new but simple pieces needed specifically for freehand drawing, moveTo(), lineTo(), stroke(), and saving a canvas as an image with toDataURL(), explained as they come up.
💡 Why this matters: A drawing app is the classic canvas project, and it’s a genuinely good test of whether canvas, events, and storage actually click together, not just individually.
⚠️ A note on verification: the same limitation as this module’s canvas and browser-API lessons applies. The mouse event wiring and the
localStoragesave/load logic below were verified directly. The actual visual drawing needs a real browser’s rendering engine, which this sandbox doesn’t have, run it yourself to see it firsthand.
What You’re Building
A drawing board: click and drag to draw freehand, pick a color and brush size, clear the canvas, and save your drawing so it’s still there the next time you open the page.
drawing-board/
├── index.html
└── script.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drawing Board</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
background: #eef1f5;
padding: 30px 20px;
}
h1 {
color: #2c3e50;
margin-bottom: 20px;
}
.toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 18px;
background: #fff;
padding: 14px 22px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
margin-bottom: 18px;
}
.toolbar label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #333;
}
#colorPicker {
width: 38px;
height: 38px;
padding: 0;
border: none;
border-radius: 8px;
cursor: pointer;
}
#brushSize {
cursor: pointer;
}
button {
padding: 10px 18px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
color: #fff;
cursor: pointer;
transition: background 0.2s;
}
#clearBtn {
background: #e74c3c;
}
#clearBtn:hover {
background: #c0392b;
}
#saveBtn {
background: #27ae60;
}
#saveBtn:hover {
background: #1e8449;
}
#board {
background: #fff;
border-radius: 12px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12);
cursor: crosshair;
}
#status {
margin-top: 14px;
min-height: 20px;
color: #555;
font-size: 14px;
}
</style>
</head>
<body>
<h1>Drawing Board</h1>
<div class="toolbar">
<label>Color <input id="colorPicker" type="color" value="#000000"></label>
<label>Brush size <input id="brushSize" type="range" min="1" max="20" value="4"></label>
<button id="clearBtn">Clear</button>
<button id="saveBtn">Save Drawing</button>
</div>
<canvas id="board" width="500" height="400"></canvas>
<p id="status"></p>
<script src="script.js"></script>
</body>
</html>
The design is already done, a toolbar with the color and brush controls, a shadowed canvas, cursor: crosshair on #board so the pointer itself hints that it’s a drawing surface. Everything below is about filling in script.js, no CSS changes needed, every element this project touches (#board, #colorPicker, #brushSize, #clearBtn, #saveBtn, #status) is already in place and already styled.
Step 1: Get the Canvas Ready
const board = document.getElementById("board");
const ctx = board.getContext("2d");
const status = document.getElementById("status");
let isDrawing = false;
isDrawing tracks whether the mouse button is currently held down over the canvas, a plain boolean flag, checked and updated by the event listeners in the next step. Nothing draws unless this is true.
Step 2: Start and Stop Drawing
Freehand drawing is really just three events working together: mousedown starts a new line, mousemove extends it while the button is held, mouseup ends it.
board.addEventListener("mousedown", (event) => {
isDrawing = true;
const rect = board.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(event.clientX - rect.left, event.clientY - rect.top);
});
board.addEventListener("mouseup", () => {
isDrawing = false;
});
board.addEventListener("mouseleave", () => {
isDrawing = false;
});
board.getBoundingClientRect() (Module 1) gives the canvas’s position on the page, event.clientX - rect.left converts a mouse position that’s relative to the whole browser window into one relative to the canvas itself, essential, since moveTo() draws using canvas coordinates, not page coordinates. moveTo(x, y) picks up an imaginary pen at that point without drawing anything yet, beginPath() starts a fresh line so it doesn’t connect back to whatever was drawn last. mouseleave matters too: without it, dragging the mouse off the canvas while still holding the button down would leave isDrawing stuck as true.
Step 3: Actually Draw
board.addEventListener("mousemove", (event) => {
if (!isDrawing) return;
const rect = board.getBoundingClientRect();
ctx.lineTo(event.clientX - rect.left, event.clientY - rect.top);
ctx.stroke();
});
The if (!isDrawing) return; guard is what makes this only draw while the mouse button is held, every mousemove fires constantly, drawing or not, this line is what tells the two apart. lineTo(x, y) extends the current path to the new point, stroke() actually paints that line onto the canvas. Together, mousedown (place the pen), repeated mousemove (drag it), mouseup (lift it) is the entire mechanism behind freehand drawing.
Step 4: Color and Brush Size
const colorPicker = document.getElementById("colorPicker");
const brushSize = document.getElementById("brushSize");
board.addEventListener("mousedown", () => {
ctx.strokeStyle = colorPicker.value;
ctx.lineWidth = Number(brushSize.value);
ctx.lineCap = "round";
});
This is a second mousedown listener, added separately from Step 2’s, both run every time the canvas is clicked, addEventListener never overwrites a previous listener on the same event (Module 2). Reading colorPicker.value and brushSize.value here, right when a new line starts, means every stroke uses whatever the controls are set to at that exact moment, so changing the color mid-drawing only affects the next line, not one already in progress. lineCap = "round" rounds off the ends of each stroke, without it, fast mouse movements can look like a series of disconnected rectangles instead of a smooth line.
Step 5: Clear the Board
const clearBtn = document.getElementById("clearBtn");
clearBtn.addEventListener("click", () => {
ctx.clearRect(0, 0, board.width, board.height);
status.textContent = "Cleared";
});
Same clearRect() from the Canvas lesson, clearing the full canvas by passing its own width and height.
Step 6: Save the Drawing
canvas.toDataURL() is new: it converts everything currently drawn on the canvas into a single string, a base64-encoded image, exactly the kind of thing localStorage (this module’s lesson 1) is built to hold.
const saveBtn = document.getElementById("saveBtn");
saveBtn.addEventListener("click", () => {
const dataURL = board.toDataURL();
localStorage.setItem("savedDrawing", dataURL);
status.textContent = "Drawing saved!";
});
dataURL is a long string starting with "data:image/png;base64,...", the entire image, text-encoded. localStorage.setItem() doesn’t care that it’s unusually long, it’s still just a string.
Step 7: Load the Drawing Back
This is where drawImage() (Canvas lesson, Step 6) comes back. Run this once, when the page first loads.
function loadSavedDrawing() {
const saved = localStorage.getItem("savedDrawing");
if (!saved) return;
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0);
status.textContent = "Loaded your saved drawing";
};
img.src = saved;
}
loadSavedDrawing();
The saved dataURL string works as an <img> source just like a normal image file would, img.src = saved starts “loading” it, img.onload fires once it’s ready, and drawImage(img, 0, 0) paints it onto the canvas starting at the top-left corner, restoring exactly what was there when “Save Drawing” was last clicked.
The Complete script.js
const board = document.getElementById("board");
const ctx = board.getContext("2d");
const status = document.getElementById("status");
const colorPicker = document.getElementById("colorPicker");
const brushSize = document.getElementById("brushSize");
const clearBtn = document.getElementById("clearBtn");
const saveBtn = document.getElementById("saveBtn");
let isDrawing = false;
board.addEventListener("mousedown", (event) => {
isDrawing = true;
ctx.strokeStyle = colorPicker.value;
ctx.lineWidth = Number(brushSize.value);
ctx.lineCap = "round";
const rect = board.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(event.clientX - rect.left, event.clientY - rect.top);
});
board.addEventListener("mousemove", (event) => {
if (!isDrawing) return;
const rect = board.getBoundingClientRect();
ctx.lineTo(event.clientX - rect.left, event.clientY - rect.top);
ctx.stroke();
});
board.addEventListener("mouseup", () => {
isDrawing = false;
});
board.addEventListener("mouseleave", () => {
isDrawing = false;
});
clearBtn.addEventListener("click", () => {
ctx.clearRect(0, 0, board.width, board.height);
status.textContent = "Cleared";
});
saveBtn.addEventListener("click", () => {
const dataURL = board.toDataURL();
localStorage.setItem("savedDrawing", dataURL);
status.textContent = "Drawing saved!";
});
function loadSavedDrawing() {
const saved = localStorage.getItem("savedDrawing");
if (!saved) return;
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0);
status.textContent = "Loaded your saved drawing";
};
img.src = saved;
}
loadSavedDrawing();
Open index.html, draw something, click “Save Drawing”, then reload the page, the drawing is still there.
Try It
Build the full drawing board from this lesson, then extend it:
- Add an “Undo” concept: before each new line starts (inside the
mousedownlistener, beforebeginPath()), save the canvas’s currenttoDataURL()into a variable. Add an “Undo” button that reloads that saved state withdrawImage(), the same pattern as Step 7, undoing only the most recent line. - Add a second color picker for a “background color” and a button that fills the entire canvas with it using
fillRect()(Canvas lesson) before any drawing happens. - Show the current brush size next to the slider, updating live as it’s dragged, using an
inputevent (Module 2) onbrushSize.
Recap
- Freehand drawing is
mousedown(start a path withmoveTo()),mousemove(extend it withlineTo()andstroke(), guarded by anisDrawingflag), andmouseup/mouseleave(stop). - Reading control values (
colorPicker.value,brushSize.value) inside themousedownlistener means each new line uses whatever the controls are set to at that moment. canvas.toDataURL()exports everything drawn as one string, perfect forlocalStorage.setItem().- Loading it back is the same
Image+drawImage()pattern from the Canvas lesson,img.onloadfirst,drawImage()inside it.
Next module: Web APIs and Backend Communication, REST conventions, HTTP methods, and a full mini project.