Drag Bound with Konva.Layer - javascript

I am using KonvaJs in my project. I need to implement drag bound to Konva.Layer. My layer has so many other shapes and images. I need to restrict the movement of layer up to 50% of it's width and height. The way I have done in this plunkr. The problem arises when user zoom-in or zoom-out the layer using mouse wheel. After the zoom, I don't know why the drag bound is behaving differently. Seems like I am not able to do the Math correctly. I need to have the same behavior i.e. the way movement of layer is restricted when user does not perform zoom. This is what I am doing:
//... a helper object for zooming
var zoomHelper = {
stage: null,
scale: 1,
zoomFactor: 1.1,
origin: {
x: 0,
y: 0
},
zoom: function(event) {
event.preventDefault();
var delta;
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
if (event.originalEvent.detail > 0) {
//scroll down
delta = 0.2;
} else {
//scroll up
delta = 0;
}
} else {
if (event.originalEvent.wheelDelta < 0) {
//scroll down
delta = 0.2;
} else {
//scroll up
delta = 0;
}
}
var evt = event.originalEvent,
mx = evt.clientX - zoomHelper.stage.getX(),
my = evt.clientY - zoomHelper.stage.getY(),
zoom = (zoomHelper.zoomFactor - delta),
newscale = zoomHelper.scale * zoom;
zoomHelper.origin.x = mx / zoomHelper.scale + zoomHelper.origin
.x - mx / newscale;
zoomHelper.origin.y = my / zoomHelper.scale + zoomHelper.origin
.y - my / newscale;
zoomHelper.stage.setOffset({
x: zoomHelper.origin.x,
y: zoomHelper.origin.y
});
zoomHelper.stage.setScale({
x: newscale,
y: newscale
});
zoomHelper.stage.draw();
zoomHelper.scale *= zoom;
preCalculation();
}
};
// Code goes here
var w = window.innerWidth;
var h = window.innerHeight;
var height, minX, minY, maxX, maxY;
var stage = new Konva.Stage({
container: 'container',
width: w,
height: h
});
zoomHelper.stage =stage;
var layer = new Konva.Layer({
draggable: true,
dragBoundFunc: function(pos) {
console.log('called');
var X = pos.x;
var Y = pos.y;
if (X < minX) {
X = minX;
}
if (X > maxX) {
X = maxX;
}
if (Y < minY) {
Y = minY;
}
if (Y > maxY) {
Y = maxY;
}
return ({
x: X,
y: Y
});
}
});
stage.add(layer);
function preCalculation(){
// pre-calc some bounds so dragBoundFunc has less calc's to do
height = layer.getHeight();
minX = stage.getX() - layer.getWidth() / 2;
maxX = stage.getX() + stage.getWidth() - layer.getWidth() / 2;
minY = stage.getY() - layer.getHeight() / 2;
maxY = stage.getY() + stage.getHeight() - layer.getHeight() / 2;
console.log(height, minX, minY, maxX, maxY);
}
preCalculation();
var img = new Image();
img.onload = function() {
var floorImage = new Konva.Image({
image: img,
width: w,
height: h
});
layer.add(floorImage);
layer.draw();
};
img.src = 'https://s.yimg.com/pw/images/coverphoto02_h.jpg.v3';
$(stage.container).on('mousewheel DOMMouseScroll', zoomHelper.zoom);

While using dragBoundFunc you have to return absolute position of layer. As you are changing attributes of top node (stage) it can be hard to maintain absolute position. So you can try to set bound function inside 'dragmove' event:
layer.on('dragmove', function() {
var x = Math.max(minX, Math.min(maxX, layer.x()));
var y = Math.max(minY, Math.min(maxY, layer.y()));
layer.x(x);
layer.y(y);
});
http://plnkr.co/edit/31MUmOjXBUVuaHVJsL3c?p=preview

Related

Contain a rotated object inside another rotated object FabricJS

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

Move dimension lines away from a polygon

