In the interest of making kragen-hacks posts easier to enjoy, I'm translating 74 of them into dynamic HTML to reduce the hassle of running them. This one is, in a vague sense, a physical simulation. Probably requires Mozilla, might work in Opera, Konqueror, or Safari.
This will eventually be at http://pobox.com/~kragen/sw/dots.html. <html><head><title>Generate a bunch of random dots. Make them move.</title> <script type="text/javascript"> // Translated from an old kragen-hacks post "simulation of physical // systems", 1998-11-14, in Perl. It's kind of maddening reading and // especially translating this code, as I wouldn't write it nearly as // badly these days. // However, it's really remarkable how slow this code is. On my // 1.1GHz laptop, with 100 dots, it reports, "Each step takes 586ms to // calculate (450.44ms avg so far) and 70ms to draw, then 195ms after // drawing doing other unknown things." That's something like 5 times // slower than the Perl+PostScript version, despite some algorithmic // optimization and some micro-optimization. The NxN inner loop where // it calculates forces among all the dot pairs does seem to be the // bottleneck, since the calculation time goes down as N^2 if I reduce // the number of dots. // By comparison, the Perl+PostScript version runs through 1000 frames // in 2 min 20.5 seconds, 140.5 seconds, which is about 140ms per // frame --- and in theory it included the time to draw the results, // although that mode of output doesn't seem to work as well on my // current machine as it did on the machine where I developed it. // JavaScript on my machine seems to need something like 15 // microseconds per multiply --- i.e. 15000 instructions.. // Bugs: // - dots collide and then blow up // - in general, when timesteps get too big, things tend to blow up // --- and some of the dots never make it back // - trail lengths are a particular number of simulation steps, not a // particular number of time units // - trails that cross wraparound walls draw strangely // - there should be a dot object --- to heck with all these arrays of // arrays // - it's way too slow! function $(id) { return document.getElementById(id) } function forEachRange(n, func) { for (var ii = 0; ii < n; ii++) { func(ii) } } function forEach(list, func) { forEachRange(list.length, function(ii) { func(list[ii]) }) } function map(func, list) { var rv = [] forEach(list, function(item) { rv.push(func(item)) }) return rv } function range(n) { var rv = [] forEachRange(n, function(ii) { rv.push(ii) }) return rv } function rand(n) { return Math.floor(Math.random() * n) } function setTextContent(node, value) { while (node.firstChild) node.removeChild(node.firstChild) node.appendChild(document.createTextNode(value)) } var max_x = 600 var max_y = 400 function randompoint() { // Initially the points are confined to a small part of the canvas // to make them act more interestingly. return [ rand(max_x / 2), rand(max_y / 2) ] } // the canvas arc function takes six arguments: // center x, center y, radius, start angle (radians clockwise from right), // end angle, boolean saying whether to draw counterclockwise. // PostScript's arc function is much the same, but doesn't have that // last argument (it always draws counterclockwise) and takes angles // in degrees, counterclockwise from right. But I suspect I wasn't up // to discovering that experimentally when I first wrote this program, // which is why my original definition of "dotat" depended on round // line-caps. function dotat(ctx, point) { ctx.beginPath() ctx.arc(point[0], point[1], 1.5, 0, Math.PI*2, 0) ctx.fill() } // Probably these would be better as properties of a dot object: // position, paths, velocities. But I'm following the Perl original, // albeit with some concessions for dynamicism. // var dots, paths, velocities, time_units_per_step, frameno, delta_dots; function randvelocity() { return [rand(20)-10, rand(20)-10] } function set_number_of_dots(n) { while (dots.length > n) { dots.pop() velocities.pop() paths.pop() delta_dots.pop() } while (dots.length < n) { var npoint = randompoint() dots.push(npoint) paths.push(map(function() { return [npoint[0], npoint[1]] }, range(5))) velocities.push(randvelocity()) delta_dots.push([0, 0]) } } function initialize() { $('thecanvas').width = max_x $('thecanvas').height = max_y dots = [] paths = [] velocities = [] delta_dots = [] set_number_of_dots(50) frameno = 1 time_units_per_step = null } function round(n) { return Math.round(n * 100) / 100 } function showframe() { setTextContent($('frameno_display'), frameno) frameno++ setTextContent($('tups'), round(time_units_per_step)) var cvs = $('thecanvas') var ctx = cvs.getContext('2d') ctx.clearRect(0, 0, cvs.width, cvs.height) ctx.strokeStyle = "#777777" forEach(paths, function(path) { var firstpoint = path[0] ctx.beginPath() ctx.moveTo(firstpoint[0], firstpoint[1]) forEachRange(path.length - 1, function(ii) { var point = path[ii + 1] ctx.lineTo(point[0], point[1]) }) ctx.stroke() }) ctx.fillStyle = "#777777" forEach(dots, function(dot) { dotat(ctx, dot) }) } function hypsq(point) { return point[0]*point[0] + point[1]*point[1] } /* an inverse square law. */ /* To cut down on error, each timestep is either */ /* four space units for the fastest particle, or four acceleration units for */ /* the particle that's accelerating fastest. */ var law_constant = 20 var handle_walls var friction_per_time_unit = 0.1 var inertia = true function make_dots_repel() { for (ii = 0; ii < dots.length; ii++) { delta_dots[ii][0] = 0 delta_dots[ii][1] = 0 } var ii, jj, idot, jdot, delta, dx, dy, rsq, denom; // delta = [0, 0] for (ii = 1; ii < dots.length; ii++) { idot = dots[ii] for (jj = 0; jj < ii; jj++) { jdot = dots[jj] //delta[0] = idot[0] - jdot[0] //delta[1] = idot[1] - jdot[1] dx = idot[0] - jdot[0] dy = idot[1] - jdot[1] // inlining just this one function call speeds up this routine // by 45% on 50 points: // rsq = hypsq(delta) // rsq = delta[0] * delta[0] + delta[1] * delta[1] rsq = dx*dx + dy*dy if (rsq < 1) rsq = 1 // what was I thinking when I wrote this? denom = rsq * Math.sqrt(rsq) // force in x direction is x direction divided by // r; x direction is delta x divided by r. delta_dots[ii][0] += dx/denom delta_dots[ii][1] += dy/denom delta_dots[jj][0] -= dx/denom delta_dots[jj][1] -= dy/denom } } // Each "time" step, we move the fastest particle at most 1 step // and accelerate the fastest-accelerating particle at most 1 // unit; so we adjust the size of the timestep accordingly. var velsq, accelsq, maxchangesq = 0 if (inertia) { for (ii = 0; ii < dots.length; ii++) { velsq = hypsq(velocities[ii]) if (velsq > maxchangesq) maxchangesq = velsq } } for (ii = 0; ii < dots.length; ii++) { accelsq = hypsq(delta_dots[ii]) if (accelsq > maxchangesq) maxchangesq = accelsq } var scale = 4/Math.sqrt(maxchangesq) time_units_per_step = scale // NOTE: my original code had a bug here --- this line was meant to // make friction per time-step not vary with time-step size, but // since I wrote 1-(0.1**tups) instead of ((1-0.1)**tups), I was // getting enormous friction when time steps were short. var friction_factor = Math.pow((1 - friction_per_time_unit), time_units_per_step) var where_to_apply_force = inertia ? velocities : dots; forEachRange(dots.length, function(ii) { velocities[ii][0] *= friction_factor velocities[ii][1] *= friction_factor apply_force(scale, where_to_apply_force[ii], delta_dots[ii]) if (inertia) { dots[ii][0] += velocities[ii][0] * scale dots[ii][1] += velocities[ii][1] * scale } handle_walls(dots[ii], velocities[ii]) paths[ii].shift() paths[ii].push([dots[ii][0], dots[ii][1]]) }) } function apply_force_repel(timescale, thing, force) { thing[0] += force[0] * timescale * law_constant thing[1] += force[1] * timescale * law_constant } apply_force = apply_force_repel function apply_force_rotate(timescale, thing, force) { thing[0] += force[1] * timescale * law_constant thing[1] += -force[0] * timescale * law_constant } function apply_force_attract(timescale, thing, force) { thing[0] -= force[0] * timescale * law_constant thing[1] -= force[1] * timescale * law_constant } function apply_force_atrepel(timescale, thing, force) { thing[0] += force[0] * timescale * law_constant thing[1] -= force[1] * timescale * law_constant } function handle_walls_stopping(dot, velocity) { if (dot[0] > max_x) { dot[0] = max_x velocity[0] = 0 } if (dot[1] > max_y) { dot[1] = max_y velocity[1] = 0 } if (dot[0] < 0) { dot[0] = 0 velocity[0] = 0 } if (dot[1] < 0) { dot[1] = 0 velocity[1] = 0 } } handle_walls = handle_walls_stopping function handle_walls_bouncy(dot, velocity) { if (dot[0] > max_x) { dot[0] = max_x - (dot[0] - max_x) velocity[0] = -velocity[0] } if (dot[1] > max_y) { dot[1] = max_y - (dot[1] - max_y) velocity[1] = -velocity[1] } if (dot[0] < 0) { dot[0] = -dot[0] velocity[0] = -velocity[0] } if (dot[1] < 0) { dot[1] = -dot[1] velocity[1] = -velocity[1] } } function handle_walls_wrap(dot, velocity) { if (dot[0] > max_x) dot[0] -= max_x if (dot[1] > max_y) dot[1] -= max_y if (dot[0] < 0) dot[0] += max_x if (dot[1] < 0) dot[1] += max_y } function setwalls(select) { var val = select.options[select.selectedIndex].value handle_walls = ({ stopping: handle_walls_stopping, bouncy: handle_walls_bouncy, wrap: handle_walls_wrap, })[val] } function setdots(select) { set_number_of_dots(parseInt(select.options[select.selectedIndex].value)) } var idleratio = 1 function setidleratio(select) { idleratio = parseInt(select.options[select.selectedIndex].value) } function setfriction(select) { friction_per_time_unit = parseFloat(select.options[select.selectedIndex].value) } function setforce(select) { apply_force = ({ repel: apply_force_repel, rotate: apply_force_rotate, attract: apply_force_attract, atrepel: apply_force_atrepel, })[select.options[select.selectedIndex].value] } function setinertia(checkbox) { inertia = checkbox.checked } function timeit(thunk) { var start = new Date() thunk() return (new Date()).getTime() - start.getTime() } var total_calcms = 0 var nturns = 0 function animate() { var start = new Date() var calcms = timeit(make_dots_repel) setTextContent($('calcms'), calcms) total_calcms += calcms nturns += 1 setTextContent($('calcms_avg'), round(total_calcms / nturns)) setTextContent($('drawms'), timeit(showframe)) var mysterious_time = new Date() function reschedule() { // We run this guy from a setTimeout-0 because apparently the // actual work done inside the showframe function is almost an // order of magnitude smaller (60ms vs. 500ms) than some work that // it apparently defers for later. I think it's horrible and // appalling that my browser's canvas can't display 100 dots in // less than 500ms, but whatever. This keeps it from bogging down // the machine too much. var now = new Date() setTextContent($('weirdms'), now.getTime() - mysterious_time.getTime()) var duration = now.getTime() - start.getTime() var idletime = duration * idleratio setTextContent($('total'), idletime) setTimeout(animate, idletime) } setTimeout(reschedule, 0) } function start_animation() { initialize() showframe() animate() } </script> </head><body onLoad="start_animation()"> <h1>Generate a bunch of random dots. Make them move.</h1> <p>I'm really terribly sorry this is so incredibly slow. I don't know how Mozilla managed to make JavaScript ten times slower than Perl, but I guess JavaScript just isn't the best possible language for numerical simulations. Consequently this isn't as captivating as the Perl+PostScript version was.</p> <p>Frame #<span id="frameno_display">none yet</span>. Running at <span id="tups">???</span> time units per step; each step takes <span id="calcms">???</span>ms to calculate (<span id="calcms_avg">???</span>ms avg so far) and <span id="drawms">???</span>ms to draw, then <span id="weirdms">???</span>ms after drawing doing other unknown things. (To keep from bogging the machine down, there's another idle pause after that, of <span id="total">???</span>ms.)</p> <div style="float: right"><form> <select id="walls" onchange="setwalls(this)"> <option value="stopping" selected="selected">Stopping walls</option> <option value="bouncy">Bouncy walls</option> <option value="wrap">Wraparound walls</option> </select> <br /> <select id="force" onchange="setforce(this)"> <option value="repel">Inverse square repulsion</option> <option value="rotate">Inverse square tangential</option> <option value="attract">Inverse square attraction</option> <option value="atrepel">Vertical attract, horizontal repel</option> </select> <br /> <select id="ndots" onchange="setdots(this)"> <option>2</option> <option>3</option> <option>10</option> <option>20</option> <option selected="selected">50</option> <option>100</option> <option>200</option> </select> dots <br /> <input type="checkbox" id="inertia" checked="checked" onchange="setinertia(this)"><label for="inertia" >Dots have inertia</label> <br /> <select id="friction" onchange="setfriction(this)"> <option value="0">No friction</option> <option value="0.01" selected="selected">1% friction per time unit</option> <option value="0.1" selected="selected">10% friction per time unit</option> <option value="0.5">50% friction per time unit</option> <option value="0.9">90% friction per time unit</option> <option value="0.99">99% friction per time unit</option> <option value="1">100% friction per time unit</option> </select> <br /> <select id="dutycycle" onchange="setidleratio(this)"> <option value="19">5%</option> <option value="9">10%</option> <option value="1" selected="selected">50%</option> <option value="0">100%</option> </select> duty cycle on CPU <br /> </div> <canvas width="200" height="200" id="thecanvas"></canvas> <p>If this line is right underneath the other text, without several lines of space in between, your browser probably doesn't support <canvas>. If there aren't dots moving in it, maybe your browser has some other problem, like having JavaScript turned off, or maybe my code is broken.</p> </body></html>