Skip to main content

Free Drawing Konva Demo

There are many ways to implement free drawing tools in Konva.

I see two most common and simple ways:

  1. Konva-based vector graphics (simple)
  2. Manual drawing into 2d canvas (advanced)

Free drawing with Konva nodes

So the first and probably the simplest ways is:

  1. Start a new Konva.Line on mousedown/touchstart
  2. Add new point into the line while mousemove/touchmove

That way works ok for many applications. Also it is simple to store the state of the drawing somewhere in vector representation (like React store or JSON saving into database).

import Konva from 'konva';

// create tool select
const select = document.createElement('select');
select.innerHTML = `
  <option value="brush">Brush</option>
  <option value="eraser">Eraser</option>
`;
document.body.appendChild(select);

const width = window.innerWidth;
const height = window.innerHeight - 25;

// first we need Konva core things: stage and layer
const stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

const layer = new Konva.Layer();
stage.add(layer);

let isPaint = false;
let mode = 'brush';
let lastLine;

stage.on('mousedown touchstart', function (e) {
  isPaint = true;
  const pos = stage.getPointerPosition();
  lastLine = new Konva.Line({
    stroke: '#df4b26',
    strokeWidth: 5,
    globalCompositeOperation:
      mode === 'brush' ? 'source-over' : 'destination-out',
    // round cap for smoother lines
    lineCap: 'round',
    lineJoin: 'round',
    // add point twice, so we have some drawings even on a simple click
    points: [pos.x, pos.y, pos.x, pos.y],
  });
  layer.add(lastLine);
});

stage.on('mouseup touchend', function () {
  isPaint = false;
});

// and core function - drawing
stage.on('mousemove touchmove', function (e) {
  if (!isPaint) {
    return;
  }

  // prevent scrolling on touch devices
  e.evt.preventDefault();

  const pos = stage.getPointerPosition();
  const newPoints = lastLine.points().concat([pos.x, pos.y]);
  lastLine.points(newPoints);
});

select.addEventListener('change', function () {
  mode = select.value;
});

Free drawing manually

The first approach has limitation if we want to use some low-level 2d canvas API directly. If you need advanced access to the canvas it is better to use Native Context Access

We will create special offscreen canvas where we will add all drawings. With native access to the canvas we can use low-level 2d context functions. To display the canvas on the stage we will use Konva.Image.

import Konva from 'konva';

// create tool select
const select = document.createElement('select');
select.innerHTML = `
  <option value="brush">Brush</option>
  <option value="eraser">Eraser</option>
`;
document.body.appendChild(select);

const width = window.innerWidth;
const height = window.innerHeight - 25;

// first we need Konva core things: stage and layer
const stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

const layer = new Konva.Layer();
stage.add(layer);

// then we are going to draw into special canvas element
const canvas = document.createElement('canvas');
canvas.width = stage.width();
canvas.height = stage.height();

// created canvas we can add to layer as "Konva.Image" element
const image = new Konva.Image({
  image: canvas,
  x: 0,
  y: 0,
});
layer.add(image);

// Good. Now we need to get access to context element
const context = canvas.getContext('2d');
context.strokeStyle = '#df4b26';
context.lineJoin = 'round';
context.lineWidth = 5;

let isPaint = false;
let lastPointerPosition;
let mode = 'brush';

// now we need to bind some events
// we need to start drawing on mousedown
// and stop drawing on mouseup
image.on('mousedown touchstart', function () {
  isPaint = true;
  lastPointerPosition = stage.getPointerPosition();
});

stage.on('mouseup touchend', function () {
  isPaint = false;
});

// and core function - drawing
stage.on('mousemove touchmove', function () {
  if (!isPaint) {
    return;
  }

  if (mode === 'brush') {
    context.globalCompositeOperation = 'source-over';
  }
  if (mode === 'eraser') {
    context.globalCompositeOperation = 'destination-out';
  }
  context.beginPath();

  const localPos = {
    x: lastPointerPosition.x - image.x(),
    y: lastPointerPosition.y - image.y(),
  };
  context.moveTo(localPos.x, localPos.y);
  const pos = stage.getPointerPosition();
  const newLocalPos = {
    x: pos.x - image.x(),
    y: pos.y - image.y(),
  };
  context.lineTo(newLocalPos.x, newLocalPos.y);
  context.closePath();
  context.stroke();

  lastPointerPosition = pos;
  // redraw manually
  layer.batchDraw();
});

select.addEventListener('change', function () {
  mode = select.value;
});