What is the right way to change Raphaël path elements - javascript

I try to change Raphaël path elements essentially in the following way (please regard the code includes build up stuff for a complete example):
var n = 100,
i = 0;
var values = [n];
var panel = document.createElement("div");
var paper = null;
var path = null;
panel.id = "panel";
panel.style.top = '0px';
panel.style.left = '0px';
panel.style.width = '300px';
panel.style.height = '300px';
panel.style.background = 'black';
document.getElementsByTagName('body')[0].appendChild(panel);
paper = Raphael(panel, 0, 0);
path = paper.path('m0,0');
path.attr({ stroke: '#fff', 'stroke-width': 1 });
function test () {
i = n;
while (i--)
values[i] = Math.round(Math.random() * 3);
// perform change here!
path.attr({ path: 'm0,0l0,' + values.join('l3,') });
// just a test case!
setTimeout(test, 1);
};
test();
Unfortunately this approach leaks in memory. I've tested it in FF 4 and IE 7+ with Raphaël 1.5.2 and 2.0 beta. The only difference is that Raphaël 1.5.2 leaks much faster than 2.0 beta.
What am I doing wrong?
Update
To put this question into context: I want to implement a 'realtime' graph control with Raphaël. Therefore I use an array buffer for each series and render them when the buffer size is reached, so I need only to render a given fix length series.
The only way I saw to do this in Raphaël is a path element per series which gets an update of it's path attribute .attr({path: path.attr('path') + getSvgPath(buffer)}) if necessary, followed by an translation on the x axis depending on the buffer size .animate({translation: (bufferSize*valuesDistance*-1) + ',0'}, 500, '<', callback) - for a smooth animation of the updates - and at last a shift of the path attribute after the animation to prevent ever expanding path strings: .attr({path: shiftSvgPath(path.attr('path'))}).
The functions shiftSvgPath() and getSvgPath() just returning the appropriate svg path string. So that the result always consits of one moveTo command at the beginning and a constant number of lineTo commands, either equal to the number of displayed values or plus the buffer size.

I ran into a similar problem lately. I wouldn't call it a leak, but rather a huge memory consumption from Raphael, when drawing paths. I'm just guessing it uses some caching arrays internally that eat up a lot of memory.
My approach was to ditch Raphael and draw the svg elements with plain old javascript.

Related

Rollercoaster simulation in matlab/javascript

I'm having problem with a project that I'm working on in matlab which i'm then going to implement using javascript. The purpose is to use matlab to get a better understanding of the physics before moving over to javascript. The goal is to create some sort of a Rollercoaster simulation in matlab using differential equations and euler aproximation. Then to animate a block(cart) following the path.
The problem is that I can't get the approximation working on a arbitary path (equation), and I don't understand what I'm doing wrong.
So this is what i'm trying to do:
for t=1:h:tf
%acceleration
ax(i) = (g*cos((-pi/4)))*sin(-pi/4)-((C*ro*A*vx(i)^2*sign(vx(i)))/(2*m))*sin(-pi/4);
ay(i) = (g*cos((-pi/4)))*cos(-pi/4)+((C*ro*A*vy(i)^2*sign(vy(i)))/(2*m))*cos(-pi/4);
%speed
vx(i+1) = vx(i)+h*ax(i);
vy(i+1) = vy(i)+h*ay(i);
%position
x(i+1) = x(i)+h*vx(i);
y(i+1) = y(i)+h*vy(i);
i = i+1;
speed = sqrt(vx(i)^2+vy(i)^2);
plot(x(i),y(i),'or')
pause(1/speed)
end
Where i'm following Newtons second law (F=ma => a = F/m) the only negative force component i'm using now is air resistance.
This is when i'm using a hardcoded path -> y=1-x, and it's working just fine!
But when i try to use an arbitary path, eg. 1/x, the angle is changed all the time, and i've tried things like putting each angle into an angle vector:
%% initial values
vx(1) = 0;
vy(1) = 0;
x(1) = 0;
y(1) = 6.7056;
%Track Geometry Constants
h_start = 6.7056;
l_end = 32;
b = .055;
w = .7;
p = .3;
%% Creating path
path_x = linspace(0, 23, 1000);
path_y = (exp(-b*path_x).*(cos(w*path_x - p)) + 2.2*(exp(-(b)*path_x)));
path_y = path_y*(h_start/max(path_y));
path_x = path_x*(l_end/max(path_x));
%%
alpha = zeros(size(path_y));
for k=1:1:size(path_y)-1
alpha(j) = atan(path_y(k+1)-path_y(k))/(path_x(k+1)-path_x(k));
j= j+1;
end
But this doesn't appear to work.
How can I make this work for an arbitrary path?
Thank you in advance!
There is a pretty simple error in your loop. Two errors, actually.
You are indexing alpha with a variable, j, that doesn't exist and increment it in the loop, instead of just using k, which increments automatically in the loop.
The reason this isn't giving an error is that your loop never runs. Because the size of path_y is not a single number, (the size is 1 x 1000), k=1:1:size(path_y)-1 tries to create a loop that goes from 1 to 0 in steps of positive 1. Since this isn't possible, the loop is skipped. One option is to use length, not size here.
But the most important error: you didn't check your code line by line when it stopped working to confirm that every part of your code was doing what you thought it was when you wrote it. If you'd checked what k=1:1:size(path_y)-1 was outputting, you should have been able to identify this problem very quickly.
Incidentally I think you can avoid the loop entirely (append a zero at the end if you really need this to have the same size as the path variables):
alpha = atan(diff(path_y)./diff(path_x));

