Related
I have two objects a parent (red) and a child (blue). The parent object is fixed and can't be moved, only the child object is movable and the child is always bigger than the parent. In whatever way the child object is moved it should always be contained inside the child, which means we should never see the red rectangle.
Demo: https://codesandbox.io/s/force-contain-of-object-inside-another-object-fabric-js-7nt7q
I know there are solutions to contain an object within the canvas or other object boundaries (ex. Move object within canvas boundary limit) which mainly force the top/right/bottom/left values to not exceed the parent values, but here we have the case of two rotated objects by the same degree.
I have a real-life scenario when a user uploads a photo to a frame container. The photo is normally always bigger than the frame container. The user can move the photo inside the frame, but he should not be allowed to create any empty spaces, the photo should always be contained inside the photo frame.
I would go with a pure canvas (no fabricjs), do it from scratch that way you understand well the problem you are facing, then if you need it that same logic should be easily portable to any library.
You have some rules:
The parent object is fixed and can't be moved,
The child object is movable.
The child is always bigger than the parent.
The child object is always constrained by the parent.
So my idea is to get all four corners, that way on the move we can use those coordinates to determine if it can be moved to the new location or not, the code should be easy to follow, but ask if you have any concerns.
I'm using the ray-casting algorithm:
https://github.com/substack/point-in-polygon/blob/master/index.js
With that, all we need to do is check that the corners of the child are not inside the parent and that the parent is inside the child, that is all.
I'm no expert with FabricJS so my best might not be much...
but below is my attempt to get your code going.
<canvas id="canvas" width="500" height="350"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<script>
var canvas = new fabric.Canvas("canvas");
canvas.stateful = true;
function getCoords(rect) {
var x = rect.left;
var y = rect.top;
var angle = (rect.angle * Math.PI) / 180;
var coords = [{ x, y }];
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.height * Math.cos(angle);
y += rect.height * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
return coords;
}
function inside(p, vs) {
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i].x, yi = vs[i].y;
var xj = vs[j].x, yj = vs[j].y;
var intersect =
yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
var parent = new fabric.Rect({
width: 150, height: 100, left: 200, top: 50, angle: 25, selectable: false, fill: "red"
});
var pCoords = getCoords(parent);
var child = new fabric.Rect({
width: 250, height: 175, left: 180, top: 10, angle: 25, hasControls: false, fill: "rgba(0,0,255,0.9)"
});
canvas.add(parent);
canvas.add(child);
canvas.on("object:moving", function (e) {
var cCoords = getCoords(e.target);
var inBounds = true;
cCoords.forEach(c => { if (inside(c, pCoords)) inBounds = false; });
pCoords.forEach(c => { if (!inside(c, cCoords)) inBounds = false; });
if (inBounds) {
e.target.setCoords();
e.target.saveState();
e.target.set("fill", "rgba(0,0,255,0.9)");
} else {
e.target.set("fill", "black");
e.target.animate({
left: e.target._stateProperties.left,
top: e.target._stateProperties.top
},{
duration: 500,
onChange: canvas.renderAll.bind(canvas),
easing: fabric.util.ease["easeInBounce"],
onComplete: function() {
e.target.set("fill", "rgba(0,0,255,0.9)");
}
});
}
});
</script>
That code is on sandbox as well:
https://codesandbox.io/s/force-contain-of-object-inside-another-object-fabric-js-dnvb5
It certainly is nice not to worry about coding all the click/hold/drag fabric makes that real easy...
I was experimenting with FabricJS and there a nice property of the canvas
(canvas.stateful = true;)
that allows us to keep track of where we've been, and if we go out of bounds we can revert that movement, also playing with animate that gives the user visual feedback that the movement is not allowed.
Here is another version without animation:
<canvas id="canvas" width="500" height="350"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<script>
var canvas = new fabric.Canvas("canvas");
canvas.stateful = true;
function getCoords(rect) {
var x = rect.left;
var y = rect.top;
var angle = (rect.angle * Math.PI) / 180;
var coords = [{ x, y }];
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.height * Math.cos(angle);
y += rect.height * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
return coords;
}
function inside(p, vs) {
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i].x, yi = vs[i].y;
var xj = vs[j].x, yj = vs[j].y;
var intersect =
yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
var parent = new fabric.Rect({
width: 150, height: 100, left: 200, top: 50, angle: 25, selectable: false, fill: "red"
});
var pCoords = getCoords(parent);
var child = new fabric.Rect({
width: 250, height: 175, left: 180, top: 10, angle: 25, hasControls: false, fill: "rgba(0,0,255,0.9)"
});
canvas.add(parent);
canvas.add(child);
canvas.on("object:moving", function (e) {
var cCoords = getCoords(e.target);
var inBounds = true;
cCoords.forEach(c => { if (inside(c, pCoords)) inBounds = false; });
pCoords.forEach(c => { if (!inside(c, cCoords)) inBounds = false; });
if (inBounds) {
e.target.setCoords();
e.target.saveState();
} else {
e.target.left = e.target._stateProperties.left;
e.target.top = e.target._stateProperties.top;
}
});
</script>
This algorithm also opens the door for other shapes as well, here is a hexagon version:
https://raw.githack.com/heldersepu/hs-scripts/master/HTML/canvas_contained2.html
you can just create the new Class, with your object inside, and do all actions only with parent of the children, something like this:
fabric.RectWithRect = fabric.util.createClass(fabric.Rect, {
type: 'rectWithRect',
textOffsetLeft: 0,
textOffsetTop: 0,
_prevObjectStacking: null,
_prevAngle: 0,
minWidth: 50,
minHeight: 50,
_currentScaleFactorX: 1,
_currentScaleFactorY: 1,
_lastLeft: 0,
_lastTop: 0,
recalcTextPosition: function () {
//this.insideRect.setCoords();
const sin = Math.sin(fabric.util.degreesToRadians(this.angle))
const cos = Math.cos(fabric.util.degreesToRadians(this.angle))
const newTop = sin * this.insideRectOffsetLeft + cos * this.insideRectOffsetTop
const newLeft = cos * this.insideRectOffsetLeft - sin * this.insideRectOffsetTop
const rectLeftTop = this.getPointByOrigin('left', 'top')
this.insideRect.set('left', rectLeftTop.x + newLeft)
this.insideRect.set('top', rectLeftTop.y + newTop)
this.insideRect.set('width', this.width - 40)
this.insideRect.set('height', this.height - 40)
this.insideRect.set('scaleX', this.scaleX)
this.insideRect.set('scaleY', this.scaleY)
},
initialize: function (textOptions, rectOptions) {
this.callSuper('initialize', rectOptions)
this.insideRect = new fabric.Rect({
...textOptions,
dirty: false,
objectCaching: false,
selectable: false,
evented: false,
fragmentType: 'rectWidthRect'
});
canvas.bringToFront(this.insideRect);
this.insideRect.width = this.width - 40;
this.insideRect.height = this.height - 40;
this.insideRect.left = this.left + 20;
this.insideRect.top = this.top + 20;
this.insideRectOffsetLeft = this.insideRect.left - this.left
this.insideRectOffsetTop = this.insideRect.top - this.top
this.on('moving', function(e){
this.recalcTextPosition();
})
this.on('rotating',function(){
this.insideRect.rotate(this.insideRect.angle + this.angle - this._prevAngle)
this.recalcTextPosition()
this._prevAngle = this.angle
})
this.on('scaling', function(fEvent){
this.recalcTextPosition();
});
this.on('added', function(){
this.canvas.add(this.insideRect)
});
this.on('removed', function(){
this.canvas.remove(this.insideRect)
});
this.on('mousedown:before', function(){
this._prevObjectStacking = this.canvas.preserveObjectStacking
this.canvas.preserveObjectStacking = true
});
this.on('deselected', function(){
this.canvas.preserveObjectStacking = this._prevObjectStacking
});
}
});
and then just add your element to your canvas as usual:
var rectWithRect = new fabric.RectWithRect(
{
fill: "red",
}, // children rect options
{
left:100,
top:100,
width: 300,
height: 100,
dirty: false,
objectCaching: false,
strokeWidth: 0,
fill: 'blue'
} // parent rect options
);
canvas.add(rectWithRect);
by the way, you can use method like this to create nested elements, text with background and other.
Codesandbox DEMO
I am using KineticJS in my project. I need to connect two shapes using a curved line. One of the shapes can be dragged. I am able to put the curved line between shapes. The problem arises when user starts dragging the shapes. The requirement is that it should be properly curved (please refer to screen shots), irrespective of distance between them and their position with respect to each other. I am doing this:
var utils = {
_getCenter: function(x1, y1, x2, y2) {
return {
x: (x1 + x2) / 2,
y: (y1 + y2) / 2
}
},
// Converts from degrees to radians.
_radians: function(degrees) {
return degrees * Math.PI / 180;
},
// Converts from radians to degrees.
_degrees: function(radians) {
return radians * 180 / Math.PI;
}
};
function amplitude(point) {
var rad_90 = utils._radians(90);
var rad_45 = utils._radians(45);
var rad_60 = utils._radians(60);
console.log(rad_90);
return {
x: point.x * Math.cos(rad_60),
y: point.y * Math.sin(rad_60)
};
}
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Kinetic.Stage({
container: 'container',
width: width,
height: height
});
var layer = new Kinetic.Layer();
var circle = new Kinetic.Circle({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
radius: 20,
fill: 'red',
stroke: 'black',
strokeWidth: 2
});
var attachedCircle = new Kinetic.Circle({
x: stage.getWidth() / 4,
y: stage.getHeight() / 4,
radius: 20,
fill: 'red',
stroke: 'black',
strokeWidth: 2,
draggable: true
});
var center = amplitude(utils._getCenter(circle.getX(), circle.getY(), attachedCircle.getX(), attachedCircle.getY()));
var line = new Kinetic.Line({
points: [circle.getX(), circle.getY(), center.x, center.y, attachedCircle.getX(), attachedCircle.getY()],
fill: 'black',
stroke: 'green',
strokeWidth: 3,
/*
* line segments with a length of 33px
* with a gap of 10px
*/
dash: [33, 10],
id: 'line',
tension: 0.5
});
attachedCircle.on('dragmove', function(e) {
var targetCircle = e.target;
var tempCenter = amplitude(utils._getCenter(circle.getX(), circle.getY(), targetCircle.getX(), targetCircle.getY()));
console.log(tempCenter);
line.setPoints([circle.getX(), circle.getY(), tempCenter.x, tempCenter.y, targetCircle.getX(), targetCircle.getY()]);
});
// add the shape to the layer
layer.add(line);
layer.add(attachedCircle);
layer.add(circle);
// add the layer to the stage
stage.add(layer);
I don't know what I am missing. I have created the plunkr for this.
To define you amplitude function you need to use two input points:
function amplidure2(p1, p2) {
var alpha = Math.atan((p1.x - p2.x) / (p1.y - p2.y)) + Math.PI / 2;
if (p1.y < p2.y) {
alpha += Math.PI;
}
var center = utils._getCenter(p1.x, p1.y, p2.x, p2.y);
var r = 50;
return {
x: center.x + r * Math.sin(alpha),
y: center.y + r * Math.cos(alpha)
}
}
DEMO
I want to calculate the radius of an inverted circle.
I managed to implement everything but, after hours of struggle, I could not find a formula to calculate the correct inverted radius.
More info about circle inversion:
http://en.wikipedia.org/wiki/Inversive_geometry
https://www.youtube.com/watch?v=sG_6nlMZ8f4
My code so far: http://codepen.io/rafaelcastrocouto/pen/Mwjdga
It seems to be working but you can easily tell it's totally wrong.
var c = $('#c'),
b = $('body'),
canvas = c[0],
ctx = canvas.getContext('2d'),
pi = Math.PI,
r = 100,
mr = 30,
width, height, hw, hh;
var setup = function() {
width = b.width();
height = b.height();
hw = width/2;
hh = height/2;
canvas.width = width;
canvas.height = height;
mid();
};
var mid = function() {
circle(hw,hh,0.25);
circle(hw,hh,r);
}
var circle = function(x,y,r) {
ctx.beginPath();
ctx.arc(x,y,r,0,pi*2);
ctx.stroke();
ctx.closePath();
};
var move = function(evt) {
var x = evt.clientX,
y = evt.clientY;
ctx.clearRect(0,0,width,height);
mid();
circle(x,y,mr);
var dx = x-hw,
dy = y-hh,
d = dist(dx,dy),
nd = r*r/d,
nx = dx*nd/d,
ny = dy*nd/d,
nr = mr*mr*pi/d; // whats the correct formula?
console.log(nr);
circle(nx+hw, ny+hh, nr);
};
var dist = function(x,y) {
return Math.pow(x*x + y*y, 1/2);
};
$(setup);
$(window).resize(setup);
$(window).mousemove(move);
Need help from the math experts!
As you said, inverting the centre of a circle doesn't give you the centre of the other one. Likewise if we invert two oposite points of one circle, it doesn't mean they'll be opposing points on the inverted circle.
Since three points describe a unique circle we can use these to find the equation for the inverse circle. That gives us the centre of the inverse circle. We can then find the distance from the centre to one of the inverted points, that's the radius.
The following c++ code gives the centre. (I don't know javascript). The function v.norm2() gives the squared norm of the vector v.
Vector2D getcircle(Vector2D p1, Vector2D p2, Vector2D p3){
Vector2D result;
long double div = 2*(p1.x*(p2.y-p3.y)-p1.y*(p2.x-p3.x)+p2.x*p3.y-p3.x*p2.y);
result.x = (p1.norm2()*(p2.y-p3.y)+p2.norm2()*(p3.y-p1.y)+p3.norm2()*(p1.y-p2.y))/div;
result.y = (p1.norm2()*(p3.x-p2.x)+p2.norm2()*(p1.x-p3.x)+p3.norm2()*(p2.x-p1.x))/div;
return result;
}
So if you have a circle c of radius r, and you are inverting respect to another circle C and radius R, you could do something like
float getRadius(Vector2D C, float R, Vector2D c, float r){
Vector2D p1 = Vector2D(c.x + r, c.y).invert(C, R);
Vector2D p2 = Vector2D(c.x - r, c.y).invert(C, R);
Vector2D p3 = Vector2D(c.x, c.y + r).invert(C, R);
return (getcircle(p1, p2, p3) - p1).norm();
}
Here is an image of a circle with centre (130, -130) and radius 128, and it's inversion respect to another circle (not shown) of centre (0, 0) and radius 40.
The red points on the big circle are polar opposites. They are then inverted and shown on the little circle where you can see they are not polar opposites.
My error was that I was assuming that the center of the inverted circle also respected OP x OP' = r2, but as the image below shows, it clearly does not. The solution was to calculate two points on the circle and reflect each one, then use half the distance between this points to find the radius.
So this is the correct code:
var c = $('#c'),
b = $('body'),
canvas = c[0],
ctx = canvas.getContext('2d'),
fixedRadius = 100,
saved = [],
width, height,
half = {
w: 0,
h: 0
},
mouse = {
r: 31,
x: 0,
y: 0
},
reflect = {
x: 0,
y: 0,
r: 0
};
var setup = function() {
width = b.width();
height = b.height();
half.w = width/2;
half.h = height/2;
canvas.width = width;
canvas.height = height;
move();
};
var mid = function() {
circle(half.w,half.h,1.5);
circle(half.w,half.h,fixedRadius);
};
var circle = function(x,y,r,c) {
ctx.strokeStyle = c || 'black';
ctx.beginPath();
ctx.arc(x,y,r,0,Math.PI*2);
ctx.stroke();
ctx.closePath();
};
var line = function(x1,y1,x2,y2,c) {
ctx.strokeStyle = c || 'black';
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.stroke();
ctx.closePath();
};
var axis = function () {
line(half.w,0,half.w,height,'#ccc');
line(0,half.h,width,half.h,'#ccc');
};
var move = function(evt) {
mouse.x = evt ? evt.clientX : half.w;
mouse.y = evt ? evt.clientY : half.h + 11;
ctx.clearRect(0,0,width,height);
axis();
mid();
circle(mouse.x,mouse.y,mouse.r);
circle(mouse.x,mouse.y,1,'grey');
var di = {
x: mouse.x - half.w, // orange
y: mouse.y - half.h // green
}
di.v = dist(di.x,di.y);
var a = Math.atan2(di.y,di.x); // angle
line(mouse.x - di.x,mouse.y,mouse.x,mouse.y,'orange');
line(mouse.x,mouse.y - di.y,mouse.x,mouse.y,'green');
var p1 = {
v: di.v + mouse.r // cyan
};
p1.x = half.w + (Math.cos(a) * p1.v);
p1.y = half.h + (Math.sin(a) * p1.v);
circle(p1.x,p1.y,1.5,'cyan');
var p2 = {
v: di.v - mouse.r // red
};
p2.x = half.w+Math.cos(a)*p2.v;
p2.y = half.h+Math.sin(a)*p2.v;
circle(p2.x,p2.y,1.5,'red');
var rp1 = {
v: Math.pow(fixedRadius,2) / p1.v // cyan
};
rp1.x = Math.cos(a) * rp1.v,
rp1.y = Math.sin(a) * rp1.v;
circle(rp1.x+half.w,rp1.y+half.h,1.5,'cyan');
var rp2 = {
v: Math.pow(fixedRadius,2) / p2.v // red
};
rp2.x = Math.cos(a) * rp2.v,
rp2.y = Math.sin(a) * rp2.v;
circle(rp2.x+half.w,rp2.y+half.h,1.5,'red');
var newDi = {
v: dist(rp1.x - rp2.x, rp1.y - rp2.y)
};
newDi.r = newDi.v/2,
newDi.x = rp1.x + (Math.cos(a) * newDi.r), // yellow
newDi.y = rp1.y + (Math.sin(a) * newDi.r); // purple
if (p2.v < 0) {
newDi.x = rp1.x - (Math.cos(a) * newDi.r),
newDi.y = rp1.y - (Math.sin(a) * newDi.r);
}
reflect.x = half.w+newDi.x;
reflect.y = half.h+newDi.y
// reflected lines
if (di.v<fixedRadius) line(rp1.x+half.w,rp1.y+half.h,p1.x,p1.y,'cyan');
else line(rp2.x+half.w,rp2.y+half.h,p2.x,p2.y,'red');
line(p1.x,p1.y,half.w,half.h,'#ccc');
line(rp2.x+half.w,rp2.y+half.h,half.w,half.h,'#ccc');
line(reflect.x-newDi.x,reflect.y,reflect.x,reflect.y,'yellow');
line(reflect.x,reflect.y-newDi.y,reflect.x,reflect.y,'purple');
// reflected circle
circle(reflect.x, reflect.y, newDi.r);
circle(reflect.x,reflect.y,1,'grey');
circles(); // saved circles
reflect.r = newDi.r;
};
var dist = function(x,y) {
return Math.pow(x*x + y*y, 1/2);
};
var scroll = function(evt) {
if(evt.originalEvent.wheelDelta > 0) {
mouse.r++;
} else {
mouse.r--;
}
move(evt);
};
var click = function(evt) {
saved.push(['c',mouse.x,mouse.y,mouse.r]);
saved.push(['c',reflect.x,reflect.y,reflect.r]);
saved.push(['l',mouse.x,mouse.y,reflect.x,reflect.y]);
};
var circles = function() {
for(var i = 0; i < saved.length; i++) {
var s = saved[i];
if (s[0]=='c') circle(s[1],s[2],s[3],'grey');
if (s[0]=='l') line(s[1],s[2],s[3],s[4],'grey');
}
};
$(setup);
$(window)
.on('resize', setup)
.on('mousemove', move)
.on('mousewheel', scroll)
.on('click', click);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="c"></canvas>
I'm trying to prevent a rotated Label from being dragged off the screen, but I cannot figure out how to get MinX, MaxX, MinY, and MaxY of the object in its rotated state. getHeight & getWidth only return the values prior to the rotation.
Here is an example illustrating the problem:
var stage = new Kinetic.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
var layer = new Kinetic.Layer();
var labelLeft = new Kinetic.Label({
x: 95,
y: 180,
opacity: 1.0,
listening: true,
draggable: true,
rotationDeg: -45,
text: {
text: 'Pointing Arrow',
fontFamily: 'Calibri',
fontSize: 20,
padding: 5,
fill: 'white'
},
rect: {
fill: 'blue',
pointerDirection: 'left',
pointerWidth: 20,
pointerHeight: 38,
stroke: 'black',
strokeWidth: 2
},
dragBoundFunc: function (pos) {
var newY = pos.y < 50 ? 50 : pos.y;
return {
x: pos.x,
y: newY
};
}
});
layer.add(labelLeft);
// add the layer to the stage
stage.add(layer);
http://jsfiddle.net/fSNnA/4/
In this example, I use dragBoundFunc to prevent the label from being dragged above y=50. but since the label is rotated, its actual highest point (MinY) has changed, and therefore you can drag it up and partially out of view.
What I really need is a function that will return the absolute current MinX, MaxX, MinY, and MaxY - taking into account the angle of rotation and length of text will not always be the same.
Can anyone help?
Here's how to calculate the bounding-box size of a rotated rectangle (label).
var w = label.getWidth();
var h = label.getHeight();
var rads = label.getRotation();
var c = Math.abs(Math.cos(rads));
var s = Math.abs(Math.sin(rads));
var newWidth = h * s + w * c;
var newHeight = h * c + w * s;
Then assuming you'll always rotate around the center, the left/top boundaries are:
var centerX = label.getX()+label.getWidth()/2;
var centerY = label.getY()+label.getHeight()/2;
var MinX = centerX - newWidth/2;
var MinY = centerY - newHeight/2;
[ Disclaimer: This is just off the top of my head--review it accordingly! ]
I'm having some problems with scaling a container to a fixed point.
In my case I'm trying to scale (zoom) a stage to the mouse cursor.
Here is a way to do with pure canvas:
http://phrogz.net/tmp/canvas_zoom_to_cursor.html (as discussed at Zoom Canvas to Mouse Cursor)
I just can't get figure out how to apply the same logic while using the KineticJS API.
Sample code:
var position = this.stage.getUserPosition();
var scale = Math.max(this.stage.getScale().x + (0.05 * (scaleUp ? 1 : -1)), 0);
this.stage.setScale(scale);
// Adjust scale to position...?
this.stage.draw();
After a lot of struggling and searching and trying, using the tip provided by #Eric Rowell and the code posted in the SO question Zoom in on a point (using scale and translate) I finally got the zooming in and out of a fixed point working using KineticJS.
Here's a working DEMO.
And here's the code:
var ui = {
stage: null,
scale: 1,
zoomFactor: 1.1,
origin: {
x: 0,
y: 0
},
zoom: function(event) {
event.preventDefault();
var evt = event.originalEvent,
mx = evt.clientX /* - canvas.offsetLeft */,
my = evt.clientY /* - canvas.offsetTop */,
wheel = evt.wheelDelta / 120;
var zoom = (ui.zoomFactor - (evt.wheelDelta < 0 ? 0.2 : 0));
var newscale = ui.scale * zoom;
ui.origin.x = mx / ui.scale + ui.origin.x - mx / newscale;
ui.origin.y = my / ui.scale + ui.origin.y - my / newscale;
ui.stage.setOffset(ui.origin.x, ui.origin.y);
ui.stage.setScale(newscale);
ui.stage.draw();
ui.scale *= zoom;
}
};
$(function() {
var width = $(document).width() - 2,
height = $(document).height() - 5;
var stage = ui.stage = new Kinetic.Stage({
container: 'container',
width: width,
height: height
});
var layer = new Kinetic.Layer({
draggable: true
});
var rectX = stage.getWidth() / 2 - 50;
var rectY = stage.getHeight() / 2 - 25;
var box = new Kinetic.Circle({
x: 100,
y: 100,
radius: 50,
fill: '#00D200',
stroke: 'black',
strokeWidth: 2,
});
// add cursor styling
box.on('mouseover', function() {
document.body.style.cursor = 'pointer';
});
box.on('mouseout', function() {
document.body.style.cursor = 'default';
});
layer.add(box);
stage.add(layer);
$(stage.content).on('mousewheel', ui.zoom);
});
You need to offset the stage such that it's center point is positioned at the fixed point. Here's an example, because the center point of the stage is defaulted to the upper left corner of the canvas. Let's say that your stage is 600px wide and 400px tall, and you want the stage to zoom from the center. You would need to do this:
var stage = new Kinetic.Stage({
container: 'container',
width: 600,
height: 400,
offset: [300, 200]
};
updated #juan.facorro's demo to scale shape instead of stage
jsFiddle
var ui = {
stage: null,
box: null,
scale: 1,
zoomFactor: 1.1,
zoom: function(event) {
event.preventDefault();
var evt = event.originalEvent,
mx = evt.offsetX,
my = evt.offsetY,
wheel = evt.wheelDelta / 120; //n or -n
var zoom = (ui.zoomFactor - (evt.wheelDelta < 0 ? 0.2 : 0));
var newscale = ui.scale * zoom;
var origin = ui.box.getPosition();
origin.x = mx - (mx - origin.x) * zoom;
origin.y = my - (my - origin.y) * zoom;
ui.box.setPosition(origin.x, origin.y);
ui.box.setScale(newscale);
ui.stage.draw();
ui.scale *= zoom;
}
};
$(function() {
var width = $(document).width() - 2,
height = $(document).height() - 5;
var stage = ui.stage = new Kinetic.Stage({
container: 'container',
width: width,
height: height
});
var layer = new Kinetic.Layer();
var rectX = stage.getWidth() / 2 - 50;
var rectY = stage.getHeight() / 2 - 25;
var box = ui.box = new Kinetic.Circle({
x: 100,
y: 100,
radius: 50,
fill: '#00D200',
stroke: 'black',
strokeWidth: 2,
draggable: true
});
// add cursor styling
box.on('mouseover', function() {
document.body.style.cursor = 'pointer';
});
box.on('mouseout', function() {
document.body.style.cursor = 'default';
});
layer.add(box);
stage.add(layer);
$(stage.content).on('mousewheel', ui.zoom);
});
The demo above only works if the x and y coordinates of the stage are 0. If e.g. the stage is draggable it will change these coordinates while dragging so they need to be included in the offset calculation. This can be achieved by subtracting them from the canvas offsets:
jsfiddle
zoom: function(event) {
event.preventDefault();
var evt = event.originalEvent,
mx = evt.offsetX - ui.scale.getX(),
my = evt.offsetY - ui.scale.getY(),
var zoom = (ui.zoomFactor - (evt.wheelDelta < 0 ? 0.2 : 0));
var newscale = ui.scale * zoom;
var origin = ui.box.getPosition();
origin.x = mx - (mx - origin.x) * zoom;
origin.y = my - (my - origin.y) * zoom;
ui.box.setPosition(origin.x, origin.y);
ui.box.setScale(newscale);
ui.stage.draw();
ui.scale *= zoom;
}