Konva.js: [Free drawing & Drag & Zoom] Can't draw correctly with pointer after Drag or Zoom
Asked Answered
R

1

5

Any discussion is welcomed. Thanks for reading!

What I am trying to do

I'm trying to implement simple paper(whiteboard) using Konva.js.

So far I've implemented Drag, Zoom and Free drawing on paper.

I referred to

  1. https://konvajs.org/docs/sandbox/Zooming_Relative_To_Pointer.html for Zoom
  2. https://konvajs.org/docs/sandbox/Free_Drawing.html for Free drawing

I want to draw only on the region with beige color background, and I want to draw exactly under the pointer even though it is zoomed or dragged.

But Free drawing and both Drag and Zoom features don't work well together.

Bug

Can't draw correctly after dragging or zooming.

Where I think wrong thing is happening, but can't fix

I think something is wrong in the 2 parts bellow.

  1. Implementation of zoom
  2. How I use stage.getPointerPosition() in drawing implementation
  3. Or implementations of these two doesn't fit together

Code

Minimum code is here.

/* ---- Mode management ---- */
let modeSelector = document.getElementById('mode-selector');
let mode = modeSelector.value;
modeSelector.addEventListener('change', () => {
  // Discaed event handlers used by old mode
  switch (mode) {
    case 'Hand': {
      endHand();
      break;
    }
    case 'Pen': {
      endPen();
      break;
    }
  }

  // Set event handlers for new mode
  mode = modeSelector.value;
  switch (mode) {
    case 'Hand': {
      useHand();
      break;
    }
    case 'Pen': {
      usePen();
      break;
    }
  }
});


/* ---- Konva Objects ---- */
let stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth,
  height: window.innerHeight
});

// A layer that is only used for background color
let backgroundLayer = new Konva.Layer();
let backgroundColor = new Konva.Image({
  width: window.innerWidth,
  height: window.innerHeight,
  fill: 'rgb(242,233,226)'
}) 
backgroundLayer.add(backgroundColor);
stage.add(backgroundLayer);
backgroundLayer.draw();

// A layer for free drawing
let drawLayer = new Konva.Layer();
stage.add(drawLayer);


/* ---- Functions for mode change ----*/
function useHand () {
  // Make stage draggable
  stage.draggable(true);

  // Make stage zoomable
  // *** Code is copy and pasted from
  // *** https://konvajs.org/docs/sandbox/Zooming_Relative_To_Pointer.htmlhttps://konvajs.org/docs/sandbox/Zooming_Relative_To_Pointer.html
  let scaleBy = 1.3;
  stage.on('wheel', (evt) => {
    evt.evt.preventDefault();
    let oldScale = stage.scaleX();

    let mousePointTo = {
      x: stage.getPointerPosition().x / oldScale - stage.x() / oldScale,
      y: stage.getPointerPosition().y / oldScale - stage.y() / oldScale
    };

    let newScale = evt.evt.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy;
    stage.scale({ x: newScale, y: newScale });

    let newPos = {
      x: -(mousePointTo.x - stage.getPointerPosition().x / newScale) * newScale,
      y: -(mousePointTo.y - stage.getPointerPosition().y / newScale) * newScale
    };
    stage.position(newPos);
    stage.batchDraw();
  });
}

function endHand () {
  stage.draggable(false);
  stage.off('wheel');
}

function usePen () {
  let isDrawing = false;
  let currentLine;
  stage.on('mousedown', (evt) => {
    // Start drawing
    isDrawing = true;
    // Create new line object
    let pos = stage.getPointerPosition();
    currentLine = new Konva.Line({
      stroke: 'black',
      strokeWidth: 3,
      points: [pos.x, pos.y]
    });
    drawLayer.add(currentLine);
  });

  stage.on('mousemove', (evt) => {
    if (!isDrawing) {
      return;
    }
    
    // If drawing, add new point to the current line object
    let pos = stage.getPointerPosition();
    let newPoints = currentLine.points().concat([pos.x, pos.y]);
    currentLine.points(newPoints);
    drawLayer.batchDraw();
  });

  stage.on('mouseup', (evt) => {
    // End drawing
    isDrawing = false;
  });
}

function endPen () {
  stage.off('mousedown');
  stage.off('mousemove');
  stage.off('mouseup');
}


/* ---- Init ---- */
useHand();
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Paper</title>
</head>
<body>
  <select id="mode-selector">
    <option value="Hand">Hand</option>
    <option value="Pen">Pen</option>
  </select>
  <div id="container"></div>

  <script src="https://unpkg.com/[email protected]/konva.min.js"></script>
  <!-- <script src="konvaTest.js"></script> -->
  <script src="buggyPaper.js"></script>
</body>
</html>
Recriminate answered 14/3, 2020 at 5:45 Comment(0)
D
8

stage.getPointerPosition() returns absolute position of pointer (related top-left corner of canvas container).

As you are transforming (moving and scaling a stage) you need to find a relative position, so you can use it for the line.

Relative mouse position demo demonstrates how to do that:

function getRelativePointerPosition(node) {
  // the function will return pointer position relative to the passed node
  var transform = node.getAbsoluteTransform().copy();
  // to detect relative position we need to invert transform
  transform.invert();

  // get pointer (say mouse or touch) position
  var pos = node.getStage().getPointerPosition();

  // now we find a relative point
  return transform.point(pos);
}

