Contain a rotated object inside another rotated object FabricJS

牧云@^-^@ 提交于 2021-02-07 18:09:19

问题


I have two objects a parent (red) and a child (blue). The parent object is fixed and can't be moved, only the child object is movable and the child is always bigger than the parent. In whatever way the child object is moved it should always be contained inside the child, which means we should never see the red rectangle.

Demo: https://codesandbox.io/s/force-contain-of-object-inside-another-object-fabric-js-7nt7q

I know there are solutions to contain an object within the canvas or other object boundaries (ex. Move object within canvas boundary limit) which mainly force the top/right/bottom/left values to not exceed the parent values, but here we have the case of two rotated objects by the same degree.

I have a real-life scenario when a user uploads a photo to a frame container. The photo is normally always bigger than the frame container. The user can move the photo inside the frame, but he should not be allowed to create any empty spaces, the photo should always be contained inside the photo frame.


回答1:


I would go with a pure canvas (no fabricjs), do it from scratch that way you understand well the problem you are facing, then if you need it that same logic should be easily portable to any library.

You have some rules:

  • The parent object is fixed and can't be moved,
  • The child object is movable.
  • The child is always bigger than the parent.
  • The child object is always constrained by the parent.

So my idea is to get all four corners, that way on the move we can use those coordinates to determine if it can be moved to the new location or not, the code should be easy to follow, but ask if you have any concerns.

I'm using the ray-casting algorithm:
https://github.com/substack/point-in-polygon/blob/master/index.js
With that, all we need to do is check that the corners of the child are not inside the parent and that the parent is inside the child, that is all.

I'm no expert with FabricJS so my best might not be much...
but below is my attempt to get your code going.

<canvas id="canvas" width="500" height="350"></canvas>

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<script>
    var canvas = new fabric.Canvas("canvas");
    canvas.stateful = true;

    function getCoords(rect) {
        var x = rect.left;
        var y = rect.top;
        var angle = (rect.angle * Math.PI) / 180;

        var coords = [{ x, y }];
        x += rect.width * Math.cos(angle);
        y += rect.width * Math.sin(angle);
        coords.push({ x, y });

        angle += Math.PI / 2;
        x += rect.height * Math.cos(angle);
        y += rect.height * Math.sin(angle);
        coords.push({ x, y });

        angle += Math.PI / 2;
        x += rect.width * Math.cos(angle);
        y += rect.width * Math.sin(angle);
        coords.push({ x, y });
        return coords;
    }

    function inside(p, vs) {
        var inside = false;
        for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
            var xi = vs[i].x, yi = vs[i].y;
            var xj = vs[j].x, yj = vs[j].y;
            var intersect =
                yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi;
            if (intersect) inside = !inside;
        }
        return inside;
    }

    var parent = new fabric.Rect({
        width: 150, height: 100, left: 200, top: 50, angle: 25, selectable: false, fill: "red"
    });
    var pCoords = getCoords(parent);

    var child = new fabric.Rect({
        width: 250, height: 175, left: 180, top: 10, angle: 25, hasControls: false, fill: "rgba(0,0,255,0.9)"
    });

    canvas.add(parent);
    canvas.add(child);

    canvas.on("object:moving", function (e) {
        var cCoords = getCoords(e.target);
        var inBounds = true;
        cCoords.forEach(c => { if (inside(c, pCoords)) inBounds = false; });
        pCoords.forEach(c => { if (!inside(c, cCoords)) inBounds = false; });
        if (inBounds) {
            e.target.setCoords();
            e.target.saveState();
            e.target.set("fill", "rgba(0,0,255,0.9)");            
        } else {
            e.target.set("fill", "black");
            e.target.animate({
                left: e.target._stateProperties.left,
                top: e.target._stateProperties.top
              },{
                duration: 500,
                onChange: canvas.renderAll.bind(canvas),
                easing: fabric.util.ease["easeInBounce"],
                onComplete: function() {
                  e.target.set("fill", "rgba(0,0,255,0.9)");
                }
            });
        }
    });
</script>

That code is on sandbox as well:
https://codesandbox.io/s/force-contain-of-object-inside-another-object-fabric-js-dnvb5

It certainly is nice not to worry about coding all the click/hold/drag fabric makes that real easy...

I was experimenting with FabricJS and there a nice property of the canvas
(canvas.stateful = true;)
that allows us to keep track of where we've been, and if we go out of bounds we can revert that movement, also playing with animate that gives the user visual feedback that the movement is not allowed.

Here is another version without animation:

<canvas id="canvas" width="500" height="350"></canvas>

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<script>
    var canvas = new fabric.Canvas("canvas");
    canvas.stateful = true;

    function getCoords(rect) {
        var x = rect.left;
        var y = rect.top;
        var angle = (rect.angle * Math.PI) / 180;

        var coords = [{ x, y }];
        x += rect.width * Math.cos(angle);
        y += rect.width * Math.sin(angle);
        coords.push({ x, y });

        angle += Math.PI / 2;
        x += rect.height * Math.cos(angle);
        y += rect.height * Math.sin(angle);
        coords.push({ x, y });

        angle += Math.PI / 2;
        x += rect.width * Math.cos(angle);
        y += rect.width * Math.sin(angle);
        coords.push({ x, y });
        return coords;
    }

    function inside(p, vs) {
        var inside = false;
        for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
            var xi = vs[i].x, yi = vs[i].y;
            var xj = vs[j].x, yj = vs[j].y;
            var intersect =
                yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi;
            if (intersect) inside = !inside;
        }
        return inside;
    }

    var parent = new fabric.Rect({
        width: 150, height: 100, left: 200, top: 50, angle: 25, selectable: false, fill: "red"
    });
    var pCoords = getCoords(parent);

    var child = new fabric.Rect({
        width: 250, height: 175, left: 180, top: 10, angle: 25, hasControls: false, fill: "rgba(0,0,255,0.9)"
    });

    canvas.add(parent);
    canvas.add(child);

    canvas.on("object:moving", function (e) {
        var cCoords = getCoords(e.target);
        var inBounds = true;
        cCoords.forEach(c => { if (inside(c, pCoords)) inBounds = false; });
        pCoords.forEach(c => { if (!inside(c, cCoords)) inBounds = false; });
        if (inBounds) {
            e.target.setCoords();
            e.target.saveState();
        } else {
            e.target.left = e.target._stateProperties.left;
            e.target.top = e.target._stateProperties.top;
        }
    });
</script>

This algorithm also opens the door for other shapes as well, here is a hexagon version:
https://raw.githack.com/heldersepu/hs-scripts/master/HTML/canvas_contained2.html




回答2:


you can just create the new Class, with your object inside, and do all actions only with parent of the children, something like this:

 fabric.RectWithRect = fabric.util.createClass(fabric.Rect, {
      type: 'rectWithRect',
      textOffsetLeft: 0,
      textOffsetTop: 0,
      _prevObjectStacking: null,
      _prevAngle: 0,
      minWidth: 50,
      minHeight: 50,   

      _currentScaleFactorX: 1,
      _currentScaleFactorY: 1,

      _lastLeft: 0,
      _lastTop: 0,

      recalcTextPosition: function () {
           //this.insideRect.setCoords();
           const sin = Math.sin(fabric.util.degreesToRadians(this.angle))
           const cos = Math.cos(fabric.util.degreesToRadians(this.angle))
           const newTop = sin * this.insideRectOffsetLeft + cos * this.insideRectOffsetTop
           const newLeft = cos * this.insideRectOffsetLeft - sin * this.insideRectOffsetTop
           const rectLeftTop = this.getPointByOrigin('left', 'top')
           this.insideRect.set('left', rectLeftTop.x + newLeft)
           this.insideRect.set('top', rectLeftTop.y + newTop)
           this.insideRect.set('width', this.width - 40)
           this.insideRect.set('height', this.height - 40)
           this.insideRect.set('scaleX', this.scaleX)
           this.insideRect.set('scaleY', this.scaleY)
      },

     initialize: function (textOptions, rectOptions) {
       this.callSuper('initialize', rectOptions)

       this.insideRect = new fabric.Rect({
         ...textOptions,
         dirty: false,
         objectCaching: false,
         selectable: false,
         evented: false,
         fragmentType: 'rectWidthRect'
       });
       canvas.bringToFront(this.insideRect);
       this.insideRect.width = this.width - 40;
       this.insideRect.height = this.height - 40;
       this.insideRect.left = this.left + 20;
       this.insideRect.top = this.top + 20;
       this.insideRectOffsetLeft = this.insideRect.left - this.left
       this.insideRectOffsetTop = this.insideRect.top - this.top


       this.on('moving', function(e){
         this.recalcTextPosition();
       })

       this.on('rotating',function(){
         this.insideRect.rotate(this.insideRect.angle + this.angle - this._prevAngle)
         this.recalcTextPosition()
         this._prevAngle = this.angle
       })

       this.on('scaling', function(fEvent){
           this.recalcTextPosition();
       });

       this.on('added', function(){
         this.canvas.add(this.insideRect)
       });

       this.on('removed', function(){
         this.canvas.remove(this.insideRect)
       });

       this.on('mousedown:before', function(){
         this._prevObjectStacking = this.canvas.preserveObjectStacking
         this.canvas.preserveObjectStacking = true
       });

       this.on('deselected', function(){
         this.canvas.preserveObjectStacking = this._prevObjectStacking
       });

     }
 });

and then just add your element to your canvas as usual:

var rectWithRect = new fabric.RectWithRect(
            {
              fill: "red",
            }, // children rect options
           {
             left:100,
             top:100,
             width: 300,
             height: 100,
             dirty: false,
             objectCaching: false,
             strokeWidth: 0,
             fill: 'blue'
           } // parent rect options
      );


canvas.add(rectWithRect);

by the way, you can use method like this to create nested elements, text with background and other.

Codesandbox DEMO



来源:https://stackoverflow.com/questions/60822474/contain-a-rotated-object-inside-another-rotated-object-fabricjs

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!