【问题标题】:Contain a rotated object inside another rotated object FabricJS在另一个旋转对象 FabricJS 中包含一个旋转对象
【发布时间】:2020-07-04 10:37:05
【问题描述】:

我有两个对象,一个父对象(红色)和一个子对象(蓝色)。 父对象是固定的,不能移动,只有子对象是可移动的,子对象总是比父对象大。无论以何种方式移动子对象,它都应始终包含在子对象内,这意味着我们永远不会看到红色矩形。

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

我知道有一些解决方案可以在画布或其他对象边界(例如Move object within canvas boundary limit)内包含一个对象,主要是强制顶部/右侧/底部/左侧值不超过父值,但这里我们有 两个物体旋转相同度数的情况。

当用户将照片上传到框架容器时,我有一个真实的场景。照片通常总是比框架容器大。用户可以在相框内移动照片,但不应允许他创建任何空白空间,照片应始终包含在相框内。

【问题讨论】:

  • 嘿亚当,你看到下面我的回答了吗?我相信这就是您正在寻找的。​​span>

标签: javascript canvas fabricjs


【解决方案1】:

我会使用纯画布(没有 fabricjs),从头开始做,这样你就可以很好地理解你面临的问题,然后如果你需要它,同样的逻辑应该很容易移植到任何图书馆。

你有一些规则:

  • 父对象是固定的,不能移动,
  • 子对象是可移动的。
  • 孩子总是比父母大。
  • 子对象始终受父对象的约束。

所以我的想法是获取所有四个角,这样在移动中我们可以使用这些坐标来确定它是否可以移动到新位置,代码应该很容易理解,但是问你有没有任何疑虑。

我正在使用光线投射算法:
https://github.com/substack/point-in-polygon/blob/master/index.js
有了这个,我们需要做的就是检查孩子的角落不在父母里面,父母在孩子里面,就是这样。

我不是 FabricJS 方面的专家,所以我的最佳表现可能并不多...
但下面是我尝试让您的代码运行的尝试。

<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>

该代码也在沙盒中:
https://codesandbox.io/s/force-contain-of-object-inside-another-object-fabric-js-dnvb5

不用担心对所有单击/按住/拖动结构进行编码,这确实很容易……

我正在尝试使用 FabricJS,并且画布有一个很好的属性
(canvas.stateful = true;)
这使我们能够跟踪我们去过的地方,如果我们超出界限,我们可以恢复该运动,还可以播放动画,向用户提供不允许该运动的视觉反馈。

这是另一个没有动画的版本:

<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>

这个算法也为其他形状打开了大门,这里是一个六边形版本:
https://raw.githack.com/heldersepu/hs-scripts/master/HTML/canvas_contained2.html

【讨论】:

  • 这就是我一直在寻找的东西,更新很棒,因为我现在可以理解图像不在正确的位置,我只需要了解如何强制元素不更改位置,如果它在里面,但我认为唯一的方法是以某种方式强制它的子元素角以适应父元素的角。
  • 是的stateful 是要走的路!看看我上次的更新
  • 这太棒了,它有效!我不需要动画,只是简单地强制孩子不超过父母的边界,所以我改变了你的代码,简单地将孩子的左上角值设置为父母的值。有没有办法理解我应该强制使用哪些值?前任。如果我从右向左拖动它应该强制右上角的值,但我怎么得到它? codesandbox.io/s/…。如果你能解决最后一个问题,你应该得到赏金,这将立即给你。
  • 我认为 inside 函数是完美的,也许一旦它返回 false 它应该调用另一个函数,该函数返回一个超出边界名称的数组,例如。 getExceededBoundaries 返回 ex。 ['top', 'left'] 所以我会知道将子元素的左上角设置为父元素的值,否则,如果它只返回顶部,那么我会知道将子元素的顶部值设置为父母。
  • 是的,如果您不想对其进行动画处理,我们将删除该块 e.target.animate 并设置左侧和顶部:e.target.left = e.target._stateProperties.left (顶部相同)是被移动对象的最后已知良好状态...我们不想将其设置为父级,技术上您可以,但这将是一个视觉跳跃,用户可能不明白为什么...我添加了一个示例sn-p
【解决方案2】:

你可以创建一个新的Class,里面有你的object,然后只用children中的parent来做所有的动作,像这样:

 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
       });

     }
 });

然后像往常一样将您的element 添加到您的canvas

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);

顺便说一句,您可以使用这样的方法来创建nested 元素、带背景的文本等。

Codesandbox DEMO

【讨论】:

  • 解决方案为两个元素设置相同的位置和角度,但在我的情况下,父对象是固定的,不能移动,就像我只能移动子对象,父对象永远不应该移动.
猜你喜欢
  • 2013-05-21
  • 2016-06-13
  • 1970-01-01
  • 1970-01-01
  • 2016-12-14
  • 2018-04-24
  • 1970-01-01
  • 2015-11-20
  • 2018-04-06
相关资源
最近更新 更多