/* ---- Mode management ---- */
let modeSelector = document.getElementById('mode-selector');
let mode = modeSelector.value;
modeSelector.addEventListener('change', () => {
  // Discaed event handlers used by old mode
  switch (mode) {
    case 'Hand': {
      endHand();
      break;
    }
    case 'Pen': {
      endPen();
      break;
    }
  }

  // Set event handlers for new mode
  mode = modeSelector.value;
  switch (mode) {
    case 'Hand': {
      useHand();
      break;
    }
    case 'Pen': {
      usePen();
      break;
    }
  }
});


/* ---- Konva Objects ---- */
let stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth,
  height: window.innerHeight
});

// A layer that is only used for background color
let backgroundLayer = new Konva.Layer();
let backgroundColor = new Konva.Image({
  width: window.innerWidth,
  height: window.innerHeight,
  fill: 'rgb(242,233,226)'
}) 
backgroundLayer.add(backgroundColor);
stage.add(backgroundLayer);
backgroundLayer.draw();

// A layer for free drawing
let drawLayer = new Konva.Layer();
stage.add(drawLayer);


/* ---- Functions for mode change ----*/
function useHand () {
  // Make stage draggable
  stage.draggable(true);

  // Make stage zoomable
  // *** Code is copy and pasted from
  // *** https://konvajs.org/docs/sandbox/Zooming_Relative_To_Pointer.htmlhttps://konvajs.org/docs/sandbox/Zooming_Relative_To_Pointer.html
  let scaleBy = 1.3;
  stage.on('wheel', (evt) => {
    evt.evt.preventDefault();
    let oldScale = stage.scaleX();

    let mousePointTo = {
      x: stage.getPointerPosition().x / oldScale - stage.x() / oldScale,
      y: stage.getPointerPosition().y / oldScale - stage.y() / oldScale
    };

    let newScale = evt.evt.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy;
    stage.scale({ x: newScale, y: newScale });

    let newPos = {
      x: -(mousePointTo.x - stage.getPointerPosition().x / newScale) * newScale,
      y: -(mousePointTo.y - stage.getPointerPosition().y / newScale) * newScale
    };
    stage.position(newPos);
    stage.batchDraw();
  });
}

function endHand () {
  stage.draggable(false);
  stage.off('wheel');
}

function getRelativePointerPosition(node) {
    // the function will return pointer position relative to the passed node
    var transform = node.getAbsoluteTransform().copy();
    // to detect relative position we need to invert transform
    transform.invert();

    // get pointer (say mouse or touch) position
    var pos = node.getStage().getPointerPosition();

    // now we find relative point
    return transform.point(pos);
  }
function usePen () {
  let isDrawing = false;
  let currentLine;
  stage.on('mousedown', (evt) => {
    // Start drawing
    isDrawing = true;
    // Create new line object
    let pos = getRelativePointerPosition(stage);
    currentLine = new Konva.Line({
      stroke: 'black',
      strokeWidth: 3,
      points: [pos.x, pos.y]
    });
    drawLayer.add(currentLine);
  });

  stage.on('mousemove', (evt) => {
    if (!isDrawing) {
      return;
    }
    
    // If drawing, add new point to the current line object
    let pos = getRelativePointerPosition(stage);
    let newPoints = currentLine.points().concat([pos.x, pos.y]);
    currentLine.points(newPoints);
    drawLayer.batchDraw();
  });

  stage.on('mouseup', (evt) => {
    // End drawing
    isDrawing = false;
  });
}

function endPen () {
  stage.off('mousedown');
  stage.off('mousemove');
  stage.off('mouseup');
}


/* ---- Init ---- */
useHand();
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Paper</title>
</head>
<body>
  <select id="mode-selector">
    <option value="Hand">Hand</option>
    <option value="Pen">Pen</option>
  </select>
  <div id="container"></div>

  <script src="https://unpkg.com/[email protected]/konva.min.js"></script>
  <!-- <script src="konvaTest.js"></script> -->
  <script src="buggyPaper.js"></script>
</body>
</html>
Dinky answered 14/3, 2020 at 13:0 Comment(6)
Thanks a lot! This solution fix the pointer position problem!Recriminate
But currently, it seems to be possible to draw outside of the beige area. How can I also scale stage width & height so that the stage size become exactly equal to the size of beige area while zooming?Recriminate
I want Drawing to be possible only on the beige area. Because the beige area is simulation of paper. And we can't draw outside of the paper.Recriminate
The best is that there are two layers: one is the Desk layer and the other is Paper layer. And the Paper layer is on the Desk layer. Then we can move the Paper layer by dragging anywhere on the Desk layer, and also can zoom the Paper layer by mousewheel-ing anywhere on the Desk layer. And also, at any moment we can draw only on the Paper layer.Recriminate
Hey, I solved the problem myself. I choose to use Group instead of Layer for drawing. Setting Background Image to the group let me detect whether mouse pointer is on the group or not, and using the function you told me to calculate pointer's relative position to the group makes it possible to draw correctly on the group! Thanks!Recriminate
lavrton/konva.js has no this function build-in: .getRelativePointerPosition() !Stratosphere

© 2022 - 2024 — McMap. All rights reserved.