So I have multiple points which creates a polygon. I have calculated the center point of the polygon and used that to create dimension lines for each side of a polygon. My problem is that some dimension lines don't move to a new position that I calculated. To have a better view here's an image of what I have and what I want to happen.
What I have:
What I want to achieve:
I added a red circle to the issue I'm having it should move to new point which is the blue lines.
Here's code snippet:
var viewHeight = window.innerHeight / 2;
var viewWidth = window.innerWidth / 2;
var scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera(
viewWidth / - 2, viewWidth / 2, viewHeight / 2, viewHeight / - 2, 1, 1000);
camera.translateZ(15);
camera.updateProjectionMatrix();
scene.add(camera);
var points = [
{
start: {x: -99.74425000000002, y: 68.61000000000001},
end: {x: -99.74425000000002, y: -37.87599999999999}
},
{
start: {x: -99.74425000000002, y: -37.87599999999999},
end: {x: 69.22574999999995, y: -37.87599999999999}
},
{
start: {x: 69.22574999999995, y: -37.87599999999999},
end: {x: 69.22574999999995, y: -1.2999999999999832}
},
{
start: {x: 69.22574999999995, y: -1.2999999999999832},
end: {x: 120.02574999999996, y: -1.2999999999999827}
},
{
start: {x: 120.02574999999996, y: -1.2999999999999827},
end: {x: 120.02574999999996, y: 68.61000000000001}
},
{
start: {x: 120.02574999999996, y: 68.61000000000001},
end: {x: -99.74425000000002, y: 68.61000000000001}
}
];
var vertices = [];
for (var i in points) {
var line = points[i];
vertices.push(new THREE.Vector2(line.start.x, line.start.y));
};
// build floor
var floor = buildFloor(vertices);
scene.add(floor);
// get center point of floor
var center = findCentroid(vertices);
// build dimension lines
for (var i in points) {
var line = points[i];
var lineCenterPoint = findLineCenterPoint(line.start, line.end);
var degrees = getAngle(center.x, center.y, lineCenterPoint.x, lineCenterPoint.y);
var radians = degrees * Math.PI / 180;
var startX = line.start.x;
var startY = line.start.y;
var endX = line.end.x;
var endY = line.end.y;
if (startY == endY) {
// horizontal
// move point y outward to a new point
var newY = startY + Math.sin(radians) * 20;
startY = newY;
endY = newY;
} else if (startX == endX) {
// vertical
// move point x outward to a new point
var x = startX + Math.cos(radians) * 20;
startX = x;
endX = x;
} else {
// todo diagonal lines
// don't know what should be the formula for this
}
var from = new THREE.Vector3(startX, startY, 0);
var to = new THREE.Vector3(endX, endY, 0);
var direction = to.clone().sub(from);
var length = direction.length();
var hex = 0x0;
var arrorGroupHelper = new THREE.Group();
arrorGroupHelper.add(new THREE.ArrowHelper(direction.normalize(), from, length, hex, 1, 10));
arrorGroupHelper.add(new THREE.ArrowHelper(direction.negate(), to, length, hex, 1, 10));
scene.add(arrorGroupHelper);
var text = document.createElement('div');
text.style.position = 'absolute';
text.style.zIndex = 5;
text.style.backgroundColor = "ffffff";
text.innerHTML = length.toFixed(2);
let interiorCenter = to.clone().add(from).multiplyScalar(0.5);
let textPos = interiorCenter.project( camera );
var widthHalf = viewWidth;
var heightHalf = viewHeight;
var style = 'translate(-50%,-50%) translate(' + ( textPos.x * widthHalf + widthHalf ) + 'px,' + ( - textPos.y * heightHalf + heightHalf ) + 'px)';
text.style.transform = style;
document.getElementById('container').append(text);
}
var threejsCanvas = document.getElementById('threejs-canvas');
var renderer = new THREE.WebGLRenderer({
canvas : threejsCanvas,
alpha : true
});
renderer.autoClear = false;
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.render( scene, camera );
animate();
window.addEventListener( 'resize', onWindowResize, false );
function onWindowResize() {
var width = window.innerWidth / 2;
var height = window.innerHeight / 2;
camera.left = -width / 2;
camera.right = width / 2
camera.top = height / 2;
camera.bottom = -height / 2;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
render();
}
function render() {
renderer.clear();
renderer.render( scene, camera );
}
function findLineCenterPoint(a, b) {
return { x: (b.x - a.x) / 2 + a.x, y: (b.y - a.y) / 2 + a.y };
}
function findCentroid (arr) {
var minX, maxX, minY, maxY;
for (var i = 0; i < arr.length; i++)
{
var x = arr[i]['x'], y = arr[i]['y'];
minX = (x < minX || minX == null) ? x : minX;
maxX = (x > maxX || maxX == null) ? x : maxX;
minY = (y < minY || minY == null) ? y : minY;
maxY = (y > maxY || maxY == null) ? y : maxY;
}
return {x:(minX + maxX) / 2, y:(minY + maxY) / 2};
}
function buildFloor(vertices) {
var material = new THREE.MeshBasicMaterial({
color: 0xcccccc,
});
var shape = new THREE.Shape(vertices);
var geometry = new THREE.ShapeGeometry(shape);
var floor = new THREE.Mesh(geometry, material);
return floor;
}
function getAngle(originX, originY, targetX, targetY) {
var dx = originX - targetX;
var dy = originY - targetY;
var theta = Math.atan2(-dy, -dx);
theta *= 180 / Math.PI;
if (theta < 0) theta += 360;
return theta;
}
#container {
position: relative;
}
#threejs-canvas {
position:absolute;
z-index: 2;
}
<script src="//cdn.rawgit.com/mrdoob/three.js/master/build/three.min.js"></script>
<div id="container">
<canvas id="threejs-canvas"></canvas>
</div>
Here's my question:
How do I move the dimension lines encircled on red on the image above to the new points which is the blue lines.
Thanks. Hope someone could shed a light on this.

