Raphael.js attr function sets wrong value - javascript

I'm implementing a drag and drop system with Raphael.js. For this, i'm storing the original x and y position on mousedown, and if there is a collision on mouseup, I want to reset the position to the original one. Here's the bit of code that does the resetting ("this" refers to the raphael object here):
var transformString = "t" + this.original_x + "," + this.original_y;
this.attr("transform", transformString);
What's weird is that after setting the attribute, the transform string changes by a couple pixels. I debugged this with:
var transformString = "t" + this.original_x + "," + this.original_y;
this.attr("transform", transformString);
console.log("transformString: " + transformString);
console.log("transformAttrib: " + this.attr("transform"));
AFAIK, both logged values should be equal in any case. But they are sometimes off by as much as 20px. Does anyone know what's going on here?
E: Here is a simplified version, without the collision testing, which still reproduces the bug: http://jsfiddle.net/6ozsfdaf

I'm not sure why this is happening. I tried even capturing the co-ordinates before onstart using onmousedown event, even that didnt work. Also different methods provided by Raphael to get the co-ordinates using getBBox(), accessing x and y directly, didnt help.
So what I thought is, we should Maintain and Track the coordinates manually. So I have used your original_x and original_y variables which captures the position of the <path> after you create and set with some transform value. Below is the code of the same
Here is the working fiddle.
this.raph = R.path(svgPath).attr({
stroke: "hsb(0, 1, 1)",
fill: "#fff",
opacity: 1.0,
cx: 100,
cy: 900
}).transform("t" + x + "," + y);
this.raph.original_x = x;
this.raph.original_y = y;
//comment the lines in start method which captures original_x and original_y
//this.original_x = Raphael.parseTransformString(this.attr("transform"))[0][1];
//this.original_y = Raphael.parseTransformString(this.attr("transform"))[0][2];
More info regarding tracking the co-ordinates:
We will have one more coordinate say updated_x and updated_y, which will be updated in the move method. onFinish/onUp method, we can have the check whether we should update the new position or not. Here, it just asks whether new position should be updated or not and based on our input, it sets the final result.
Check this fiddle:
this.start = function() {
if (this.reference.static) return;
//this.original_x = Raphael.parseTransformString(this.attr("transform"))[0][1];
//this.original_y = Raphael.parseTransformString(this.attr("transform"))[0][2];
this.animate({r: 70, opacity: 0.25}, 500, ">");
this.updated_x = this.original_x;
this.updated_y = this.original_y;
};
this.move = function(dx, dy) {
//var ts = Raphael.parseTransformString(this.attr("transform"));
this.updated_x = this.original_x + dx;
this.updated_y = this.original_y + dy;
//ts[0][1] = this.original_x + dx;
//ts[0][2] = this.original_y + dy;
this.attr({transform: 't' + this.updated_x + ',' + this.updated_y});
};
this.up = function() {
if(confirm("Shall I update the new position??")) {
this.original_x = this.updated_x;
this.original_y = this.updated_y;
}
var transformString = "t" + this.original_x + "," + this.original_y;
this.attr("transform", transformString);
this.attr({fill: "#fff"});
this.animate({r: 50, opacity: 1}, 500, ">");
};

I'm not quite sure why that problem happens yet, but I'm wondering if this may be a better solution anyway. Rather than parsing the strings each time, just store the transform and use that.
I've also switched it to use the transform() method, rather than the attr(transform:..) method. Whilst I think that would normally work, its not quite right logically, as SVG attributes don't take a Raphael transform string, but I assume Raph would intercept that and deal with it (but maybe more error prone).
Its also worth bearing in mind in a transform string that 't' is a relative transform and 'T' is an absolute transform (I don't think thats the issue as there's no preceding transform, but I was wondering if its also related).
this.start = function() {
if (this.reference.static) return;
this.original_t = this.transform();
this.animate({r: 70, opacity: 0.25}, 500, ">");
};
this.move = function(dx, dy) {
this.transform( this.original_t + 't' + dx + ',' + dy);
};
this.up = function() {
this.transform( this.original_t );
console.log("transformString: " + this.original_t);
console.log("transformAttrib: " + this.transform());
this.attr({fill: "#fff"});
this.animate({r: 50, opacity: 1}, 500, ">");
};
jsfiddle

