Question

I want to let users rotate objects on the Fabric.js powered canvas, but limit their rotation to 90 deg increments. Idea is that as they rotate and then stop, the object would "snap" into place at the closest angle.

This works okay by listening to "object:rotating", setting the closest angle, then calling object.setAngle(angleClosestTo90). However, once the object has been "snapped", the controls for scaling/rotating the image are off. You can still select them, but it's finicky - I guess the object is expected to be at it's last location, not the one recalculated via setAngle().

This JSFiddle demonstrates the issue:

var canvas = new fabric.Canvas("c");

var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 90,
  height: 90
});

canvas.add(rect);
canvas.renderAll();

var lastClosestAngle = 0,
    snapAfterRotate = false;

function calculateClosestAngle(targetObj) {
  // if angle > 360 then mod 360
  var currentAngle = targetObj.angle;
  var normalizedAngle = Math.abs(Math.round(Math.asin(Math.sin(currentAngle * Math.PI/360.0)) * 360.0/Math.PI));
  console.log("normalized: ", normalizedAngle);
  snapAfterRotate = true;
  if (normalizedAngle >= 45 && normalizedAngle < 135) {
    return 90;
  } else if (normalizedAngle >= 135 && normalizedAngle < 225) {
    return 180;
  } else if (normalizedAngle >= 225 && normalizedAngle < 315) {
    return 270;
  } else if ((normalizedAngle >= 315 && normalizedAngle <= 360) || (normalizedAngle >= 0 && normalizedAngle < 45)) {
    return 0;
  }
  return currentAngle;
}

canvas.on("object:rotating", function(rotEvtData) {
  var targetObj = rotEvtData.target;
  lastClosestAngle = calculateClosestAngle(targetObj);
});

canvas.on("object:modified", function(modEvtData) {
  // modified fires after object has been rotated 
  var modifiedObj = modEvtData.target;
  if (modifiedObj.angle && snapAfterRotate) {
      modifiedObj.setAngle(lastClosestAngle);
      snapAfterRotate = false;
      canvas.renderAll();
  }
})

Rotate the box, then let go of the mouse. The object will be selected, but try to rotate it again. The pointer is hard to find/unavailable.

If you click away from the object, then reselect (or just move the object after rotating), it is fine.

Was it helpful?

Solution

Run setCoords() after setAngle(). This will update the position of the controls. Tried it in your fiddle. Works fine.

   canvas.on("object:modified", function(modEvtData) {
     // modified fires after object has been rotated 
     var modifiedObj = modEvtData.target;
     if (modifiedObj.angle && snapAfterRotate) {
         modifiedObj.setAngle(lastClosestAngle).setCoords();
         snapAfterRotate = false;
         canvas.renderAll();
     }
   })

regards, Benick

OTHER TIPS

FabricJS now supports this natively through 'setAngle', along with 'snapThreshold' property.

https://github.com/kangax/fabric.js/pull/3383

Example:

canvas.add(new fabric.Circle({
  snapAngle: 90,
  snapThreshold: 45,
  left: 100, 
  top: 100, 
  radius:50,
  fill: '#9f9', 
  centeredRotation: true
}));

https://jsfiddle.net/2t2d1zj9/1/

enter image description here

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top