Drag and dropping using Raphael js - javascript

I'm trying to build a puzzle game in javascript, using raphael to take care of the drag and drop and rotation of the pieces.
The problem I'm facing is: Whenever I rotate an image, its coordinate system is rotated as well and the drag and drop doesn't work anymore as expected.
Edit2: Here is a simple test case. Drag around the ellipse, rotate it, and then try to drag again:
http://www.tiagoserafim.com/tests/dragdrop.html
Edit: To clarify, whenever I move 1 pixel with the mouse to the right (x++), the image also moves 1 pixel on the x-coord, but on its own coordinate system, that maybe rotate, as the image below shows.
(source: oreilly.com)
As explained on SVG Essentials, this is the expected behavior.
My question is: Is there an elegant way to do what I want, or I'll be forced to manually calculate the correct coords by using rotation matrix?
Other JS libraries or suggestions will be very welcome, even if they mean losing the IE support.

As also noted in that article, the order of transformations is important.
Translate object so the center point (Or whatever other point you want to rotate around) is at 0,0
Rotate
Translate back to previous position
Also note that there is an overload of rotate that already does this.
<html>
<head>
<script type="text/javascript" src="http://github.com/DmitryBaranovskiy/raphael/blob/master/raphael-min.js?raw=true"></script>
<script type="text/javascript">
function Draw()
{
var x = 150, y = 150;
var rotation = 0;
var paper = Raphael(0, 0, 800, 800);
var e = paper.ellipse(x, y, 30, 10);
paper.path("M150 150L800 150");
window.setInterval(function()
{
x += 10;
rotation += 10;
e.translate(10, 0);
e.rotate(rotation, x, y);
}, 500);
}
</script>
</head>
<body onload="Draw()">
</body>
</html>

Related

How do I translate mouse movement distances to SVG coordinate space?

