Draw an arrow that is always pointing away from an object - javascript

I have a canvas with a circle and a triangle with a line connecting them both. I'm attempting to use the triangle as an arrow. I'd like for the arrow to always point away from the center of the circle if either object is moved. I thought I could use the angle of the line to rotate the triangle but the results are unsatisfactory and I'm certain my approach is wrong.
Any suggestions or tips for solving this problem? In my linked JSFiddle you can move either object around and the angle of the triangle is rotated, but it's never quite right:
https://jsfiddle.net/zh1bkfs5/
const c = new fabric.Canvas('c');
c.preserveObjectStacking = true;
function radiansToDegrees (radians) {
return radians * 180 / Math.PI;
}
function calcAngle (opposite, adjacent) {
return radiansToDegrees(Math.atan(opposite / adjacent));
}
function syncObjects (e) {
const x1 = triangle.left;
const y1 = triangle.top;
const x2 = circle.left + circle.radius;
const y2 = circle.top + circle.radius;
line.set('x1', x1);
line.set('y1', y1);
line.set('x2', x2);
line.set('y2', y2);
const angle = calcAngle(y2 - y1, x1 - x2);
triangle.set('angle', -angle);
triangle.setCoords();
c.renderAll();
}
const circle = new fabric.Circle({
fill: 'black',
radius: 20,
top: 100,
left: 100,
hasBorders: false,
hasControls: false
});
const triangle = new fabric.Triangle({
fill: 'orange',
height: 20,
width: 20,
top: 50,
left: 120,
hasBorders: false,
hasControls: false,
originX: 'center',
originY: 'center'
});
const coords = [
triangle.left,
triangle.top,
circle.left + circle.radius,
circle.top + circle.radius
];
const line = new fabric.Line(coords, {
stroke: 'orange',
strokeWidth: 1,
selectable: false
});
c.add(circle, line, triangle);
c.on('object:moving', syncObjects);

Change the part of your syncObject function to this:
const x = x1 - x2;
const y = y2 - y1;
const angle = calcAngle(x, y);
if(x < 0 && y > 0 || x > 0 && y > 0) {
triangle.set('angle', angle);
} else if(x < 0 && y < 0) {
triangle.set('angle', 180 + angle);
} if(x > 0 && y < 0) {
triangle.set('angle', -180 + angle);
}
this should work. Your updated fiddle https://jsfiddle.net/zh1bkfs5/30/

I had my the values for "adjacent" and "opposite" flipped, after updating that I no longer needed to invert the angle. I'm now 99% there, just have a case where the triangle is sometimes flipped the wrong way but should be easy to fix at least my initial approach worked.
Update: https://jsfiddle.net/zh1bkfs5/1/
const c = new fabric.Canvas('c');
c.preserveObjectStacking = true;
function radiansToDegrees (radians) {
return radians * 180 / Math.PI;
}
function calcAngle (opposite, adjacent) {
return radiansToDegrees(Math.atan(opposite / adjacent));
}
function syncObjects (e) {
const x1 = triangle.left;
const y1 = triangle.top;
const x2 = circle.left + circle.radius;
const y2 = circle.top + circle.radius;
line.set('x1', x1);
line.set('y1', y1);
line.set('x2', x2);
line.set('y2', y2);
const angle = calcAngle(x1 - x2, y2 - y1);
triangle.set('angle', angle);
triangle.setCoords();
c.renderAll();
}
const circle = new fabric.Circle({
fill: 'black',
radius: 20,
top: 100,
left: 100,
hasBorders: false,
hasControls: false
});
const triangle = new fabric.Triangle({
fill: 'orange',
height: 20,
width: 20,
top: 50,
left: 120,
hasBorders: false,
hasControls: false,
originX: 'center',
originY: 'center'
});
const coords = [
triangle.left,
triangle.top,
circle.left + circle.radius,
circle.top + circle.radius
];
const line = new fabric.Line(coords, {
stroke: 'orange',
strokeWidth: 1,
selectable: false
});
c.add(circle, line, triangle);
c.on('object:moving', syncObjects);

