javascriptfabricjs

Resize Fabric Rect without resizing Textbox


In this jsFiddle I have a Fabric group containing a Rect and a Textbox. I need to be able to scale the Rect without scaling the text, so I'm trying to ungroup when the group is selected, and group again when the selection is cleared. Also:

How to make the jsFiddle work?

NOTE

The text and the rectangle always move together, even before or after any selection.

var canvas = window._canvas = new fabric.Canvas('c');

var text = new fabric.Textbox("Some text", {
     width: 100,
     height: 22,
     fontSize: 12,
     editable: true
});

var rect = new fabric.Rect({
     width: 100,
     height: 22,
     fill: 'yellow'
});

var group = new fabric.Group([ rect, text ], {
  left: 30,
  top: 30
});

canvas.add(group);

group.on('selected', function (e){
   canvas.remove(group);
   canvas.add(rect);
   canvas.add(text);
   canvas.renderAll();
   canvas.setActiveObject(rect);

});

canvas.on('selection:cleared', function(e) {
    group = new fabric.Group([ rect, text ], {});
});

Solution

  • EDIT: the OP clarified some of the requirements, so the solution was edited accordingly.

    While grouping may seem like a good idea at first, if you think about it, the only group feature your text/rect combination needs is the ability to move together. Which means that creating a group and trying to disable all the unwanted features is actually harder than glueing Text to your Rect and only handling events that you care about.

    Fabric.js has a wonderful subclassing mechanism, which we'll use to extend fabric.Rect class.

    The code below is pretty much self-explanatory, I'll just note several key aspects:

    const canvas = new fabric.Canvas('c')
    
    fabric.RectWithText = fabric.util.createClass(fabric.Rect, {
        type: 'rectWithText',
        text: null,
        textOffsetLeft: 0,
        textOffsetTop: 0,
        _prevObjectStacking: null,
        _prevAngle: 0,
      
        recalcTextPosition: function () {
          const sin = Math.sin(fabric.util.degreesToRadians(this.angle))
          const cos = Math.cos(fabric.util.degreesToRadians(this.angle))
          const newTop = sin * this.textOffsetLeft + cos * this.textOffsetTop
          const newLeft = cos * this.textOffsetLeft - sin * this.textOffsetTop
          const rectLeftTop = this.getPointByOrigin('left', 'top')
          this.text.set('left', rectLeftTop.x + newLeft)
          this.text.set('top', rectLeftTop.y + newTop)
        },
        
        initialize: function (rectOptions, textOptions, text) {
          this.callSuper('initialize', rectOptions)
          this.text = new fabric.Textbox(text, {
            ...textOptions,
            selectable: false,
            evented: false,
          })
          this.textOffsetLeft = this.text.left - this.left
          this.textOffsetTop = this.text.top - this.top
          this.on('moving', () => {
            this.recalcTextPosition()
          })
          this.on('rotating', () => {
            this.text.rotate(this.text.angle + this.angle - this._prevAngle)
            this.recalcTextPosition()
            this._prevAngle = this.angle
          })
          this.on('scaling', (e) => {
            this.recalcTextPosition()
          })
          this.on('added', () => {
            this.canvas.add(this.text)
          })
          this.on('removed', () => {
            this.canvas.remove(this.text)
          })
          this.on('mousedown:before', () => {
            this._prevObjectStacking = this.canvas.preserveObjectStacking
            this.canvas.preserveObjectStacking = true
          })
          this.on('mousedblclick', () => {
            this.text.selectable = true
            this.text.evented = true
            this.canvas.setActiveObject(this.text)
            this.text.enterEditing()
            this.selectable = false
          })
          this.on('deselected', () => {
            this.canvas.preserveObjectStacking = this._prevObjectStacking
          })
          this.text.on('editing:exited', () => {
            this.text.selectable = false
            this.text.evented = false
            this.selectable = true
          })
        }
    })
    
    const rectOptions = {
      left: 10,
      top: 10,
      width: 200,
      height: 75,
      fill: 'rgba(30, 30, 30, 0.3)',
    }
    const textOptions = {
      left: 35,
      top: 30,
      width: 150,
      fill: 'white',
      shadow: new fabric.Shadow({
        color: 'rgba(34, 34, 100, 0.4)',
        blur: 2,
        offsetX: -2,
        offsetY: 2
      }),
      fontSize: 30,
    }
    const rectWithText = new fabric.RectWithText(rectOptions, textOptions, 'Some text')
    canvas.add(rectWithText)
    body {
      background: ivory;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.4.0/fabric.js"></script>
    <canvas id="c" width="300" height="200"></canvas>