I have an SVG visualization of the distribution of CSS4 color keywords in HSL space here: https://meyerweb.com/eric/css/colors/hsl-dist.html
I recently added zooming via the mouse wheel, and panning via mouse clack-and-drag. I’m able to convert a point from screen space to SVG coordinate space using matrixTransform, .getScreenCTM(), and .inverse() thanks to example code I found online, but how do I convert mouse movements during dragging? Right now I’m just shifting the viewBox coordinates by the X and Y values from event, which means the image drag is faster than the mouse movement when zoomed in.
As an example, suppose I’m zoomed in on the image and am dragging to pan, and I jerk the mouse leftwards and slightly downwards. event.movementX returns -37 and event.movementY returns 6. How do I determine how far that equates to in SVG coordinates, so that the viewBox coordinates are shifted properly?
(Note: I’m aware that there are libraries for this sort of thing, but I’m intentionally writing vanilla JS code in order to learn more about both SVG and JS. So please, don’t post “lol just use library X” and leave it at that. Thanks!)
Edited to add: I was asked to post code. Posting the entire JS seems overlong, but this is the function that fires on mousemove events:
function dragger(event) {
var target = document.getElementById('color-wheel');
var coords = parseViewBox(target);
coords.x -= event.movementX;
coords.y -= event.movementY;
changeViewBox(target,coords);
}
If more is needed, then view source on the linked page; all the JS is at the top of the page. Nothing is external except for a file that just contains all the HSL values and color names for the visualization.
My recommendation:
Don't worry about the movementX/Y properties on the event.
Just worry about where the mouse started and where it is now.
(This has the additional benefit that you get the same result even if you miss some events: maybe because the mouse moved out of the window, or maybe because you want to group events so you only run the code once per animation frame.)
For where the mouse started, you measure that on the mousedown event.
Convert it to a position in the SVG coordinates, using the method you were using,
with .getScreenCTM().inverse() and .matrixTransform().
After this conversion, you don't care where on the screen this point is. You only care about where it is in the picture. That's the point in the picture that you're always going to move to be underneath the mouse.
On the mousemove events, you use that same conversion method to find out where the mouse currently is within the current SVG coordinate system. Then you figure out how far that is from the point (again, in SVG coordinates) that you want underneath the mouse. That's the amount that you use to transform the graphic. I've followed your example and am doing the transform by shifting the x and y parts of the viewBox:
function move(e) {
var targetPoint = svgCoords(event, svg);
shiftViewBox(anchorPoint.x - targetPoint.x,
anchorPoint.y - targetPoint.y);
}
You can also shift the graphic around with a transform on a group (<g> element) within the SVG; just be sure to use that same group element for the getScreenCTM() call that converts from the clientX/Y event coordinates.
Full demo for the drag to pan. I've skipped all your drawing code and the zooming effect.
But the zoom should still work, because the only position you're saving in global values is already converted into SVG coordinates.
var svg = document.querySelector("svg");
var anchorPoint;
function shiftViewBox(deltaX, deltaY) {
svg.viewBox.baseVal.x += deltaX;
svg.viewBox.baseVal.y += deltaY;
}
function svgCoords(event,elem) {
var ctm = elem.getScreenCTM();
var pt = svg.createSVGPoint();
// Note: rest of method could work with another element,
// if you don't want to listen to drags on the entire svg.
// But createSVGPoint only exists on <svg> elements.
pt.x = event.clientX;
pt.y = event.clientY;
return pt.matrixTransform(ctm.inverse());
}
svg.addEventListener("mousedown", function(e) {
anchorPoint = svgCoords(event, svg);
window.addEventListener("mousemove", move);
window.addEventListener("mouseup", cancelMove);
});
function cancelMove(e) {
window.removeEventListener("mousemove", move);
window.removeEventListener("mouseup", cancelMove);
anchorPoint = undefined;
}
function move(e) {
var targetPoint = svgCoords(event, svg);
shiftViewBox(anchorPoint.x - targetPoint.x,
anchorPoint.y - targetPoint.y);
}
body {
display: grid;
margin: 0;
min-height: 100vh;
}
svg {
margin: auto;
width: 70vmin;
height: 70vmin;
border: thin solid gray;
cursor: move;
}
<svg viewBox="-40 -40 80 80">
<polygon fill="skyBlue"
points="0 -40, 40 0, 0 40 -40 0" />
</svg>
So the script needs something so that the vectors moved by the SVG are coordinated against the vectors moved by the mouse on screen. Despite the event being on your target, your SVG, the MouseEvent properties relate to your screen alone.
The movementX read-only property of the MouseEvent interface provides the difference in the X coordinate of the mouse pointer between the given event and the previous mousemove event. In other words, the value of the property is computed like this: currentEvent.movementX = currentEvent.screenX - previousEvent.screenX.
From https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX
The screenX read-only property of the MouseEvent interface provides the horizontal coordinate (offset) of the mouse pointer in global (screen) coordinates.
So what you're measuring, and to the best of my knowledge the only thing you can measure direcly without additional libraries or complication, is the movement of the pointer in pixel terms across the screen. The only way to make this work in terms of vector for movement of your SVG is to translate the on screen movement to the dimensions that are relevant to your scaled SVG.
My initial thinking was that you would be able to work out the scaling of the SVG object, using some combination of its viewbox and its actual width on the screen. Naturally what would initially appear sensible is not. This approach won't work, if it appears to it would be purely by chance.
But it turns out that the solution is essentially to use the same type of code you've used in your scaling when you approach your mouse movements. The .getScreenCTM() and .inverse() functions are exactly what you'll need again. But instead of trying to find a single point on the SVG to work from, you need to find out what the on-screen distance translates to in the SVG by comparing two points on the SVG instead.
What I provide here isn't necessarily the most optimal solution but hopefully helps explain and gives you something to work further from...
function dragger(event) {
var target = document.getElementById('color-wheel');
var coords = parseViewBox(target);
//Get an initial point in the SVG to start measuring from
var start_pt = target.createSVGPoint();
start_pt.x = 0;
start_pt.y = 0;
var svgcoord = start_pt.matrixTransform(target.getScreenCTM().inverse());
//Create a point within the same SVG that is equivalent to
//the px movement by the pointer
var comparison_pt = target.createSVGPoint();
comparison_pt.x = event.movementX;
comparison_pt.y = event.movementY;
var svgcoord_plus_movement = comparison_pt.matrixTransform(target.getScreenCTM().inverse());
//Use the two SVG points created from screen position values to determine
//the in-SVG distance to change coordinates
coords.x -= (svgcoord_plus_movement.x - svgcoord.x);
//Repeat the above, but for the Y axis
coords.y -= (svgcoord_plus_movement.y - svgcoord.y);
//Deliver the changes to the SVG to update the view
changeViewBox(target,coords);
}
Sorry for the long winded answer, but hopefully it explains it from the beginning enough that anyone else looking to find an answer can get the whole picture if they've not come as far as you have in this script.
From MouseEvent, we have clientX and movememntX. Taken together, we can deduce our last location. We can then take the transform of our current location and subtract it from the transform of our last location:
element.onpointermove = e => {
const { clientX, clientY, movementX, movementY } = e;
const DOM_pt = svg.createSVGPoint();
DOM_pt.x = clientX;
DOM_pt.y = clientY;
const { x, y } = DOM_pt.matrixTransform(svgs[i].getScreenCTM().inverse());
DOM_pt.x += movementX;
DOM_pt.y += movementY;
const { x: last_x, y: last_y } = DOM_pt.matrixTransform(svgs[i].getScreenCTM().inverse());
const dx = last_x - x;
const dy = last_y - y;
// TODO: use dx & dy
}