Found an interesting fix: you can avoid the situation if you add an epsilon to both this.original_x and this.original_y. The problem seems to disappear if this.original_x and this.original_y are not exactly the same as the starting coordinates. Check out: http://jsfiddle.net/6ozsfdaf/13/
this.up = function() {
var ts;
this.original_x += 0.0000000001;
this.original_y += 0.0000000001;
var transformString = "t" + this.original_x + "," + this.original_y;
this.attr("transform", transformString);
console.log("transformString: " + transformString);
console.log("transformAttrib: " + this.attr("transform"));
this.attr({fill: "#fff"});
this.animate({r: 50, opacity: 1}, 500, ">");
};
EDIT
Found the problem. In Raphael, Raphael.parseTransformString()'s output is cached and reused. In your move() method, you modify the output of Raphael.parseTransformString(), and Raphael tries to use your modified array when you supply it with the same string. This happens when the first drag() event is registered. You ask it to parse the current place, and then update the output array of arrays with the new location. And then, way later, when this.up() is called, you supply the Raphael.parseTransformString() with the same string. Raphael then uses your modified array of arrays. This is the fixed fiddle: http://jsfiddle.net/6ozsfdaf/16/
And here is the code (use a new array of arrays to transform when moved each time):
this.move = function(dx, dy) {
var ts = [];
ts.push(new Array('t'));
ts[0][1] = this.original_x + dx;
ts[0][2] = this.original_y + dy;
this.attr({transform: ts.toString()});
};

Related

Snap.svg & JavaScript: Creating Shapes and Animating Each on Delay inside a For Loop

first-time/long-time (quack, quack).
I'm a bit frustrated, and just about stumped, by this riddle I can't quite solve in Snap.svg. It's probably an oversight that I'll kick myself for missing, but I'm not seeing it at this point.
I have x & y data that I draw from a DOM element and store into a series of arrays, filter based on certain values in certain columns, and eventually create multiple instances of the ChartLine object in js. Basically, it sorts a certain column by quantity, assigns each value's row a color from a RainbowVis.js object, pushes all the relevant values from each row into an array for y, and draws a path on the line chart where y is the value and x is a steadily-increasing integer in a For loop.
What I'm currently doing here, in the draw() function, is: for each relevant column, create a <circle> with the variable "dot" with the object's x & y variables, assign the attributes, animate the radius from 0 to 8 in a quarter-second, and add the x & y values of i to a string to be used in a <path> I create right after the For loop. I then animate the path, etc., etc.
Without the setTimeout(), it works well. The circles and paths all animate simultaneously on load. However, I want to add a delay to each .animate with the number of milliseconds increasing by polyDelayInterval in each iteration, so each "dot" animates as the line arrives at it. At the VERY least, I want to animate all of the "dots" after the path is done animating.
The problem is, no matter what I've tried so far, I can only get the last set of "dots" (at the highest x value for each line) to animate; the rest stay at r:0. I've read several somewhat-similar posts, both here and elswhere; I've searched up and down the Snap.svg's docs on their site. I just cannot find what I'm doing wrong. Thanks in advance!
var svgMFLC = Snap('svg#ElementID');
function ChartLine(x, y, color, row) {
this.x = x;
this.y = y;
this.color = color;
this.row = row;
var propNames = Object.keys(this.row);
var yAdjust;
var resetX = this.x;
for (var i = 0; i < propNames.length; i++) { // get only the calculated score columns and their values
if (propNames[i].toLowerCase().includes(calcKeyword)) {
yAdjustedToChartArea = chartBottom - (this.row[propNames[i]] * yInterval);
this.y.push(yAdjustedToChartArea); // returns the value of that score column and pushes it to the y array
}
}
this.draw = function () {
var points = "M"; // the string that will determine the coordinates of each line
var dotShadow = svgMFLC.filter(Snap.filter.shadow(0, 0, 2, "#000000", 0.4));
var polyTime = 1500; // in milliseconds
var dot;
var polyDelayInterval = polyTime / (semesterCols.length - 1);
for (var i = 0; i < semesterCols.length; i++) { // for each data point, create a "dot"
dot = svgMFLC.circle(this.x, this.y[i], 0);
dot.attr({
fill: this.color,
stroke: "none",
filter: dotShadow,
class: "chartPointMFLC"
});
setTimeout(function () {
dot.animate({ r: 8 }, 250, mina.easeout);
}, polyDelayInterval * i);
points += this.x + " " + this.y[i] + " L";
this.x = this.x + xInterval;
}
points = points.slice(0, -2); // take away the excessive " L" from the end of the points string
var poly = svgMFLC.path(points);
var polyLength = poly.getTotalLength();
poly.attr({
fill: "none",
stroke: this.color,
class: "chartLineMFLC",
strokeDasharray: polyLength + " " + polyLength, // setting the strokeDash attributes will help create the "drawing the line" effect when animated
strokeDashoffset: polyLength
});
poly.animate({ strokeDashoffset: 0.00 }, polyTime);
this.x = resetX;
}
}
I can't put a tested solution up without the full code to test, but the problem is almost certainly that you at least need to get a closure for your 'dot' element.
So this line...
setTimeout(function () {
dot.animate({ r: 8 }, 250, mina.easeout);
}, polyDelayInterval * i);
When it comes to call that function, 'dot' will be the last 'dot' from the loop. So you need to create a closure (create functional scope for dot).
So something a bit like...
(function() {
var laterDot = dot;
setTimeout(function () {
laterDot.animate({ r: 8 }, 250, mina.easeout);
}, polyDelayInterval * i)
})();

