Optimize pendulum animation (javascript) - javascript

TL;DR: I wrote plotly-based javascript simulation of mathematical pendulum. It works very slow. I'm looking for ideas on how to optimize it. Currently trying "bare" d3.js and struggling from the problem of coordinate transformation between SVG's coordinates and my own logical coordinates.
I'm writing web-textbook on ordinary differential equations and want to include interactive simulation and visualization of mathematical pendulum. Visualization should contain pendulum itself, its potential energy graph and full energy contour plot. Then user can choose the initial condition by clicking on energy contour plot, then animation should begin showing how the point moves in the phase space.
I wrote an example of such simulation:
https://jsfiddle.net/ischurov/p1krqnt6/
It's plotly-based. I create three axes and put there necessary graphs. The point that represent current state of the system is also a graph in plotly (i.e. scatter plot with the only one point).
Animation works as follows: I get current coordinates of the point in the phase space, calculate the new position of this point after some time, then update my graph according to this new position. The corresponding code:
var div = document.getElementById('myDiv');
function updateState(phi, v) {
var update = {x: [[phi], [phi], [0, Math.sin(phi)]], y: [[v],
[PotentialEnergy(phi)], [0, -Math.cos(phi)]]};
Plotly.restyle(div, update, [phaseDotIndex, 3, 4]);
}
myPlot.on('plotly_click', function(data){
if(data.points[0].data.type == 'contour'){
updateState(data.points[0].x, data.points[0].y);
}
});
var animate = null;
$('.animate_button').click(function(){
var div = document.getElementById('myDiv');
if(animate === null) {
var phi = div.data[phaseDotIndex].x[0],
v = div.data[phaseDotIndex].y[0],
E = FullEnergy(phi, v);
animate = setInterval(function() {
var phi = div.data[phaseDotIndex].x[0],
v = div.data[phaseDotIndex].y[0],
step = 0.1, newphi, newv, update;
newphi = phi + v * step;
newv = v + step * Force(phi);
/* skip some tweaks here */
updateState(phi, v);
},
100)
}
else
{
clearInterval(animate);
animate = null;
}
}
This code works almost as expected, but really slow and not smooth — at least, under Firefox (If I decrease update interval it works even worse).
I'm looking for ways to optimize this.
I believe that performance problems are due to plotly's update process: in order to move one point it have to recalculate the whole picture and it is slow. So I'm looking for ways to do it in different way.
Are there any ideas?
I'm looked for some direct d3.js approach which can be faster. I see the following steps here:
Draw a graph of potential energy and contour plot of full energy.
Draw the pendulum itself.
Put small circles on the graphs of potential energy and contour plot.
Make 'onclick' event handler to allow user to choose the initial state.
Run animation loop by updating the position of the circles and the pendulum according to current state.
To proceed with step 1, I can use third-party d3.js libraries like conrec for contour plots and/or excellent maurizzzio's function plot or even plotly itself (but I'm not going to use plotly to update the graph). Step 2 seem to be doable, but I didn't try it yet. The most difficult for now are steps 3 and 4 as I don't understand how to transform SVG coordinates into my graph's coordinates (that are plotted with some library) and vice-versa.
Or maybe there are more simple ways to do it?

I'm the author of function plot which is built on top of d3, luckily d3 has methods to perform mappings in d3-scale so assuming that you have a canvas of width x height dimensions which should be mapped linearly to the rectangle [xMin, yMin] x [xMax, yMax] in 2D euclidean space you'd need to create two linear scales
var xScale = d3.scale.linear()
.domain([xMin, xMax])
.range([0, width])
var yScale = d3.scale.linear()
.domain([yMin, yMax])
.range([height, 0])
Note that in SVG the y axis is flipped and because of that the yScale range's flipped too, then any 2D euclidean point is transformed to SVG coordinates as follows
var xCanvas = xScale(point.x)
var yCanvas = yScale(point.y)
The inverse transformation is given by
var xLogical = xScale.invert(point.x)
var xLogical = yScale.invert(point.y)
A possible solution I wrote to your problem using the above is
var instance = functionPlot({
target: '#demo',
disableZoom: true,
data: [{
fn: 'sin(10*(-cos(x) + y^2/2-1))',
fnType: 'implicit'
}]
})
var xScale = instance.meta.xScale
var yScale = instance.meta.yScale
var canvas = instance.canvas
var circle = canvas.append('circle')
.attr("r", 5)
.style("fill", "purple")
var start = Date.now()
function animate() {
// move the point along the circle of radius 1
var t = (Date.now() - start) * 0.003
var xLogical = Math.cos(t)
var yLogical = Math.sin(t)
var xCanvas = xScale(xLogical)
var yCanvas = yScale(yLogical)
circle
.attr('cx', xCanvas)
.attr('cy', yCanvas)
requestAnimationFrame(animate)
}
animate()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://maurizzzio.github.io/function-plot/js/function-plot.js"></script>
<div id="demo"></div>
Issue in function plot's GitHub

