Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • The Rect and the Textbox are grouped to be able to move them together.
  • The text needs to be editable.
  • The text needs to be on top of the rect (i.e. should be always visible).

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 ], {});
});
like image 742
ps0604 Avatar asked Dec 18 '22 16:12

ps0604


1 Answers

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:

  • rectOptions and textOptions passed into the fabric.RectWithText constructor, are the objects you would normally pass into fabric.Rect and fabric.Textbox constructors, accordingly.
  • the Textbox is referenced by a text property of the RectWithText instance.
  • recalcTextPosition uses a couple of trigonometry formulas to calculate text's position in relation to the rectangle, given the initial offset between the two.
  • we need to keep an eye on the rectangle's moving, scaling, and rotating events to recalculate text's position smoothly.
  • mousedown:before, mousedblclick, editing:exited make sure the text stays editable on double click.
  • we're keeping the text on top by modifying canvas.preserveObjectStacking when the object is clicked on.

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>
like image 129
shkaper Avatar answered Jan 11 '23 23:01

shkaper