Snap.svg draggin Path Element

I'm trying to drag a Path element with Snap.SVG, I'm using drag method
when method is called without parmeteres it seems to work ok, but when I add the listeners I cant make it work, It's looks like is not posible to change to position x,y attributes
start = () ->
console.log("Stop move, ox=" + this.ox + ", oy=" + this.oy);
moveFunc = (dx, dy, posx, posy) ->
this.attr( { cx: posx , cy: posy } )
stop = () ->
console.log("Stop move, ox=" + this.ox + ", oy=" + this.oy);
p.drag( moveFunc, start, stop)
The previos code doesnt work with path element, but it does with circle.
Next code can move it but when drag it again loose the last position
moveFunc = (dx, dy, posx, posy) ->
this.transform( "translate("+dx+", "+dy+")")
So guessing, last method do the trick for translate it, but it doesnt really change the position.
To move a path, you will need to transform it, try this format.
this.transform('t' + dx + ',' + dy );
However, as mentioned it won't keep the last position. For that you need to store the transform on the mouse down....
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 );
}
example

Can't get mouse coordinates through javascript

Recently I have been trying to get a grasp of javascript. I've been experimenting through the past couple of months with different code to see what all is possible through javascript. Recently I've been trying to find various ways to capture the mouse coordinates through javascript with a canvas. Here's my attempt: http://jsfiddle.net/f8n2one4/
As you can see, there's a MouseHandler which is supposed to capture the mouse coordinates through the ClientX and ClientY variables. However, when I try to display these variables, nothing comes up. Why is that?
document.getElementById("test").innerHTML = "x: " + x + " y: " + y;
Shouldn't it show the x and y values?
The x and y variable are out of the scope of your draw() method.
try the following:
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
player();
MouseHandler.init(document);
var clientPosition = MouseHandler.getPos();
document.getElementById("test").innerHTML = "x: " + clientPosition.x + " y: " + clientPosition.y;
}
See it working here.
I would recommend not to initialize the whole MouseHandler every interval, just getting the position on every interval.
Also if you are animating use .requestanimationframe() instead of setInterval, you'll get better performance.
In your example are you are initializing the mouse handler (with MouseHandler.init(element)) but the x and y values have to be accessed using the getPos() method:
document.getElementById("test").innerHTML =
"x: " + MouseHandler.getPos().x +
" y: " + MouseHandler.getPos().y;
You can see a working update to your fiddle here
The x and y do not exist. You need to use:
document.getElementById("test").innerHTML = "x: " + MouseHandler.getPos().x + " y: " + MouseHandler.getPos().y;
Fiddle: http://jsfiddle.net/f8n2one4/3/
It's actually really simple, your x, y are out of scope.
You could access them via your MouseHandler variable :
MouseHandler.getPos().x
MouseHandler.getPos().y
Or try OOP Javascript and instantiating the MouseHandler with a 'new' operator
var MouseHandler = function() {
this.x = 0;
this.y = 0;
}
var myMouseHandler = new MouseHandler();
console.log(myMouseHandler.x + myMouseHandler.y);

Snap SVG: Dragging group doesn't update elements