Change an object for a png image on javascript

I adapted an open source game to fit for my fantasy book series, Eloik.
This game
I'd like to replace the blue arc for a png image (about same size).
I know I have to draw an image but how to??
Here's the portion of the code :`
// Shield - Boomlight
context.beginPath();
context.strokeStyle = '#0066cc';
context.lineWidth = 10;
context.arc( player.position.x, player.position.y, player.radius,
player.angle + 1.6, player.angle - 1.6, true );
context.stroke();`
I tried that following code but... The png image doesn't appears at the right spot and it's not interactive with the game as the arc...`
<html>
<body>
<img id="boom" width="176" height="134" src="http://eloik.com/wp/wp-
content/uploads/2017/05/BOOMLIGHT-jeu-bd.png" alt="">
*In the Javascript :
<script>
window.onload = function() {
var image = new Image();
image.src="http://eloik.com/wp/wp-content/uploads/2017/05/BOOMLIGHT-jeu-
bd.png";
context.beginPath();
context.drawImage(image, 10, 10);
}
</script>
</body>
</html> `
So now, what's wrong ?
Thanks ! :)
First, in order to use drawImage, we need to load it. You can do it like this:
/* core.js: line 57 */
// Create a handle for the image
var shieldImage;
/* core.js: line 133 */
// Create a new image
shieldImage = new Image();
// When it's loaded, execute animate
shieldImage.onload = animate;
// Set the src
shieldImage.src = "http://eloik.com/wp/wp-content/uploads/2017/05/BOOMLIGHT-jeu-bd.png";
This way, the animate function will only be called once the image has loaded. Then, in order to position your image and rotate it, you can do this:
/* core.js: line 420 */
// Set the origin of the canvas on the player position
context.translate(player.position.x, player.position.y);
// Rotate the canvas
context.rotate(player.angle + Math.PI + .2);
// Draw the shield
context.drawImage(shieldImage, -player.radius, -player.radius, player.radius*1.5, player.radius*2.3);
// Rotate the canvas back
context.rotate(-player.angle - Math.PI - .2);
// Reset the initial origin of the canvas
context.translate(-player.position.x, -player.position.y);
Since we cannot rotate the image itself, we use this trick, which consists in rotating the canvas, drawing, and reverting the rotation of the canvas. We also translate it in order to have the rotation axis on the player position.
You'll also notice I added some numbers in there. That's because your shield image is not a perfect circle. I distorted it so that it does not look weird with the current collision system (which is based on a circle). If you want to keep the oval shape of the image, you'll need to make more serious changes to the rest of the code so that collisions apply to that shape.
And that's it, your blue arc is replaced with your PNG image (Updated JS here)
PS: You have a cool last name ! - same as mine

Re-aligning Javascript/Canvas drawing?

So I have a semi-complex canvas drawing someone gave me. It draws an image vertically (i.e., top-down). Let's assume its a stick figure with facial features.
This is done in Javascript and Canvas. i.e.: ctx.beginPath(), ctx.moveTo(x,y), ctx.lineTo(1,1), etc.
I want the stick figure to move towards some point (x,y) and to face that direction while moving toward it. For example, if the x,y is near the bottom right, I want the stick figure to be oriented in a way such that its feet are facing towards the bottom right while it is moving.
The main question is, how would I go about doing this (i.e changing the stickman), knowing that I have a "hardcoded" drawing (in this example, stickman) that has been given to me?
You can render the received image on a separate canvas (doesn't need to be displayed) and use ctx.canvas.toDataURL() to convert it to an image. You could then embed the resulting image in your canvas and apply transforms to it more easily.
I mentioned this in a comment on the question but it sounded like fun, so I implemented a proof of concept.
var canvasObject = function(ctx) {
ctx.beginPath();
ctx.moveTo(0,0);
ctx.arc(30,30,15,0,2*Math.PI);
ctx.fillStyle='red';
ctx.fill();
return ctx;
}
var myCtx = document.querySelector('canvas').getContext('2d');
var objCtx = document.createElement('canvas').getContext('2d');
var renderedObjUrl = canvasObject(objCtx).canvas.toDataURL();
var renderedObj = document.createElement('img');
renderedObj.setAttribute('src', renderedObjUrl);
myCtx.drawImage(renderedObj, 30, 10);
<canvas id="myCanvas" width="600" height="400"></canvas>

Rotating a shape in KineticJS seems to move it out of sync with it's group

I am working on some image viewing tools in KineticJS. I have a rotate tool. When you move the mouse over an image, a line appears from the centre of the image to the mouse position, and then when you click and move, the line follows and the image rotates around that point, in sync with the line. This works great.
My issue is, I have the following set up:
Canvas->
Stage->
Layer->
GroupA->
GroupB->
Image
This is because I draw tabs for options on GroupA and regard it as a container for the image. GroupB is used because I flip GroupB to flip the image ( and down the track, any objects like Text and Paths that I add to the image ), so that it flips but stays in place. This all works well. I am also hoping when I offer zoom, to zoom groupb and thus zoom anything drawn on the image, but for groupA to create clipping and continue to support drag buttons, etc.
The object I am rotating, is GroupA. Here is the method I call to set up rotation:
this.init = function(control)
{
console.log("Initing rotate for : " + control.id());
RotateTool.isMouseDown = false;
RotateTool.startRot = isNaN(control.getRotationDeg()) ? 0 : control.getRotationDeg();
RotateTool.lastAngle = control.parent.rotation() / RotateTool.factor;
RotateTool.startAngle = control.parent.rotation();
this.image = control.parent;
var center = this.getCentrePoint();
RotateTool.middleX = this.image.getAbsolutePosition().x + center.x;
RotateTool.middleY = this.image.getAbsolutePosition().y + center.y;
this.image.x(this.image.x() + center.x - this.image.offsetX());
this.image.y(this.image.y() + center.y - this.image.offsetY());
this.image.offsetX(center.x);
this.image.offsetY(center.y);
}
getCentrePoint is a method that uses trig to get the size of the image, based on the rotation. As I draw a line to the centre of the image, I can tell it's working well, to start with. I've also stepped in to it and it always returns values only slightly higher than the actual width and height, they always look like they should be about what I'd expect, for the angle of the image.
Here is the code I use on mouse move to rotate the image:
this.layerMouseMove = function (evt, layer)
{
if (RotateTool.isRotating == false)
return;
if (!Util.isNull(this.image) && !Util.isNull(this.line))
{
if (Item.moving && !RotateTool.isRotating)
{
console.log("layer mousemove item moving");
RotateTool.layerMouseUp(evt, layer);
}
else
{
var pt = this.translatePoint(evt.x, evt.y);
var x = pt.x;
var y = pt.y;
var end = this.getPoint(x, y, .8);
RotateTool.line.points([this.middleX, this.middleY, end.x, end.y]);
RotateTool.line.parent.draw();
RotateTool.sign.x(x - 20);
RotateTool.sign.y(y - 20);
var angle = Util.findAngle({ x: RotateTool.startX, y: RotateTool.startY }, { x: x, y: y }, { x: RotateTool.middleX, y: RotateTool.middleY });
var newRot = (angle) + RotateTool.startAngle;
RotateTool.image.rotation(newRot);
console.log(newRot);
}
}
}
Much of this code is ephemeral, it's maintaining the line ( which is 80% of the length from the centre to my mouse, as I also show a rotate icon, over the mouse.
Sorry for the long windedness, I'm trying to make sure I am clear, and that it's obvious that I've done a lot of work before asking for help.
So, here is my issue. After I've rotated a few times, when I click again, the 'centre' point that the line draws to, is way off the bottom right of my screen, and if I set a break point, sure enough, the absolute position of my groups are no longer in sync. This seems to me like my rotation has moved the image in the manner I hoped, but moved my group off screen. When I set offsetX and offsetY, do I need to also set it on all the children ? But, it's the bottom child I can see, and the top group I set those things on, so I don't really understand how this is happening.
I do notice my image jumps a few pixels when I move the mouse over it ( which is when the init method is called ) so I feel like perhaps I am just out slightly somewhere, and it's causing this flow on effect. I've done some more testing, and my image always jumps slightly up and to the right when I move the mouse over it, and the rotate tool continues to work reliably, so long as I don't move the mouse over the image again, causing my init method to call. It seems like every time this is called, is when it breaks. So, I could just call it once, but I'd have to associate the data with the image then, for the simple reason that once I have many images, I'll need to change my data as my selected image changes. Either way, I'd prefer to understand and resolve the issue, than just hide it.
Any help appreciated.

Rotate sprite javascript

I have a sprite animation, a small cannon rendered using a 3D app. I have exactly 360 frames for a 360 degree turn. Each image has a 100x100 pixel size.
So basically what I am trying todo is when I click anywhere in the page, the barrel of the cannon needs to rotate to point at the mouse cursor, sound simple maybe but I can't really get it to work very well, perhaps cause my math skills is lacking :P
What I currently have is something like this
/* This is my div with the cannon background image (360 images stitched into one) each "image area" is 100x100px */
obj.cannon = $('#cannon');
/* Get the X/Y of the cannon loc in the dom */
var cannonX = $(obj.cannon).offset().left;
var cannonY = $(obj.cannon).offset().top;
/* Get radians using atan2 */
var radians = Math.atan2(e.pageY-cannonY, e.pageX-cannonX);
/* Convert to degrees */
var degrees = radians * (180/Math.PI);
And this is where I am, I mean since the image width is 100px and I need to move the background-position by 100px to move the cannon one degree right, because 360 images * 100px = 36000px in total. So the stitched sprite is like 36000px wide.
So
Insert weird calculation here based on the current backgroundPosition of the image-sprite and apply new backgroundPosition based on where you click with the mouse cursor, then use some sort of setTimeout(animateIt, speed); to "animate" the background position to the new position.
function animateIt(){
if(newpos!=targetpos) { //Use < > here if we need to add or remove
newpos+=100; //Until we arrive at the new backgroundPosition
$(obj.cannon).css({backgroundPosition: newpos+'px' });
setTimeout(animateIt, speed);
}
}
Am I at all on the right track here, am I thinking correctly about this? I feel stupid, this should be a simple thing but right now I am having a brain meltdown I think =P. My problem is I don't know how to properly arrive at the "new target backgroundposition" and then animate it ++ or -- based on the current background position :/
Well, here is a simplified working example with 10 images.
I'll post the code and jsFiddle now, and I might come back later to cover it in depth. But basically you just order your images correctly, and then you pick the segment by using (Segments - Math.floor(degree / (360 / segments))). You may have to adjust your 0 degree. For example, I made my 0 equal to what would normal by 90.
Pay attention to the fact that the screen coordinates, x and y, increase right and down. This makes the degrees of atan work clockwise instead of the usual counter clockwise in coordinate systems where x and y increase right and up.
I added in some text output that shows the degrees and image segment being shown.
jQuery handles normalizing the x and y position nicely. Just take care that your CSS setup is cross browser.
Working jsFiddle example
Here's our image:
Here's our HTML:
<div id="main"><div id="img"></div></div>
<div id="info">
<span></span><br/>
<span></span>
</div>
​
CSS:
div#main {
width:500px;
height:500px;
border:2px #000 solid; }
div#img {
width:94px;
height:119px;
overflow:hidden;
top:50%;
left:50%;
margin-left:-45px;
margin-top:-60px;
position:relative;
background-image:url('http://imgur.com/3UPki.png');
background-position:0;}
div#info {
position: absolute;
bottom:0;
left:0; }
​
Javascript / jQuery:
$(function() {
var position = $("div#img").position(),
mouseX, mouseY, imgX, imgY, degree;
imgX = position.left;
imgY = position.top;
$("#main").mousemove(function(e) {
// degree is arctan y over x (soh,cah,toa)
degree = Math.atan2((e.pageY - imgY),(e.pageX - imgX))*(180 / Math.PI);
degree = (degree - 90) % 360;
// jQuery normalizes pageX and pageY
// transfrom from -180 to 180 ==> 0 to 360
if (degree < 0) degree = 180 + (180 - (Math.abs(degree)));
rotate(degree);
$("span:first").html("Segment: " + (9 - Math.floor(degree / 36)));
$("span:last").html("Degree: " + Math.floor(degree));
});
function rotate(degree) {
var off = 9 - Math.floor(degree / 36);
$("div#img").css("background-position",-off*94);
}
}); ​
Working jsFiddle example
Keep in mind that the degrees you get from atan will start pointing right for zero degrees and go clockwise from there (-90 is up, 90 is down).
Each position of your image should correspond to a specific angle. Once you have the degrees measured (it looks like you have that part right), use some type of mapping to translate your degrees to the proper image offset. I don't know what your image looks like so I can't help with that. Assuming your image starts pointing right, and goes around clockwise from there, the degrees will correspond directly the the offset for the right image. (I suggest you arrange your frames like this for ease...)

Categories

Resources