Konva - Change offset position after zoom and rotation - javascript

I'm trying to implement a rotation and zoom feature with a slider. I need an image to always rotate from the center of the viewport. The main idea is changing the position and offset of the stage after drag event. Here's what I've tried
const width = window.innerWidth
const height = window.innerHeight
// scale is temp
let stage = new Konva.Stage({
container: 'canvas',
width,
height,
draggable: true,
offset: {
x: width / 2,
y: height / 2
},
x: width / 2,
y: height / 2
})
let mapLayer = new Konva.Layer()
let imageObj = new Image()
imageObj.onload = function () {
let map = new Konva.Image({
image: imageObj,
prevX: 0,
prevY: 0
})
layer.add(map)
stage.add(mapLayer)
stage.on('dragstart', () => {
map.prevX = map.getAbsolutePosition().x
map.prevY = map.getAbsolutePosition().y
})
stage.on('dragend', () => {
let curX = map.getAbsolutePosition().x
let curY = map.getAbsolutePosition().y
let deltaX = Math.abs(map.prevX - curX)
let deltaY = Math.abs(map.prevY - curY)
if (curX > map.prevX) {
stage.offsetX(stage.offsetX() - deltaX)
stage.x(stage.x() - deltaX)
} else {
stage.offsetX(stage.offsetX() + deltaX)
stage.x(stage.x() + deltaX)
}
if (curY > map.prevY) {
stage.offsetY(stage.offsetY() - deltaY)
stage.y(stage.y() - deltaY)
} else {
stage.offsetY(stage.offsetY() + deltaY)
stage.y(stage.y() + deltaY)
}
stage.draw()
})
}
(if you want a full source code, clone it from here and run yarn run dev from terminal, the app lives on localhost:3000
It works fine when the image is in the normal position (not zoomed and rotated yet) but after ANY kind of rotation or zooming, dragging the stage will cause the stage to be re-positioned in a weird fashion (the offset is still correct though). How can I set position and offset correctly?

The solution to your initial issue below.
Note that I put a circle and a tooltip as well to a single layer to be able to drag them as a single one. Also, it should be added after map to have binding work.
import Konva from 'konva'
import map from './../resources/unpar.svg'
const width = window.innerWidth
const height = window.innerHeight
// scale is temp
let stage = new Konva.Stage({
container: 'canvas',
width,
height,
offset: {
x: width / 2,
y: height / 2
},
x: width / 2,
y: height / 2
})
let layer = new Konva.Layer({draggable: true})
let testCircle = new Konva.Circle({
x: 633,
y: 590,
radius: 10,
fill: 'white',
stroke: 'black'
})
testCircle.on('mousemove', function () {
tooltip.position({
x: testCircle.x() - 90,
y: testCircle.y() - 50
})
tooltip.text('Pintu Depan Gedung 10')
tooltip.show()
layer.batchDraw()
})
testCircle.on('mouseout', function () {
tooltip.hide()
layer.draw()
})
var tooltip = new Konva.Text({
text: '',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
textFill: 'white',
fill: 'black',
alpha: 0.75,
visible: false
})
let imageObj = new Image()
imageObj.onload = function () {
const map = new Konva.Image({
image: imageObj,
})
layer.add(map)
layer.add(testCircle)
layer.add(tooltip)
stage.add(layer)
}
imageObj.src = map
export default stage

Related

Limit dragged line to an arc / radius of a given length

I'm currently using Phaser 3, although my question isn't technically restricted to that framework, as it's more of a general JS/canvas/maths question, but:
I have a line drawn with graphics(). It’s anchored at one end, and the other end is draggable. I made a quick demo and so far, so good - you can see what I have already on CodePen.
Dragging the marker around and redrawing the line is no problem, but what I’d like is for that line to have a maximum length of 100, so even if you’re still dragging beyond that point, the line would still follow the mouse, but not get any longer than 100. Dragging inside that maximum radius, the line would shrink as normal.
I’ve put together a visual that hopefully explains it:
The issue is that I suspect this is VERY MATHS and I am very, very weak with maths. Could anyone explain like I’m five what I need to do to my code to achieve this?
Edit: Adding code in a snippet here, as requested:
var config = {
type: Phaser.AUTO,
width: 800,
height: 400,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
scene: {
preload: preload,
create: create,
update: update
}
};
var path;
var curve;
var graphics;
var game = new Phaser.Game(config);
function preload() {
this.load.spritesheet('dragcircle', 'https://labs.phaser.io/assets/sprites/dragcircle.png', { frameWidth: 16 });
}
function create() {
graphics = this.add.graphics();
path = { t: 0, vec: new Phaser.Math.Vector2() };
curve = new Phaser.Curves.Line([ 400, 390, 300, 230 ]);
var point0 = this.add.image(curve.p0.x, curve.p0.y, 'dragcircle', 0);
var point1 = this.add.image(curve.p1.x, curve.p1.y, 'dragcircle', 0).setInteractive();
point1.setData('vector', curve.p1);
this.input.setDraggable(point1);
this.input.on('drag', function (pointer, gameObject, dragX, dragY) {
gameObject.x = dragX;
gameObject.y = dragY;
gameObject.data.get('vector').set(dragX, dragY);
});
this.input.on('dragend', function (pointer, gameObject) {
let distance = Phaser.Math.Distance.Between(curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y);
console.log(distance);
});
}
function update() {
graphics.clear();
graphics.lineStyle(2, 0xffffff, 1);
curve.draw(graphics);
curve.getPoint(path.t, path.vec);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/3.55.2/phaser.min.js"></script>
You are right, you would need some math, but phaser has many helper functions, that will do the heavy lifting.
The main idea is, of this solution is
define a maxLength
get the the new point on drag, and create a real Phaser Vector2
here is some math is needed, to create the vector, just calculate destination point minus origin point
new Phaser.Math.Vector2(pointer.x - point0.x, pointer.y - point0.y) (origin point being the starting point of the desired vector, and destination point being the mouse pointer)
calculate the length of the created vector and compare it with the maxLength
if too long adjust the vector, with the handy function setLength (link to the documentation, this is where you would have needed math, but thankfully Phaser does it for us)
set the new coordinates for point1 and the curve endpoint
Here a quick demo (based on your code):
var config = {
type: Phaser.AUTO,
width: 500,
height: 170,
scene: {
preload: preload,
create: create,
update: update
}
};
var curve;
var graphics;
var game = new Phaser.Game(config);
function preload() {
this.load.spritesheet('dragcircle', 'https://labs.phaser.io/assets/sprites/dragcircle.png', { frameWidth: 16 });
}
function create() {
graphics = this.add.graphics();
curve = new Phaser.Curves.Line([ config.width/2, config.height - 20, config.width/2, 10 ]);
// define a length, could be a global constant
let maxLength = curve.p0.y - curve.p1.y;
var point0 = this.add.image(curve.p0.x, curve.p0.y, 'dragcircle', 0);
var point1 = this.add.image(curve.p1.x, curve.p1.y, 'dragcircle', 0).setInteractive();
this.input.setDraggable(point1);
// Just add for Debug Info
this.add.circle(curve.p0.x, curve.p0.y, maxLength)
.setStrokeStyle(1, 0xffffff, .5)
this.input.on('drag', function (pointer) {
let vector = new Phaser.Math.Vector2(pointer.x - point0.x, pointer.y - point0.y);
let distance = Phaser.Math.Distance.Between( point0.x, point0.y, pointer.x, pointer.y);
if(distance > maxLength){
vector.setLength(maxLength);
}
point1.x = point0.x + vector.x;
point1.y = point0.y + vector.y;
curve.p1.x = point1.x;
curve.p1.y = point1.y;
});
// NOT REALLY NEEDED
/*this.input.on('dragend', function (pointer, gameObject) {
let distance = Phaser.Math.Distance.Between(curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y);
console.log(distance);
});*/
}
function update() {
graphics.clear();
graphics.lineStyle(2, 0xffffff, 1);
curve.draw(graphics);
}
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>
Optional - Code Version using Phaser.GameObjects.Line:
This uses less code, and thanks to the Line GameObject (link to Documentation), you can directly use the vector to update the line, and also don't need the update function, graphics and so.
const config = {
type: Phaser.CANVAS,
width: 500,
height: 160,
scene: {
create
}
};
const game = new Phaser.Game(config);
const MAX_LINE_LENGTH = 100;
function create() {
let points = [ {x: config.width/2, y: config.height - 20}, {x: config.width/2, y: config.height - 120} ];
let point0 = this.add.circle(points[0].x, points[0].y, 6)
.setStrokeStyle(4, 0xff0000);
let point1 = this.add.circle(points[1].x, points[1].y, 6)
.setStrokeStyle(4, 0xff0000)
.setInteractive();
this.input.setDraggable(point1);
// Just add for Debug Info
this.add.circle(point0.x, point0.y, MAX_LINE_LENGTH)
.setStrokeStyle(1, 0xffffff, .5);
let line = this.add.line(points[0].x, points[0].y, 0, 0, 0, -100, 0x00ff00)
.setOrigin(0);
this.input.on('drag', function (pointer) {
let vector = new Phaser.Math.Vector2(pointer.x - point0.x, pointer.y - point0.y);
let distance = Phaser.Math.Distance.Between( point0.x, point0.y, pointer.x, pointer.y);
if(distance > MAX_LINE_LENGTH){
vector.setLength(MAX_LINE_LENGTH);
}
point1.x = point0.x + vector.x;
point1.y = point0.y + vector.y;
line.setTo(0, 0, vector.x, vector.y);
});
}
<script src="//cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>

How to flip a rotated image in konvajs

I am trying to flip a group (horizontally) using Konvajs.Following the advice of this post, I am using the scaleX property. This works---mostly. However, it doesn't flip around the center.
function reverse(shape){
var layer = shape.getLayer();
var oldScaleX = shape.attrs.scaleX;
var width = shape.getClientRect().width;
var adjuster = oldScaleX * width;
var startX = shape.attrs.x + adjuster;
var startY = shape.attrs.y;
shape.scaleX(-oldScaleX);
shape.position({x: startX, y: startY});
layer.draw();
};
I tried using the offsetX and offsetY properties like in this post, but it doesn't seem to do anything.
shape.offsetX(shape.width()/2);
shape.offsetY(shape.height()/2);
shape.x(shape.x() - shape.attrs.offsetX);
shape.y(shape.y() - shape.attrs.offsetY);
The shapes will flip initially, but if they are rotated first, they tend to jump around.
Codepen
Based on my function posted in this other question, here is a snippet to rotate a shape around its centre. In this case the shape is a group composed of 2 rects but any shape will work.
// Set up the canvas / stage
var stage = new Konva.Stage({container: 'container', width: 600, height: 200});
var layer = new Konva.Layer({draggable: false});
stage.add(layer);
var shape = new Konva.Group({x: 80, y: 40, width: 60, height: 60 });
var r1 = new Konva.Rect({ x:10, y:10, width: 70, height: 40, fill: 'cyan'})
var r2 = new Konva.Rect({ x:50, y:10, width: 40, height: 100, fill: 'cyan'})
shape.add(r1)
shape.add(r2)
layer.add(shape)
// set the group rotate point. Note - x,y is relative to top-left of stage !
var pos = shape.getClientRect();
RotatePoint(shape, {x: pos.x + pos.width/2, y: pos.y + pos.height/2});
stage.draw();
// This is where the flip happens
var scaleX = 1, inc = -0.2; // set inc = 1 for full flip in one call
function flipShape(){
scaleX = scaleX + inc;
shape.scaleX(scaleX); // and that's it
// fun with colors on front and back of shape
r1.fill(scaleX < 0 ? 'magenta' : 'cyan');
r2.fill(scaleX < 0 ? 'magenta' : 'cyan');
$('#info').html('Set ScaleX(' + scaleX.toFixed(2) + ')'); // What's happening?
layer.draw(); // show the change
inc = (scaleX % 1 === 0 ? -1 * inc : inc); // decide next increment, reversed at 1 & -1
}
$('#flip').on('click', function(e){
flipShape(); // click button - flip shape a bit
})
/*
Set the offset for rotation to the given location and re-position the shape
*/
function RotatePoint(shape, pos){ // where pos = {x: xpos, y: yPos}
var initialPos = shape.getAbsolutePosition();
var moveBy = {x: pos.x - initialPos.x, y: pos.y - initialPos.y};
// offset is relative to initial x,y of shape, so deduct x,y.
shape.offsetX(moveBy.x);
shape.offsetY(moveBy.y);
// reposition x,y because changing offset moves it.
shape.x(initialPos.x + moveBy.x);
shape.y(initialPos.y + moveBy.y);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/2.5.1/konva.js"></script>
<p>Clck the button to progressively flip a shape using scaleX <button id='flip'>Flip a bit</button> <span id='info'></span></p>
<div id='container' style="position: absolute; top: 0; z-index: -1; display: inline-block; width: 600px, height: 200px; background-color: silver; "></div>

Is there a way to resize a circle by dragging the circumference outward / inward using Konva js?

Using Konva js, Is there a way to drag a circle's circumference without showing the resizers elements, in order to resize the circle ( make the radius grow)?
Using a Transformer - displays the resizers, and stretches rectangles by changing the scale. I want to actually resize the circle (larger radius) without showing the resizers.
All help will be appreciated. Thx
You may need to use two circles for that. One circle is your main shape, another circle for detecting events on stroke (the second circle can be transparent if you don't want to see it on the screen).
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 50,
fill: 'green'
});
layer.add(circle);
const border = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 50,
stroke: 'black',
strokeWidth: 6,
fillEnabled: false
});
layer.add(border);
function distance(p1, p2) {
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
}
border.on('mouseenter', () => {
border.stroke('red');
layer.batchDraw();
})
border.on('mouseleave', () => {
border.stroke('black');
layer.batchDraw();
})
border.on('mousedown', () => {
// attach move event
stage.on('mousemove.resizer', () => {
const center = border.position();
const pointer = stage.getPointerPosition();
const radius = distance(center, pointer);
border.radius(radius);
circle.radius(radius)
layer.batchDraw();
});
// remove all events at end
stage.on('mouseup.resizer', () => {
stage.off('.resizer')
});
})
layer.draw();
<script src="https://unpkg.com/konva#^2/konva.min.js"></script>
<div id="container"></div>