How do you zoom into a specific point (no canvas)?

The goal is simple, using a mousewheel, zoom into a specific point (where the mouse is). This means after zooming the mouse will be in the same roughly the same spot of the picture.
(Purely illustrative, I don't care if you use dolphins, ducks or madonna for the image)
I do not wish to use canvas, and so far I've tried something like this:
HTML
<img src="whatever">
JS
function zoom(e){
var deltaScale = deltaScale || -e.deltaY / 1000;
var newScale = scale + deltaScale;
var newWidth = img.naturalWidth * newScale;
var newHeight = img.naturalHeight * newScale;
var x = e.pageX;
var y = e.pageY;
var newX = x * newWidth / img.width;
var newY = y * newHeight / img.height;
var deltaX = newX - x;
var deltaY = newY - y;
setScale(newScale);
setPosDelta(-deltaX,-deltaY);
}
function setPosDelta(dX, dY) {
var imgPos = getPosition();
setPosition(imgPos.x + dX, imgPos.y + dY);
}
function getPosition() {
var x = parseFloat(img.style.left);
var y = parseFloat(img.style.top);
return {
x: x,
y: y
}
}
function setScale(n) {
scale = n;
img.width = img.naturalWidth * n;
img.height = img.naturalHeight * n;
}
What this attempts to do is calculate the x,y coordinates of the dolphin's eye before and after the zoom, and after calculating the distance between those two points, substracts it from the left,top position in order to correct the zoom displacement, with no particular success.
The zoom occurs naturally extending the image to the right and to the bottom, so the correction tries to pull back to the left and to the top in order to keep the mouse on that damn dolphin eye! But it definitely doesn't.
Tell me, what's wrong with the code/math? I feel this question is not too broad, considering I couldn't find any solutions besides the canvas one.
Thanks!
[EDIT] IMPORTANT
CSS transform order matters, if you follow the selected answer, make sure you order the transition first, and then the scale. CSS transforms are executed backwards (right to left) so the scaling would be processed first, and then the translation.
Here is an implementation of zooming to a point. The code uses the CSS 2D transform and includes panning the image on a click and drag. This is easy because of no change in scale.
The trick when zooming is to normalize the offset amount using the current scale (in other words: divide it by the current scale) first, then apply the new scale to that normalized offset. This keeps the cursor exactly where it is independent of scale.
var scale = 1,
panning = false,
xoff = 0,
yoff = 0,
start = {x: 0, y: 0},
doc = document.getElementById("document");
function setTransform() {
doc.style.transform = "translate(" + xoff + "px, " + yoff + "px) scale(" + scale + ")";
}
doc.onmousedown = function(e) {
e.preventDefault();
start = {x: e.clientX - xoff, y: e.clientY - yoff};
panning = true;
}
doc.onmouseup = function(e) {
panning = false;
}
doc.onmousemove = function(e) {
e.preventDefault();
if (!panning) {
return;
}
xoff = (e.clientX - start.x);
yoff = (e.clientY - start.y);
setTransform();
}
doc.onwheel = function(e) {
e.preventDefault();
// take the scale into account with the offset
var xs = (e.clientX - xoff) / scale,
ys = (e.clientY - yoff) / scale,
delta = (e.wheelDelta ? e.wheelDelta : -e.deltaY);
// get scroll direction & set zoom level
(delta > 0) ? (scale *= 1.2) : (scale /= 1.2);
// reverse the offset amount with the new scale
xoff = e.clientX - xs * scale;
yoff = e.clientY - ys * scale;
setTransform();
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#document {
width: 100%;
height: 100%;
transform-origin: 0px 0px;
transform: scale(1) translate(0px, 0px);
}
<div id="document">
<img style="width: 100%"
src="https://i.imgur.com/fHyEMsl.jpg"
crossOrigin="" />
</div>
This is an implementation that is closer to your original idea using top and left offsets and modifying the width attribute of the image instead of using the css transform in my other answer.
var scale = 1.0,
img = document.getElementById("image"),
deltaX = 0,
deltaY = 0;
// set the initial scale once the image is loaded
img.onload = function() {
scale = image.offsetWidth / image.naturalWidth;
}
img.onwheel = function(e) {
e.preventDefault();
// first, remove the scale so we have the native offset
var xoff = (e.clientX - deltaX) / scale,
yoff = (e.clientY - deltaY) / scale,
delta = (e.wheelDelta ? e.wheelDelta : -e.deltaY);
// get scroll direction & set zoom level
(delta > 0) ? (scale *= 1.05) : (scale /= 1.05);
// limit the smallest size so the image does not disappear
if (img.naturalWidth * scale < 16) {
scale = 16 / img.naturalWidth;
}
// apply the new scale to the native offset
deltaX = e.clientX - xoff * scale;
deltaY = e.clientY - yoff * scale;
// now modify the attributes of the image to reflect the changes
img.style.top = deltaY + "px";
img.style.left = deltaX + "px";
img.style.width = (img.naturalWidth * scale) + "px";
}
window.onresize = function(e) {
document.getElementById("wrapper").style.width = window.innerWidth + "px";
document.getElementById("wrapper").style.height = window.innerHeight + "px";
}
window.onload = function(e) {
document.getElementById("wrapper").style.width = window.innerWidth + "px";
document.getElementById("wrapper").style.height = window.innerHeight + "px";
}
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
div {
overflow: hidden;
}
<div id="wrapper" style="position:relative;">
<img id="image" style="width:100%;position:absolute;top:0px;left:0px;"
src="https://i.imgur.com/fHyEMsl.jpg"
crossOrigin="" />
</div>
I liked the both posts from fmacdee. I factored the code he created out to be a reusable version that can be called on any image.
just call:
var imageScaler = new ImageScaler(document.getElementById("image"));
imageScaler.setup();
and include this code somewhere in your project:
var ImageScaler = function(img)
{
this.img = img;
this.scale = this.getImageScale();
this.panning = false;
this.start = {x: 0, y: 0};
this.delta = {x: 0, y: 0};
};
ImageScaler.prototype =
{
constructor: ImageScaler,
setup: function()
{
this.setupEvents();
},
setupEvents: function()
{
var img = this.img;
var callBack = this.onScale.bind(this);
var touchDown = this.touchDown.bind(this),
touhcMove = this.touchMove.bind(this),
touchUp = this.touchUp.bind(this);
img.onwheel = callBack;
img.onmousedown = touchDown;
img.onmousemove = touhcMove;
img.onmouseup = touchUp;
},
getImageScale: function()
{
var img = this.img;
return img.offsetWidth / img.naturalWidth;
},
getMouseDirection: function(e)
{
return (e.wheelDelta ? e.wheelDelta : -e.deltaY);
},
getOffset: function(e)
{
var scale = this.scale,
delta = this.delta;
// first, remove the scale so we have the native offset
return {
x: (e.clientX - delta.x) / scale,
y: (e.clientY - delta.y) / scale
};
},
scaleElement: function(x, y, scale)
{
var img = this.img;
img.style.top = y + "px";
img.style.left = x + "px";
img.style.width = (img.naturalWidth * scale) + "px";
},
minScale: 0.2,
updateScale: function(delta)
{
// get scroll direction & set zoom level
var scale = (delta > 0) ? (this.scale *= 1.05) : (this.scale /= 1.05);
// limit the smallest size so the image does not disappear
if (scale <= this.minScale)
{
this.scale = this.minScale;
}
return this.scale;
},
touchDown: function(e)
{
var delta = this.delta;
this.start = {x: e.clientX - delta.x, y: e.clientY - delta.y};
this.panning = true;
},
touchMove: function(e)
{
e.preventDefault();
if (this.panning === false)
{
return;
}
var delta = this.delta,
start = this.start;
delta.x = (e.clientX - start.x);
delta.y = (e.clientY - start.y);
console.log(delta, start)
this.scaleElement(delta.x, delta.y, this.scale);
},
touchUp: function(e)
{
this.panning = false;
},
onScale: function(e)
{
var offset = this.getOffset(e);
e.preventDefault();
// get scroll direction & set zoom level
var delta = this.getMouseDirection(e);
var scale = this.updateScale(delta);
// apply the new scale to the native offset
delta = this.delta;
delta.x = e.clientX - offset.x * scale;
delta.y = e.clientY - offset.y * scale;
this.scaleElement(delta.x, delta.y, scale);
}
};
I made a fiddle to view the results: http://jsfiddle.net/acqo5n8s/12/

Zoom and pan problems

So i'm attempting to create a script that Zooms in a image, centered at mouse x, y, and with panning.
And it almost works, almost...
When you zoom and pan in the image, it kinda jumps a little, so my math is off, as always.
If u could point me in the right direction it would be awesome, my mind is going numb from trying to subtract different offsets, and it's kinda like a big blur atm !
JSBin Example
/**
MouseDown: Pan image
ScrollWheel: Zoom In image
*/
var $doc = $(".document");
var scale = 1;
var panning = false;
var start = {x:0, y:0}
var offset = {left:0, top: 0}
$(window).bind("mousedown", (e) => {
e.preventDefault();
start = {x: e.clientX, y: e.clientY};
updateOffset();
panning = true;
})
.bind("mouseup", (e) => {
updateOffset();
panning = false;
})
.bind("mousemove", (e)=> {
e.preventDefault();
if(!panning) return;
var x = (e.clientX - start.x) + offset.left;
var y = (e.clientY - start.y) + offset.top;
$doc.css({
"transform": "translate("+ (x) +"px, "+ (y) +"px) scale(" +scale +")"
});
})
.bind("mousewheel", (e)=>{
e.preventDefault();
// get scroll direction & set zoom level
(e.originalEvent.wheelDelta /120 > 0) ? (scale *= 1.2) : (scale /= 1.2)
var x = e.clientX - offset.left;
var y = e.clientY - offset.top;
var originX = x
var originY = y
var translateX = offset.left;
var translateY = offset.top;
$doc.css({
"transform-origin": originX+ "px " + originY + "px",
"transform": "translate("+ translateX +"px, "+ translateY +"px) scale("+scale+")"
})
updateOffset();
});
// Helpers --------------------------------------------------------
// graps the transform styles from the element
function getMatrix($el) {
if(!$el.css("transform")) {
return false;
}
var arr = $el.css("transform").match(/\((.*)\)/)[1].split(",");
return {
scale: parseInt(arr[0]),
tx: parseInt(arr[4]),
ty: parseInt(arr[5])
}
}
function updateOffset () {
var m = getMatrix($doc)
offset = {
top: m.ty,
left: m.tx
};
}
You need to compensate for the change in scale when calculating the offset:
.bind("mousewheel", (e)=>{
//
// Zoom
//
e.preventDefault();
// take the scale into account with the offset
var xs = (e.clientX - offset.left) / scale;
var ys = (e.clientY - offset.top) / scale;
// get scroll direction & set zoom level
(e.originalEvent.wheelDelta /120 > 0) ? (scale *= 1.2) : (scale /= 1.2)
// reverse the offset amount with the new scale
var x = e.clientX - xs * scale;
var y = e.clientY - ys * scale;
$doc.css({
"transform": "translate("+ x +"px, "+ y +"px) scale("+scale+")"
})
updateOffset();
});
Oh, and you have to use parseFloat instead of parseInt in your getMatrix() call or it just loses accuracy over time!
// graps the transform styles from the element
function getMatrix($el) {
if(!$el.css("transform")) {
return false;
}
var arr = $el.css("transform").match(/\((.*)\)/)[1].split(",");
return {
scale: parseFloat(arr[0]),
tx: parseFloat(arr[4]),
ty: parseFloat(arr[5])
}
}

Scaling to a fixed point in KineticJS

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;
}

Categories