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/konva@4.0.0/konva.min.js"></script>
  <!-- <script src="konvaTest.js"></script> -->
  <script src="buggyPaper.js"></script>
</body>
</html>
dynamicnoodle
  • 51
  • 1
  • 4

1 Answers1

7

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/konva@4.0.0/konva.min.js"></script>
  <!-- <script src="konvaTest.js"></script> -->
  <script src="buggyPaper.js"></script>
</body>
</html>
lavrton
  • 18,973
  • 4
  • 30
  • 63
  • Thanks a lot! This solution fix the pointer position problem! – dynamicnoodle Mar 15 '20 at 04:58
  • 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? – dynamicnoodle Mar 15 '20 at 05:00
  • 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. – dynamicnoodle Mar 15 '20 at 05:11
  • 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. – dynamicnoodle Mar 15 '20 at 05:20
  • 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! – dynamicnoodle Mar 16 '20 at 05:35
  • lavrton/konva.js has no this function build-in: .getRelativePointerPosition() ! – Aerodynamic Dec 30 '22 at 14:04