I realise that this isn't exactly what you want but it does demonstrate that your code can be compact and still do what you require.
The following is some code that demonstrates some maths to animate a pendulum, extracted from one of my javascript widgets but the math logic should still be usable in your own project.
Create an image object - pendulumSet, a nice round ball will do.
// variables set for the pendulum
var gravity = -0.0110808; // tuned to -0.110808 as it approximates a 1 second interval
var acceleration = 0.1; //0.1
var velocity = 0.18; //.18
var angle = 8; // 8 (.4 radians = 22.91 degrees)
Create a timer with an interval of 0.01 seconds
Put this in the timer:
acceleration = gravity * angle;
velocity += acceleration;
angle += velocity;
pendulumSet.rotation = angle +180; // rotation as per a widget engine
That's it...
You might have to use something like the following to achieve the rotation in native javascript:
pendulumSet.style.transform = "rotate(90deg)"; // change the 90deg
This code above will simulate a pendulum in a much more compact fashion. This is 1/100th of a second, graphic animation and mathematical calculation so it will take some cpu, that is unavoidable. Nevertheless, the code is compact and the impact is minimal. Depending upon the engine interpreting the code, the cpu usage will be approx. 20-25% of a 2.5ghz core2duo from 2009, easily handled by more modern, faster cpus. Running similar code in Firefox may be noticeably slower as Firefox, in my experience, seems to perform similar animations less efficiently. You just have to try it and see.
The code is taken from this example javascript widget:
A steampunk clock for the desktop
So you can see the exact same code in operation.

Related

Zooming in a D3 visualization that uses WebGL through stardustjs

I'm currently making a force network visualization that involves a large number of nodes and edges (over 50k+) using a new library called stardust.js. Stardust uses WebGL to make the rendering of nodes and edges much quicker than Canvas/D3.
However, I am unable to figure out how to add zoom and pan to this visualization.
According to this thread on stardust's google group, the creator of the stardust library mentions that there is no support for zoom and pan right now, but it is possible to implement this by setting in the mark specification by setting zoom and pan specifications as parameters.
import { Circle } from P2D;
mark MyMark(translateX: float, translateY: float, scale: float, x: float, y: float, radius: float) {
Circle(Vector2(x * scale + translateX, y * scale + translateY), radius);
}
https://stackoverflow.com/editing-help
// In the js code you can do something like:
marks.attr("translateX", 10).attr("translateY", 15).attr("scale", 2);
This library uses a kind of Typescript language where one defines "marks" (which is what all the nodes and edges are), and it should be possible to define these marks with the above parameters. But how can one implement this?
Is there an easier way to do this? Can one add a library like Pixi.js on to this visualization to make it zoom and pan?
There is no need to define custom marks (it can be done with custom marks).
The position of the objects is controlled by a Stardust.scale().
var positions = Stardust.array("Vector2")
.value(d => [d.x, d.y])
.data(nodes);
var positionScale = Stardust.scale.custom("array(pos, value)")
.attr("pos", "Vector2Array", positions)
By modifying the value function you can zoom and translate.
By attaching the zoom to the canvas the star dust drag is no longer working. But that is a different problem.
I used the example https://stardustjs.github.io/examples/graph/
In the zoom callback save the zoom parameters and request a new render of the graph.
var fps = new FPS();
var zoom_scale = 1.0, zoom_t_x = 0.0, zoom_t_y = 0.0;
d3.select(canvas).call(d3.zoom().on("zoom", zoomed));
function zoomed() {
zoom_scale = d3.event.transform.k;
zoom_t_x = d3.event.transform.x;
zoom_t_y = d3.event.transform.y;
requestRender();
}
function render() {
positions.value(d => [d.x*zoom_scale + zoom_t_x, d.y*zoom_scale + zoom_t_y]);
......
}
The example contains an error.
When you use a slider the simulation never stops because the alphaTarget is set to 0.3.
force.alphaTarget(0.3).restart();
It should be changed to
force.alpha(0.3).alphaTarget(0).restart();

How to change sizing of D3 axis?

Working within a React framework, I have a JSON data set of weights and I am trying to display on a simple number line using D3.js, where the left end is the min, the right end is the max, and a particular individuals weight is a tick with the color red.
I've coded up a solution that creates this, however I cannot for the life of me figure out how to make it larger. Here's the code, where this.state.range = [126.4, 212.2] and this.state.avg = 167:
var svgContainer = d3.select("body").append("svg").attr("width",500).attr("height", 200);
var axisScale = d3.scaleLinear().domain(this.state.range).range(this.state.range);
var axisBot = d3.axisBottom(axisScale);
var ticks = [this.state.range[0], this.state.range[1], this.state.avg, this.state.patient.weight];
axisBot.tickValues(ticks);
var xAxisGroup = svgContainer.append("g").call(axisBot);
d3.selectAll('g.tick').filter(function(d){ return d==186.4;} ).select('line').style('stroke','red');
This code produces this result:
Can anyone please tell me how to a) make this number line larger and b) how to center the line?
I apologize if this is a bit remedial, however it is my first time working with D3.js.
In your code, the domain and the range are the same (assuming that this.state.range is an array):
var axisScale = d3.scaleLinear()
.domain(this.state.range)
.range(this.state.range);
You have to set the range according to the positions you want in your SVG. For instance, without any padding, if your SVG has a width of 500px:
var axisScale = d3.scaleLinear()
.domain(this.state.range)
.range([0, 500]);
So, the range goes from left border (x = 0) to the right border (x = 500).
Remember: domain refers to the input values, while range refers to the output positions.

d3.js Force layout only applied vertically

I'm doing a data visualisation with d3. To give you some context,
the graph contains about 400 nodes (all data is loaded from multiple
json files) that are connected to each other
they are all mapped by year in a timeline (x axis)
the position in the y axis is completely randomized
the nodes have all different sizes
Now my question:
How can I distribute the nodes in the y axis so that they don't overlap?
You can checkout the full sourcecode on the GitHub Repository (work in progress - currently on the real-database branch).
This is a screenshot of how it currently looks:
Basically, in the tick() function, reset the nodes array x values to what you want them to be (presumably some scale to do with year), and the node and links will be drawn at those x values, and subsequent force calculations will start again from those values too
force.on("tick", function() {
// Adjust to what you want nodePos to be, here I'm just doing it by index
graph.nodes.forEach (function(nodePos,i) {
nodePos.x = i * 15;
//nodePos.x = xscale (data[i].year); // or whatever
})
// then normal node/link layout
I've forked this standard force-directed example by blt909 to show one way it could be done -->
http://jsfiddle.net/vztydams/
PS If you have a lot of items and very few discrete x values, best to give them a bit of wiggle room at first (i.e. a range in x they're contained to rather than a value), then slowly narrow that range down. Otherwise nodes will get 'stuck' behind each other.
Edit 02/03/16:
Hi Alvaro, essentially the graph.nodes is your linked data, as these are the objects that are attached to the displayed nodes as the data.
So if I set up a scale, and stick in a random year per datum:
var dom = [1994,2014];
var xscale = d3.scale.linear().domain(dom).range([20,400]);
graph.nodes.forEach (function(datum) {
datum.year = dom[0] + Math.floor (Math.random() * (dom[1] - dom[0]));
});
...
We can then restrict the x position of each node's datum like this:
graph.nodes.forEach (function(d,i) {
//d.x = i * 15;
d.x = xscale(d.year);
})
(As I say, if you have a lot of nodes and few years, you'd be better restricting to a range and then narrowing that range down on each subsequent tick)
http://jsfiddle.net/vztydams/2/

How to place one element inside another in raphael js?

Im trying to create an interactive seating layout like this Seats.io. However I dont need the exact features but just few things such as:
Plotting seats anywhere on the screen
Plotting list of seats from one point to another
Seats hover as circle when plotting from one mouse click point to another
After much research in Jquery and simultaneously on raphaeljs, I have decided to start working with raphaeljs. Im totally new to the vector graphics. So obviously there might be something that I may be missing. I have followed this fiddle to draw a straight line. I have also created another script to plot circles anywhere on the window(the circles will mean seats) following is the script
window.onload = function () {
var height = $(document).outerHeight(true);
var width = $(document).width();
var radius = 10;
var paper = Raphael(0, 0, width, height);
var i = 0;
$(document).click(function (e) {
i = i + 1;
var x = e.pageX;
var y = e.pageY;
var seat = paper.circle(x, y, radius)
.attr({stroke: "none", fill: "#f00", opacity: .4})
.data("i", i);
seat.mouseover(function () {
this.attr("opacity", 1);
});
seat.mouseout(function () {
this.attr("opacity", .4);
});
});
}
using the above script I'm able to plot circles(seats) on my screen. Now based on the fiddle example lines are drawn using 'path', so is it possible to load circles on every path and draw them as sequential line of circles one after the other, or do I have to take any different approach.
Also on a side note is there any opensource project or code for the Seats.io
Any help would be really appreciated
Ben from seats.io here.
http://raphaeljs.com/reference.html#Element.getPointAtLength is indeed what we use. You'll basically need to
calculate a helper path between start and end point. You already have that.
calculate the distance between seats (based on seat size): helperPath.getTotalLength() / (numberOfSeats - 1);
for each seat, call getPointAtLength and draw a circle around that
point: helperPath.getPointAtLength(distanceBetweenSeatsOnHelperPath * i++)
Obviously, it gets more interesting if you want to snap to a grid to align rows, curve rows, etc, but you should be able to get started with the above.

How can I make Raphael.js elements "wiggle" on the canvas?

I'm working on a project that uses SVG with Raphael.js. One component is a group of circles, each of which "wiggles" around randomly - that is, slowly moves along the x and y axes a small amount, and in random directions. Think of it like putting a marble on your palm and shaking your palm around slowly.
Is anyone aware of a Raphael.js plugin or code example that already accomplishes something like this? I'm not terribly particular about the effect - it just needs to be subtle/smooth and continuous.
If I need to create something on my own, do you have any suggestions for how I might go about it? My initial idea is along these lines:
Draw a circle on the canvas.
Start a loop that:
Randomly finds x and y coordinates within some circular boundary anchored on the circle's center point.
Animates the circle from its current location to those coordinates over a random time interval, using in/out easing to smooth the effect.
My concern is that this might look too mechanical - i.e., I assume it will look more like the circle is tracing a star pattern, or having a a seizure, or something like that. Ideally it would curve smoothly through the random points that it generates, but that seems far more complex.
If you can recommend any other code (preferably JavaScript) that I could adapt, that would be great too - e.g., a jQuery plugin or the like. I found one named jquery-wiggle, but that seems to only work along one axis.
Thanks in advance for any advice!
Something like the following could do it:
var paper = Raphael('canvas', 300, 300);
var circle_count = 40;
var wbound = 10; // how far an element can wiggle.
var circleholder = paper.set();
function rdm(from, to){
return Math.floor(Math.random() * (to - from + 1) + from);
}
// add a wiggle method to elements
Raphael.el.wiggle = function() {
var newcx = this.attrs.origCx + rdm(-wbound, wbound);
var newcy = this.attrs.origCy + rdm(-wbound, wbound);
this.animate({cx: newcx, cy: newcy}, 500, '<');
}
// draw our circles
// hackish: setting circle.attrs.origCx
for (var i=0;i<circle_count;i++) {
var cx = rdm(0, 280);
var cy = rdm(0, 280);
var rad = rdm(0, 15);
var circle = paper.circle(cx, cy, rad);
circle.attrs.origCx = cx;
circle.attrs.origCy = cy;
circleholder.push(circle);
}
// loop over all circles and wiggle
function wiggleall() {
for (var i=0;i<circleholder.length;i++) {
circleholder[i].wiggle();
}
}
// call wiggleAll every second
setInterval(function() {wiggleall()}, 1000);
http://jsfiddle.net/UDWW6/1/
Changing the easing, and delays between certain things happening should at least help in making things look a little more natural. Hope that helps.
You can accomplish a similar effect by extending Raphael's default easing formulas:
Raphael.easing_formulas["wiggle"] = function(n) { return Math.random() * 5 };
[shape].animate({transform:"T1,1"}, 500, "wiggle", function(e) {
this.transform("T0,0");
});
Easing functions take a ratio of time elapsed to total time and manipulate it. The returned value is applied to the properties being animated.
This easing function ignores n and returns a random value. You can create any wiggle you like by playing with the return formula.
A callback function is necessary if you want the shape to end up back where it began, since applying a transformation that does not move the shape does not produce an animation. You'll probably have to alter the transformation values.
Hope this is useful!
There is a very good set of easing effects available in Raphael.
Here's a random set of circles that are "given" bounce easing.
Dynamically add animation to objects
The full range of easing effects can be found here. You can play around with them and reference the latest documentation at the same time.
Putting calls in a loop is not the thing to do, though. Use callbacks, which are readily available.

Categories