Suppose I have a map of the world.
And I'd like each continent to be an area where I could attach shapes to and drag/reshape them, while always being clipped by the continent's shape borders/limits.
Here's what I have so far:
const stage = new Konva.Stage({
container: 'stage',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.arc(250, 120, 50, 0, Math.PI * 2, false);
ctx.arc(150, 120, 60, 0, Math.PI * 2, false);
},
});
const shape = new Konva.Rect({
x: 150,
y: 70,
width: 100,
height: 50,
fill: "green",
stroke: "black",
strokeWidth: 4,
draggable: true,
});
group.add(shape);
layer.add(group);
stage.add(layer);
body {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/8.4.0/konva.min.js"></script>
<div id="stage"></div>
My question is, how could I use the clipFunc to draw a continent's limits? Could I use and image? svg path? I can't seem to find the answer in the docs.
[Edit: Added a new option 2, added demos for options 2 & 3 codepen + snippets.]
TLDR: Nothing totally automatic but two possible options.
Just to confirm - based on
And I'd like each continent to be an area where I could attach shapes
to and drag/reshape them, while always being clipped by the
continent's shape borders/limits.
I think you are asking how to limit the boundaries for dragging a shape to an 'arbitrary' region. I say arbitrary because it is a non-geometric region (not a square, circle, pentagon etc).
It would be fabulous to have a baked-in function to achieve this but sadly I am not aware that it is possible. Here's why:
Dragbounds limits: In terms of what you get 'out of the box', how Konva handles constraining drag position is via the node.dragBoundFunc(). Here is the example from the Konva docs which is straightforward.
// get drag bound function
var dragBoundFunc = node.dragBoundFunc();
// create vertical drag and drop
node.dragBoundFunc(function(pos){
// important pos - is absolute position of the node
// you should return absolute position too
return {
x: this.absolutePosition().x,
y: pos.y
};
});
The gist of this is that we are able to use the code in the dragBoundFunc function to decide if we like the position the shape is being dragged to, or not. If not we can override that 'next' position with our own.
Ok - so that is how dragging is constrained via dragBoundFunc. You can also use the node.on('dragmove') to achieve the same effect - the code would be very similar.
Hit testing
To decide in the dragBoundFunc whether to accept the proposed position of the shape being dragged, we need to carry out 'hit testing'.
[Aside: An important consideration is that, to make a pleasing UI, we should be hit testing at the boundary of the shape that is being dragged - not where the mouse pointer or finger are positioned. Example - think of a circle being dragged with the mouse pointer at its center - we want to show the user the 'hit' UI when the perimeter of the circle goes 'out of bounds' from the perspective of the dragBoundFunc, not when the center hits that point. What this means in effect is that our logic should check the perimeter of the shape for collision with the boundary - that might be simple or more difficult, depending on the shape.]
So we know we want to hit test our dragging shape against an arbitrary, enclosing boundary (the country border).
Option #1: Konva built-in method.
[Update] On developing the demo for this option I discovered that its mainstay, getIntersection(pt), is deliberately disabled (will always return null) when used in a dragmove situation. This is by design and done for performance because the overhead for the process is so costly.
What getIntersection does is to look at a given pixel, from topmost shape down, of the shapes that might overlap the given x, y point. It stops at the first hit. To do this is draws in an off-screen canvas each shape, checks the pixel, and repeats until no shapes remain. As you can tell, quite a costly process to run in-between mousemove steps.
The proposal for this option was to check a bunch of static border points on the stage via getIntersection - if the dragging shape came up as the hit then we would know the border was being crossed.
What point do we give it to check ? So here's the rub - you would have to predefine points on your map that were on the borders. How many points? Enough so that your draggable shapes can't stray very far over the mesh of border points without the hit-test firing. Do it correctly and this would be a very efficient method of hit testing. And it's not as if the borders will be changing regularly.
I made a simple point creator here. This is the view after I created the points around Wombania.
** Option #2: The concept is to create an off-screen canvas the same size as the client rect of the shape being dragged and create a clone of the drag shape therein. Space around the shape would be transparent, shape itself would be colored. We now use a set of pre-defined points along the country boundary, example above. We filter the points inside the shape's clientRect - so we only have a handful to test. We then 'translate' those points to the appropriate location in the off-screen hit canvas and check the color of the pixels at those points - any color whatsoever indicates the dragging shape is 'over' the point we are testing. Any hit means we can break out of the loop and report a boundary collision.
This is optimised in the following ways:
1 - we only make the offscreen canvas once at the dragstart.
2 - we only test the minimum number of boundary points - only those falling in the bounding box of the dragging shape.
Demo here at codepen. Snippet below - best consumed full screen.
const scale = 1,
stage = new Konva.Stage({
container: "container",
width: 500,
height: 400,
draggable: false
}),
layer = new Konva.Layer({
draggable: false
}),
imageShape = new Konva.Image({
x: 0,
y: 0,
draggable: false
}),
// Rect drawn to show client rect of dragging shape
theShapeRect = new Konva.Rect({
stroke: "silver",
strokeWidth: 1,
listening: false
}),
// small dots to show check points
pointCircle = new Konva.Circle({
radius: 30,
fill: "silver",
draggable: false
}),
// the three draggable shape defs - select by button
dragShapes = {
circle: new Konva.Circle({
radius: 30,
fill: "lime",
draggable: true,
visible: false
}),
rectangle: new Konva.Rect({
width: 60,
height: 60,
fill: "lime",
draggable: true,
visible: false
}),
star: new Konva.Star({
numPoints: 6,
innerRadius: 40,
outerRadius: 70,
fill: "lime",
draggable: true,
visible: false
})
},
// data for the check points.
data = `{"pt0":{"x":85.5,"y":44.5},"pt1":{"x":76,"y":62},"pt2":{"x":60,"y":78},"pt3":{"x":47,"y":94},"pt4":{"x":33,"y":115},"pt5":{"x":26,"y":133},"pt6":{"x":17,"y":149},"pt7":{"x":27,"y":171},"pt8":{"x":45,"y":186},"pt9":{"x":69,"y":187},"pt10":{"x":87,"y":191},"pt11":{"x":104,"y":194},"pt12":{"x":123,"y":214},"pt13":{"x":124,"y":238},"pt14":{"x":120,"y":260},"pt15":{"x":94,"y":265},"pt16":{"x":92,"y":275},"pt17":{"x":113,"y":281},"pt18":{"x":130,"y":280},"pt19":{"x":148,"y":280},"pt20":{"x":156,"y":261},"pt21":{"x":169,"y":248},"pt22":{"x":188,"y":251},"pt23":{"x":201,"y":263},"pt24":{"x":207,"y":274},"pt25":{"x":195,"y":281},"pt26":{"x":181,"y":285},"pt27":{"x":183,"y":291},"pt28":{"x":194,"y":293},"pt29":{"x":222,"y":293},"pt30":{"x":242,"y":284},"pt31":{"x":245,"y":257},"pt32":{"x":247,"y":238},"pt33":{"x":263,"y":236},"pt34":{"x":278,"y":240},"pt35":{"x":293,"y":239},"pt36":{"x":305,"y":238},"pt37":{"x":315,"y":237},"pt38":{"x":333,"y":236},"pt39":{"x":337,"y":248},"pt40":{"x":324,"y":258},"pt41":{"x":303,"y":263},"pt42":{"x":314,"y":267},"pt43":{"x":326,"y":273},"pt44":{"x":347,"y":273},"pt45":{"x":364,"y":273},"pt46":{"x":378,"y":260},"pt47":{"x":401,"y":263},"pt48":{"x":422,"y":272},"pt49":{"x":429,"y":278},"pt50":{"x":414,"y":281},"pt51":{"x":400,"y":287},"pt52":{"x":411,"y":294},"pt53":{"x":434,"y":292},"pt54":{"x":462,"y":287},"pt55":{"x":478,"y":275},"pt56":{"x":474,"y":259},"pt57":{"x":466,"y":233},"pt58":{"x":470,"y":208},"pt59":{"x":483,"y":189},"pt60":{"x":484,"y":169},"pt61":{"x":494,"y":153},"pt62":{"x":496,"y":129},"pt63":{"x":489,"y":106},"pt64":{"x":472,"y":91},"pt65":{"x":458,"y":78},"pt66":{"x":443,"y":65},"pt67":{"x":428,"y":54},"pt68":{"x":412,"y":41},"pt69":{"x":394,"y":31},"pt70":{"x":369,"y":23},"pt71":{"x":346,"y":22},"pt72":{"x":323,"y":22},"pt73":{"x":300,"y":23},"pt74":{"x":278,"y":24},"pt75":{"x":265,"y":26},"pt76":{"x":251,"y":30},"pt77":{"x":235,"y":32},"pt78":{"x":220,"y":38},"pt79":{"x":203,"y":44},"pt80":{"x":189,"y":53},"pt81":{"x":174,"y":57},"pt82":{"x":163,"y":51},"pt83":{"x":148,"y":53},"pt84":{"x":128,"y":52},"pt85":{"x":100,"y":51}}`,
// load the data into an object.
pointsList = JSON.parse(data);
// shape is set when the shape-type button is clicked.
let theShape = undefined;
// Add shapes to the layer and layer to stage
layer.add(
imageShape,
dragShapes.circle, // not visible at this point
dragShapes.rectangle, // not visible at this point
dragShapes.star, // not visible at this point
theShapeRect
);
stage.add(layer);
// Make the hit stage where we will do color sampling
const hitStage = new Konva.Stage({
container: "container2",
width: 300,
height: 300,
draggable: true
}),
hitLayer = new Konva.Layer(),
ctx = hitLayer.getCanvas().getContext(); // Get the convas context for access to pixel data
hitStage.add(hitLayer);
// Make an HTML image variable to act as the image loader, load the image
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
imageShape.image(img); // when loaded give the image to the Konva image shape
};
img.src = "https://assets.codepen.io/255591/map_of_wombania2.svg"; // start image loading - fires onload above.
// draw a small grey dot centered on each test point
for (const [key, pt] of Object.entries(pointsList)) {
layer.add(
pointCircle.clone({
name: key + " point",
radius: 5,
x: pt.x,
y: pt.y
})
);
}
// Function to get the color data for given point on a given canvas context
function getRGBAInfo(ctx, point) {
// get the image data for one pixel at the computed point
const pixel = ctx.getImageData(point.x, point.y, 1, 1);
const data = pixel.data;
// for fun, we show the rgba value at the pixel
const rgba =
"pt " +
JSON.stringify(point) +
` rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
// console.log(rgba);
return data;
}
// function to reset collided point colors
function clearPoints() {
// clear the collision point colors
const points = stage.find(".point");
for (const point of points) {
point.fill("silver");
}
}
// variable to track whether we collided or not.
let hit = false;
// user clicks a shape-select button
$(".shapeButton").on("click", function () {
setShape($(this).data("shape"));
});
// Set the active shape.
function setShape(shapeName) {
clearPoints();
if (theShape) {
theShape.visible(false);
}
theShape = dragShapes[shapeName];
// Somewhere in Wombania....
theShape.position({
x: 300,
y: 120
});
// finally we see the shape !
theShape.visible(true);
// and set the bounding rect visualising rect
theShapeRect.position(theShape.getClientRect());
theShapeRect.size(theShape.getClientRect());
// better clear any listeners on the shape just in case
theShape.off();
// fires once as the drag commences
theShape.on("dragstart", function (evt) {
// clear the hitLayer for color testing
hitLayer.destroyChildren();
// make a copy of the dragging shape, positioned at top-left of hit canvas
// Note I fill shape with solid color - if you drag a Konva.Group then make a filled rect
// the pos & size of the group.getClientRect and add that into the group after cloning.
const clone = evt.target.clone({ fill: "red", stroke: "red" });
clone.position({
x: clone.width() / 2,
y: clone.height() / 2
});
hitLayer.add(clone);
// cloning copies some events so better clear them as they are not needed on the clone.
clone.off();
// reset the boundary point color
clearPoints();
// position the client rect visulaiser
theShapeRect.position(theShape.getClientRect());
theShapeRect.size(theShape.getClientRect());
});
// Will run on each drag move event
theShape.on("dragmove", function (evt) {
// assume no collisions - we will know by the end of the event
hit = false;
// position the client rect visulaiser
theShapeRect.position(theShape.getClientRect());
// Get the translation vector from the drag shape in the main canvas to the location
// in the hit canvas. We use thit to translate the check points in the main canvas
// to their positions in the hit canvas
const translateDist = {
x: -this.position().x + this.width() / 2,
y: -this.position().y + this.width() / 2
};
// get a rect around the current pos of the draggging shape, use to check if points
// are within this rect. If YES then process them, otherwise ignore.
const checkRect = this.getClientRect();
// Walk the set of check points...
for (const [key, pt] of Object.entries(pointsList)) {
// Is this point in the client rect of the dragging shape ?...
if (
checkRect.x < pt.x &&
checkRect.y < pt.y &&
checkRect.x + checkRect.width > pt.x &&
checkRect.y + checkRect.height > pt.y
) {
//...yes - so we pocess it
// translate the point to its position in the hit canvas.
let pointTranslated = {
x: pt.x + translateDist.x,
y: pt.y + translateDist.y
};
// get the color info of the point
const colorInfo = getRGBAInfo(ctx, pointTranslated);
// Is there any color there, anything, at all, maybe ?
if (colorInfo[0] + colorInfo[1] + colorInfo[2] + colorInfo[3] > 0) {
// if we find color then we have a collision!
hit = true;
// set the color of the collided point to visualise it
stage.findOne("." + key).fill("black");
// !Important: In live code we could 'break' here because it is not
// important to know _all_ the hits. I will process them all for demo purposes.
// break;
}
}
}
// Phew - after all that point fettling, if we got a hit then say so !
if (hit) {
$("#alarm").html("Boundary collision");
evt.target.fill("red");
} else {
evt.target.fill("lime");
$("#alarm").html("Still good");
}
});
}
body {
margin: 10px;
background-color: #f0f0f0;
}
.container {
border: 1px solid black;
display: inline-block;
}
alarm {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#8/konva.min.js"></script>
<p><span id='info'>Pick a shape, drag it around the country without hitting the edges!</span></p>
<p><span id='alarm'>.</span></p>
<p>
<button class="shapeButton" data-shape='circle'>Circle</button>
<button class="shapeButton" data-shape='rectangle'>Rectangle</button>
<button class="shapeButton" data-shape='star'>Star</button>
</span></p>
<div id="container" class='container'></div>
<div id="container2" class='container'></div>
Option #3: Alpha value checking.
The gist of this method is to have the color fill of each country have a specific alpha value in its RGBA setting. You can then check the colors at specific points on the perimeter of your dragging shape. Lets say we set the alpha for France to 250,
the Channel is 249, Spain 248, Italy 247, etc. If you are dragging your shape around 'inside' France, you expect an alpha value of 250. If you see anything else under any of those perimeter points then some part of your shape has crossed the border. [In practice, the HTML canvas will add some antialiasing along the border line so you will see some values outside those that you set but these have a low impact affect and can be ignored.]
One point is that you can't test the color on the main canvas if the shape being dragged is visible - because you will be getting the fill, stroke, or antialised pixel color of the shape!
To solve this you need a second stage - this can be memory only, so not visible on the page - where you load either a copy of the main stage with the dragging shape invisible, or you load the image of the map only. Let's call this the hit-stage. Assuming you keep the position of the hit-stage in line with the main-stage, then everything will work. Based on the location of the dragging shape and its perimeter points, you check the pixel colors on the hit-canvas. If the values match the country you are expecting then no hit, but if you see a different alpha value then you hit or passed the border. Actually you don't even need to know the color for the starting country - just note the color under the mouse point when the drag commences and look out for a different alpha value under the perimeter points.
There's a working demo of the 2-stage approach at codePen here. The demo just uses a country boundary and 'elsewhere' but you would use the same technique to construct an atlas of countries with different alpha values for your needs.
This is the JavaScript from the codepen demo. Best seen in full screen though when I checked it after copying from codepen some of the detections on right hand side did not fire, so maybe view the codepen if you can.
const countries = [
{ name: "wombania", alpha: 252 },
// add more countries as required
{ name: "Elsewhere", alpha: 0 }
],
scale = 1,
stage = new Konva.Stage({
container: "container",
width: 500,
height: 400,
draggable: false,
scale: {
x: scale,
y: scale
}
}),
layer = new Konva.Layer({
draggable: false
}),
imageShape = new Konva.Image({
x: 0,
y: 0,
draggable: false
}),
circle = new Konva.Circle({
radius: 30,
fill: "lime",
draggable: true,
x: 300,
y: 120,
scale: {
x: scale,
y: scale
}
});
let currentCountry = undefined;
const hitStage = new Konva.Stage({
container: "container2",
width: 500,
height: 400,
draggable: false
}),
hitLayer = new Konva.Layer(),
hitImage = new Konva.Image(),
ctx = hitLayer.getCanvas().getContext(); // Get the convas context for access to pixel data
layer.add(imageShape, circle);
stage.add(layer);
hitLayer.add(hitImage);
hitStage.add(hitLayer);
// Make an HTML image variable to act as the image loader, load the image
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
imageShape.image(img); // when loaded give the image to the Konva image shape
hitImage.image(img); // and to the hit canvas
const hitImageObj = new Image();
};
img.src = "https://assets.codepen.io/255591/map_of_wombania2.svg"; // start image loading - fires onload above.
// Will run on each drag move event
circle.on("dragmove", function () {
// get 20 points on the perimeter to check.
let hitCountry = currentCountry;
for (let angle = 0; angle < 360; angle = angle + 18) {
const angleRadians = (angle * Math.PI) / 180;
let point = {
x: parseInt(
circle.position().x + Math.cos(angleRadians) * circle.radius(),
10
),
y: parseInt(
circle.position().y + Math.sin(angleRadians) * circle.radius(),
10
)
};
// get the image data for one pixel at the computed point
const pixel = ctx.getImageData(point.x, point.y, 1, 1);
const data = pixel.data;
// for fun, we show the rgba value at the pixel
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
// console.log("color at (" + point.x + ", " + point.y + "):", rgba);
// Here comes the good part.
// We know the alpha value for the current country - any other value means
// we crossed the border!
let country = getCountryAtPoint(point);
if (country && country.name !== currentCountry.name) {
hitCountry = country;
break; // jump out of the loop now because we know we got a hit.
}
}
// After checking the points what did the hit indicator show ?
if (hitCountry.alpha !== currentCountry.alpha) {
circle.fill("magenta");
$("#alarm").html("You crossed the border into " + hitCountry.name);
} else {
circle.fill("lime");
$("#alarm").html("Still inside " + hitCountry.name);
}
});
function getRGBAInfo(ctx, point) {
// get the image data for one pixel at the computed point
const pixel = ctx.getImageData(point.x, point.y, 1, 1);
const data = pixel.data;
// for fun, we show the rgba value at the pixel
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
return data;
}
imageShape.on("mousemove", function () {
const point = stage.getPointerPosition();
getRGBAInfo(ctx, point);
});
function getCountryAtPoint(point) {
const colorInfo = getRGBAInfo(ctx, point);
for (const country of countries) {
if (country.alpha === colorInfo[3]) {
$("#info2").html("Selected: " + country.name);
return country;
}
}
}
imageShape.on("mousedown", function () {
currentCountry = getCountryAtPoint(stage.getPointerPosition());
});
circle.on("mousedown", function () {
currentCountry = getCountryAtPoint(stage.getPointerPosition());
});
body {
margin: 10px;
background-color: #f0f0f0;
}
.container {
border: 1px solid black;
display: inline-block;
}
alarm {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#8/konva.min.js"></script>
<p><span id='info'>Drag the circle around the country without hitting the edges!</span></p>
<p><span id='info2'>Selected: none</span> <span id='alarm'></span></p>
<div id="container" class='container'></div>
<div id="container2" class='container'></div>
PS. As a bonus, knowing the alpha values of the countries gives you an instant way to know which country the user clicks on. See the mousedown event.
To use an image, you can use the drawImage method of the canvas context in the clipFunc
const image = new Image();
image.src = 'image.png';
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.drawImage(image, 0, 0);
},
});
To use an SVG path, you can use the clip method of the canvas context in the clipFunc
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.clip('M10,10 h80 v80 h-80 Z');
},
});
I'd like to rotate an Image about an absolute rotate point with kinectJS. For example about
var rotatePoint = {x: 500, y: 500}
The rotation schould be initialised by clicking at the image and moving the mouse, i.e. by dragging and dropping the image. Therby it schould rotate about the same angle, the mouse is drawing.
Yesterday I worked all day at this problem an coudn't find a solution.
Any ideas?
Thanks!
Thank you!
I finaly got the clue.
In the following sample-code the image imgObj rotates about
imgObj.rotPoint = {x: 500, y: 500}
There where several problems to solve:
You can't just set an absolute rotate point, you have to change the offset relative to its standard-position (upper left edge of the image). Then you have to move the image back, because the change of the offset moved the image.
For the roation I enabled dragging and used a dragBoundFunc.
Setting
return {x: this.getAbsolutePosition().x, y: this.getAbsolutePosition().y};
will asure you, that there will be no real dragging - just rotation.
For the rotation itself you need six values:
Three values at the beginning of the dragging:
posMouseStart: x-y-position of the mouse
radMouseStart: angle between the positive x-axis and the vector from the rotation point to posMouseStart in radians
radImageStart: the current rotation of the image (maybe it is already rotated?)
I get these values by binding onmousedown and by just using these values if there is a dragging and dropping.
Three values that change all the time while dragging and dropping:
posMouseNow: x-y-position of the mouse right in that moment
radMouseNow: angle between the positive x-axis and the vector from the rotation point to posMouseStart in radians right in that moment
radImageNow: the current rotation of the image (maybe it is already rotated?) right in that moment
I get these values in my dragBoundFunc.
While using Math.atan2() for getting the angles, you have to concern, that you're not in an x-y-coordinatesystem but in an x-(-y)-coordinatesystem - so use: Math.atan2(-y,x).
By subtracting radMouseStart from radMouseNow you get the angle you would have to rotate about, to get the image from the start-position to the now-position. But, when we would rotate about this angle, the image would rotate like crazy.
Why is it like that? - There are several miliseconds between "start" and "now" where the image is already rotating. So: when you're "now" rotating, you don't beginn at radMouseStart but at radImageNow - radImageStart + radMouseStart.
\o/ all problems are solved \o/
The code:
var imgObj = new Kinetic.Image
({
image: YourImg,
x: YourSize.x,
y: YourSize.y,
draggable: true
});
imgObj.rotPoint = {x: 500, y: 500};
imgObj.setOffset
(
imgObj.rotPoint.x - imgObj.getAbsolutePosition().x,
imgObj.rotPoint.y - imgObj.getAbsolutePosition().y
);
imgObj.move(imgObj.getOffsetX(),imgObj.getOffsetY());
var o = {x: imgObj.rotPoint.x, y: imgObj.rotPoint.y}; // shortcut
imgObj.on('mousedown', function()
{
posMouseStart = stage.getMousePosition();
radMouseStart = Math.atan2(-(posMouseStart.y - o.y), posMouseStart.x - o.x);
radImageStart = this.getRotation();
});
imgObj.setDragBoundFunc(function(pos)
{
var posMouseNow = stage.getMousePosition();
var radMouseNow = Math.atan2(-(posMouseNow.y - o.y), posMouseNow.x - o.x);
var radImageNow = this.getRotation();
var radMouseDiff = -(radMouseNow - radMouseStart);
this.rotate(radImageStart + radMouseDiff - radImageNow);
return {x: this.getAbsolutePosition().x, y: this.getAbsolutePosition().y};
});
You can move an image around a fixed rotation point using some trigonometry
Calculate the angle of the mouse position relative to the rotation point using Math.atan2:
var dx=mouseX-rotationPointX;
var dy=mouseY-rotationPointY;
var radianAngle=Math.atan2(dy,dx);
Move the image around the rotationPoint using Math.cos and Math.sin:
var x=rotationPointX+radius*Math.cos(radianAngle)-imgWidth/2;
var y=rotationPointY+radius*Math.sin(radianAngle)-imgHeight/2;
image.setPosition(x,y);
Here's working example code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Prototype</title>
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<script src="http://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v4.5.4.min.js"></script>
<style>
body{ padding:15px; }
#container{
border:solid 1px #ccc;
margin-top: 10px;
width:300px;
height:300px;
}
</style>
<script>
$(function(){
var stage = new Kinetic.Stage({
container: 'container',
width: 300,
height: 300
});
var layer = new Kinetic.Layer();
stage.add(layer);
var rx=150;
var ry=150;
var radius=75;
var image;;
var imgWidth=50;
var imgHeight=50;
var rotationPoint=new Kinetic.Circle({
x:rx,
y:ry,
radius:10,
fill:"green"
});
layer.add(rotationPoint);
$(stage.getContent()).on('mousemove', function (event) {
// get the current mouse position on the stage
var pos=stage.getMousePosition();
var mouseX=parseInt(pos.x);
var mouseY=parseInt(pos.y);
// calculate the mouse angle relative
// to the rotation point [rx,ry]
var dx=mouseX-rx;
var dy=mouseY-ry;
var radianAngle=Math.atan2(dy,dx);
// "normalize" the angle so it always is in a proper range (0-2*PI)
radianAngle=(radianAngle+Math.PI*2)%(Math.PI*2);
// calculate the new image x,y
// based on the angle
var x=rx+radius*Math.cos(radianAngle)-imgWidth/2;
var y=ry+radius*Math.sin(radianAngle)-imgHeight/2;
image.setPosition(x,y);
layer.draw();
});
var img=new Image();
img.onload=function(){
image=new Kinetic.Image({
image:img,
x:0,
y:0,
width:imgWidth,
height:imgHeight,
});
layer.add(image);
layer.draw();
}
img.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/house-icon.png";
}); // end $(function(){});
</script>
</head>
<body>
<p>Move mouse to rotate image around</p>
<p>the green dot which is located at</p>
<p>the rotation point</p>
<div id="container"></div>
</body>
</html>
In the following fiddle, you can click and drag around the image, and it will not be able to exit the blue border. By clicking the red and green rectangles, you can rotate the image. However when you click and drag a rotated object, the image does not follow the mouse. I would like the image to follow the mouse even if it is rotated.
http://jsfiddle.net/n3Sn5/
I think the issue occurs within my move function
move = function (dx, dy)
{
nowX = Math.min(boundary.attr("x")+boundary.attr("width")-this.attr("width"), this.ox + dx);
nowY = Math.min(boundary.attr("y")+boundary.attr("height")-this.attr("height"), this.oy + dy);
nowX = Math.max(boundary.attr("x"), nowX);
nowY = Math.max(boundary.attr("y"), nowY);
this.attr({x: nowX, y: nowY });
}
One thing to notice is that when you click and drag a rotated object, after you release your mouse click, if you rotate the image, it snaps to where your mouse was when you released the mouse click, even obeying the boundary.
I was able to get the rotated image to drag with the mouse previously, but by adding the boundary rectangle, i had to use a more complex approach.
If anyone has an idea of what I need to change, I would be very grateful!
Thanks!
The required output can be achieved in a bit different way. Please check the fiddle at http://jsfiddle.net/6BbRL/. I have trimmed to code to keep the basic parts for demo.
var paper = Raphael(0, 0, 475, 475),
boxX = 100,
boxY = 100,
boxWidth = 300,
boxHeight = 200,
// EDITED
imgWidth = 50,
imgHeight = 50,
box = paper.rect(boxX, boxY, boxWidth, boxHeight).attr({fill:"#ffffff"}),
// EDITED
html5 = paper.image("http://www.w3.org/html/logo/downloads/HTML5_Badge_512.png",boxX+boxWidth-imgWidth,boxY+boxHeight-imgHeight,imgWidth,imgHeight)
.attr({cursor: "move"}),
elementCounterClockwise = paper.rect(180, 0, 50, 50).attr({fill:"#ff5555", cursor:"pointer"}),
elementClockwise = paper.rect(250, 0, 50, 50).attr({ fill: "#55ff55", cursor: "pointer" }),
boundary = paper.rect(50,50,400,300).attr({stroke: '#3333FF'}),
transform,
// EDITED
xBound = {min: 50 + imgWidth/2, max: 450 - imgWidth/2},
yBound = {min: 50 + imgHeight/2, max: 350 - imgHeight/2};
start = function (x, y) {
// Find min and max values of dx and dy for "html5" element and store them for validating dx and dy in move()
// This is required to impose a rectagular bound on drag movement of "html5" element.
transform = html5.transform();
}
move = function (dx, dy, x, y) {
// To restrict movement of the dragged element, Validate dx and dy before applying below.
// Here, dx and dy are shifts along x and y axes, with respect to drag start position.
// EDITED
var deltaX = x > xBound.max && xBound.max - x || x < xBound.min && xBound.min - x || 0;
deltaY = y > yBound.max && yBound.max - y || y < yBound.min && yBound.min - y || 0;
this.attr({transform: transform + 'T'+ [dx + deltaX, dy + deltaY]});
}
up = function () {
};
html5.drag(move, start, up);
elementClockwise.click(function() {
html5.animate({transform: '...r90'}, 100);
})
elementCounterClockwise.click(function() {
html5.animate({transform: '...r-90'}, 100);
})
Use of '...' to append a transformation to the pre-existing transformation state (Raphael API) is important for the rotational issue. While, for translating the element on drag requires absolute translation, which neglects the rotational state of the element while translating the element.
//EDIT NOTE
Drag bounding is worked on and updated. However, there remains an issue with incorporating the difference between mouse position and image center.
I can help you with your rotation and drag problem, you need to store the rotation and apply it after you have moved the object.
elementClockwise.node.onclick = function()
{
html5.animate({'transform': html5.transform() +'r90'}, 100, onAnimComplete);
}
elementCounterClockwise.node.onclick = function()
{
html5.animate({'transform': html5.transform() +'r-90'}, 100, onAnimComplete);
}
function onAnimComplete(){
default_transform = html5.transform();
}
At present I can't get the boundary to work, but will have a try later.
http://jsfiddle.net/n3Sn5/2/
I bumped into the following problem, hope someone will know how to help me:
I work with the JavaScript library Raphael. Now, what I want to do is, when I have many Raphael SVG elements, to simply select more elements with "rectangle selection", i.e. by dragging the mouse starting from the graph's background to create a selection rectangle (I hope I was clear enough), and move the elements which are in this rectangle.
For now, I've found something like this (someone posted it from a previous question of mine):
var paper = Raphael(0, 0, '100%', '100%');
var circle = paper.circle(75, 75, 50);
var rect = paper.rect(150, 150, 50, 50);
var set = paper.set();
set.push(circle, rect);
set.attr({
fill: 'red',
stroke: 0
});
var ox = 0;
var oy = 0;
var dragging = false;
set.mousedown(function(event) {
ox = event.screenX;
oy = event.screenY;
set.attr({
opacity: .5
});
dragging = true;
});
set.mousemove(function(event) {
if (dragging) {
set.translate(event.screenX - ox, event.screenY - oy);
ox = event.screenX;
oy = event.screenY;
}
});
set.mouseup(function(event) {
dragging = false;
set.attr({
opacity: 1
});
});
This code can be executed on jsfiddle. But, as you can see, this selects ALL the elements, by simply adding them to a Raphael set.
Now, I think that my problem will be solved by:
Making a rectangle selection
Adding the nodes which are in the rectangle to a Raphael set
Move only the selected items (i.e. move only the items which are in the Raphael set with set.mousemove)
My problem for now would be the first two issues.
Any ideas how to do this?
Thank you in advance!
Fun problem. You can do this by placing a rectangular "mat" the size of the canvas behind all of your other objects and attaching a drag event to it for selecting other elements. (Note this solution uses the newer version of Raphael, 2.1.0:
var paper = Raphael(0, 0, '100%', '100%');
//make an object in the background on which to attach drag events
var mat = paper.rect(0, 0, paper.width, paper.height).attr("fill", "#FFF");
var circle = paper.circle(75, 75, 50);
var rect = paper.rect(150, 150, 50, 50);
var set = paper.set();
set.push(circle, rect);
set.attr({
fill: 'red',
stroke: 0
});
//the box we're going to draw to track the selection
var box;
//set that will receive the selected items
var selections = paper.set();
Now, we add a drag event -- similar to the mouseover events but with three functions (see documentation), and draw a box to track the selection space:
//DRAG FUNCTIONS
//when mouse goes down over background, start drawing selection box
function dragstart (x, y, event) {
box = paper.rect(x, y, 0, 0).attr("stroke", "#9999FF");
}
// When mouse moves during drag, adjust box.
// If the drag is to the left or above original point,
// you have to translate the whole box and invert the dx
// or dy values since .rect() doesn't take negative width or height
function dragmove (dx, dy, x, y, event) {
var xoffset = 0,
yoffset = 0;
if (dx < 0) {
xoffset = dx;
dx = -1 * dx;
}
if (dy < 0) {
yoffset = dy;
dy = -1 * dy;
}
box.transform("T" + xoffset + "," + yoffset);
box.attr("width", dx);
box.attr("height", dy);
}
function dragend (event) {
//get the bounds of the selections
var bounds = box.getBBox();
box.remove();
reset();
console.log(bounds);
for (var c in set.items) {
// Here, we want to get the x,y vales of each object
// regardless of what sort of shape it is.
// But rect uses rx and ry, circle uses cx and cy, etc
// So we'll see if the bounding boxes intercept instead
var mybounds = set[c].getBBox();
//do bounding boxes overlap?
//is one of this object's x extremes between the selection's xe xtremes?
if (mybounds.x >= bounds.x && mybounds.x <= bounds.x2 || mybounds.x2 >= bounds.x && mybounds.x2 <= bounds.x2) {
//same for y
if (mybounds.y >= bounds.y && mybounds.y <= bounds.y2 || mybounds.y2 >= bounds.y && mybounds.y2 <= bounds.y2) {
selections.push(set[c]);
}
}
selections.attr("opacity", 0.5);
}
}
function reset () {
//empty selections and reset opacity;
selections = paper.set();
set.attr("opacity", 1);
}
mat.drag(dragmove, dragstart, dragend);
mat.click(function(e) {
reset();
});
Just like that, you have a new set (selections) that contains every object that was selected by the mouse drag. You can then apply your mouseover events from the original to that set.
Note that this will select circle objects if you nick the corner of their bounding box with your selection box, even if it doesn't overlap with the area of the circle. You could make a special case for circles if this is a problem.
jsFiddle