Snap.svg - drag event handler - javascript

Question is about the onstart event handler for Element.drag in the newly announced Snap.svg.
The intention of the code below is to register event handlers for the start and stop of a drag (onstart/onstop) on an svg object.
var s = Snap(800,600);
var bigCircle = s.circle(300,150,100);
bigCircle.drag(null,
function(){
console.log("Move started");
},
function(){
console.log("Move stopped");
}
);
The console messages work fine on drag start and stop, but the null overrides the default onmove function - resulting in no actual drag happening. How do I pass something that says "I don't want to mess with the default onmove"?
(Note: I'd prefer to register an event handler by means of assignment, like the familiar onClick, but that's a different matter.)
Note added after few hours:
The Raphael.js documentation and examples provide some clues. At least now I know how to pass in a proper function for onmove that provides the default move behavior:
var s = Snap(800,600);
var bigCircle = s.circle(300,150,100);
start = function() {
this.ox = parseInt(this.attr("cx"));
this.oy = parseInt(this.attr("cy"));
console.log("Start move, ox=" + this.ox + ", oy=" + this.oy);
}
move = function(dx, dy) {
this.attr({"cx": this.ox + dx, "cy": this.oy + dy});
}
stop = function() {
this.ox = parseInt(this.attr("cx"));
this.oy = parseInt(this.attr("cy"));
console.log("Stop move, ox=" + this.ox + ", oy=" + this.oy);
}
bigCircle.drag(move, start, stop);

I'm not sure if I'm misunderstanding what you exactly want...don't you want to implement the drag ?
So for example...
var s = Snap(400,400);
var bigCircle = s.circle(150, 150, 100);
var moveFunc = function (dx, dy, posx, posy) {
this.attr( { cx: posx , cy: posy } ); // basic drag, you would want to adjust to take care of where you grab etc.
};
bigCircle.drag( moveFunc,
function(){
console.log("Move started");
},
function(){
console.log("Move stopped");
}
);
JSBin here http://jsbin.com/akoCAkA/1/edit?html,js,output

There is an example how to drag with SnapSVG here: http://svg.dabbles.info/snaptut-drag.html
var s = Snap("#svgout");
var rect = s.rect(20,20,40,40);
var circle = s.circle(60,150,50);
var move = function(dx,dy) {
this.attr({
transform: this.data('origTransform') + (this.data('origTransform') ? "T" : "t") + [dx, dy]
});
}
var start = function() {
this.data('origTransform', this.transform().local );
}
var stop = function() {
console.log('finished dragging');
}
rect.drag(move, start, stop );
circle.drag(move, start, stop );

After struggling for some hours to do this with snap.js, I finally discovered svg.js and its draggable plugin, with which it is so much easier:
var draw = SVG('svg');
var circle = draw.circle(10).attr({cx:30,cy:30,fill:'#f06'});
circle.dragend = function(delta, event) {
alert(this.attr('cx'))
}
circle.draggable();
So, I switched to svg.js ...

The eve.on method wasn't working for me, so I did some poking around and managed to recreate the onmove function. The other two (onstart and onend) require no specific code to work apparently:
var S = Snap(300,300);
var bigCircle = S.circle(150, 150, 100);
bigCircle.drag(onDragMove, onDragStart, onDragEnd);
var ddx = 0;
var ddy = 0;
var dxDone = 0;
var dyDone = 0;
function onDragMove (dx, dy, posx, posy) {
dx = dx + dxDone; // dx and dy reset to 0 for some reason when this function begins
dy = dy + dyDone; // retain the last move's position as the starting point
this.attr( { transform: 't'+dx+','+dy } );
ddx = dx;
ddy = dy;
console.log('moving...');
};
function onDragStart(x,y,e) {
console.log('start!');
};
function onDragEnd(e) {
dxDone = ddx;
dyDone = ddy;
console.log('end!');
};
Please note however that this should only be used for one dragged object at a time. If you need a custom drag for another object, you'll have to rename the functions (ie onDragStart2) and the four variables declared outside of them (ie ddx2) after duplicating it.
Also, the 'transform' string format I passed (tx,y) came from what I found after doing console.log( this.attr('transform') ). I'm not familiar with matrix() just yet, so this way seemed easier.
Hope this helps!

