In my use case, we're allowing a user to define "zones" (polygons) on a map. The basic polygon editing functionality, enabled by just setting editable: true, works well. However, I need some additional functionality.
For example, when the user starts dragging a vertex, I want to highlight nearby vertices on other polygons, and if the user drags over one of them, it will "snap" the lat/lng of the vertex they were dragging to be identical to the vertex the dragged over.
Has anyone successfully inserted some "extra" code into the editing process? Are there any intermediate events being fired on those vertex handles (while dragging, mouse moving, etc.) that I can hook into, interpret, and draw some extra things on the map? What I'm hoping for is someone who can tell me "Oh, if polygon.obfuscatedVariable is set, those are drag handles, and you can listen for mousemove on polygon.obfuscatedVariable[3], retrieve the lat/long, etc."
Hacks and jury-rigged solutions are acceptable: since the built-in editing is so close to what I want, I really don't feel like recreating it from scratch.
I'd since forgotten about this question, but here's our solution to the problem. Hopefully it's helpful!
Short version:
Whenever a mousedown occurs on a shape, check if it's over a vertex, and if it is, use elementFromPoint to save a reference to the actual HTML <div> representing the vertex handle. In subsequent mousemove and mouseup events, you can poll the screen position of this <div> and compare it to the locations of other points on the map.
Long version:
I've yanked out the relevant functions from our application, so you'll need to ignore some of our specific functions and objects etc., but this does show our working "snap-to-point" implementation.
First off, we will be doing pixel-to-lat-lng conversions, so you'll need a simple Overlay defined somewhere (you always need one to do these calculations):
_this.editor.overlay = new g.maps.OverlayView();
_this.editor.overlay.draw = function () {};
_this.editor.overlay.setMap(This.map);
Now, anytime we've initialized a shape on the map, we add a mousedown event handler to it.
g.maps.event.addListener(_this.shape, 'mousedown', function (event) {
if (event.vertex >= 0) {
var pixel = _this.editor.overlay.getProjection().fromLatLngToContainerPixel(_this.shape.getPath().getAt(event.vertex));
var offset = _this.mapElement.offset();
var handle = document.elementFromPoint(pixel.x + offset.left, pixel.y + offset.top);
if (handle) {
_this.dragHandle = $(handle);
_this.snappablePoints = _this.editor.snappablePoints(_this);
} else {
_this.dragHandle = null;
_this.snappablePoints = null;
}
}
});
(You'll notice the call to snappablePoints, which is just an internal utility function that collects all the points that would be valid for this point to snap to. We do it here because it would be an expensive loop to perform on every single mousemove.)
Now, in our shape's mousemove listener, because we've saved a reference to that <div>, we can poll its screen position and compare it to the screen position of other points on the map. If a point is within a certain pixel range (I think ours is 8 pixels), we save it and hover a little icon telling the user we're going to snap.
g.maps.event.addListener(this.shape, 'mousemove', function (event) {
var projection, pixel, pixel2, offset, dist, i;
if (event.vertex >= 0 && this.dragHandle) {
// If dragHandle is set and we're moving over a vertex, we must be dragging an
// editable polygon point.
offset = this.editor.mapElement.offset();
pixel = {
x: this.dragHandle.offset().left - offset.left + this.dragHandle.width() / 2,
y: this.dragHandle.offset().top - offset.top + this.dragHandle.height() / 2
};
// Search through all previously saved snappable points, looking for one within the snap radius.
projection = this.editor.overlay.getProjection();
this.snapToPoint = null;
for(i = 0; i < this.snappablePoints.length; i++) {
pixel2 = projection.fromLatLngToContainerPixel(this.snappablePoints[i]);
dist = (pixel.x - pixel2.x) * (pixel.x - pixel2.x) + (pixel.y - pixel2.y) * (pixel.y - pixel2.y);
if (dist <= SNAP_RADIUS) {
this.snapToPoint = this.snappablePoints[i];
$('#zone-editor #snapping').css('left', pixel.x + 10 + offset.left).css('top', pixel.y - 12 + offset.top).show();
break;
}
}
if (!this.snapToPoint) {
$('#zone-editor #snapping').hide();
}
});
A little cleanup when the user stops moving the mouse:
g.maps.event.addListener(this.shape, 'mouseup', function (event) {
// Immediately clear dragHandle, so that everybody knows we aren't dragging any more.
// We'll let the path updated event deal with any actual snapping or point saving.
_this.dragHandle = null;
$('#zone-editor #snapping').hide();
});
Last, we actually handle the "snapping", which is really just a tiny bit of logic in an event listener on the shape's path.
g.maps.event.addListener(this.shape.getPath(), 'set_at', function (index, element) {
if (this.snapToPoint) {
// The updated point was dragged by the user, and we have a snap-to point.
// Overwrite the recently saved point and let another path update trigger.
var point = this.snapToPoint;
this.snapToPoint = null;
this.shape.getPath().setAt(index, point);
} else {
// Update our internal list of points and hit the server
this.refreshPoints();
this.save();
};
// Clear any junk variables whenever the path is updated
this.dragHandle = null;
this.snapToPoint = null;
this.snappablePoints = null;
});
Fin.
Related
I need to develop a switch statement to run a method when a mouse event has dragged a certain fraction of a page (similar to breakpoints on a page). For example, when a user clicks on an item and drags it 1/12 the width of their screen, I need to run a function once, but if they keep dragging to 2/12 of the screen width, then I need to run the function again.
I created the following switch statement...it works, but I have to copy paste the case statement 12 times to account for a case where the user would drag all the way from left to right. I chose 12, because my page layout uses css grid with 12 columns as the layout:
// gridState is used to keep track of which state we are in since we only want
// to trigger the function `resizeDragColumns()` a single time, when we transition to a new breakpoint
let gridState = 0
switch(true){
// 0.083 = width ratio of a single column in a 12 column grid (e.g. 1/12 = 0.083)
case Math.abs(percentDragged) < (0.083 * 1):
if (gridState != 1) {
this.resizeDragColumns(...)
gridState = 1;
}
break;
case Math.abs(percentDragged) < (0.083 * 2):
if (gridState != 2) {
this.resizeDragColumns(...)
gridState = 2;
}
break;
....
// there are 10 more switch statements just like this
// notice how the only thing that changes is the gridState
// variable and the comparator in the case statement (e.g. 0.083 * 1, 0.083 * 2 ..)
}
This switch statement is contained within an Event observable that is tracking the mouse drag distance and converting it to the percent distance the user has dragged the cursor across the page.
So for example, as the user is dragging, percentDragged is calculated like so:
// point.x = current X position of cursor
// mouseDownCursorPosition = X position of the cursor when the mousedown event was fired (e.g. starting x position)
// rowRef = the container the mouse is dragging within
const percentDragged = (point.x - mouseDownCursorPosition) / rowRef.getBoundingClientRect().width;
IN SUMMARY
I want to dynamically create a switch statement that will trigger a function A SINGLE TIME when the user drags their cursor into certain "breakpoints" on the page.
Is there a simpler way to accomplish this task? This would also be helpful to have since the user can change the grid size to anything they want (8 columns instead of 12), so the switch statement should only have 8 cases instead of 12. Thanks!
Don't use switch in this case, it's quite verbose and error-prone. Instead, use math to identify the (0.083 * NUM) factor:
const draggedColumns = Math.ceil(Math.abs(percentDragged) / 0.083);
if (gridState !== draggedColumns) {
this.resizeDragColumns(...)
gridState = draggedColumns;
}
Kind of an opinion-based one, but here's how I'd do it. I don't think a switch is the right way, because is meant to be used with static and fixed number of cases.
let colDragged = Math.ceil(Math.abs(percentDragged) / 0.083);
if (gridState != colDragged ) {
this.resizeDragColumns(...)
gridState = colDragged ;
}
I trying to figure out, how to create a smooth cursor follow animation with cocos2d js
Here some code:
var listener = cc.EventListener.create({
event: cc.EventListener.MOUSE,
onMouseMove: function (event) {
var str = "MousePosition X: " + event.getLocationX() + " Y:" + event.getLocationY();
var actionBy = cc.MoveTo.create(2, cc.p(event.getLocationX(), $scope.bar._position.y));
$scope.bar.stopAllActions();
$scope.bar.runAction(
cc.sequence(
//cc.rotateTo(2, 0),
actionBy
)
);
// do something...
}});
The problem here, its stuck because the event is fired to often and the "$scope.bar.stopAllActions();" stops the animation.
If I remove the "$scope.bar.stopAllActions();" the animation is driving crazy, the "bar" is flying over the screen.
I just wont the bar follow the mouse cursor, just like the dog follow human
The problem here is that if you remove stopAllActions(); you'd be trying to run an action on an object that's already got an action of the same time. And in either case you are firing an animation too often on an object, I've rarely seen this work as expected.
I'd try running the action directly instead of putting it within a cc.Sequence first, but if that doesn't work, you've got two choices, both rely on manual labor:
a. Simply put the bar on the position of the mouse whenever it moves:
$scope.bar.x = event.getLocationX();
b. Assuming you want to "juice up" the game, just setting the bar on the mouse will be boring, so you could make the bar progressively catch up with the mouse:
In your constant section:
var EASING_CONSTANT = 0.9;
In the event handler:
$scope.bar.x += ($scope.bar.x - event.getLocationX()) * EASING_CONSTANT;
The lower your EASING_CONSTANT the slower the bar'll catch up to the mouse (only use values between 0 and 1).
Now, if you try to use this, you'll realize the bar never quite catches up with your mouse, so you'll have to place this code within an update function (in your main game layer's update method, for example) so it runs each frame.
BUT! Then you won't have access to an event object, so you'll have to end up with something like this:
In the event handler:
$scope.bar.targetX = event.getLocationX();
In the update method:
$scope.bar.x += ($scope.bar.x - $scope.bar.targetX) * EASING_CONSTANT;
There are lots of easing functions you can use, I just gave you the simplest example.
For instance, note that you also have the event.getDeltaX() method that'll return you the difference in position between the last call of the event and the current (how much the mouse moved since the last call of the event). With this you could do something along the lines of:
In your constant section:
var EASING_CONSTANT = 0.9;
var WOBBLING_CONSTANT = 10;
In the event handler:
$scope.bar.targetX = event.getLocationX();
$scope.bar.mouseDeltaX = event.getDeltaX();
In the update method:
$scope.bar.x += ($scope.bar.x - $scope.bar.targetX) * EASING_CONSTANT + Math.cos($scope.bar.mouseDeltaX) * WOBBLING_CONSTANT;
Just get creative and start messing around with the formula until you find a behaviour that "feels" right. But be sure to start simple first!
PS: I'm guessing you want your "bar" to only move on the x-axis, so.. are you making an arkanoid clone? :D
I have done lots of examples on dragging an object created by Raphael's library. Now I am working with sets and was also able to write a code to drag them.
Now my problem appeared when I rotate an object and then drag it.
Check out this code example: demo
var paper = Raphael('stage', 300, 300);
var r = paper.rect(50,100,30,50).attr({fill:"#FFF"}).rotate(45),
t = paper.text(30, 140, "Hello");
var p = paper.set(r, t);
r.set = p, t.set = p;
p.newTX=0,p.newTY=0,p.fDx=0,p.fDy=0,p.tAddX,p.tAddY,p.reInitialize=false,
start = function () {
},
move = function (dx, dy) {
var a = this.set;
a.tAddX=dx-a.fDx,a.tAddY=dy-a.fDy,a.fDx=dx,a.fDy=dy;
if(a.reInitialize)
{
a.tAddX=0,a.fDx=0,a.tAddY=0;a.fDy=0,a.reInitialize=false;
}
else
{
a.newTX+=a.tAddX,a.newTY+=a.tAddY;
a.attr({transform: "t"+a.newTX+","+a.newTY});
}
},
up = function () {
this.set.reInitialize=true;
};
p.drag(move, start, up);
By examining the DEMO you can see that the set is created with rotated rectangle, but as soon you drag it, it goes back to the 0 degree state. Why? Any solutions?
The problem is that whenever an element is transformed by applying a string containing instructions to move, rotate, scale etc, it resets the transformation object, and hence previous transformations get lost. To avoid this, add "..." at the beginning of the transformation string. Like,
var el = paper.rect(10, 20, 300, 200);
// translate 100, 100, rotate 45°, translate -100, 0
el.transform("t100,100r45t-100,0");
// NOW, to move the element further by 50 px in both directions
el.transform("...t50,50");
If "t50,50" is used instead of "...t50,50", then transformation effect for "t100,100r45t-100,0" is lost and transformation effect for "t50,50" rules.
Raphael reference for further study: http://raphaeljs.com/reference.html#Element.transform
Hope this helps.
I found an easy solution to this problem. Since I need to have a diamond instead of rectangle, I have created a path that represents that diamond. Then this path becomes just like a square 45 degree rotated.
This turned out to be easy because dragging functionality I had for my program works perfectly with paths.
I have an application with many draggable objects that can also be rotated in 90 degree increments. I'm trying to figure out how to stop the user from dragging the objects outside the Raphael paper (canvas).
This is fairly simple for unrotated objects. I can simply see if the current x and y coordinates are less than 0 and set them to 0 instead. I can adjust similarly by checking if they are outside the canvas width and height.
However, a problem arises when the object is rotated because for some odd reason the coordinate plane rotates as well. Is there an easy way to keep objects inside the canvas? Or is there an example of some this somewhere?
I have spent many hours fiddling with this and I can't seem to make sense of the rotated coordinate plane in order to adjust my calculations. Even when debugging the current coordinates, they seem to shift oddly if I drag an object, release it, and then drag the object again.
Any help is greatly appreciated.
Thanks,
Ryan
I had a similar problem, I needed to move a shape within the boundaries of another shape, so what I did was:
element.drag(onstart, onmove, onend);
...
onStart: function(x,y,e){
// Initialize values so it doesn't recalculate per iteration
// this allows to resume dragging from the point it were left
App.oldX = 0;
App.oldY = 0;
App.currentCircleX = App.fingerPath.attr('cx');
App.currentCircleY = App.fingerPath.attr('cy');
},
onMove: function(dx,dy,x,y,e){
App.setDirection(dx,dy);
},
onEnd: function(e){
// nothing to do here for now
},
// this function tells the element to move only if it's within the bound area
setDirection: function(dx, dy){
var isXYinside;
this.newX = this.currentCircleX - (this.oldX - dx);
this.newY = this.currentCircleY - (this.oldY - dy);
// HERE is the key, this method receives your bounding path and evaluates the positions given and then returns true or false
isXYinside = Raphael.isPointInsidePath(this.viewportPath, this.newX, this.newY);
this.oldX = dx;
this.oldY = dy;
// so if it is within the bound area, will move, otherwise will just stay there
if (isXYinside) {
this.fingerPath.attr({
"cx": this.newX,
"cy": this.newY
});
this.currentCircleX = this.newX;
this.currentCircleY = this.newY;
}
}
I know this is an old one, but I stumbled upon this question when trying to figure out a way to do it. So here's my 2 cents in case someone has this problem.
Reference:
Raphael.isPointInsidePath
Have you tried Element.getBBox()
There Are 2 flavones which give the result before rotation and after rotation
You should toggle the Boolean argument and test it
Somehow this doesn't work...
var paper = Raphael("test", 500, 500);
var testpath = paper.path('M100 100L190 190');
var a = paper.rect(0,0,10,10);
a.attr('fill', 'silver');
a.mousedown( function() {
testpath.animate({x: 400}, 1000);
});
I can move rects this way but not paths, why is that, and how do I move a path object then?!
With the latest version of Raphael, you can do this:
var _transformedPath = Raphael.transformPath('M100 100L190 190', 'T400,0');
testpath.animate({path: _transformedPath}, 1000);
This saves you from the trouble of having to clone a temp object.
It seems a path object doesn't get a x,y value - so your animation probably still runs, but does nothing. Try instead animating the path function:
testpath.animate({path:'M400 100L490 190'},1000);
It makes it a bit trickier to write the animation, but you have the benefit of getting rotation and scaling for free!
BTW: I'm sure this is just an example, but in your above code testpath gets put in the global scope because you don't initialize as var testpath
Solved, with thanx to Rudu!
You need to create a new path to animate to. You can do this with clone() and then apply the transformations to that clone. Seems very complex for a simple move like this, but it works...
var paper = Raphael("test", 500, 500);
var testpath = paper.path('M100 100L190 190');
var a = paper.rect(0,0,10,10);
a.attr('fill', 'silver');
a.mousedown( function() {
var temp = testpath.clone();
temp.translate(400,0);
testpath.animate({path: temp.attr('path')}, 1000);
temp.remove();
});
TimDog answer was best solution.
In addition, just remember, transform string in this case means, that it will add 400 points to every path point/line X coordinate, and 0 points to every Y coordinate.
That means, M100 100L190 190 will turn into M500 100L590 190.
So, if you need to move a path element to another position, the difference between current position and new position coordinates should be calculated. You can use first element to do that:
var newCoordinates = [300, 200],
curPos = testpath.path[0],
newPosX = newCoordinates[0] - curPos[1],
newPosY = newCoordinates[1] - curPos[2];
var _transformedPath = Raphael.transformPath(testpath.path, "T"+newPosX+","+newPosY);
testpath.animate({path: _transformedPath});
Hope this will help someone.
Here's some code that generalises the best of the above answers and gives Raphael paths a simple .attr({pathXY: [newXPos, newYPos]}) attribute similar to .attr({x: newXPosition}) and .animate({x: newXPosition}) for shapes.
This lets you move your path to a fixed, absolute position or move it by a relative amount in a standard way without hardcoding path strings or custom calculations.
Edit: Code below works in IE7 and IE8. An earlier version of this failed in IE8 / VML mode due to a Raphael bug that returns arrays to .attr('path') in SVG mode but strings to .attr('path') in VML mode.
Code
Add this code (Raphael customAttribute, and helper function) after defining paper, use as below.
paper.customAttributes.pathXY = function( x,y ) {
// use with .attr({pathXY: [x,y]});
// call element.pathXY() before animating with .animate({pathXY: [x,y]})
var pathArray = Raphael.parsePathString(this.attr('path'));
var transformArray = ['T', x - this.pathXY('x'), y - this.pathXY('y') ];
return {
path: Raphael.transformPath( pathArray, transformArray)
};
};
Raphael.st.pathXY = function(xy) {
// pass 'x' or 'y' to get average x or y pos of set
// pass nothing to initiate set for pathXY animation
// recursive to work for sets, sets of sets, etc
var sum = 0, counter = 0;
this.forEach( function( element ){
var position = ( element.pathXY(xy) );
if(position){
sum += parseFloat(position);
counter++;
}
});
return (sum / counter);
};
Raphael.el.pathXY = function(xy) {
// pass 'x' or 'y' to get x or y pos of element
// pass nothing to initiate element for pathXY animation
// can use in same way for elements and sets alike
if(xy == 'x' || xy == 'y'){ // to get x or y of path
xy = (xy == 'x') ? 1 : 2;
var pathPos = Raphael.parsePathString(this.attr('path'))[0][xy];
return pathPos;
} else { // to initialise a path's pathXY, for animation
this.attr({pathXY: [this.pathXY('x'),this.pathXY('y')]});
}
};
Usage
For absolute translation (move to fixed X,Y position) - Live JSBIN demo
Works with any path or set of paths including sets of sets (demo). Note that since Raphael sets are arrays not groups, it moves each item in the set to the defined position - not the centre of the set.
// moves to x=200, y=300 regardless of previous transformations
path.attr({pathXY: [200,300]});
// moves x only, keeps current y position
path.attr({pathXY: [200,path.pathXY('y')]});
// moves y only, keeps current x position
path.attr({pathXY: [path.pathXY('x'),300]});
Raphael needs to handle both x and y co-ordinates together in the same customAttribute so they can animate together and so they stay in sync with each other.
For relative translation (move by +/- X,Y) - Live JSBIN demo
// moves down, right by 10
path.attr({pathXY: [ path.pathXY('x')+10, path.pathXY('y')+10 ]},500);
This also works with sets, but again don't forget that Raphael's sets aren't like groups - each object moves to one position relative to the average position of the set, so results may not be what are expected (example demo).
For animation (move a path to relative or absolute positions)
Before animating the first time, you need to set the pathXY values, due to a bug/missing feature up to Raphael 2.1.0 where all customAttributes need to be given a numeric value before they are animated (otherwise, they will turn every number into NaN and do nothing, failing silently with no errors, or not animating and jumping straight to the final position).
Before using .animate({pathXY: [newX,newY]});, run this helper function:
somePath.pathXY();
Yet another way is to use "transform" attribute:
testpath.animate({transform: "t400,0"}, 1000);
to move the path to the right by 400px, relative to the original position.
This should work for all shapes, including paths and rectangles.
Note that:
"transform" attribute is independent of x, y, cx, cy, etc. So these attributes are not updated by the animation above.
The value of "transform" attribute is always based on the original position, not the current position. If you apply the animation below after the animation above, it will move it 800px to the left relatively, instead of moving it back to its original position.
testpath.animate({transform: "t-400,0"}, 1000);