how to rotate line in fabricjs by clicking any point to select/drag?
I have a line with one end at a fixed point that can be rotated or stretched by the user.
jsfiddle here
It seems that in fabricJS the only way to select a line/object for rotation is the little selection box in the middle. Also, a line is narrow so it is hard to select. Typically one must drag a rectangular selection box across the line to select it, then grab the unlabeled rotation box.
I want to simplify this to: click anywhere on the line and drag to rotate.
ideas?
thx.
code snippet:
var canvas = new fabric.Canvas("c", {stateful: true});
var line1 = new fabric.Line([ 100, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
selectable: true,
evented: false,
minScaleLimit: 0.25,
lockRotation: false,
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true
});
javascript fabricjs
add a comment |
I have a line with one end at a fixed point that can be rotated or stretched by the user.
jsfiddle here
It seems that in fabricJS the only way to select a line/object for rotation is the little selection box in the middle. Also, a line is narrow so it is hard to select. Typically one must drag a rectangular selection box across the line to select it, then grab the unlabeled rotation box.
I want to simplify this to: click anywhere on the line and drag to rotate.
ideas?
thx.
code snippet:
var canvas = new fabric.Canvas("c", {stateful: true});
var line1 = new fabric.Line([ 100, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
selectable: true,
evented: false,
minScaleLimit: 0.25,
lockRotation: false,
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true
});
javascript fabricjs
What's the reason behind settingevented: false
?
– shkaper
Nov 20 '18 at 18:08
just copy/pasted from an example. didn't think it important....
– danda
Nov 21 '18 at 19:47
add a comment |
I have a line with one end at a fixed point that can be rotated or stretched by the user.
jsfiddle here
It seems that in fabricJS the only way to select a line/object for rotation is the little selection box in the middle. Also, a line is narrow so it is hard to select. Typically one must drag a rectangular selection box across the line to select it, then grab the unlabeled rotation box.
I want to simplify this to: click anywhere on the line and drag to rotate.
ideas?
thx.
code snippet:
var canvas = new fabric.Canvas("c", {stateful: true});
var line1 = new fabric.Line([ 100, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
selectable: true,
evented: false,
minScaleLimit: 0.25,
lockRotation: false,
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true
});
javascript fabricjs
I have a line with one end at a fixed point that can be rotated or stretched by the user.
jsfiddle here
It seems that in fabricJS the only way to select a line/object for rotation is the little selection box in the middle. Also, a line is narrow so it is hard to select. Typically one must drag a rectangular selection box across the line to select it, then grab the unlabeled rotation box.
I want to simplify this to: click anywhere on the line and drag to rotate.
ideas?
thx.
code snippet:
var canvas = new fabric.Canvas("c", {stateful: true});
var line1 = new fabric.Line([ 100, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
selectable: true,
evented: false,
minScaleLimit: 0.25,
lockRotation: false,
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true
});
javascript fabricjs
javascript fabricjs
asked Nov 19 '18 at 17:31
danda
133
133
What's the reason behind settingevented: false
?
– shkaper
Nov 20 '18 at 18:08
just copy/pasted from an example. didn't think it important....
– danda
Nov 21 '18 at 19:47
add a comment |
What's the reason behind settingevented: false
?
– shkaper
Nov 20 '18 at 18:08
just copy/pasted from an example. didn't think it important....
– danda
Nov 21 '18 at 19:47
What's the reason behind setting
evented: false
?– shkaper
Nov 20 '18 at 18:08
What's the reason behind setting
evented: false
?– shkaper
Nov 20 '18 at 18:08
just copy/pasted from an example. didn't think it important....
– danda
Nov 21 '18 at 19:47
just copy/pasted from an example. didn't think it important....
– danda
Nov 21 '18 at 19:47
add a comment |
1 Answer
1
active
oldest
votes
Here's one way to do what you need.
The idea is that on each scale
event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject()
, supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.
This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100]
as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.
Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line
class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale
handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options)
, where x1, y1
- fixed point, x2, y2
- draggable tip.
Lastly, we're changing some of the properties. E.g. evented: false
was the reason you couldn't select the line on click.
Below is the snippet with more comments, just in case.
const canvas = new fabric.Canvas("c", {stateful: true})
fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now
initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})
this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})
this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})
const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});
const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});
canvas.add(line1, line2)
// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})
// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>
Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
– danda
Dec 3 '18 at 14:05
@danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
– shkaper
Dec 3 '18 at 14:34
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53379861%2fhow-to-rotate-line-in-fabricjs-by-clicking-any-point-to-select-drag%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
Here's one way to do what you need.
The idea is that on each scale
event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject()
, supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.
This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100]
as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.
Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line
class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale
handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options)
, where x1, y1
- fixed point, x2, y2
- draggable tip.
Lastly, we're changing some of the properties. E.g. evented: false
was the reason you couldn't select the line on click.
Below is the snippet with more comments, just in case.
const canvas = new fabric.Canvas("c", {stateful: true})
fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now
initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})
this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})
this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})
const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});
const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});
canvas.add(line1, line2)
// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})
// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>
Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
– danda
Dec 3 '18 at 14:05
@danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
– shkaper
Dec 3 '18 at 14:34
add a comment |
Here's one way to do what you need.
The idea is that on each scale
event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject()
, supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.
This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100]
as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.
Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line
class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale
handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options)
, where x1, y1
- fixed point, x2, y2
- draggable tip.
Lastly, we're changing some of the properties. E.g. evented: false
was the reason you couldn't select the line on click.
Below is the snippet with more comments, just in case.
const canvas = new fabric.Canvas("c", {stateful: true})
fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now
initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})
this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})
this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})
const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});
const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});
canvas.add(line1, line2)
// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})
// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>
Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
– danda
Dec 3 '18 at 14:05
@danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
– shkaper
Dec 3 '18 at 14:34
add a comment |
Here's one way to do what you need.
The idea is that on each scale
event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject()
, supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.
This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100]
as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.
Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line
class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale
handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options)
, where x1, y1
- fixed point, x2, y2
- draggable tip.
Lastly, we're changing some of the properties. E.g. evented: false
was the reason you couldn't select the line on click.
Below is the snippet with more comments, just in case.
const canvas = new fabric.Canvas("c", {stateful: true})
fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now
initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})
this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})
this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})
const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});
const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});
canvas.add(line1, line2)
// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})
// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>
Here's one way to do what you need.
The idea is that on each scale
event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject()
, supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.
This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100]
as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.
Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line
class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale
handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options)
, where x1, y1
- fixed point, x2, y2
- draggable tip.
Lastly, we're changing some of the properties. E.g. evented: false
was the reason you couldn't select the line on click.
Below is the snippet with more comments, just in case.
const canvas = new fabric.Canvas("c", {stateful: true})
fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now
initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})
this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})
this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})
const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});
const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});
canvas.add(line1, line2)
// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})
// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>
const canvas = new fabric.Canvas("c", {stateful: true})
fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now
initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})
this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})
this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})
const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});
const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});
canvas.add(line1, line2)
// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})
// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>
const canvas = new fabric.Canvas("c", {stateful: true})
fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now
initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,
originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.
lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})
this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})
this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})
const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});
const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});
canvas.add(line1, line2)
// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})
// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>
answered Nov 22 '18 at 5:59
shkaper
1,042514
1,042514
Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
– danda
Dec 3 '18 at 14:05
@danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
– shkaper
Dec 3 '18 at 14:34
add a comment |
Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
– danda
Dec 3 '18 at 14:05
@danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
– shkaper
Dec 3 '18 at 14:34
Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
– danda
Dec 3 '18 at 14:05
Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
– danda
Dec 3 '18 at 14:05
@danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
– shkaper
Dec 3 '18 at 14:34
@danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
– shkaper
Dec 3 '18 at 14:34
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53379861%2fhow-to-rotate-line-in-fabricjs-by-clicking-any-point-to-select-drag%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
What's the reason behind setting
evented: false
?– shkaper
Nov 20 '18 at 18:08
just copy/pasted from an example. didn't think it important....
– danda
Nov 21 '18 at 19:47