Previous page : The onset of Chaos. Bouncing balls.
Next page : Bouncing balls, the maths
Chaotic Bounces – the program
One of the main tricks to get everything deterministic is to do all calculations pretending the canvas has a particular size, and then multiply all the values when drawing with a scale factor to match the actual canvas size.
This then looks like this when drawing.
1 2 3 4 5 6 |
for (let x = xS; x <= -xS + 1; x++) { ctx.lineTo( (x + bowl.radius) * canvasScale, (a * x ** 2 + c) * canvasScale ); } |
At each collision, the time for the next collision is calculated, or rather the time it will take for the next collision to occur. This time, and what type of collision that will happen is stored for each ball. As the time ticks on it is compared to that time to know if a new bounce has occurred. The bounces could be against the bowl, one of the walls or the ceiling.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
move: function (time) { for (let i in balls.ball) { let ball = this.ball[i]; // Calculate time since last bounce. let tR = time - ball.t0; // If the time exceeds the calculated time to the next then // bounce, else proceed. if (tR > ball.tBounce) { bounce.bounce(i, tR); } else { [ball.x, ball.y] = this.position(i, tR); } } }, |
I first do the assignment:
1 |
let ball = this.ball[i]; |
This is to not have to write ball[i] repeatedly. I use this trick quite often. I did a timing test on the two variants, and if anything, this way of doing it was about 2% faster on Chrome. In Firefox the difference was about 10% in favour of the method I have used here. Firefox was also about 30% slower than Chrome.
We get the time to the next bounce from ball.tBounce. We get the time at the start of one particular parabolic motion from ball.t0
The position of the collisions with the bowl is done by solving the quartic equation one gets from combining the equation of the bowl with the equation of the ball’s parabolic trajectory.
The button presses are redirected to the keyboard event function. Like this:
1 2 |
const step = document.getElementById("step"); step.addEventListener("click", () => keyDown({ key: "S" })); |
It is done using anonymous functions to preserve the value of “this”. The touch events are redirected to the mouse events in a similar faction.
Initially, I had one setInterval loop for the timing and one requestAnimationframe loop for the graphics, but Firefox throttled the setInteval loop so it became way too slow. I instead moved the calculations inside the requequestAnimationFrame-loop, and rescaled the speed depending on the actual frame-time.
The average distance between adjacent balls is calculated every 10th step.
An interesting bug
In very rare occasions the ball bounced away from the canvas when it hit a corner. After some debugging, I found that it happens when the time to the next bounce was less than one step time. That meant that the calculation of the time for the bounce against a wall or the ceiling became negative…
I fixed this by adding an absolute value calculation, and then in the bounce function, I did a bit of recursion. That happens in the last few lines of the function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function theAnimationLoop() { let turns = running ? timing.loopTurns : 1; for (let i = 0; i < turns; i++) { timing.step++; if (timing.step % 100 === 0) distance.distanceCalc(); timing.time += timing.stepTime; balls.move(timing.time); } clearDrawDraw(); distance.draw(); timing.show(showTime, "time =", timing.time); if (timing.timeToChaos > 0) { timing.show( showTimeToChaos, ", time to Chaos &asymp; ", timing.timeToChaos ); } else showTimeToChaos.innerText = ""; if (running) requestAnimationFrame(theAnimationLoop); } |
…and another one
I noticed that the calculation of the frame time became way off if one happened to view another page as the measurements were done. I added an event handler if the page became out of focus (blur) that reset the variables in the loop.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
... timing.frameTimeTest = function (durationTime) { timing.step++; if (timing.step === 1) { start.innerText = "Wait"; start.disabled = true; } if (timing.step === 10) timing.tOld = durationTime; if (timing.step < 110) { requestAnimationFrame(timing.frameTimeTest); } else { start.innerText = "Start"; start.disabled = false; timing.frameTime = (durationTime - timing.tOld) / 100; timing.time = 0; timing.step = 0; timing.loopTurns = Math.floor(timing.frameTime / timing.stepTime); } }; window.addEventListener("blur", blur); // This restarts the frame test if the windows looses focus. function blur() { timing.step = 0; timing.tOld = 0; } |
Up a level : Fractals and Chaos
Previous page : The onset of Chaos. Bouncing balls.
Next page : Bouncing balls, the mathsLast modified: