Speech bubbles in Canvas/FabricJS?

Viewed 719

Looking for a way to make a regular speech bubble in my website's FabricJS canvas. Now before you flag this post, I did see this question, it just has no proper answers and is designed for WordPress so it's not particularly of any use to me.

What I'm wanting is pretty clear: A speech bubble with text in it and a tail/handle that you can drag to point it to something.

I've found this library but I can't seem to get it to show up in my FabricJS canvas? If you could either explain to me how to add this library into my canvas or provide another way of making speech bubbles, that would be sublime.

2 Answers

I dug a bit into Fabric.js and managed to create a procedual speech bubble, but I'm not able to quickly convert it into a Fabric.js class (which would make sense if you want to have multiple speech bubbles on your canvas). Maybe it's still helpful for you or someone else https://codepen.io/timohausmann/pen/poywXzg

It basically creates a Textbox and based on the bounding box of the text updates the position of the Rect around it.

var bound = textbox.getBoundingRect();
rect.left = bound.left - boxPadding;
rect.top = bound.top - boxPadding;
rect.width = bound.width + (boxPadding*2);
rect.height = bound.height + (boxPadding*2);

For the tail I simply created a transparent Rect that you can drag around and use its coordinates to draw a polygon with three points between the "handle" and the textbox center [A]. To make sure the tail maintains a certain width no matter the position, I calculate the degree between handle and the speech bubble center [B]. To keep the position of textbox and handle in sync, I calculate how much textbox moved and simply add the difference to the handles position [C].

//calculate degree between textbox and handle [B]
var angleRadians = Math.atan2(handle.top - textbox.top, 
                              handle.left - textbox.left);
var offsetX = Math.cos(angleRadians + (Math.PI/2));
var offsetY = Math.sin(angleRadians + (Math.PI/2));

//update the polygon [A]
poly.points[0].x = handle.left;
poly.points[0].y = handle.top;
poly.points[1].x = textbox.left - (offsetX * arrowWidth);
poly.points[1].y = textbox.top - (offsetY * arrowWidth); 
poly.points[2].x = textbox.left + (offsetX * arrowWidth);
poly.points[2].y = textbox.top + (offsetY * arrowWidth);

//update the handle when the textbox moved [C]
if(textbox.left !== textbox.lastLeft || 
   textbox.top !== textbox.lastTop) {
  handle.left += (textbox.left - textbox.lastLeft);
  handle.top += (textbox.top - textbox.lastTop);
  handle.setCoords();
}

Disclaimer: I'm not a Fabric.js expert, maybe there are a few shortcuts possible with the library.

The answer by @Til Hausmann works nicely (thanks!). I run into some problems when I tried to store and load the canvas data (via canvas.toJSON and canvas.loadFromJSON, resp.), though.

After some fiddling around, this could be resolved by

  • storing lastLeft and lastTop for both polygons in the updateBubble() method:
poly.lastLeft = Math.min(handle.left, textBox.left);
poly.lastTop = Math.min(handle.top, textBox.top);
  • setting the left / top properties for the polygons after the data were loaded:
canvas.loadFromJSON(jsonData, () => {
   const poly = // ...
   const poly2 = // ...
    
   poly.left = poly.lastLeft;
   poly.top = poly.lastTop;

    poly2.left = poly2.lastLeft;
    poly2.top = poly2.lastTop;

    // ...

    // Important:
    canvas.renderAll();
});
  • passing the full set of shape properites to canvas.toJSON()
canvas.toJSON(
   ['lastLeft', 'lastTop'].concat(
    Object.keys(handleProperties),
    Object.keys(polyProperties),
    Object.keys(poly2Properties),
    Object.keys(textRectProperties)
))

I was surprised that step (3) is actually necessary, but it didn't work without it...

Related