A browser-based paint application built with vanilla JavaScript ES6, HTML5 Canvas API, and SASS.
Inspired by classic desktop paint programs, DAVINCI runs entirely in the browser with no frameworks or dependencies.
🔗 Live Demo | 🎬 Video Demo
DAVINCI is a fully functional online drawing application that demonstrates core JavaScript ES6 concepts including classes, modules, Canvas API manipulation, and event-driven programming. The project also showcases a complete deployment pipeline using Docker, Vagrant, and Kubernetes (Minikube).
The application must be served from a web server (not opened as a local file).
docker run -it -d -p 8080:80 bdostumski/davinci:1.0.0Then open: http://localhost:8080
vagrant upminikube start --nodes=2
kubectl apply -f davinci-deployment.yaml
minikube service davinci-service| Technology | Purpose |
|---|---|
| HTML5 | Application structure and Canvas element |
| SASS / CSS3 | Styling and responsive toolbox layout |
| JavaScript ES6 | Application logic (classes, modules, arrow functions, sets) |
| Canvas API | 2D drawing surface and pixel manipulation |
| NGINX | Static file web server |
| Docker | Containerised deployment |
| Vagrant | Local VM-based development environment |
| Kubernetes | Container orchestration with Minikube |
The UI is divided into four main sections:
┌─────────────────────────────────────────────┐
│ LEFT CENTER RIGHT │
│ Drawing Canvas Commands │
│ Tools (Drawing (Open, Save, │
│ + Sizes Field) Undo, ...) │
│─────────────────────────────────────────────│
│ BOTTOM — Colors │
└─────────────────────────────────────────────┘
| Tool | Stroke Size |
|---|---|
| Pencil | 1px – 5px |
| Brush | 3px – 15px |
| Paint Bucket | — |
| Eraser | 3px – 15px |
| Line | 1px – 5px |
| Rectangle | 1px – 5px |
| Circle | 1px – 5px |
| Ellipse | 1px – 5px |
| Triangle | 1px – 5px |
| Command | Description |
|---|---|
| Open | New Drawing / Load from URL / Browse local file |
| Save | Exports canvas as a .png file |
| Resize | Resize the canvas width & height |
| Undo | Step back up to 10 actions |
| Fill Shape | Toggle fill color for closed shapes |
| Effects | Toggle trail/echo drawing effects |
- 27 predefined color swatches
- Custom color via native
<input type="color">color picker
learning-javascript/
│
├── index.html # Main application HTML
├── Dockerfile # Docker image definition (NGINX)
├── Vagrantfile # Vagrant VM configuration
├── bootstrap.sh # VM provisioning script
├── davinci-deployment.yaml # Kubernetes deployment + service manifest
│
├── scripts/
│ ├── main.js # Entry point — event listeners & tool switching
│ ├── paint.js # Core Paint class — drawing engine
│ ├── fill.js # Fill class — flood fill algorithm
│ └── coords.js # Utility — coordinate & distance helpers
│
├── styles/
│ └── main.css # Compiled SASS stylesheet
│
└── images/
└── paint/ # Tool icons and favicon
File: scripts/fill.js
Used by: Paint Bucket tool
The classic flood fill algorithm fills a contiguous region of pixels with a new color. Instead of a recursive approach (which risks stack overflow on large canvases), this implementation uses an explicit stack array to simulate BFS/DFS iteratively.
How it works:
- On click, read the target color of the clicked pixel directly from
ImageData. - Push the starting pixel onto
fillStack. - In each iteration, pop a pixel — if its color matches the target, recolor it and push its 4 neighbours (up, down, left, right) onto the stack.
- Continue until the stack is empty, then write the modified
ImageDataback to the canvas in oneputImageData()call.
Time Complexity: O(N) — where N is the number of pixels in the filled region
Space Complexity: O(N) — stack holds pending pixels
// Iterative batch processing — processes all pending pixels per frame
replacementColor() {
if (this.fillStack.length) {
let range = this.fillStack.length;
for (let i = 0; i < range; i++) {
this.floodFill(this.fillStack[i][0], this.fillStack[i][1], this.fillStack[i][2]);
}
this.fillStack.splice(0, range);
this.replacementColor(); // tail recursion over batches
} else {
this.ctx.putImageData(this.imageData, 0, 0);
}
}File: scripts/paint.js
Used by: Undo command
A stack (LIFO) stores canvas snapshots (ImageData) before each stroke begins. The stack is bounded to 10 entries; once full, the oldest entry is removed using shift() (sliding window).
if (this.undoStack.length >= this.undoLimit) this.undoStack.shift(); // evict oldest
this.undoStack.push(this.ctx.getImageData(0, 0, w, h)); // push snapshot
// On undo:
this.ctx.putImageData(this.undoStack[this.undoStack.length - 1], 0, 0);
this.undoStack.pop();Data Structure: Array used as a bounded stack
Max size: 10 snapshots
Eviction policy: FIFO on overflow (shift oldest), LIFO on undo (pop latest)
File: scripts/paint.js → drawShape()
Used by: Line, Rectangle, Circle, Ellipse, Triangle tools
To show a real-time preview while the user drags, the canvas is reset to a saved snapshot on every mousemove event, then the shape is redrawn from scratch using the current mouse position. This is the standard snapshot + redraw pattern for interactive Canvas drawing.
onMouseDown(e) {
this.savedData = this.ctx.getImageData(0, 0, w, h); // snapshot before drag
}
drawShape() {
this.ctx.putImageData(this.savedData, 0, 0); // restore clean canvas
this.ctx.beginPath();
// ... draw shape to current mouse position
this.ctx.stroke();
}This ensures no ghost trails appear during drag for shape tools.
File: scripts/coords.js
Used by: Circle tool
The radius of the circle is computed as the Euclidean distance between the mouse-down point (center) and the current mouse position:
radius = √( (x₂ - x₁)² + (y₂ - y₁)² )
export function Distance(startPosition, currentPosition) {
return Math.sqrt(
Math.pow(currentPosition.x - startPosition.x, 2) +
Math.pow(currentPosition.y - startPosition.y, 2)
);
}File: scripts/coords.js
Used by: All drawing tools
Raw mouse events report coordinates relative to the viewport, not the canvas element. The Coords() function normalises these using getBoundingClientRect():
export function Coords(e, canvas) {
const rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}This ensures accurate drawing regardless of the canvas position on the page.
File: scripts/fill.js
Used by: Paint Bucket tool
CSS hex color strings (e.g. #FF5733) must be converted to [R, G, B, A] arrays for direct ImageData pixel comparison and manipulation. This is done via regex parsing + base-16 integer conversion:
hexToRGBA(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return [
parseInt(result[1], 16), // R
parseInt(result[2], 16), // G
parseInt(result[3], 16), // B
255 // A (fully opaque)
];
}Borislav Aleksandrov Dostumski GitHub: @bdostumski
This project is open source and available for learning purposes.
javascript ecmascript6 drawing-app paint-application drawing-on-canvas drawing-online paint-app drawing-application
