Creative

WHITEBOARD

Draw freehand, shapes, and text. Export your drawing as PNG.

Size 3
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();