how to access image data in Elm?

How do we get the Pixel data from images in Elm?
Here in JavaScript, is code to get the color of a set of pixels in a figure (taken from here)
var image = new Image;
image.src = "starry-night.jpg";
var canvas = d3.select("body").append("canvas");
var context = canvas.node().getContext("2d");
context.drawImage(image, 0, 0);
// beware variable name "image" got used twice
image = context.getImageData(0, 0, width, height);
var x = Math.random()*width,
y = Math.random()*height,
i = (y * width + x) << 2;
pixelColor = d3.rgb(image.data[i + 0], image.data[i + 1], image.data[i + 2]) + "";
The code loads an image to a <canvas> element, then extracts the color of a pixel from the canvas using image.getImageData().
Can we interface the image.data object in Elm? Right now I don't think it's possible...
Right now Collage types are list of forms...
HTML is also a module that can put imags in the DOM.
SVG allows for some simple global image transformations but nothing at the pixel level
Elm has the Virtual Dom. In fact of problems like this, might be addressed in virtual-dom which is lower level so we are not encouraged to do this directly.
However, Elm makes a clear distinction between Collage elements and SVG elements, with no clear interface to the getImageData() function.
Do I write my own with Elm's new interOp feature?
Does a way already exist in Elm? Or a new one has to be written?
JavaScript
The << operator is called Left Shift
As suggested by #SimonH, use a port to JS until Elm provides a first-hand way to do so (if it ever does). The same approach would apply to anything you can't yet do in Elm.
I'm just answering as an answer rather than a comment for the sake of others who come here.

Is there any way to get these 'canvas' lines drawn in one loop?

