Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions projects/drawing-canvas/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!doctype html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
Expand All @@ -10,9 +10,31 @@
<header><h1>Drawing Canvas</h1></header>
<main id="app">
<!-- TODO: <canvas> area with drawing via mouse/touch -->
<div class="canvas-container">
<canvas id="drawingCanvas"></canvas>
</div>

<!-- TODO: Controls: color picker, brush size, clear, save as PNG -->
<div class="toolbar" role="toolbar" aria-label="Drawing tools">
<div class="tool-group">
<label for="colorPicker">Color:</label>
<input type="color" id="colorPicker" value="#000000" aria-label="Brush color">
</div>

<div class="tool-group">
<label for="brushSize">Brush Size:</label>
<input type="range" id="brushSize" min="1" max="50" value="5" aria-label="Brush size">
<span id="brushSizeValue">5</span>
</div>

<button class="button clear" id="clearButton" aria-label="Clear canvas">Clear</button>
<button class="button save" id="saveButton" aria-label="Save drawing as PNG">Save as PNG</button>
</div>

<p class="warning">Note: Resizing the window will clear your drawing.</p>

<!-- TODO: Optional: eraser mode, background grid, undo/redo -->
</main>
<script defer src="main.js"></script>
</body>
</html>
</html>
168 changes: 158 additions & 10 deletions projects/drawing-canvas/main.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,160 @@
/**
* TODO: Drawing Canvas
* - Initialize <canvas>, handle mouse/touch drawing
* - Color picker, brush size, clear canvas
* - Save canvas as image (toDataURL)
* Optional: Eraser mode, undo/redo, pressure sensitivity (Pointer Events)
*/

document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', () => {
console.log('Drawing Canvas ready');

// TODO: Canvas setup and draw handlers
});
const canvas = document.getElementById('drawingCanvas');
const ctx = canvas.getContext('2d');
const colorPicker = document.getElementById('colorPicker');
const brushSize = document.getElementById('brushSize');
const brushSizeValue = document.getElementById('brushSizeValue');
const clearButton = document.getElementById('clearButton');
const saveButton = document.getElementById('saveButton');

let isDrawing = false;
let lastX = 0;
let lastY = 0;
let dpr = window.devicePixelRatio || 1;

// Initialize canvas
function initCanvas() {
const container = canvas.parentElement;
const width = container.clientWidth;
const height = container.clientHeight;

// Set canvas size with DPR scaling
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;

// Scale context for high DPI displays
ctx.scale(dpr, dpr);

// Set initial drawing styles
ctx.strokeStyle = colorPicker.value;
ctx.lineWidth = brushSize.value;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';

// Clear canvas with white background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
}

// Drawing functions
function startDrawing(e) {
isDrawing = true;
const pos = getPointerPos(e);
[lastX, lastY] = [pos.x, pos.y];
}

function draw(e) {
if (!isDrawing) return;

e.preventDefault();
const pos = getPointerPos(e);

ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();

[lastX, lastY] = [pos.x, pos.y];
}

function stopDrawing() {
isDrawing = false;
}

function getPointerPos(e) {
const rect = canvas.getBoundingClientRect();
let clientX, clientY;

if (e.type.includes('touch')) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}

return {
x: (clientX - rect.left) * (canvas.width / dpr / rect.width),
y: (clientY - rect.top) * (canvas.height / dpr / rect.height)
};
}

// Event handlers for controls
colorPicker.addEventListener('input', (e) => {
ctx.strokeStyle = e.target.value;
});

brushSize.addEventListener('input', (e) => {
const size = e.target.value;
ctx.lineWidth = size;
brushSizeValue.textContent = size;
});

clearButton.addEventListener('click', () => {
if (confirm('Are you sure you want to clear the canvas?')) {
const width = canvas.width / dpr;
const height = canvas.height / dpr;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
}
});

saveButton.addEventListener('click', () => {
// Create a temporary canvas for saving at original resolution
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');

tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;

// Draw the high-resolution content to temp canvas
tempCtx.drawImage(canvas, 0, 0);

// Create download link
const link = document.createElement('a');
link.download = `drawing-${new Date().getTime()}.png`;
link.href = tempCanvas.toDataURL('image/png');
link.click();
});

// Pointer event handlers for drawing
canvas.addEventListener('pointerdown', startDrawing);
canvas.addEventListener('pointermove', draw);
canvas.addEventListener('pointerup', stopDrawing);
canvas.addEventListener('pointerout', stopDrawing);

// Touch event handlers for mobile devices
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
startDrawing(e);
});

canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
draw(e);
});

canvas.addEventListener('touchend', stopDrawing);

// Handle window resize
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
if (confirm('Resizing will clear your current drawing. Continue?')) {
initCanvas();
}
}, 250);
});

// Prevent context menu on canvas
canvas.addEventListener('contextmenu', (e) => e.preventDefault());

// Initialize the canvas
initCanvas();
});
130 changes: 128 additions & 2 deletions projects/drawing-canvas/styles.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/* TODO: Style the drawing canvas and toolbar */
/* TODO: Style the drawing canvas and toolbar */
:root {
--bg: #0b1220;
--fg: #f8fafc;
--accent: #f472b6;
--toolbar-bg: #1e293b;
--toolbar-border: #334155;
--canvas-bg: #ffffff;
}
* { box-sizing: border-box; }
body {
Expand All @@ -13,5 +16,128 @@ body {
min-height: 100vh;
display: grid;
place-items: center;
padding: 1rem;
}
#app { width: min(94vw, 960px); }
#app {
width: min(94vw, 960px);
display: flex;
flex-direction: column;
gap: 1rem;
}

header h1 {
text-align: center;
margin: 0 0 1rem 0;
color: var(--accent);
}

.canvas-container {
position: relative;
width: 100%;
height: 60vh;
border: 2px solid var(--toolbar-border);
border-radius: 8px;
overflow: hidden;
background: var(--canvas-bg);
}

#drawingCanvas {
display: block;
width: 100%;
height: 100%;
cursor: crosshair;
touch-action: none;
}

.toolbar {
display: flex;
flex-wrap: wrap;
gap: 1rem;
padding: 1rem;
background: var(--toolbar-bg);
border: 1px solid var(--toolbar-border);
border-radius: 8px;
align-items: center;
justify-content: center;
}

.tool-group {
display: flex;
align-items: center;
gap: 0.5rem;
}

.tool-group label {
font-size: 0.9rem;
font-weight: 500;
}

#colorPicker {
width: 50px;
height: 40px;
border: 2px solid var(--toolbar-border);
border-radius: 4px;
cursor: pointer;
background: #000000;
}

#brushSize {
width: 120px;
}

#brushSizeValue {
min-width: 30px;
text-align: center;
font-size: 0.9rem;
}

.button {
padding: 0.75rem 1.5rem;
background: var(--accent);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: opacity 0.2s;
}

.button:hover {
opacity: 0.9;
}

.button:active {
transform: translateY(1px);
}

.button.clear {
background: #ef4444;
}

.button.save {
background: #10b981;
}

.warning {
color: #f59e0b;
font-size: 0.8rem;
text-align: center;
margin-top: 0.5rem;
}

/* Responsive design */
@media (max-width: 768px) {
.toolbar {
flex-direction: column;
align-items: stretch;
}

.tool-group {
justify-content: space-between;
}

.canvas-container {
height: 50vh;
}
}
Loading