Related

Standard method of getting containing box?

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/>
So the getBoundingBox is a method of the Object class of fabricjs.
Nothing stops you from rewriting this method for each shape of which you can think of.
I ll start with circle and triangle, i'll let you imagine polygon. It gets harder and harder when shapes are paths or when circle is scaled as an ellipse.
Circle is the hardest.
I sampled the circle at 30, 60, 90 degrees for all the quadrants. still is not perfect. You may need to increase sampling or find a better formula (maybe sample every 15 degrees will make the trick ).
Triangle is the easier since it has 3 points of interest.
Polygon is derived from triangle, nothing difficult here.
fabric.Circle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:0}, {x:this.width/2, y:0}, {x:0, y: -this.height/2}, {x: 0, y: this.height/2}, {x: 0, y: -this.height/2}, {x: 0.433 * this.width, y: this.height/4}, {x: -0.433 * this.width, y: this.height/4}, {y: 0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: -this.width/4}, {y: 0.433 * this.height, x: -this.width/4}, {x: 0.433 * this.width, y: -this.height/4}, {x: -0.433 * this.width, y: -this.height/4}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Triangle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:this.height/2}, {x:this.width/2, y:this.height/2}, {x:0, y: -this.height/2}, {x: 0, y: 0}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Polygon.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = this.points;
var offsetX = this.pathOffset.x;
var offsetY = this.pathOffset.y;
points = points.map(function(p) {
return fabric.util.transformPoint({x: p.x - offsetX , y: p.y -
offsetY}, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
$(function ()
{
fabric.util.makeBoundingBoxFromPoints = function(points) {
var minX = fabric.util.array.min(points, 'x'),
maxX = fabric.util.array.max(points, 'x'),
width = Math.abs(minX - maxX),
minY = fabric.util.array.min(points, 'y'),
maxY = fabric.util.array.max(points, 'y'),
height = Math.abs(minY - maxY);
return {
left: minX,
top: minY,
width: width,
height: height
};
};
fabric.Circle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:0}, {x:this.width/2, y:0}, {x:0, y: -this.height/2}, {x: 0, y: this.height/2}, {x: 0, y: -this.height/2}, {x: 0.433 * this.width, y: this.height/4}, {x: -0.433 * this.width, y: this.height/4}, {y: 0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: -this.width/4}, {y: 0.433 * this.height, x: -this.width/4}, {x: 0.433 * this.width, y: -this.height/4}, {x: -0.433 * this.width, y: -this.height/4}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Triangle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:this.height/2}, {x:this.width/2, y:this.height/2}, {x:0, y: -this.height/2}, {x: 0, y: 0}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Polygon.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = this.points;
var offsetX = this.pathOffset.x;
var offsetY = this.pathOffset.y;
points = points.map(function(p) {
return fabric.util.transformPoint({x: p.x - offsetX , y: p.y -
offsetY}, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
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();
if(bound)
{
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:
export the selected fabric object toDataURL()
place it in a hidden canvas and get the pixels with getImageData()
scan the pixels to retrieve x1, x2, y1, y2 edge coords of the shape
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>

Fabric.js - wrong group position

I would like add arrow to my canvas - user click on canvas, fabric draw line + triangle. Then, when user move mouse line is "moving" and arrow angle is changed. "Drawing mode" is end when mouse up. This works great, but positions and selection area are wrong.
I write that for mouse:down:
// Draw new arrow using move
canvas.fabric.on('mouse:down', function(options){
$scope.lineOptions = {
stroke: '#000',
selectable: true,
strokeWidth: '2',
padding: 5,
hasBorders: false,
hasControls: false,
//originX: 'top',
//originY: 'left',
lockScalingX: true,
lockScalingY: true
};
var zoomFactor = Math.pow(canvas.currentZoom, -1);
var x = (options.e.clientX - canvas.fabric._offset.left) * zoomFactor;
var y = (options.e.clientY - canvas.fabric._offset.top) * zoomFactor;
var points = [ 0, 0, 0, 0 ];
temp_line = new fabric.Line(points, $scope.lineOptions);
temp_arrow = new fabric.Triangle({
left: x,
top: y,
angle: -45,
originX: 'center',
originY: 'center',
width: 20,
height: 20,
fill: '#000'
});
arrowShape = new fabric.Group([ temp_line, temp_arrow ], {
left: x,
top: y
});
canvas.fabric.add(arrowShape);
});
Zoom factor is here negligible.
There is code for mouse:move:
canvas.fabric.on('mouse:move', function(o){
var pointer = canvas.fabric.getPointer(o.e);
arrowShape.item(0).set({ x2: pointer.x, y2: pointer.y });
arrowShape.item(1).set({ left: pointer.x, top: pointer.y });
x1 = arrowShape.item(0).get('x1');
y1 = arrowShape.item(0).get('y1');
x2 = arrowShape.item(0).get('x2');
y2 = arrowShape.item(0).get('y2');
angle = calcArrowAngle(x1, y1, x2, y2);
arrowShape.item(1).set('angle', angle + 90);
//arrowShape.item(0).setCoords();
//arrowShape.item(1).setCoords();
//arrowShape.setCoords();
canvas.fabric.renderAll();
});
function calcArrowAngle(x1, y1, x2, y2) {
var angle = 0,
x, y;
x = (x2 - x1);
y = (y2 - y1);
if (x === 0) {
angle = (y === 0) ? 0 : (y > 0) ? Math.PI / 2 : Math.PI * 3 / 2;
} else if (y === 0) {
angle = (x > 0) ? 0 : Math.PI;
} else {
angle = (x < 0) ? Math.atan(y / x) + Math.PI : (y < 0) ? Math.atan(y / x) + (2 * Math.PI) : Math.atan(y / x);
}
return (angle * 180 / Math.PI);
}
and for mouse:up:
canvas.fabric.on('mouse:up', function(o){
canvas.fabric.setActiveObject(arrowShape);
// My drawing mode
$scope.drawMode = false;
});
Adding works great, change direction works great, change angle works great... but group selection and line start point is wrong:
OriginX/originY are default, so they should be top/left.. but the aren't. Also, group selection area are wrong, too small for both shapes. I forgot about something... but about what?

fabric.js arrow snapped to grid

I've adapted some code to snap a line to a grid using Fabric.js, as in the code below. What I want is for it to instead be an arrow. I can't seem to get the arrowhead (triangle) to move properly. I kept breaking the code by trying to add a triangle as the arrowhead, so I removed all code attempting to add it. Please help me add the arrowhead, thank you!!
var canvas = new fabric.Canvas('c', { selection: false });
var grid = 50;
for (var i = 0; i < (600 / grid); i++) {
canvas.add(new fabric.Line([ i * grid, 0, i * grid, 600], { stroke: '#ccc', selectable: false }));
canvas.add(new fabric.Line([ 0, i * grid, 600, i * grid], { stroke: '#ccc', selectable: false }))
}
var line, isDown;
canvas.on('mouse:down', function(o){
canvas.remove(line);
isDown = true;
var pointer = canvas.getPointer(o.e);
var points = [ Math.round(pointer.x / grid) * grid, Math.round(pointer.y / grid) * grid, pointer.x, pointer.y ];
line = new fabric.Line(points, {
strokeWidth: 5,
fill: 'red',
stroke: 'red',
originX: 'center',
originY: 'center'
});
canvas.add(line);
});
canvas.on('mouse:move', function(o){
if (!isDown) return;
var pointer = canvas.getPointer(o.e);
line.set({ x2: pointer.x, y2: pointer.y });
canvas.renderAll();
});
canvas.on('mouse:up', function(o){
var pointer = canvas.getPointer(o.e);
isDown = false;
line.set({ x2: Math.round(pointer.x/ grid) * grid, y2: Math.round(pointer.y/ grid) * grid });
canvas.renderAll();
});
<script src="https://rawgithub.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id="c" width="500" height="500" style="border:1px solid #ccc"></canvas>
Got 'em! For me, the trick was defining the center of the triangle following the method in this fiddle: http://jsfiddle.net/ug2gskj1/
left: line.get('x1') + deltaX;
top: line.get('y1') + deltaY;
where
centerX = (line.x1 + line.x2) / 2;
centerY = (line.y1 + line.y2) / 2;
deltaX = line.left - centerX;
deltaY = line.top - centerY;
window.onload = function() {
var canvas = new fabric.Canvas('c', {
selection: false
});
var grid = 50;
for (var i = 0; i < (600 / grid); i++) {
canvas.add(new fabric.Line([i * grid, 0, i * grid, 600], {
stroke: '#ccc',
selectable: false
}));
canvas.add(new fabric.Line([0, i * grid, 600, i * grid], {
stroke: '#ccc',
selectable: false
}))
}
var line, triangle, isDown;
function calcArrowAngle(x1, y1, x2, y2) {
var angle = 0,
x, y;
x = (x2 - x1);
y = (y2 - y1);
if (x === 0) {
angle = (y === 0) ? 0 : (y > 0) ? Math.PI / 2 : Math.PI * 3 / 2;
} else if (y === 0) {
angle = (x > 0) ? 0 : Math.PI;
} else {
angle = (x < 0) ? Math.atan(y / x) + Math.PI : (y < 0) ? Math.atan(y / x) + (2 * Math.PI) : Math.atan(y / x);
}
return (angle * 180 / Math.PI + 90);
}
canvas.on('mouse:down', function(o) {
canvas.remove(line, triangle);
isDown = true;
var pointer = canvas.getPointer(o.e);
var points = [Math.round(pointer.x / grid) * grid, Math.round(pointer.y / grid) * grid, pointer.x, pointer.y];
line = new fabric.Line(points, {
strokeWidth: 5,
fill: 'red',
stroke: 'red',
originX: 'center',
originY: 'center'
});
centerX = (line.x1 + line.x2) / 2;
centerY = (line.y1 + line.y2) / 2;
deltaX = line.left - centerX;
deltaY = line.top - centerY;
triangle = new fabric.Triangle({
left: line.get('x1') + deltaX,
top: line.get('y1') + deltaY,
originX: 'center',
originY: 'center',
hasBorders: false,
hasControls: false,
lockScalingX: true,
lockScalingY: true,
lockRotation: true,
pointType: 'arrow_start',
angle: -45,
width: 20,
height: 20,
fill: 'red'
});
canvas.add(line, triangle);
});
canvas.on('mouse:move', function(o) {
//function angle(x1,y1,x2,y2){angle=Math.atan((y2-y1)/(x2-x1))*180/Math.PI+90; return angle;}
if (!isDown) return;
var pointer = canvas.getPointer(o.e);
line.set({
x2: pointer.x,
y2: pointer.y
});
triangle.set({
'left': pointer.x + deltaX,
'top': pointer.y + deltaY,
'angle': calcArrowAngle(line.x1, line.y1, line.x2, line.y2)
});
canvas.renderAll();
});
canvas.on('mouse:up', function(o) {
var pointer = canvas.getPointer(o.e);
isDown = false;
snappedxCoordinate = Math.round(pointer.x / grid) * grid;
snappedyCoordinate = Math.round(pointer.y / grid) * grid;
snappedxCoordinateArrowhead = Math.round((pointer.x + deltaX) / grid) * grid;
snappedyCoordinateArrowhead = Math.round((pointer.y + deltaY) / grid) * grid;
line.set({
x2: snappedxCoordinate,
y2: snappedyCoordinate
});
triangle.set({
'left': snappedxCoordinateArrowhead,
'top': snappedyCoordinateArrowhead,
'angle': calcArrowAngle(line.x1, line.y1, line.x2, line.y2)
});
canvas.renderAll();
});
}
<script src="https://rawgithub.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id="c" width="500" height="500" style="border:1px solid #ccc"></canvas>

How to restrict resizing a circle/rectangle to some radius/width/height in fabricjs?

I am using Fabricjs and trying to restrict a rectangle so that it cannot be resized after it reaches a minimum radius in case of circle and minimum width/height in case of rectangle :
This is what I have done so far : JSFIDDLE .
Code :
`
var canvas = new fabric.Canvas("canvas2");
var circle, isDown, origX, origY;
circle = new fabric.Circle({
left: 20,
top: 30,
originX: 'left',
originY: 'top',
radius: 30,
angle: 0,
fill: '',
stroke:'red',
strokeWidth:3,
});
canvas.add(circle);
function checkscale(e) {
var obj = e.target;
obj.setCoords();
var b = obj.getBoundingRect();
if(obj.toJSON().objects[0].radius == undefined) {
//rectangle case
if(obj.width < 27) {
obj.width = 27;
}
if(obj.height < 27) {
obj.height = 27;
}
return;
} else {
//circle case
if(obj.width < 48) {
obj.width = 48;
}
if(obj.height < 48) {
obj.height = 48;
}
return;
}
if (!(b.left >= minX && maxX >= b.left + b.width && maxY >= b.top + b.height && b.top >= minY)) {
obj.left = obj.lastLeft;
obj.top = obj.lastTop;
obj.scaleX= obj.lastScaleX
obj.scaleY= obj.lastScaleY
} else {
obj.lastLeft = obj.left;
obj.lastTop = obj.top;
obj.lastScaleX = obj.scaleX
obj.lastScaleY = obj.scaleY
}
}
canvas.observe("object:scaling", checkscale);
`
Can anybody do this?
You do not need to mess up with radius, toJson and width and height.
You are scaling a fabricJs object, so scaleX and scaleY is all you need.
Consider upgrading to fabricJs 1.6
var canvas = new fabric.Canvas("canvas2");
var circle, isDown, origX, origY;
circle = new fabric.Circle({
left: 20,
top: 30,
originX: 'left',
originY: 'top',
radius: 30,
angle: 0,
fill: '',
stroke:'red',
strokeWidth:3,
lockScalingFlip: true
});
canvas.add(circle);
function checkscale(e) {
var obj = e.target;
//var dim = obj._getTransformedDimensions();
if (obj.getWidth() < 50) {
obj.scaleX = obj.lastScaleX;
} else {
obj.lastScaleX = obj.scaleX;
}
if (obj.getHeight() < 50) {
obj.scaleY = obj.lastScaleY;
} else {
obj.lastScaleY = obj.scaleY;
}
}
canvas.observe("object:scaling", checkscale);
<script src="http://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js" ></script>
<canvas id="canvas2" width=500 height=500 style="height:500px;width:500px;"></canvas>

kineticjs performance lag

I am working on a radial control similar to the HTML5 wheel of fortune example. I've modified the original here with an example of some additional functionality I require: http://jsfiddle.net/fEm9P/ When you click on the inner kinetic wedges they will shrink and expand within the larger wedges. Unfortunately when I rotate the wheel it lags behind the pointer. It's not too bad here but it's really noticeable on a mobile.
I know this is due to the fact that I'm not caching the wheel. When I do cache the wheel (uncomment lines 239-249) the inner wedges no longer respond to mouse/touch but the response on rotation is perfect. I have also tried adding the inner wedges to a separate layer and caching the main wheel only. I then rotate the inner wheel with the outer one. Doing it this way is a little better but still not viable on mobile.
Any suggestions would be greatly appreciated.
Stephen
//constants
var MAX_ANGULAR_VELOCITY = 360 * 5;
var NUM_WEDGES = 25;
var WHEEL_RADIUS = 410;
var ANGULAR_FRICTION = 0.2;
// globals
var angularVelocity = 360;
var lastRotation = 0;
var controlled = false;
var target, activeWedge, stage, layer, wheel,
pointer, pointerTween, startRotation, startX, startY;
var currentVolume, action;
function purifyColor(color) {
var randIndex = Math.round(Math.random() * 3);
color[randIndex] = 0;
return color;
}
function getRandomColor() {
var r = 100 + Math.round(Math.random() * 55);
var g = 100 + Math.round(Math.random() * 55);
var b = 100 + Math.round(Math.random() * 55);
var color = [r, g, b];
color = purifyColor(color);
color = purifyColor(color);
return color;
}
function bind() {
wheel.on('mousedown', function(evt) {
var mousePos = stage.getPointerPosition();
angularVelocity = 0;
controlled = true;
target = evt.targetNode;
startRotation = this.rotation();
startX = mousePos.x;
startY = mousePos.y;
});
// add listeners to container
document.body.addEventListener('mouseup', function() {
controlled = false;
action = null;
if(angularVelocity > MAX_ANGULAR_VELOCITY) {
angularVelocity = MAX_ANGULAR_VELOCITY;
}
else if(angularVelocity < -1 * MAX_ANGULAR_VELOCITY) {
angularVelocity = -1 * MAX_ANGULAR_VELOCITY;
}
angularVelocities = [];
}, false);
document.body.addEventListener('mousemove', function(evt) {
var mousePos = stage.getPointerPosition();
var x1, y1;
if(action == 'increase') {
x1 = (mousePos.x-(stage.getWidth() / 2));
y1 = (mousePos.y-WHEEL_RADIUS+20);
var r = Math.sqrt(x1 * x1 + y1 * y1);
if (r>500){
r=500;
} else if (r<100){
r=100;
};
currentVolume.setRadius(r);
layer.draw();
} else {
if(controlled && mousePos && target) {
x1 = mousePos.x - wheel.x();
y1 = mousePos.y - wheel.y();
var x2 = startX - wheel.x();
var y2 = startY - wheel.y();
var angle1 = Math.atan(y1 / x1) * 180 / Math.PI;
var angle2 = Math.atan(y2 / x2) * 180 / Math.PI;
var angleDiff = angle2 - angle1;
if ((x1 < 0 && x2 >=0) || (x2 < 0 && x1 >=0)) {
angleDiff += 180;
}
wheel.setRotation(startRotation - angleDiff);
}
};
}, false);
}
function getRandomReward() {
var mainDigit = Math.round(Math.random() * 9);
return mainDigit + '\n0\n0';
}
function addWedge(n) {
var s = getRandomColor();
var reward = getRandomReward();
var r = s[0];
var g = s[1];
var b = s[2];
var angle = 360 / NUM_WEDGES;
var endColor = 'rgb(' + r + ',' + g + ',' + b + ')';
r += 100;
g += 100;
b += 100;
var startColor = 'rgb(' + r + ',' + g + ',' + b + ')';
var wedge = new Kinetic.Group({
rotation: n * 360 / NUM_WEDGES,
});
var wedgeBackground = new Kinetic.Wedge({
radius: WHEEL_RADIUS,
angle: angle,
fillRadialGradientStartRadius: 0,
fillRadialGradientEndRadius: WHEEL_RADIUS,
fillRadialGradientColorStops: [0, startColor, 1, endColor],
fill: '#64e9f8',
fillPriority: 'radial-gradient',
stroke: '#ccc',
strokeWidth: 2,
rotation: (90 + angle/2) * -1
});
wedge.add(wedgeBackground);
var text = new Kinetic.Text({
text: reward,
fontFamily: 'Calibri',
fontSize: 50,
fill: 'white',
align: 'center',
stroke: 'yellow',
strokeWidth: 1,
listening: false
});
text.offsetX(text.width()/2);
text.offsetY(WHEEL_RADIUS - 15);
wedge.add(text);
volume = createVolumeControl(angle, endColor);
wedge.add(volume);
wheel.add(wedge);
}
var activeWedge;
function createVolumeControl(angle, colour){
var volume = new Kinetic.Wedge({
radius: 100,
angle: angle,
fill: colour,
stroke: '#000000',
rotation: (90 + angle/2) * -1
});
volume.on("mousedown touchstart", function() {
currentVolume = this;
action='increase';
});
return volume;
}
function animate(frame) {
// wheel
var angularVelocityChange = angularVelocity * frame.timeDiff * (1 - ANGULAR_FRICTION) / 1000;
angularVelocity -= angularVelocityChange;
if(controlled) {
angularVelocity = ((wheel.getRotation() - lastRotation) * 1000 / frame.timeDiff);
}
else {
wheel.rotate(frame.timeDiff * angularVelocity / 1000);
}
lastRotation = wheel.getRotation();
// pointer
var intersectedWedge = layer.getIntersection({x: stage.width()/2, y: 50});
if (intersectedWedge && (!activeWedge || activeWedge._id !== intersectedWedge._id)) {
pointerTween.reset();
pointerTween.play();
activeWedge = intersectedWedge;
}
}
function init() {
stage = new Kinetic.Stage({
container: 'container',
width: 578,
height: 500
});
layer = new Kinetic.Layer();
wheel = new Kinetic.Group({
x: stage.getWidth() / 2,
y: WHEEL_RADIUS + 20
});
for(var n = 0; n < NUM_WEDGES; n++) {
addWedge(n);
}
pointer = new Kinetic.Wedge({
fillRadialGradientStartPoint: 0,
fillRadialGradientStartRadius: 0,
fillRadialGradientEndPoint: 0,
fillRadialGradientEndRadius: 30,
fillRadialGradientColorStops: [0, 'white', 1, 'red'],
stroke: 'white',
strokeWidth: 2,
lineJoin: 'round',
angle: 30,
radius: 30,
x: stage.getWidth() / 2,
y: 20,
rotation: -105,
shadowColor: 'black',
shadowOffset: {x:3,y:3},
shadowBlur: 2,
shadowOpacity: 0.5
});
// add components to the stage
layer.add(wheel);
layer.add(pointer);
stage.add(layer);
pointerTween = new Kinetic.Tween({
node: pointer,
duration: 0.1,
easing: Kinetic.Easings.EaseInOut,
y: 30
});
pointerTween.finish();
var radiusPlus2 = WHEEL_RADIUS + 2;
wheel.cache({
x: -1* radiusPlus2,
y: -1* radiusPlus2,
width: radiusPlus2 * 2,
height: radiusPlus2 * 2
}).offset({
x: radiusPlus2,
y: radiusPlus2
});
layer.draw();
// bind events
bind();
var anim = new Kinetic.Animation(animate, layer);
//document.getElementById('debug').appendChild(layer.hitCanvas._canvas);
// wait one second and then spin the wheel
setTimeout(function() {
anim.start();
}, 1000);
}
init();
I made a couple of changes to the script which greatly improved the response time. The first was replacing layer.draw() with layer.batchDraw(). As the draw function was being called on each touchmove event it was making the interaction clunky. BatchDraw on the other hand will stack up draw requests internally "limit the number of redraws per second based on the maximum number of frames per second" (http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-batch-draw).
The jumping around of the canvas I seeing originally when I cached/cleared the wheel was due to the fact that I wasn't resetting the offset on the wheel when I cleared the cache.
http://jsfiddle.net/leydar/a7tkA/5
wheel.clearCache().offset({
x: 0,
y: 0
});
I hope this is of benefit to someone else. It's still not perfectly responsive but it's at least going in the right direction.
Stephen

Categories

Resources