Create and save your drawings locally
Vanilla CSS version
Local Storage is a web browser feature that allows you to store data directly on a user's computer. Unlike cookies, it has a much larger storage capacity (typically 5-10MB) and doesn't expire automatically.
Local Storage stores data as key-value pairs. Here's how the data flows:
Most browsers: 5-10MB per domain
Note: This app stores drawings as images, which are quite large. You can typically store 10-20 drawings before reaching limits.
โ Chrome, Firefox, Safari, Edge, and most modern browsers
โ Internet Explorer (use SessionStorage instead)
Here's how this app uses Local Storage:
// Get existing data
const drawings = JSON.parse(localStorage.getItem('drawings')) || [];
// Create new drawing object
const drawing = {
id: Date.now(),
name: 'My Drawing',
data: canvas.toDataURL(), // Convert canvas to image
date: new Date().toLocaleString()
};
// Add to array and save
drawings.push(drawing);
localStorage.setItem('drawings', JSON.stringify(drawings));
// Retrieve all drawings
const drawings = JSON.parse(localStorage.getItem('drawings')) || [];
// Loop through and display
drawings.forEach(drawing => {
console.log(drawing.name, drawing.date);
});
// Remove a specific drawing
let drawings = JSON.parse(localStorage.getItem('drawings'));
drawings = drawings.filter(d => d.id !== idToDelete);
localStorage.setItem('drawings', JSON.stringify(drawings));
// Clear all data
localStorage.clear();
JSON.stringify() when saving objectsJSON.parse() when retrieving objectslocalStorage.getItem('key') || '{}''userDrawings' instead of 'd'Open your browser's Developer Tools (F12), go to the "Storage" or "Application" tab, and check the "Local Storage" section to see how your drawings are being stored as JSON!
This app was built twice โ once using only hand-written CSS, and once using Bootstrap 5. Both versions look nearly identical, but the code behind them is very different. The comparison is a great way to understand the trade-offs every web developer faces when starting a new project.
Every style rule in this version was written from scratch in style.css. That file is roughly 690 lines long and handles everything: the header gradient, the toolbar layout, the modal, the tab system, the drawing cards, responsive breakpoints, and the sticky footer.
script.js@media queriesThe Bootstrap 5 sister version uses the same HTML structure but relies on Bootstrap's pre-built components and utility classes for the heavy lifting. Its custom CSS file is only about 130 lines โ an 81% reduction โ covering just the brand gradient, glass button, canvas styling, and footer.
data-bs-toggle HTML attribute โ no JavaScript at allgrid-template-columns (this version) vs Bootstrap row-cols-* classes.drawing-card div (this version) vs Bootstrap .card componentOpen either link below in a new tab to compare the two versions side by side:
The Bootstrap version's modal includes a link back to this page so you can switch between them easily.
The original version of this app (script0.js / index0.html) had three bugs that silently broke the Line, Circle, and Rectangle tools. The Pen and Eraser worked fine, so the bugs were easy to miss. Here's what went wrong โ and why it's a great set of lessons for any JavaScript developer.
Shapes were drawn with near-zero size and appeared as dots. The variable lastX/lastY was used to remember where the mouse was first pressed down, but those same variables were overwritten on every mousemove event. By the time the mouse button was released, lastX/lastY held the final mouse position โ not the starting point โ so both ends of the shape were basically the same location.
Fix: Two new dedicated variables, startX and startY, are set once at mousedown and never changed during the drag.
Nothing appeared on the canvas while dragging with a shape tool. The draw() function (which runs on every mousemove) contained only empty comment placeholders for line, circle, and rectangle โ no actual drawing code. Students would drag across the canvas, see nothing, then get a tiny dot on release.
Fix: A snapshot of the canvas pixels (ImageData) is captured at mousedown. On each mousemove, the snapshot is restored and the preview shape is redrawn on top โ the standard "rubber-band" preview technique.
Even when a shape was drawn on mouseup, it immediately disappeared. The stopDrawing() function called redrawCanvas() to restore the canvas baseline, then drew the final shape. The problem: redrawCanvas() uses img.onload โ which is asynchronous. JavaScript ran the shape-drawing code right away, then the onload callback fired a moment later and wiped the canvas, erasing the shape.
Fix: Replace the async redrawCanvas() call with the synchronous ctx.putImageData(), which restores pixels instantly so the shape draw happens in the correct order.
Key lesson: In JavaScript, code placed after an async call does not wait for that async call to finish. Always use synchronous APIs when the next line of code depends on the result being ready.