Creative
WHITEBOARD
Draw freehand, shapes, and text. Export your drawing as PNG.
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
const canvas = document.getElementById('drawCanvas');
const ctx = canvas.getContext('2d');
let tool = 'pen', drawing = false, startX, startY;
let history = [], historyIdx = -1;
let snapshot = null;
function resize() {
const w = canvas.parentElement.clientWidth;
const h = Math.max(500, window.innerHeight - 300);
if (canvas.width !== w || canvas.height !== h) {
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
canvas.width = w; canvas.height = h;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, w, h);
ctx.putImageData(imgData, 0, 0);
}
}
function init() {
canvas.width = canvas.parentElement.clientWidth;
canvas.height = Math.max(500, window.innerHeight - 300);
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
saveState();
}
function getPos(e) {
const r = canvas.getBoundingClientRect();
const x = (e.clientX || (e.touches && e.touches[0].clientX)) - r.left;
const y = (e.clientY || (e.touches && e.touches[0].clientY)) - r.top;
return { x, y };
}
function getStroke() {
return {
color: tool === 'eraser' ? '#ffffff' : document.getElementById('strokeColor').value,
width: parseInt(document.getElementById('strokeWidth').value),
fillColor: document.getElementById('noFill').checked ? null : document.getElementById('fillColor').value
};
}
canvas.addEventListener('mousedown', onStart);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onEnd);
canvas.addEventListener('mouseleave', onEnd);
canvas.addEventListener('touchstart', e => { e.preventDefault(); onStart(e); }, { passive: false });
canvas.addEventListener('touchmove', e => { e.preventDefault(); onMove(e); }, { passive: false });
canvas.addEventListener('touchend', onEnd);
function onStart(e) {
const p = getPos(e);
startX = p.x; startY = p.y;
drawing = true;
if (tool === 'text') {
drawing = false;
const text = prompt('Enter text:');
if (text) {
const s = getStroke();
ctx.font = `${s.width * 5 + 12}px sans-serif`;
ctx.fillStyle = s.color;
ctx.fillText(text, p.x, p.y);
saveState();
}
return;
}
snapshot = ctx.getImageData(0, 0, canvas.width, canvas.height);
if (tool === 'pen' || tool === 'eraser') {
const s = getStroke();
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.strokeStyle = s.color;
ctx.lineWidth = tool === 'eraser' ? s.width * 3 : s.width;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
}
}
function onMove(e) {
if (!drawing) return;
const p = getPos(e);
const s = getStroke();
if (tool === 'pen' || tool === 'eraser') {
ctx.lineTo(p.x, p.y);
ctx.stroke();
} else {
ctx.putImageData(snapshot, 0, 0);
ctx.beginPath();
ctx.strokeStyle = s.color;
ctx.lineWidth = s.width;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
if (tool === 'line') {
ctx.moveTo(startX, startY);
ctx.lineTo(p.x, p.y);
ctx.stroke();
} else if (tool === 'rect') {
const w = p.x - startX, h = p.y - startY;
if (s.fillColor) { ctx.fillStyle = s.fillColor; ctx.fillRect(startX, startY, w, h); }
ctx.strokeRect(startX, startY, w, h);
} else if (tool === 'circle') {
const rx = Math.abs(p.x - startX) / 2, ry = Math.abs(p.y - startY) / 2;
const cx = startX + (p.x - startX) / 2, cy = startY + (p.y - startY) / 2;
ctx.beginPath();
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
if (s.fillColor) { ctx.fillStyle = s.fillColor; ctx.fill(); }
ctx.stroke();
}
}
}
function onEnd() {
if (drawing) { drawing = false; saveState(); }
}
function setTool(t) {
tool = t;
document.querySelectorAll('.tb-btn[data-tool]').forEach(b => b.classList.toggle('active', b.dataset.tool === t));
canvas.style.cursor = t === 'text' ? 'text' : 'crosshair';
}
function saveState() {
historyIdx++;
history = history.slice(0, historyIdx);
history.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
if (history.length > 30) { history.shift(); historyIdx--; }
}
function undo() { if (historyIdx > 0) { historyIdx--; ctx.putImageData(history[historyIdx], 0, 0); } }
function redo() { if (historyIdx < history.length - 1) { historyIdx++; ctx.putImageData(history[historyIdx], 0, 0); } }
function clearCanvas() {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
saveState();
}
function exportPNG() {
const a = document.createElement('a');
a.download = 'whiteboard.png';
a.href = canvas.toDataURL('image/png');
a.click();
}
document.getElementById('strokeWidth').oninput = function() {
document.getElementById('sizeVal').textContent = this.value;
};
window.addEventListener('resize', resize);
init();