Search code examples
javascriptcanvashtml5-canvasselectionfabricjs

How to select covered objects via mouse in fabricJS?


I'm trying to develop a way to select objects that are layered below and (totally) covered by other objects. One idea is to select the top object and then via doubleclick walk downwards through the layers. This is what I got at the moment:

var canvas = new fabric.Canvas("c");

fabric.util.addListener(canvas.upperCanvasEl, "dblclick", function (e) {
  var _canvas = canvas;
  var _mouse = _canvas.getPointer(e);
  var _active = _canvas.getActiveObject();
    
  if (e.target) {
    var _targets = _canvas.getObjects().filter(function (_obj) {
      return _obj.containsPoint(_mouse);
    });
      
    //console.warn(_targets);
      
    for (var _i=0, _max=_targets.length; _i<_max; _i+=1) {
      //check if target is currently active
      if (_targets[_i] == _active) {
       	//then select the one on the layer below
       	_targets[_i-1] && _canvas.setActiveObject(_targets[_i-1]);
         break;
        }
      }
    }
});

canvas
  .add(new fabric.Rect({
    top: 25,
    left: 25,
    width: 100,
    height: 100,
    fill: "red"
  }))
  .add(new fabric.Rect({
    top: 50,
    left: 50,
    width: 100,
    height: 100,
    fill: "green"
  }))
  .add(new fabric.Rect({
    top: 75,
    left: 75,
    width: 100,
    height: 100,
    fill: "blue"
  }))
  .renderAll();
canvas {
 border: 1px solid;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.3/fabric.min.js"></script>
<canvas id="c" width="300" height="200"></canvas>

As you can see, trying to select the red rectangle from within the blue one is not working. I'm only able to select the green or the blue. I guess that after the first doubleclick worked (green is selected), clicking again just selects blue so the following doubleclick is only able to get green again.

Is there a way around this? Any other ideas?


Solution

  • After some time I finally was able to solve that by myself. Clicking on an object brings it to the top. On double-clicking I try to get the object one layer behind the current object. On another dblclick I get the one behind and so on. Works great for me and also allows for the selection of fully covered objects without the need to move others.

    var canvas = new fabric.Canvas("c");
    
    canvas.on("object:selected", function (e) {
      if (e.target) {
        e.target.bringToFront();
        this.renderAll();
      }
    });
    
    var _prevActive = 0;
    var _layer = 0;
    
    //
    fabric.util.addListener(canvas.upperCanvasEl, "dblclick", function (e) {
        var _canvas = canvas;
        //current mouse position
        var _mouse = _canvas.getPointer(e);
        //active object (that has been selected on click)
        var _active = _canvas.getActiveObject();
        //possible dblclick targets (objects that share mousepointer)
        var _targets = _canvas.getObjects().filter(function (_obj) {
            return _obj.containsPoint(_mouse) && !_canvas.isTargetTransparent(_obj, _mouse.x, _mouse.y);
        });
        
        _canvas.deactivateAll();
          
        //new top layer target
        if (_prevActive !== _active) {
            //try to go one layer below current target
            _layer = Math.max(_targets.length-2, 0);
        }
        //top layer target is same as before
        else {
            //try to go one more layer down
            _layer = --_layer < 0 ? Math.max(_targets.length-2, 0) : _layer;
        }
    
        //get obj on current layer
        var _obj = _targets[_layer];
    
        if (_obj) {
        	_prevActive = _obj;
        	_obj.bringToFront();
        	_canvas.setActiveObject(_obj).renderAll();
        }
    });
    
    //create something to play with
    canvas
      //fully covered rect is selectable with dblclicks
      .add(new fabric.Rect({
        top: 75,
        left: 75,
        width: 50,
        height: 50,
        fill: "black",
        stroke: "black",
        globalCompositeOperation: "xor",
        perPixelTargetFind: true
      }))
      .add(new fabric.Circle({
        top: 25,
        left: 25,
        radius: 50,
        fill: "rgba(255,0,0,.5)",
        stroke: "black",
        perPixelTargetFind: true
      }))
      .add(new fabric.Circle({
        top: 50,
        left: 50,
        radius: 50,
        fill: "rgba(0,255,0,.5)",
        stroke: "black",
        perPixelTargetFind: true
      }))
      .add(new fabric.Circle({
        top: 75,
        left: 75,
        radius: 50,
        fill: "rgba(0,0,255,.5)",
        stroke: "black",
        perPixelTargetFind: true
      }))
      .renderAll();
    canvas {
     border: 1px solid;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.4/fabric.min.js"></script>
    <canvas id="c" width="300" height="200"></canvas>