4

Is there an example or way of drawing a box around multiple objects on the konva canvas and by doing so selecting the objects which are inside this box with some kind of indicator around them?

Andreas Zeiner
  • 537
  • 7
  • 24
  • [Edited] What you are talking about is often called a 'rubber rectangle'. Konvajs does not provide this. The draw-the-rectangle part is not hard to do though - on mousedown note the co-ords as the initial point, then as the mouse moves draw a rectangle from the initial point to the current point. Harder is that as the rectangle grows you need a hit-testing algorithm to see if the rubber-rectangle and current shapes touch, and highlight accordingly. Re hit-testing, if your shapes are rectangular or circular this is simple but needs DIY coding as there is no native collision detection. – Vanquished Wombat Nov 21 '17 at 09:11
  • thx for the hint, i'll give it a try!! – Andreas Zeiner Nov 21 '17 at 15:19

1 Answers1

23

Here's a working snippet. Uses simple rect-overlap collision detection and corrects drag rect co-ordinates if user chooses to drag north or east. Should get you going.

// Set up the canvas and shapes
var s1 = new Konva.Stage({container: 'container1', width: 300, height: 200});
var layer1 = new Konva.Layer({draggable: false});
s1.add(layer1);

// draw a background rect to catch events.
var r1 = new Konva.Rect({x: 0, y: 0, width: 300, height: 200, fill: 'gold' })    
layer1.add(r1)

// draw a rectangle to be used as the rubber area
var r2 = new Konva.Rect({x: 0, y: 0, width: 0, height: 0, stroke: 'red', dash: [2,2]})    
r2.listening(false); // stop r2 catching our mouse events.
layer1.add(r2)

var shape1;
var shapeList=[];
for ( var i = 1; i <= 6; i = i + 1){
  if (i % 2 == 0){
      shape1 = new Konva.Rect({x: 100, y: 25 * i, width: 30, height: 25, fill: 'gold', stroke: 'black' })    
      shape1.on('click', function(e){
        console.log(this.x() + ', ' + this.y())
      })
  }
  if (i % 2 == 1){
      shape1 = new Konva.Circle({x: 200, y: 30 * i, radius: 15, fill: 'red', stroke: 'black'})    
  }
  shapeList.push(shape1);  // add shape to our array of shapes to hit test later
  shape1.listening(false); // stop shape interferring with mouse
  layer1.add(shape1); 
}

s1.draw() // First draw of canvas.
var posStart;
var posNow;
var mode = '';
function startDrag(posIn){
  posStart = {x: posIn.x, y: posIn.y};
  posNow = {x: posIn.x, y: posIn.y};
}

function updateDrag(posIn){ 
  
  // update rubber rect position
  posNow = {x: posIn.x, y: posIn.y};
  var posRect = reverse(posStart,posNow);
  r2.x(posRect.x1);
  r2.y(posRect.y1);
  r2.width(posRect.x2 - posRect.x1);
  r2.height(posRect.y2 - posRect.y1);
  r2.visible(true);  
 
  // run the collision check loop
  for (i=0; i< shapeList.length; i = i + 1){
    
    if (hitCheck(shapeList[i], r2)){
        shapeList[i].stroke('lime'); // if we get a hit draw a lime stroke
        }
    else {
        shapeList[i].stroke('black'); // otherwise leave stoke black.
    }
  }
  
  s1.draw(); // redraw any changes.
  
}

// start the rubber drawing on mouse down.
r1.on('mousedown', function(e){ 
  mode = 'drawing';
  startDrag({x: e.evt.layerX, y: e.evt.layerY})
  })

// update the rubber rect on mouse move - note use of 'mode' var to avoid drawing after mouse released.
r1.on('mousemove', function(e){ 
    if (mode === 'drawing'){
      updateDrag({x: e.evt.layerX, y: e.evt.layerY})
    }
})

r1.on('mouseup', function(e){ 
    mode = '';
    r2.visible(false);
    s1.draw();
})


function hitCheck(shape1, shape2){
  
  var s1 = shape1.getClientRect(); // use this to get bounding rect for shapes other than rectangles.
  var s2 = shape2.getClientRect();

  // corners of shape 1
  var X = s1.x;
  var Y  = s1.y
  var A = s1.x + s1.width;
  var B = s1.y + s1.height;

  // corners of shape 2
  var X1 = s2.x;
  var A1 = s2.x + s2.width;
  var Y1 = s2.y
  var B1 = s2.y + s2.height;

  // Simple overlapping rect collision test
  if (A<X1 || A1<X || B<Y1 || B1<Y){
      return false
  }
  else{
    return true;
  }
    
}

// reverse co-ords if user drags left / up
function reverse(r1, r2){
  var r1x = r1.x, r1y = r1.y, r2x = r2.x,  r2y = r2.y, d;
  if (r1x > r2x ){
    d = Math.abs(r1x - r2x);
    r1x = r2x; r2x = r1x + d;
  }
  if (r1y > r2y ){
    d = Math.abs(r1y - r2y);
    r1y = r2y; r2y = r1y + d;
  }
    return ({x1: r1x, y1: r1y, x2: r2x, y2: r2y}); // return the corrected rect.     
}
p
{
  padding: 13px;
  
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://cdn.rawgit.com/konvajs/konva/1.6.5/konva.min.js"></script>
<p>Click & drag on the background to capture shapes. Drag left-top to right-bottom or in reverse. Uses simple rect-overlap collision detection.
</p>
<div id='container1' style="display: inline-block; width: 300px, height: 200px; background-color: silver; overflow: hidden; position: relative;"></div>
Vanquished Wombat
  • 9,075
  • 5
  • 28
  • 67
  • Thanks for this amazing solution. I have only one issue. When I'm zooming my stage (https://pastebin.com/wpiHuPBK) the selective rectangle is not taking the correct coordinates. Any idea how I can solve that? Thank you – Bogdan Nicovski Dec 27 '19 at 21:57
  • 1
    @BogdanNicovski - with the canvas, life is all about rectangles. For overlap math, you need to work everything back to a scale of 1:1, or apply the scaling to everything. Be consistent and all will be well. – Vanquished Wombat Dec 28 '19 at 14:18