How to rotate all objects of canvas at once using Fabric.js? - html

I am working on custom product designer which uses Fabric.js. I want to rotate all objects of canvas at once by pressing one button (rotate left, rotate right).
I have achieved this using this code :
stage.forEachObject(function(obj){
obj.setAngle(rotation).setCoords();
});
stage.renderAll();
But it has one bug that every element rotates with its own center point. I want that every element rotates with respect to whole canvas element.

Grouping and rotating the group did not work so well for me. Here is another solution based on this js fiddle.
rotateAllObjects (degrees) {
let canvasCenter = new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2) // center of canvas
let radians = fabric.util.degreesToRadians(degrees)
canvas.getObjects().forEach((obj) => {
let objectOrigin = new fabric.Point(obj.left, obj.top)
let new_loc = fabric.util.rotatePoint(objectOrigin, canvasCenter, radians)
obj.top = new_loc.y
obj.left = new_loc.x
obj.angle += degrees //rotate each object buy the same angle
obj.setCoords()
});
canvas.renderAll()
},

You could add all the objects to a group an then rotate the group. This way you can also set the center for rotation.

This is how it could be solved
function rotate(a) {
var group = new fabric.Group(canvas.getObjects());
//angle is var with scope out of this function,
//so you can use this function as rotate(90) and keep rotating
angle = (angle + a) % 360;
group.rotate(angle);
canvas.centerObject(group);
group.setCoords();
canvas.renderAll();
}

FabricJS rotate everything and maintain the relative position also.
You can download the files here - https://drive.google.com/file/d/1UV1nBdfBk6bg9SztyVoWyLJ4eEZJgZRf/view?usp=sharing

Related

Speech bubbles in Canvas/FabricJS?

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

change axis of rotation

I am taking help of sample code of Transform tutorial for rotation and position change. I am facing couple of problems.
I want to perform rotation on door and window. Currently axis of rotation pass through center. How can I change to rotate a object from axis passing through its side.
For position change > when I change position lets say for a window in any direction, it moves but it is visible on wall. I want it hide when window collide with wall.
1/ Here is a code snippet that illustrates how to rotate elements (fragments), for a more complete sample take a look at this article: Rotate Components Control for the Viewer
rotateFragments (model, fragIdsArray, axis, angle, center) {
var quaternion = new THREE.Quaternion()
quaternion.setFromAxisAngle(axis, angle)
fragIdsArray.forEach((fragId, idx) => {
var fragProxy = this.viewer.impl.getFragmentProxy(
model, fragId)
fragProxy.getAnimTransform()
var position = new THREE.Vector3(
fragProxy.position.x - center.x,
fragProxy.position.y - center.y,
fragProxy.position.z - center.z)
position.applyQuaternion(quaternion)
position.add(center)
fragProxy.position = position
fragProxy.quaternion.multiplyQuaternions(
quaternion, fragProxy.quaternion)
if (idx === 0) {
var euler = new THREE.Euler()
euler.setFromQuaternion(
fragProxy.quaternion, 0)
this.emit('rotate', {
dbIds: this.selection.dbIdArray,
fragIds: fragIdsArray,
rotation: euler,
model
})
}
fragProxy.updateAnimTransform()
})
}
2/ When you transform the geometry, you are just moving triangles around, there is no built-in logic that will hide components because they overlap, you will need to implement that yourself. You should be able to find Three.js code that computes if two meshes intersect (triangle-triangle intersection algorithm) and run that against the component you are moving and all walls that are around. Here is something that can put you on tracks: How to detect collision in three.js?
Hope that helps

HTML5 Canvas (SIMPLE PAINT APPLICATION)

I'm working on a paint application and I'm trying to centre the canvas onto the middle of the screen . Any attempts I made the detection was off(still at the top left of the screen) but it was visually appearing in the centre of the screen.
Basically it wont draw onto the canvas when I moved it to the centre of the screen.
Any help would be much appreciated , Thanks....
I HAVE MY CODE BELOW ...
It's not clear from your question how you're centring it, but you need to account for any offset of elements which contain your canvas when you attempt to map the mouse position to a position on the canvas. You can do this by including the offsetLeft and offsetTop values (see docs) of the containing element when you do your calculations.
The following will work if you're offsetting the position of the div which wraps the canvas (which I've given an id to make it easier to reference):
function move(e) {
// Get the div containing the canvas. Would be more efficient to set this once on document load
var container = document.getElementById('container');
if((e.button == 0) && (mouseIsDown)) {
g.beginPath();
document.onselectstart = function(){ return false; }
//g.fillStyle = "red";
// Account for the offset of the parent when drawing to the canvas
g.arc((e.x - container.offsetLeft) - brush, (e.y - container.offsetTop) - brush, brush, 0, Math.PI * 2);
g.fill();
g.closePath();
}
}
And a simplistic demo using your fiddle here.