JSFiddle here: JSFiddle
When dragging a group of objects, the individual objects' location attributes don't seem to be getting updated. This occurs whether I use the default drag() handler or define my own. Even the group BBox operation doesn't seem to update. Code:
var s = Snap("#svg");
var move = function (dx, dy, posx, posy) {
this.attr({
x: posx,
y: posy
});
//this.transform("t" + dx + "," + dy);
};
var block = s.rect(100, 100, 100, 100);
var circle = s.circle(100, 100, 50);
var group = s.g(block, circle);
//group.drag(move, function () {}, function () {});
group.drag();
//block.drag(move, function () {}, function () {});
//just a way to keep info coming w/o an interminable script
document.addEventListener('mousemove', function (e) {
bbox = block.getBBox();
block_x = block.attr("x");
block_y = block.attr("y");
gbbox = group.getBBox();
console.log("block is at " + block_x + "," + block_y,
" Block Bbbox is at " + bbox.x + "," + bbox.y,
" Group Bbbox is at " + gbbox.x + "," + gbbox.y);
}, false);
If I define only one object (say, a rect) and leave it out of a group, and pass my own "move" function to the call to drag, and include setting the "x" and "y" attributes explicitly, then that works. But if I include the rect in a group, then...I can't figure out how to do it, and I've tried a few ways (see the multiple commented-out lines showing things I've tried). I need to know where the rect sub-group element ends up after the drag, or at least the BBox of the whole group. Neither of these seem to be getting updated -- i.e. the console log I put in shows the same numbers forever, no matter where I move the object(s).
Can anyone help?
JSFiddle here: JSFiddle
I think this is because they are two different things, so they aren't actually interchangable.
The drag handler uses transforms. A transform doesn't affect any other attributes, its just an attribute on an element (in this case the group element).
getBBox will work in its current transform space, note this may be different to the clients (eg if the svg were zoomed in/out). So they are two slightly different methods, that do different things.
Use getBoundingClientRect if you need a bounding box relative to the client window. Use getBBox if you need a bounding box in the elements current coordinate space.
Code is using snap.svg.zpd as well, so zoom is possible. Problem is at onStopMove function. Events are fired when group is moved arround. In group is one circle(this.select('#main-inner-circle')) which does not have predefined location inside group. Im trying to get correct cx and cy of that inner circle after moving group.
self.onMove = function (dx, dy, ev, x, y) {
var clientX, clientY;
var tdx, tdy;
if ((typeof dx == 'object') && (dx.type == 'touchmove')) {
clientX = dx.changedTouches[0].clientX;
clientY = dx.changedTouches[0].clientY;
dx = clientX - this.data('ox');
dy = clientY - this.data('oy');
}
var snapInvMatrix = this.transform().diffMatrix.invert();
snapInvMatrix.e = snapInvMatrix.f = 0;
tdx = snapInvMatrix.x(dx, dy);
tdy = snapInvMatrix.y(dx, dy);
this.transform("t" + [tdx, tdy] + this.data('ot'));
}
self.onStartMove = function (x, y, ev) {
if ((typeof x == 'object') && (x.type == 'touchstart')) {
x.preventDefault();
this.data('ox', x.changedTouches[0].clientX);
this.data('oy', x.changedTouches[0].clientY);
}
this.data('ot', this.transform().local);
if (callbacks.onStartMove) {
callbacks.onStartMove();
}
}
self.onStopMove = function () {
var self = this.select('#main-inner-circle');
this.data('ot', this.transform().local);
//self.data('ot', self.transform().local);
console.log(self.getTransformedBBox());
console.log(this.getBBox());
//console.log($(self.node).offset().left - $(self.node).parent().offset().left);
var bBox = this.getBBox();
//var x = bBox.x + $(self.node).offset().left - $(self.node).parent().offset().left + self.getBBox().width / 2;
//var y = bBox.y + $(self.node).offset().top - $(self.node).parent().offset().top + self.getBBox().height / 2;
model.updateElementCoordinates(index, $(this.node).attr("rel"), { x: self.getTransformedBBox().cx, y: self.getTransformedBBox().cy });
if (callbacks.onStopMove) {
callbacks.onStopMove();
}
}
In order to post this question, I'd created the JSFiddle but left out the crucial snap.svg definitions...
<script src="http://snapsvg.io/assets/js/snap.svg-min.js"></script>
...with that, then indeed the group.getBBox() method actually works. However:
Apparently, using getBBox() is incredibly slow -- much slower than just accessing a "x" attribute of something like I was doing before grouping objects. All I know is that my code slows to a crawl if I use getBBox() (I have a lot of objects on the screen).
Further down in the same post mentioned earier ["Get coordinates of svg group on drag with snap.svg"1 recommended getBoundingClientRect(), which also works fine AND is fast enough! My new, working Fiddle showing all of these methods is here: New JSFiddle.
So, future users: use .node.getBoundingClientRect().

jQuery Element Free Rotation. Correcting Transform Origin and Translate in IE

I'm developing a jQuery plugin to make a block-level element rotatable with mouse. Now it works as expected in non-IE browsers, but have a strange behavior while rotating in Internet Explorer.
Demo is hosted at testerski.antaranian.me here, rotation plugin script is
$.fn.roll = function(angle){
var $this = this,
ie = !jQuery.support.leadingWhitespace;
if (ie) {
var cosAngle = parseFloat(parseFloat(Math.cos(angle.rad())).toFixed(8)),
sinAngle = parseFloat(parseFloat(Math.sin(angle.rad())).toFixed(8)),
tx = 0, ty = 0,
matrixFilter = '(M11=' + cosAngle + ', '
+ 'M12=' + -sinAngle + ', '
+ 'M21=' + sinAngle + ', '
+ 'M22=' + cosAngle + ','
+ 'sizingMethod=\'auto expand\')',
filter = 'progid:DXImageTransform.Microsoft.Matrix' + matrixFilter,
css = {
'-ms-filter': filter,
'filter': filter
};
debug.log(filter);
var matrix = $M([
[cosAngle, -sinAngle, tx],
[sinAngle, cosAngle, ty],
[0, 0, 1]
]);
debug.log(matrix);
$this.transformOrigin(matrix);
$this.fixIeBoundaryBug(matrix);
} else {
var css = {
'-webkit-transform': 'rotate(' + angle + 'deg)',
'-moz-transform': 'rotate(' + angle + 'deg)',
'-o-transform': 'rotate(' + angle + 'deg)'
};
}
$this.css(css);
return this;
};
I googled and found these two pages related to this subject
Grady's guide and Zoltan's guide
As I get there are some accounting needed related to Linear Algebra, but it's hard for me so if anyone have more simple tutorial, or knows the direct solution, please let me know.
Any help would be appreciated,
Antaranian.
IE's Transform Filter, unfortunately, doesn't have a concept of "transform-origin". the 'auto expand' sizingMethod will make the transformed object take the minimum amount of space possible, and you need to change it's positioning.
In cssSandpaper, I put another <div> tag around the transformed object and adjusted it's margin-left and margin-top. If you go to the cssSandpaper website and look through the code, you will see the exact formula (search for "setMatrixFilter" in cssSandpaper.js). You can hard code it into your library, or you can use cssSandpaper itself to do it (using the cssSandpaper.setTransform() method). Even though it may add a few KB to your code, I suggest this just in case I make improvements to the way I handle transforms in the future.
In any case, good luck!
Z.
Actually I've coded it according to my needs, here is the code, if anyone else is interested.
$.fn.ieRotate = function(alfa){
var self = this,
cosAlfa = Math.cos(alfa),
sinAlfa = Math.sin(alfa),
matrix = '(M11=' + cosAlfa + ', '
+ 'M12=' + -sinAlfa + ', '
+ 'M21=' + sinAlfa + ', '
+ 'M22=' + cosAlfa + ','
+ 'sizingMethod=\'auto expand\')',
// constructing the final filter string
filter = 'progid:DXImageTransform.Microsoft.Matrix' + matrix;
self.each(function(el){
var $this = $(el),
size = $this.data('size'),
pos = $this.data('pos');
$this.css({
'-ms-filter': filter,
'filter': filter,
// for IE9
'transform': 'rotate(' + angle + 'deg)'
});
// calculate the difference between element's expeced and the actual centers
var dLeft = ($this.width() - size.width) / 2,
dTop = ($this.height() - size.height) / 2;
$this.css({
top: pos.top -dTop,
left: pos.left - dLeft
});
});
return self;
};
Usage:
// caching the image object to a variable
$image = $('img#my-image');
// saving images non-rotated position and size data
$image.data('pos', {
top: $image.position().top,
left: $image.position().left
}).data('size', {
height: $image.height(),
width: $image.width()
});
// rotate image 1.2 radians
$image.ieRotate(1.2);
Thanks to #Zoltan Hawryluk, his code helped me during the development.
The position fix for IE can also be calculated analytically - see here

Categories