CodingNic

Browser APIs

Mini Project: Drawing Board

Browser APIs 35 min read

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 localStorage save/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.

text
drawing-board/
├── index.html
└── script.js
html
<!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

javascript
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.

javascript
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

javascript
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

javascript
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

javascript
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.

javascript
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.

javascript
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

javascript
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:

  1. Add an “Undo” concept: before each new line starts (inside the mousedown listener, before beginPath()), save the canvas’s current toDataURL() into a variable. Add an “Undo” button that reloads that saved state with drawImage(), the same pattern as Step 7, undoing only the most recent line.
  2. 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.
  3. Show the current brush size next to the slider, updating live as it’s dragged, using an input event (Module 2) on brushSize.

Recap

  • Freehand drawing is mousedown (start a path with moveTo()), mousemove (extend it with lineTo() and stroke(), guarded by an isDrawing flag), and mouseup/mouseleave (stop).
  • Reading control values (colorPicker.value, brushSize.value) inside the mousedown listener means each new line uses whatever the controls are set to at that moment.
  • canvas.toDataURL() exports everything drawn as one string, perfect for localStorage.setItem().
  • Loading it back is the same Image + drawImage() pattern from the Canvas lesson, img.onload first, drawImage() inside it.

Next module: Web APIs and Backend Communication, REST conventions, HTTP methods, and a full mini project.