Zoom to and from point

I'm trying to zoom a DisplayObject into a certain point. I figured it would be easy, but I've spent a day now trying to figure it out.
Basically, I think this should work. Emphasis on should.
//newPoint is the point being centered. There is no initial scaling, so I do not need to compensate for that (yet)
//scale is the zoom level
//container is the parent of the obj
//obj is the object being scaled/panned
var p:Point = new Point(
( this.container.width - this.obj.width * scale + newPoint.x * scale ) / 2,
( this.container.height - this.obj.height * scale + newPoint.y * scale ) / 2
);
this.obj.scaleX = this.obj.scaleY = scale;
this.obj.x = p.x;
this.obj.y = p.y;
It centers the point if scale is 1, but it gets further and further away from center as you increase the scale. I've tried dozens of different methods. This method, which I have seen on several sites, produced the same exact results. Anyone have any idea how to get this to work?
EDIT 10-1-12:
As a followup, I took the code snippet that LondonDrugs_MediaServices provided as a basis for my original issue. I needed to be able to zoom to a specific point at a specific scale relative to the unscaled image (think how Google Maps zooms to a specific location). To do this, I had to center my image on the point before running the translation code. I've posted the additional code below. For other uses (pinch to zoom, scrolling, and double click), I used the code provided by Vesper, which worked quite well.
//obj is the object being translated
//container is its parent
//x and y are the coordinates to be zoomed to, in untranslated scaling
//obj.scaleX and obj.scaleY are always identical in my class, so there is no need to account for that
//calculates current center point, with scaling
var center:Point = new Point( ( this.container.width - this.obj.width * this.obj.scaleX ) / 2, ( this.container.height - this.obj.height * this.obj.scaleX ) / 2 );
//calulcates the distance from center the point is, with scaling
var distanceFromCenter:Point = new Point( this.obj.width * this.obj.scaleX / 2 - x * this.obj.scaleX, this.obj.height * this.obj.scaleX / 2 - y * this.obj.scaleX );
//center the object on that specific point
this.obj.x = center.x + distanceFromCenter.x;
this.obj.y = center.y + distanceFromCenter.y;
var mat:Matrix=new Matrix();
mat.translate(-p.x,-p.y);
mat.scale(desiredScale,desiredScale);
mat.translate(p.x,p.y);
yourObject.transform.matrix=mat;
The core point is that scaling is done around (0,0), but you can do it with matrix that describes affine transformations. You first make an empty matrix (that is, a matrix that doesn't transform), then apply a set of transformations to it. First, place a desired point at (0,0) by translating by -1*coordinates, then scale, then translate back.
hie guys....
thank's your comments...
i found the answer...
code :
gambar.addEventListener(TransformGestureEvent.GESTURE_ZOOM , onZoom);
function onZoom(event:TransformGestureEvent):void {
var locX:Number=event.localX;
var locY:Number=event.localY;
var stX:Number=event.stageX;
var stY:Number=event.stageY;
var prevScaleX:Number=gambar.scaleX;
var prevScaleY:Number=gambar.scaleY;
var mat:Matrix;
var externalPoint=new Point(stX,stY);
var internalPoint=new Point(locX,locY);
gambar.scaleX *= event.scaleX;
gambar.scaleY *= event.scaleY;
if(event.scaleX>1 && gambar.scaleX>6){
gambar.scaleX=prevScaleX;
gambar.scaleY=prevScaleY;
}
if(event.scaleY>1 && gambar.scaleY>6){
gambar.scaleX=prevScaleX;
gambar.scaleY=prevScaleY;
}
if(event.scaleX<1 && gambar.scaleX<0.8){
gambar.scaleX=prevScaleX;
gambar.scaleY=prevScaleY;
}
if(event.scaleY<1 && gambar.scaleY<0.8){
gambar.scaleX=prevScaleX;
gambar.scaleY=prevScaleY;
}
mat=gambar.transform.matrix.clone();
MatrixTransformer.matchInternalPointWithExternal(mat,internalPoint,externalPoint);
gambar.transform.matrix=mat;
}
The matrix answer is absolutely correct, but if you happen to be a Club GreenSock member you can get some nice functionality with very simple code with the TransformAroundPointPlugin
http://www.greensock.com/as/docs/tween/com/greensock/plugins/TransformAroundPointPlugin.html
You can see an example in the plugin explorer here:
http://www.greensock.com/tweenlite/#plugins
I use this to tween all my zooms and have much better performance than when I tried to do this manually. IMO the whole library is worth it's weight in gold (and no I have no connection other than liking the library). If you need any of the other features I'd look into it. It also has the ThrowProps plugin ( http://www.greensock.com/throwprops/ )which is very important if you are going to have a bounding box on mobile that you want to have a smooth return into the boundaries.
Set obj.x to -p.x and obj.y to -p.y, set the container scaleX and scaleY to the desired value and add p.x to the container x and p.y to the container y. Done!

canvas isPointInPath does not work with ctx.drawImage()

I suppose this doesn't work because canvas is drawing a bitmap of a vector (and a bitmap is not a path).
Even if it did work, the bitmap is likely always has a rectangular permitter.
Is there any way to leverage something like isPointInPath when using drawImage?
example:
The top canvas is drawn using drawImage and isPointInPath does not work.
The bottom canvas is drawn using arc and isPointInPath works.
a link to my proof
** EDIT **
I draw a circle on one canvas, and use isPointInPath to see if the mouse pointer is inside the circle (bottom canvas in my example).
I also "copy" the bottom canvas to the top canvas using drawImage. Notice that isPointInPath will not work on the top canvas (most likely due to reasons I mentioned above). Is there a work-around I can use for this that will work for ANY kind of path (or bitmap)?
A canvas context has this hidden thing called the current path. ctx.beginPath, ctx.lineTo etc create this path.
When you call ctx.stroke() or ctx.fill() the canvas strokes or fills that path.
Even after it is stroked or filled, the path is still present in the context.
This path is the only thing that isPointInPath tests.
If you want to test if something is in an image you have drawn or a rectangle that was drawn with ctx.fillRect(), that is not possible using built in methods.
Typically you'd want to use a is-point-in-rectangle function that you write yourself (or get from someone else).
If you're looking for how to do pixel-perfect (instead of just the image rectangle) hit detection for an image there are various methods of doing that discussed here: Pixel perfect 2D mouse picking with Canvas
You could try reimplementing ctx.drawImage() to always draw a box behind the image itself, like so (JSFiddle example):
ctx.customDrawImage = function(image, x, y){
this.drawImage(image, x, y);
this.rect(x, y, image.width, image.height);
}
var img1 = new Image();
img1.onload = function(){
var x = y = 0;
ctx.drawImage(img1, x, y);
console.log(ctx.isPointInPath(x + 1, y + 1));
x = 1.25 * img1.width;
ctx.customDrawImage(img1, x, y);
console.log(ctx.isPointInPath(x + 1, y + 1));
Note: you might get side effects like the rectangle appearing over the image, or bleeding through from behind if you are not careful.
To me, isPointInPath failed after canvas was moved. So, I used:
mouseClientX -= gCanvasElement.offsetLeft;
mouseclientY -= gCanvasElement.offsetTop;
I had some more challenges, because my canvas element could be rescaled. So first when I draw the figures, in my case arc, I save them in an array together with a name and draw them:
if (this.coInit == false)
{
let co = new TempCO ();
co.name= sensor.Name;
co.path = new Path2D();
co.path.arc(c.X, c.Y, this.radius, 0, 2 * Math.PI);
this.coWithPath.push(co);
}
let coWP = this.coWithPath.find(c=>c.name == sensor.Name);
this.ctx.fillStyle = color;
this.ctx.fill(coWP.path);
Then in the mouse event, I loop over the items and check if the click event is in a path. But I also need to rescale the mouse coordinates according to the resized canvas:
getCursorPosition(event) {
const rect = this.ctx.canvas.getBoundingClientRect();
const x = ((event.clientX - rect.left ) / rect.width) * this.canvasWidth;
const y = ((event.clientY - rect.top) / rect.height) * this.canvasHeight;
this.coWithPath.forEach(c=>{
if (this.ctx.isPointInPath(c.path, x, y))
{
console.log("arc is hit", c);
//Switch light
}
});
}
So I get the current size of the canvas and rescale the point to the original size. Now it works!
This is how the TempCO looks like:
export class TempCO
{
path : Path2D;
name : string;
}