Kineticjs Group Image dragging

I am new to Kinetic js and having issue figuring out how to stop the image from moving outside the boundary which then creates a white space
Here is my code so far
//photo layer
var photoGroup = new Kinetic.Group({ x: layout.photoGroup.x, y: layout.photoGroup.y, draggable: true });
var Photolayer = new Kinetic.Layer();
var cropArea = new Kinetic.Rect({
x: layout.cropAreaRect.x,
y: layout.cropAreaRect.y,
width: layout.cropAreaRect.width,
height: layout.cropAreaRect.height,
fill: "white",
listening: false
});
Photolayer.add(cropArea);
Photolayer.add(photoGroup);
stage.add(Photolayer);
Here is a working code snippet illustrating a dragBoundFunc().
The gold rect is the area we intend to restrict the drag within. The blue rect shows the area we compute - different from the gold because we use the topleft of the draggable as the basis for our maths and we have to account for its own width and height. The red block could be an image or any Konvajs element.
Drag the red block!
// add a stage
var s = new Konva.Stage({
container: 'container',
width: 800,
height: 600
});
// add a layer
var l = new Konva.Layer();
s.add(l);
// Add a green rect to the LAYER just to show boundary of the stage.
var green = new Konva.Rect({stroke: 'lime', width: 800, height: 600, x: 0, y: 0});
l.add(green);
// Add a gold rect to the LAYER just to give some visual feedback on intended drag limits
var gold = new Konva.Rect({stroke: 'gold', width: 400, height: 200, x: 55, y: 55});
l.add(gold);
// Add a red rect to act as our draggable
var red = new Konva.Rect({fill: 'red', stroke: 'red', width: 40, height: 50, x: 65, y: 65, draggable: true,
dragBoundFunc: function(pos) {
var newX = pos.x;
if (newX < minX){ newX = minX};
if (newX > maxX){ newX = maxX};
var newY = pos.y;
if (newY < minY){ newY = minY};
if (newY > maxY){ newY = maxY};
$("#info").html('Info: Pos=(' + newX + ', ' + newY + ') range X=' + minX + ' - ' + maxX + ' Y=' + minY + ' - ' + maxY);
return {
x: newX,
y: newY
}
}
});
l.add(red);
// calculate the drag boundary once for performance - we need to have the draggable element size for this !
var goldPos = gold.getAbsolutePosition()
var minX = goldPos.x;
var maxX = goldPos.x + gold.width() - red.width();
var minY = goldPos.y;
var maxY = goldPos.y + gold.height() - red.height();
// Add a blue rect to the LAYER just show the computed drag limits - note the subtraction of the draggable element dimensions from maxX & Y. Note the 1px adjustment is to avoid overlapping the gold rect in the demo and is not required for live.
var blue = new Konva.Rect({stroke: 'blue', opacity: 0.2, width: 399 - red.width(), height: 199 - red.height(), x: 56, y: 56});
l.add(blue);
// bring red back to top so we can drag it
red.moveToTop();
l.draw(); // redraw the layer it all sits on.
#container {
border: 1px solid #ccc;
}
#info {
height: 20px;
border-bottom: 1px solid #ccc;
}
#hint {
font-style: italic;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/konvajs/konva/1.6.3/konva.min.js"></script>
<body>
<div id='hint'>Hint: green is stage, gold is intended drag limit, blue is computed drag limit.<br/>Drag the red block!</div>
<div id='info'>Info:</div>
<div id="container"></div>
</body>
As an example of dragBoundFunc:
const dragRect = new Konva.Rect({
x: window.innerWidth / 2 - 50,
y: window.innerHeight / 2 - 50,
width: 100,
height: 100,
fill: 'green',
draggable: true,
dragBoundFunc: (pos) => {
const leftBound = cropRect.x();
const rightBound = cropRect.x() + cropRect.width() - dragRect.width();
pos.x = Math.max(leftBound, Math.min(rightBound, pos.x));
const topBound = cropRect.y();
const bottomBound = cropRect.y() + cropRect.height() - dragRect.height();
pos.y = Math.max(topBound, Math.min(bottomBound, pos.y));
return pos;
}
});
Demo: http://jsbin.com/jilagadeqa/1/edit?js,output