I am simulating a page turn effect in html5 canvas.
On each page I am drawing lines to simulate lined paper.
These lines are drawn as the page is turned and in order to give natural perspective I am drawing them using quadratic curves based of several factors (page turn progress, closeness to the center of the page etc.. etc...)
The effect is very natural and looks great but I am looking for ways to optimize this.
Currently I am drawing every line twice, once for the actual line and once for a tiny highlight 1px below this line. I am doing this like so:
// render lines (shadows)
self.context.lineWidth = 0.35;
var midpage = (self.PAGE_HEIGHT)/2;
self.context.strokeStyle = 'rgba(0,0,0,1)';
self.context.beginPath();
for(i=3; i < 21; i++){
var lineX = (self.PAGE_HEIGHT/22)*i;
var curveX = (midpage - lineX) / (self.PAGE_HEIGHT);
self.context.moveTo(foldX, lineX);
self.context.quadraticCurveTo(foldX, lineX + ((-verticalOutdent*4) * curveX), foldX - foldWidth - Math.abs(offset.x), lineX + ((-verticalOutdent*2) * curveX));
}
self.context.stroke();
// render lines (highlights)
self.context.strokeStyle = 'rgba(255,255,255,0.5)';
self.context.beginPath();
for(i=3; i < 21; i++){
var lineX = (self.PAGE_HEIGHT/22)*i;
var curveX = (midpage - lineX) / (self.PAGE_HEIGHT);
self.context.moveTo(foldX, lineX+2);
self.context.quadraticCurveTo(foldX, lineX + ((-verticalOutdent*4) * curveX) + 1, foldX - foldWidth - Math.abs(offset.x), lineX + ((-verticalOutdent*2) * curveX) + 1);
}
self.context.stroke();
As you can see I am opening a path, looping through each line, then drawing the path. Then I repeat the whole process for the 'highlight' lines.
Is there any way to combine both of these operations into a single loop without drawing each line individually within the loop which would actually be far more expensive?
This is a micro-optimization, I am well aware of this. However this project is a personal exercise for me in order to learn html5 canvas performance best practices/optimizations.
Thanks in advance for any suggestions/comments
Paths can be stroked as many times as you like, they're not cleared when you call .stroke(), so:
create your path (as above)
.stroke() it
translate the context
change the colours
.stroke() it again
EDIT tried this myself - it didn't work - the second copy of the path didn't notice the translation of the coordinate space :(
It apparently would work if the path was created using new Path() as documented in the (draft) specification instead of the "current default path" but that doesn't appear to be supported in Chrome yet.

JS Canvas- Put Image Data With Alpha?

I've got an ipad webapp I'm working on where you can draw on a canvas and save it. Like a more basic paint program. I need to be able to upload and image to the background and draw on it. Right now that wouldn't be too difficult since I got the drawing functionality and it wouldn't be hard to just print the image to the background and draw on it. The problem I'm having is that it also needs to have manageable layers. This means it needs to support alpha pixels.
So what I've done is written a panel class that when paint is called it moves down through it's child panels and paints their buffered images to the temp image. Then I take that and paint it over the parent- continueing until the image is flattened to a temporary image.
This works fine- especially on a desktop. But to accomplish this I had to write the putImageData code from scratch which loops through the array of pixels and paints them taking the alpha in account. Like so-
var offset = (canvasW*4*y)+x*4;
for(var r = 0; r < newHeight; r++)
{
var lineOffset = (size.width*4 - columns)*r + offset;
for(var c = 0; c < columns; c+=4)
{
var start = (r*columns)+c;
var destStart = start+lineOffset;
var red = imageData[start];
var green = imageData[start+1];
var blue = imageData[start+2];
var alpha = imageData[start+3];
var destRed = canvasData[destStart];
var destGreen = canvasData[destStart+1];
var destBlue = canvasData[destStart+2];
var destAlpha = canvasData[destStart+3];
var opacity = alpha/255;
var destOpacity = destAlpha/255;
var invOpacity = 1-opacity;
var newRed = Math.abs(red - ((red-destRed)*invOpacity));
var newGreen = Math.abs(green - ((green-destGreen)*invOpacity));
var newBlue = Math.abs(blue - ((blue-destBlue)*invOpacity));
canvasData[start+lineOffset] = newRed;
canvasData[start+lineOffset+1] = newGreen;
canvasData[start+lineOffset+2] = newBlue;
canvasData[start+lineOffset+3] = 255;
}
}
This takes about 50 miliseconds per layer. Not very good for a desktop. Takes a whopping 1200 miliseconds for the ipad! So I tested it with the original putImageData (which doesn't support alpha) and it was still not very impressive but it's the best I got I'm thinking.
So here is my problem. I know there is an overal opacity for drawing with canvases but it needs to be able to draw some pixels completely opaque and some completely transparent. Is there an putImageData that includes opacity?
If not any recommendations on how I can accomplish this?
As #Jeffrey Sweeney mentioned, try stacking canvases on top of each other. For one of my Javascript library, CInk (search there for z-index), I did the same thing.
I had one container div, which I stuffed with many canvas DOMs to mimic the layers. All canvas DOMs are absolutely positioned and their z-index define the order of the layers. In your case you will have to apply style at specific layers to set its opacity.

Raphael JS : how to move/animate a path object?

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);

Categories

Resources