I can't drag group elements with custom handlers, s.drag() makes it possible. So i searched further found its possible.
Documentation:
Additionaly following drag events are triggered: drag.start. on start, drag.end. on > end and drag.move. on every move. When element is dragged over another element > drag.over. fires as well.
Solution:
s.drag();
eve.on("snap.drag.start." + s.id, function () {
console.log('cool');
});
eve.on("snap.drag.move." + s.id, function () {
console.log('cooler');
});
eve.on("snap.drag.end." + s.id, function () {
console.log('way cool');
});
eve is not documented on snapsvg it is available on raphael. i don't know this is proper way or hack.

Try this
var paper = Snap("#main");
var object = paper.circle(300,150,100)
object .attr({
stroke: "#000",
strokeWidth: 10,
strokeLinecap:"round"
});
var move1 = function(dx,dy, posx, posy) {
this.transform(this.data('origTransform') + (this.data('origTransform') ? "T" : "t") + [dx, dy])
};
var start = function() {
this.data('origTransform', this.transform().local );
}
var stop = function() {
console.log('dragging done');
}
object.drag(move1, start, stop );

Related

Trigger an event to call a function that's inside of a closure

I'm working on a project based on a nice little sample canvas drawing app someone else on the project downloaded and modified. We need to allow the user to click a button elsewhere on the page (not part of the canvas), and have it run a function that came with the sample app. However, the function is inside of a closure. Since I can't call the function directly (right? the closure prevents this? I don't often work with closures), I thought I'd be able to accomplish this by triggering a mouse event at the location the user would click to accomplish the same thing. It's not working, and I don't know why not.
I posted a greatly simplified version at this fiddle. Simple HTML code:
<div id="canvasDiv"></div>
<div id="buttonDiv">
<button>why can't I send a click to the canvas?</button>
</div>
And the simplified version of the downloaded sample app, plus my attempt to use jQuery's .trigger method to trigger the event:
var WM = {};
WM.drawingApp = function(options) {
"use strict";
var canvas, context,
// Add mouse and touch event listeners to the canvas
createUserEvents = function() {
var getElementPos = function(element) {
// straight-forward stuff removed for brevity's sake
return pos;
};
var press = function(e) {
// Mouse down location
var sizeHotspotStartX, toolIndex,
mouseX = (e.changedTouches ? e.changedTouches[0].pageX : e.pageX),
mouseY = (e.changedTouches ? e.changedTouches[0].pageY : e.pageY);
var elementPos = getElementPos(document.getElementById(options.canvasElementId || 'canvasDiv'));
mouseX -= elementPos.x;
mouseY -= elementPos.y;
announce(mouseX, mouseY);
};
var announce = function(x,y) { alert('press at: ' + x + ', ' + y); }
// Add mouse event listeners to canvas element
canvas.addEventListener("mousedown", press, false);
},
// Creates a canvas element, etc
init = function() {
// Create the canvas
canvas = document.createElement('canvas');
canvas.setAttribute('width', 100);
canvas.setAttribute('height', 100);
canvas.setAttribute('id', 'canvas');
document.getElementById(options.canvasElementId || 'canvasDiv').appendChild(canvas);
context = canvas.getContext("2d"); // Grab the 2d canvas context
createUserEvents();
};
init();
return {};
};
jQuery(document).ready(function() {
jQuery('#buttonDiv').on('click', 'button', function() {
var down = jQuery.Event("mousedown", {
pageX: 50,
pageY: 50
});
jQuery('#canvasDiv canvas').trigger(down);
});
});
As you can see by running the fiddle, if you click inside the box, you get an alert announcing where you clicked. But if you click the button, you don't get an alert. While writing this question, it occurred to me that maybe jQuery's .trigger method isn't a sufficient way to send the click. Its documentation page specifically says that .trigger "does not perfectly replicate a naturally-occurring event". We're open to solutions that don't involve jQuery.
You can define a variable var press; outside of WM, inside of WM, remove var before press and set press = function() {}. You should then be able to call press(down) at click of button
var press;
press = function(e) {
console.log(e);
// Mouse down location
var sizeHotspotStartX, toolIndex,
mouseX = (e.changedTouches ? e.changedTouches[0].pageX : e.pageX),
mouseY = (e.changedTouches ? e.changedTouches[0].pageY : e.pageY);
var elementPos = getElementPos(
document.getElementById(options.canvasElementId
|| 'canvasDiv')
);
mouseX -= elementPos.x;
mouseY -= elementPos.y;
announce(mouseX, mouseY);
};
jQuery(document).ready(function() {
jQuery('#buttonDiv').on('click', 'button', function() {
var down = jQuery.Event("mousedown", {
pageX: 50,
pageY: 50
});
press(down); // call `press` at `button` click
//jQuery('#canvasDiv canvas').trigger(down);
});
});
// based on http://www.williammalone.com/projects/html5-canvas-javascript-drawing-app-with-bucket-tool/
var press;
var WM = {};
WM.drawingApp = function(options) {
"use strict";
var canvas, context,
// Add mouse and touch event listeners to the canvas
createUserEvents = function() {
var getElementPos = function(element) {
var parentOffset, pos;
if (!element) {
pos = {
x: 0,
y: 0
};
} else {
pos = {
x: element.offsetLeft,
y: element.offsetTop
};
if (element.offsetParent) {
parentOffset = getElementPos(element.offsetParent);
pos.x += parentOffset.x;
pos.y += parentOffset.y;
}
}
return pos;
};
press = function(e) {
console.log(e)
// Mouse down location
var sizeHotspotStartX, toolIndex,
mouseX = (e.changedTouches ? e.changedTouches[0].pageX : e.pageX),
mouseY = (e.changedTouches ? e.changedTouches[0].pageY : e.pageY);
var elementPos = getElementPos(document.getElementById(options.canvasElementId || 'canvasDiv'));
mouseX -= elementPos.x;
mouseY -= elementPos.y;
announce(mouseX, mouseY);
};
var announce = function(x,y) { alert('press at: ' + x + ', ' + y); }
// Add mouse event listeners to canvas element
canvas.addEventListener("mousedown", press, false);
},
// Creates a canvas element, loads images, adds events, and draws the canvas for the first time.
init = function() {
// Create the canvas (Neccessary for IE because it doesn't know what a canvas element is)
canvas = document.createElement('canvas');
canvas.setAttribute('width', 100);
canvas.setAttribute('height', 100);
canvas.setAttribute('id', 'canvas');
document.getElementById(options.canvasElementId || 'canvasDiv').appendChild(canvas);
context = canvas.getContext("2d"); // Grab the 2d canvas context
createUserEvents();
};
init();
return {};
};
jQuery(document).ready(function() {
jQuery('#buttonDiv').on('click', 'button', function() {
var down = jQuery.Event("mousedown", {
pageX: 50,
pageY: 50
});
press(down)
//jQuery('#canvasDiv canvas').trigger(down);
});
});
var drawingApp = WM.drawingApp({
canvasElementId: "canvasDiv"
});
#canvasDiv canvas {
border: solid black 1px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js">
</script>
<div id="canvasDiv"></div>
<div id="buttonDiv">
<button>why can't I send a click to the canvas?</button>
</div>
jsfiddle https://jsfiddle.net/gkvdha3h/5/

Mouse position is only read on the first frame

i have been having trouble with reading a mouse position on a canvas. The code is working (semi) correctly as it reads the position when clicking he canvas in IE but only on one frame, in chrome it is just displaying the value as 0.
Here is the full code:
<script>
var blip = new Audio("blip.mp3");
blip.load();
var levelUp = new Audio("levelUp.mp3");
levelUp.load();
var canvas = document.getElementById('game');
var context = canvas.getContext('2d');
context.font = '18pt Calibri';
context.fillStyle = 'white';
//load and draw background image
var bgReady = false;
var background = new Image();
background.src = 'images/background.jpg';
background.onload = function(){
bgReady = true;
}
var startMessage = 'Click the canvas to start';
//load plane image
var planeReady = false;
var planeImage = new Image();
planeImage.src = 'images/plane.png';
planeImage.onload = function() {
planeReady = true;
}
//load missile image
var missileReady = false;
var missileImage = new Image();
missileImage.src = 'images/missile-flipped.gif';
missileImage.onload = function() {
missileReady = true;
}
//initialise lives and score
var score = 0;
var lives = 3;
var missilesLaunched = 0;
var missileSpeed = 5;
var level = 1;
var missileX = 960;
var missileY = Math.random() * 500;
if (missileY > 480) {
missileY = 480;
}
function getMousePos(canvas, event) {
return {
x: input.x - rect.left,
y: input.y - rect.top
};
}
function update_images(event) {
var pos = getMousePos(canvas.getBoundingClientRect(), mouseInput);
planeImage.y = pos.y;
missileX = missileX - missileSpeed;
if (missileX < - 70) {
missilesLaunched++;
missileX = 960;
missileY = Math.random() * 500;
if (missileY > 480) {
missileY = 480;
}
blip.play();
score = missilesLaunched;
if (score % 5 == 0) {
missileSpeed = missileSpeed + 2;
level++;
levelUp.play();
}
}
}
function reload_images() {
if (bgReady = true) {
context.drawImage(background, 0, 0);
}
if (planeReady = true) {
context.drawImage(planeImage, 10, planeImage.y);
}
if (missileReady = true) {
context.drawImage(missileImage, missileX, missileY);
}
context.fillText('Lives: ' + lives, 200, 30);
context.fillText('Score: ' + score, 650, 30);
context.fillText('Level: ' + missileSpeed, 420, 30);
context.fillText('Position: ' + missileImage.y, 420, 70);
}
function main(event) {
var mouseInput = { x: 0, y: 0 };
document.addEventListener("mousemove", function (event) {
mouseInput.x = event.clientX;
mouseInput.y = event.clientY;
});
update_images(event);
reload_images();
if (lives > 0) {
window.requestAnimationFrame(main);
}
else {
}
}
function start() {
context.drawImage(background, 0, 0);
context.fillText('Click the canvas to start', 350, 250);
function startMain(event) {
game.removeEventListener("click", startMain);
main(event);
}
canvas.addEventListener("mousedown", startMain);
}
start();
</script>
Joe, you should actually be capturing the mouse position every time you click...
...but you're actually also starting a new game (without stopping the old one), every time you click, too.
First problem: starting game engine several times to draw on the same instance of the canvas
Solution:
In your start function, you need to remove the mousedown event listener, after you've triggered it.
function start () {
// ... other setup
function startMain (event) {
canvas.removeEventListener("click", startMain);
main(event);
}
canvas.addEventListener("click", startMain);
}
Now it will only listen for the first click, before starting, and will only start once.
Second Problem: mouse doesn't update as expected
Solution: two issues here...
...first, you are passing event into main on first call...
...after that, you're passing main into requestAnimationFrame.
requestAnimationFrame won't call it with an event, it will call it with the number of microseconds (or ms or some other unit as a fractional precision of ms) since the page was loaded.
So the first time you got main({ type: "mousedown", ... });.
The next time you get main(4378.002358007);
So lets refactor the startMain we had above, so that main never ever collects an event, just a time.
function startMain ( ) {
canvas.removeEventListener("click", startMain);
requestAnimationFrame(main);
}
The next problem is that even if you were getting just events, you're only ever capturing a click event (which as we mentioned earlier, fires a new copy of the game logic).
Your solution is to separate the code which catches mouse events from the code which reads mouse position.
var mouseInput = { x: 0, y: 0 };
document.addEventListener("mousemove", function (event) {
mouseInput.x = event.clientX;
mouseInput.y = event.clientY;
});
function getMousePos (rect, input) {
return {
x : input.x - rect.left,
y : input.y - rect.top
};
}
// currently in updateImages (should not be there, but... a different story)
var pos = getMousePos(canvas.getBoundingClientRect(), mouseInput);
You've got other problems, too...
You're calling getMousePos and passing in game at the moment. I don't see where game is defined in your JS, so either you're making game somewhere else (begging for bugs), or it's undefined, and your app blows up right there.
You should really be building this with your console / dev-tools open, in a hands-on fashion, and cleaning bugs in each section, as you go.

Clicakble planets

Im trying to make planets that give an alert message when clicked.
Problem is, onmousedown only works on canvas, as far I tested.
Code for planets:
var planets = [];
for (var b=0;b<3;b++) {
planets.push(planet(0,360,Math.random()*600,Math.random()*600));
}
function planet(I,shiips,xpos,ypos){
I = I||{};
I.ships = shiips;
I.x=xpos;
I.y=ypos;
return I;
}
code for click detection; tests both for planet object and the image
update = function(){
planetImage.onmousedown=function(){alert("works!")};
planets[0].onmousedown=function(){alert("works!")};
}
setInterval(update,100);
Im using canvas to draw the images, if that hhelps.
I found the following code that gives mouse position, but it doesnt work for me:
(function() {
var mousePos;
window.onmousemove = handleMouseMove;
setInterval(getMousePosition, 100); // setInterval repeats every X ms
function handleMouseMove(event) {
event = event || window.event; // IE-ism
mousePos = {
x: event.clientX,
y: event.clientY
};
}
function getMousePosition() {
var pos = mousePos;
if (!pos) {
// We haven't seen any movement yet
}
else {
// Use pos.x and pox.y
}
}
})();
Im trying to keep it simple, I don't really like jquery or anything complicated.
Once again: the problem is onmousedown only works on the canvas object, i.e.
canvas.onmousedown=function(){alert("works!")};
I got it working now with this code:
update = function(){
canvas.onmousedown=function(){
var e = window.event;
var posX = e.clientX;
var posY = e.clientY;
alert("X position: "+ posX + " Y position: " + posY);
};
setInterval(update,100);

Raphael transform object diagonally and infinite setIntervals

I'm working on a small animation where the user drags a circle and the circle returns back to the starting point. I figured out a way to have the circle return to the starting point. The only problem is that it will hit one of the sides of the frame before returning. Is it possible for it to go straight back (follow the path of a line drawn between the shape and starting point).
The other problem is that my setInterval doesn't want to stop. If you try pulling it a second time it would pull it back before you release your mouse. It also seems to speed up after every time. I have tried using a while loop with a timer but the results weren't as good. Is this fixable?
var paper = Raphael(0, 0, 320, 200);
//var path = paper.path("M10 10L40 40").attr({stoke:'#000000'});
//var pathArray = path.attr("path");
var circle = paper.circle(50, 50, 20);
var newX;
var newY;
circle.attr("fill", "#f00");
circle.attr("stroke", "#fff");
var start = function () {
this.attr({cx: 50, cy: 50});
this.cx = this.attr("cx"),
this.cy = this.attr("cy");
},
move = function (dx, dy) {
var X = this.cx + dx,
Y = this.cy + dy;
this.attr({cx: X, cy: Y});
},
up = function () {
setInterval(function () {
if(circle.attr('cx') > 50){
circle.attr({cx : (circle.attr('cx') - 1)});
} else if (circle.attr('cx') < 50){
circle.attr({cx : (circle.attr('cx') + 1)});
}
if(circle.attr('cy') > 50){
circle.attr({cy : (circle.attr('cy') - 1)});
} else if (circle.attr('cy') < 50){
circle.attr({cy : (circle.attr('cy') + 1)});
}
path.attr({path: pathArray});
},2);
};
circle.drag(move, start, up);
Here's the Jfiddle: http://jsfiddle.net/Uznp2/
Thanks alot :D
I modified the "up" function to the one below
up = function () {
//starting x, y of circle to go back to
var interval = 1000;
var startingPointX = 50;
var startingPointY = 50;
var centerX = this.getBBox().x + (this.attr("r")/2);
var centerY = this.getBBox().y + (this.attr("r")/2);
var transX = (centerX - startingPointX) * -1;
var transY = (centerY - startingPointY) * -1;
this.animate({transform: "...T"+transX+", "+transY}, interval);
};
and the "start" function as follows:
var start = function () {
this.cx = this.attr("cx"),
this.cy = this.attr("cy");
}
Is this the behavior you are looking for? Sorry if I misunderstood the question.
If the circle need to get back to its initial position post drag, we can achieve that via simple animation using transform attribute.
// Assuming that (50,50) is the location the circle prior to drag-move (as seen in the code provided)
// The animation is set to execute in 1000 milliseconds, using the easing function of 'easeIn'.
up = function () {
circle.animate({transform: 'T50,50'}, 1000, 'easeIn');
};
Hope this helps.

RaphaelJS seems to lack shape hierarchies

Raphael seems to lack shape hierarchies?
I can't create a smaller circle "attached" to a larger circle, and know that it will be scaled/translated when the larger one is.
Similarly, if i put elements into a set, the drag handler connects to shapes individually and in its callbacks i have no handle on the set.
Am i overlooking something?
This the current behaviour as Raphael does not create any real element for a "set".
If you want to enable Drag'nDrop on a set, you can use the following code :
Raphael.el.set_drag = function (aSet) {
// Enable drag'n drop in a Set
var startAll = function () {
// storing original coordinates
for (var i in this.set.items) {
var comp = this.set.items[i];
try {
comp.attr({opacity: 0.3});
} catch (ex) {;}
if (comp.type == "path") {
comp.ox = comp.getBBox().x;
comp.oy = comp.getBBox().y;
}
else {
comp.ox = comp.attrs.cx || comp.attrs.x;
comp.oy = comp.attrs.cy || comp.attrs.y;
}
}
},
moveAll = function (dx, dy) {
for (var i in this.set.items) {
var comp = this.set.items[i];
if (comp.attrs.cx) // ellipse
comp.attr({cx: comp.ox + dx, cy: comp.oy + dy});
else if (comp.attrs.x)
comp.attr({x: comp.ox + dx, y: comp.oy + dy});
else // path
comp.translate(comp.ox - comp.getBBox().x + dx, comp.oy - comp.getBBox().y + dy);
}
},
upAll = function () {
for (var i in this.set.items) {
var comp = this.set.items[i];
if (comp.attrs.cx) // ellipse
comp.attr({cx: comp.ox, cy: comp.oy + dy});
else if (comp.attrs.x)
comp.attr({x: comp.ox, y: comp.oy + dy});
else // path
comp.translate(comp.ox , comp.oy - comp.getBBox().y + dy);
this.set.items[i].attr({opacity: 1});
}
};
this.set = aSet; //create a "set" property on the element
this.drag(moveAll,startAll,upAll);
return this;
}
// Create your elements
var first_element = paper.rect(10,10,100,50).attr({fill:'#f00'});
var second_element = paper.rect(30,300,100,50).attr({fill:'#0f0'});
// Add elements to your set
var myset = paper.set();
myset.push(first_element);
myset.push(second_element);
// Add handler on the elements
first_element.set_drag(myset);
second_element.set_drag(book_set);

Categories

Resources