KineticJS Arrow drawing rotating misaligned

I'm making an interactive diagram website and I'm trying to easily draw arrows in kineticJS relative to the stagesize, i've got the following:
http://jsfiddle.net/K5Zhg/17/
But as you can see the rotated arrow is not matching the intended start and ending point. I tried offset, but that messes up the correct (not rotated) arrow. Also don't know why jsfiddle show's my arrows this messy (missing bottom line), on my own machine it seems to be working fine (using v5.1.0), see also here http://tomzooi.com/dump/ip/ (using kineticjs and binding bootstrap to it, working nice so far)
code:
var stage = new Kinetic.Stage({
container: 'container',
width: 1140,
height: 500
});
var layer = new Kinetic.Layer();
var border = new Kinetic.Rect( {
x: 0,
y: 0,
width: stage.getWidth(),
height: stage.getHeight(),
stroke: ' red',
strokeWidth: 1
});
layer.add(border);
var dot = new Kinetic.Circle({
x: 0.1*stage.getWidth(),
y: 0.1*stage.getHeight(),
radius: 10,
fill: 'red'
});
var dot2 = new Kinetic.Circle({
x: 0.5*stage.getWidth(),
y: 0.1*stage.getHeight(),
radius: 10,
fill: 'red'
});
var dot3 = new Kinetic.Circle({
x: 0.5*stage.getWidth(),
y: 0.5*stage.getHeight(),
radius: 10,
fill: 'red'
});
layer.add(dot);
layer.add(dot2);
layer.add(dot3);
var arrow1 = arrow(0.1,0.1,0.5,0.1,10);
var arrow2 = arrow(0.1,0.2,0.5,0.5,10);
layer.add(arrow1);
layer.add(arrow2);
// add the layer to the stage
stage.add(layer);
function arrow(psx, psy, pex, pey, pw) {
var w = stage.getWidth();
var h = stage.getHeight();
var sx = psx*w;
var ex = pex*w;
var sy = psy*h;
var ey = pey*h;
var pr = (Math.atan2(ey-sy, ex-sx)/(Math.PI/180));
var pl = Math.sqrt(Math.pow((ex-sx),2)+Math.pow((ey-sy),2));
ex = sx+pl;
ey = sy;
var poly = new Kinetic.Line({
points: [sx,sy+pw, sx,sy-pw, ex-3*pw,ey-pw, ex-3*pw,ey-2*pw, ex,ey, ex-3*pw,ey+2*pw, ex-3*pw, sy+pw],
fill: '#EDECEB',
stroke: '#AFACA9',
strokeWidth: 2,
closed: true,
rotation: pr,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: {x:2,y:2},
shadowOpacity: 0.5
});
return poly;
}
Here is a solution: http://jsfiddle.net/K5Zhg/20/
The problem was that when you rotate the arrow shape, it rotates around x=0 and y=0. However your point was drawn on x=sx and y=sy (in your example case x=60 and y=50).
To fix this draw the points around x=0 and y=0 (and translating the other points in the array using the correct variables) and then setting the x and y property of the Kinetic.Line to sx and sy in order set the position back to its intended location. I.e.
function arrow(psx, psy, pex, pey, pw) {
var w = stage.getWidth();
var h = stage.getHeight();
var sx = psx*w;
var ex = pex*w;
var sy = psy*h;
var ey = pey*h;
// console.log(sx);
var pr = (Math.atan2(ey-sy, ex-sx)/(Math.PI/180));
var pl = Math.sqrt(Math.pow((ex-sx),2)+Math.pow((ey-sy),2));
ex = sx+pl;
ey = sy;
var poly = new Kinetic.Line({
points: [0,0+pw,
0,0-pw, ex-sx-3*pw,ey-sy-pw, ex-sx-3*pw,ey-sy-2*pw, ex-sx,ey-sy, ex-sx-3*pw,ey-sy+2*pw, ex-sx-3*pw, 0+pw],
fill: 'blue',
stroke: 'black',
strokeWidth: 2,
closed: true,
rotation: pr,
x: sx,
y: sy,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: {x:2,y:2},
shadowOpacity: 0.5
});
return poly;
}
I also changed the y value to 0.1 instead of the 0.2 such that the start of the second arrow connects with the red dot.
Oh and I updated the Fiddle to uses v5.0.1.

Categories