OK, if I have the following shapes that are rotated and then selected you will see their bounding boxes:
I am trying to write some code to align objects with respect to each other. So I would like to get each object's "containing box".
I am aware of getBoundingRect
but, for the above shapes this gives me the following:
As such, these boxes are not that useful to me. Is there a standard method of getting what I would call the "containing boxes" for all shapes? For example, I would like to be able to have the following boxes returned to me:
So, for any given shape I would like to be able get the red bounding rectangle (with no rotation).
Obviously, I could write a routine for each possible shape within fabricJS but I would prefer not to reinvent the wheel! Any ideas?
Edit Here's an interactive snippet that shows the current bounding boxes (in red):
$(function ()
{
canvas = new fabric.Canvas('c');
canvas.add(new fabric.Triangle({
left: 50,
top: 50,
fill: '#FF0000',
width: 50,
height: 50,
angle : 30
}));
canvas.add(new fabric.Circle({
left: 250,
top: 50,
fill: '#00ff00',
radius: 50,
angle : 30
}));
canvas.add(new fabric.Polygon([
{x: 185, y: 0},
{x: 250, y: 100},
{x: 385, y: 170},
{x: 0, y: 245} ], {
left: 450,
top: 50,
fill: '#0000ff',
angle : 30
}));
canvas.on("after:render", function(opt)
{
canvas.contextContainer.strokeStyle = '#FF0000';
canvas.forEachObject(function(obj)
{
var bound = obj.getBoundingRect();
canvas.contextContainer.strokeRect(
bound.left + 0.5,
bound.top + 0.5,
bound.width,
bound.height
);
});
});
canvas.renderAll();
});
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.6/fabric.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="c" width="800" height="600"></canvas><br/>
I had the same issue and I found a workaround. I created a temporary SVG from the active object and placed it outside the viewport. Then I measured the real bounding box using the native getBBox() function.
UPDATE
Apparently, the solution above works only in Firefox (76), so I came up with a different solution. Since I couldn't find a properly working, native function to get the real bounding box of a shape, I decided to scan the pixels and retrieve the boundaries from there.
Fiddle: https://jsfiddle.net/divpusher/2m7c61gw/118/
How it works:
Demo below
// ---------------------------
// the magic
var tempCanv, ctx, w, h;
function getImageData(dataUrl) {
// we need to use a temp canvas to get imagedata
if (tempCanv == null) {
tempCanv = document.createElement('canvas');
tempCanv.style.border = '1px solid blue';
tempCanv.style.visibility = 'hidden';
ctx = tempCanv.getContext('2d');
document.body.appendChild(tempCanv);
}
return new Promise(function(resolve, reject) {
if (dataUrl == null) return reject();
var image = new Image();
image.addEventListener('load', function() {
w = image.width;
h = image.height;
tempCanv.width = w;
tempCanv.height = h;
ctx.drawImage(image, 0, 0, w, h);
var imageData = ctx.getImageData(0, 0, w, h).data.buffer;
resolve(imageData, false);
});
image.src = dataUrl;
});
}
function scanPixels(imageData) {
var data = new Uint32Array(imageData),
len = data.length,
x, y, y1, y2, x1 = w, x2 = 0;
// y1
for(y = 0; y < h; y++) {
for(x = 0; x < w; x++) {
if (data[y * w + x] & 0xff000000) {
y1 = y;
y = h;
break;
}
}
}
// y2
for(y = h - 1; y > y1; y--) {
for(x = 0; x < w; x++) {
if (data[y * w + x] & 0xff000000) {
y2 = y;
y = 0;
break;
}
}
}
// x1
for(y = y1; y < y2; y++) {
for(x = 0; x < w; x++) {
if (x < x1 && data[y * w + x] & 0xff000000) {
x1 = x;
break;
}
}
}
// x2
for(y = y1; y < y2; y++) {
for(x = w - 1; x > x1; x--) {
if (x > x2 && data[y * w + x] & 0xff000000) {
x2 = x;
break;
}
}
}
return {
x1: x1,
x2: x2,
y1: y1,
y2: y2
}
}
// ---------------------------
// align buttons
function alignLeft(){
var obj = canvas.getActiveObject();
obj.set('left', 0);
obj.setCoords();
canvas.renderAll();
}
function alignLeftbyBoundRect(){
var obj = canvas.getActiveObject();
var bound = obj.getBoundingRect();
obj.set('left', (obj.left - bound.left));
obj.setCoords();
canvas.renderAll();
}
function alignRealLeft(){
var obj = canvas.getActiveObject();
getImageData(obj.toDataURL())
.then(function(data) {
var bound = obj.getBoundingRect();
var realBound = scanPixels(data);
obj.set('left', (obj.left - bound.left - realBound.x1));
obj.setCoords();
canvas.renderAll();
});
}
// ---------------------------
// set up canvas
var canvas = new fabric.Canvas('c');
var path = new fabric.Path('M 0 0 L 150 50 L 120 150 z');
path.set({
left: 170,
top: 30,
fill: 'rgba(0, 128, 0, 0.5)',
stroke: '#000',
strokeWidth: 4,
strokeLineCap: 'square',
angle: 65
});
canvas.add(path);
canvas.setActiveObject(path);
var circle = new fabric.Circle({
left: 370,
top: 30,
radius: 45,
fill: 'blue',
scaleX: 1.5,
angle: 30
});
canvas.add(circle);
canvas.forEachObject(function(obj) {
var setCoords = obj.setCoords.bind(obj);
obj.on({
moving: setCoords,
scaling: setCoords,
rotating: setCoords
});
});
canvas.on('after:render', function() {
canvas.contextContainer.strokeStyle = 'red';
canvas.forEachObject(function(obj) {
getImageData(obj.toDataURL())
.then(function(data) {
var boundRect = obj.getBoundingRect();
var realBound = scanPixels(data);
canvas.contextContainer.strokeRect(
boundRect.left + realBound.x1,
boundRect.top + realBound.y1,
realBound.x2 - realBound.x1,
realBound.y2 - realBound.y1
);
});
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.3/fabric.min.js"></script>
<p> </p>
<button onclick="alignLeft()">align left (default)</button>
<button onclick="alignLeftbyBoundRect()">align left (by bounding rect)</button>
<button onclick="alignRealLeft()">align REAL left (by pixel)</button>
<p></p>
<canvas id="c" width="600" height="250" style="border: 1px solid rgb(204, 204, 204); touch-action: none; user-select: none;" class="lower-canvas"></canvas>
<p></p>