Classe Ball
Este tutorial explica como usar classes JavaScript em animações de canvas e não é destinado a iniciantes. Recomendo passar pela nossa seção de Motor de Iluminação primeiro e tentar o tutorial de Ciclo de Cores.
Dito isso, não usarei sintaxe de classe estrita aqui porque é apenas “açúcar sintático” do JavaScript, e os conceitos subjacentes permanecem os mesmos. Se você tem interesse em aprender sobre animações mais complexas, sugiro conferir esta página para uma introdução completa. Caso contrário, vamos nos ater ao básico.
Para começar, uma classe é simplesmente uma função JavaScript comum. Funciona da mesma forma, mas adicionamos a capacidade de vincular valores a uma instância específica da função. Neste exemplo, criei uma bola que se comporta de acordo com a física básica. Ela rastreia sua posição e velocidade, bem como a “gravidade” que age sobre ela.
Quique Simples
Seção intitulada “Quique Simples”<head> <title>Simple Ball Bounce</title> <meta description="Ball bounce"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body>
<script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); // Canvas variables var width = 320; var height = 200; // X-component of ball position let ballX = 160; // Y-component of ball position let ballY = 100; // Change in ballY over time. Positive values move down, negative move up let velocityY = 0; // Change in velocityY over time. let accelerationY = 1; // Effects array let effects = [];
function update() { // 1. Overwrite the previous frame ctx.fillStyle = background; ctx.fillRect(0, 0, 320,200);
// 2. Make sure there is one instance of the Ball class in the effects array if (effects.length < 1){ effects.push(new Ball()); }
// 3. Iterate through the effects array and call each element's draw function effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
// 4. Declare the Ball class function Ball(){ // 5. Set instance variables this.bx = ballX; this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; this.impact = false; // 6. Define draw function this.draw = function(){ // 7. Change the y-velocity by the y-acceleration this.vy += this.ay;
// 8. If an impact with the ground has been detected, set the velocity upwards and set the impact boolean to false. This check happens before the next function so that we draw the impact frame instead of skipping it. if (this.impact){ this.vy = -10; this.impact = false; }
// 9. Detect a future impact. If the ball position plus its radius plus its velocity would put it under the floor, we run a few operations - if (this.by + this.radius + this.vy > 200){ // Set the velocity to 0 so the ball stays where we put it this frame. this.vy = 0; // Set impact to true so the next run-through draws the up-bounce this.impact = true; // Next frame draws the up-bounce. This frame detected a future collision. If we want to see the impact itself we have to force it. While the ball's outer radius is below the floor, we move the ball up. while (this.by + this.radius < 200){ this.by++; } } // 10. Change the y-component by the y-velocity. this.by += this.vy; // Draw shape. ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script><head> <title>Simple Ball Bounce</title> <meta description="Ball bounce"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body><script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); // Canvas variables var width = 320; var height = 200; // X-component of ball position let ballX = 160; // Y-component of ball position let ballY = 100; // Change in ballY over time. Positive values move down, negative move up let velocityY = 0; // Change in velocityY over time. let accelerationY = 1; // Effects array let effects = [];
function update() { // 1. Overwrite the previous frame ctx.fillStyle = background; ctx.fillRect(0, 0, 320,200);
// 2. Make sure there is one instance of the Ball class in the effects array if (effects.length < 1){ effects.push(new Ball()); }
// 3. Iterate through the effects array and call each element's draw function effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
// 4. Declare the Ball class function Ball(){ // 5. Set instance variables this.bx = ballX; this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; this.impact = false; // 6. Define draw function this.draw = function(){ // 7. Change the y-velocity by the y-acceleration this.vy += this.ay;
// 8. If an impact with the ground has been detected, set the velocity upwards and set the impact boolean to false. This check happens before the next function so that we draw the impact frame instead of skipping it. if (this.impact){ this.vy = -10; this.impact = false; }
// 9. Detect a future impact. If the ball position plus its radius plus its velocity would put it under the floor, we run a few operations - if (this.by + this.radius + this.vy > 200){ // Set the velocity to 0 so the ball stays where we put it this frame. this.vy = 0; // Set impact to true so the next run-through draws the up-bounce this.impact = true; // Next frame draws the up-bounce. This frame detected a future collision. If we want to see the impact itself we have to force it. While the ball's outer radius is below the floor, we move the ball up. while (this.by + this.radius < 200){ this.by++; } } // 10. Change the y-component by the y-velocity. this.by += this.vy; // Draw shape. ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script>function update() { // 1. Overwrite the previous frame ctx.fillStyle = background; ctx.fillRect(0, 0, 320,200);
// 2. Make sure there is one instance of the Ball class in the effects array if (effects.length < 1){ effects.push(new Ball()); }
// 3. Iterate through the effects array and call each element's draw function effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }// 4. Declare the Ball class function Ball(){ // 5. Set instance variables this.bx = ballX; this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; this.impact = false; // 6. Define draw function this.draw = function(){ // 7. Change the y-velocity by the y-acceleration this.vy += this.ay;
// 8. If an impact with the ground has been detected, set the velocity upwards and set the impact boolean to false. This check happens before the next function so that we draw the impact frame instead of skipping it. if (this.impact){ this.vy = -10; this.impact = false; }
// 9. Detect a future impact. If the ball position plus its radius plus its velocity would put it under the floor, we run a few operations - if (this.by + this.radius + this.vy > 200){ // Set the velocity to 0 so the ball stays where we put it this frame. this.vy = 0; // Set impact to true so the next run-through draws the up-bounce this.impact = true; // Next frame draws the up-bounce. This frame detected a future collision. If we want to see the impact itself we have to force it. While the ball's outer radius is below the floor, we move the ball up. while (this.by + this.radius < 200){ this.by++; } } // 10. Change the y-component by the y-velocity. this.by += this.vy; // Draw shape. ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
Quique Completo
Seção intitulada “Quique Completo”Depois de passar pela complexidade inicial, é mais fácil adicionar comportamentos mais avançados. No próximo exemplo, incorporei colisões com as paredes e introduzi algumas variações aleatórias na direção x para manter a bola em movimento.
<head> <title>Full Bounce</title> <meta description="Snake-like climbing gradient effect"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body>
<script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); var width = 320; var height = 200; var hue = 0; let ballX = 160; // 1. Added velocityX let velocityX = 0; let ballY = 100; let velocityY = 0; let accelerationY = 1; let effects = [];
function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
if (effects.length < 1){ effects.push(new Ball()); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
function Ball(){ this.bx = ballX; // 2. Give x a random initial velocity this.vx = 100 * Math.sin(Date.now()); this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; // 3. Add x and y-components to the impact boolean for horizontal contact this.impactX = false; this.impactY = false;
this.draw = function(){ this.vy += this.ay;
// 4. Each frame, add a small amount of random velocity in the x-direction to keep momentum going this.vx += Math.sin(Date.now());
if (this.impactY){ this.vy = -10; this.impactY = false; } // 5. If a horizontal impact is detected, reverse the x-velocity and subtract a small amount of force to prevent an out-of-control ball. Mirroring or adding to the force here can have interesting consequences. if (this.impactX){ this.vx *= -.9; this.impactX = false; }
if (this.by + this.radius + this.vy > height){ this.vy = 0; this.impactY = true; while (this.by + this.radius < height){ this.by++; } } // 6. Detect a horizontal impact left or right and adjust like we did the y-component. if (this.bx + this.radius + this.vx > width){ this.vx = 0; this.impactX = true; while (this.bx + this.radius > width){ this.bx--; } } else if (this.bx - this.radius + this.vx < 0){ this.vx = 0; this.impactX = true; while (this.bx - this.radius < 0){ this.bx++; } } // 7. Change the x- and y-components by their velocity. this.by += this.vy; this.bx += this.vx;
ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script><head> <title>Full Bounce</title> <meta description="Snake-like climbing gradient effect"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body><script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); var width = 320; var height = 200; var hue = 0; let ballX = 160; // 1. Added velocityX let velocityX = 0; let ballY = 100; let velocityY = 0; let accelerationY = 1; let effects = [];
function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
if (effects.length < 1){ effects.push(new Ball()); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
function Ball(){ this.bx = ballX; // 2. Give x a random initial velocity this.vx = 100 * Math.sin(Date.now()); this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; // 3. Add x and y-compnents to the impact boolean for horizontal contact this.impactX = false; this.impactY = false;
this.draw = function(){ this.vy += this.ay;
// 4. Each frame, add a small amount of random velocity in the x-direction to keep momentum going this.vx += Math.sin(Date.now());
if (this.impactY){ this.vy = -10; this.impactY = false; } // 5. If a horizontal impact is detected, reverse the x-velocity and subtract a small amount of force to prevent an out-of-control ball. Mirroring or adding to the force here can have interesting consequences. if (this.impactX){ this.vx *= -.9; this.impactX = false; }
if (this.by + this.radius + this.vy > height){ this.vy = 0; this.impactY = true; while (this.by + this.radius < height){ this.by++; } } // 6. Detect a horizontal impact left or right and adjust like we did the y-component. if (this.bx + this.radius + this.vx > width){ this.vx = 0; this.impactX = true; while (this.bx + this.radius > width){ this.bx--; } } else if (this.bx - this.radius + this.vx < 0){ this.vx = 0; this.impactX = true; while (this.bx - this.radius < 0){ this.bx++; } } // 7. Change the x- and y-components by their velocity. this.by += this.vy; this.bx += this.vx;
ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script>function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
if (effects.length < 1){ effects.push(new Ball()); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }function Ball(){ this.bx = ballX; // 2. Give x a random initial velocity this.vx = 100 * Math.sin(Date.now()); this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; // 3. Add x and y-compnents to the impact boolean for horizontal contact this.impactX = false; this.impactY = false;
this.draw = function(){ this.vy += this.ay;
// 4. Each frame, add a small amount of random velocity in the x-direction to keep momentum going this.vx += Math.sin(Date.now());
if (this.impactY){ this.vy = -10; this.impactY = false; } // 5. If a horizontal impact is detected, reverse the x-velocity and subtract a small amount of force to prevent an out-of-control ball. Mirroring or adding to the force here can have interesting consequences. if (this.impactX){ this.vx *= -.9; this.impactX = false; }
if (this.by + this.radius + this.vy > height){ this.vy = 0; this.impactY = true; while (this.by + this.radius < height){ this.by++; } } // 6. Detect a horizontal impact left or right and adjust like we did the y-component. if (this.bx + this.radius + this.vx > width){ this.vx = 0; this.impactX = true; while (this.bx + this.radius > width){ this.bx--; } } else if (this.bx - this.radius + this.vx < 0){ this.vx = 0; this.impactX = true; while (this.bx - this.radius < 0){ this.bx++; } } // 7. Change the x- and y-components by their velocity. this.by += this.vy; this.bx += this.vx;
ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
Múltiplas Bolas
Seção intitulada “Múltiplas Bolas”Vou ser honesto com você: este exemplo é Complicado com C maiúsculo, e provavelmente exige mais esforço do que o necessário para a maioria dos efeitos básicos. Para não sobrecarregar o código com comentários, vamos abordar alguns conceitos complicados aqui.
Primeiro, como uma bola sabe que vai colidir com outra? A cada frame, cada instância da classe ball deve verificar a posição de cada outra instância. Comparando suas posições, podemos determinar se duas bolas estão dentro do raio uma da outra e, em seguida, inverter suas velocidades x e y de acordo.
Às vezes, uma bola pode ganhar velocidade suficiente para atravessar outra, fazendo-as ocupar quase o mesmo espaço. A matemática necessária para ajustar as posições de ambas as bolas seria muito complexa, então simplesmente removo a instância da bola que estamos verificando. Como a função update garante um número mínimo de bolas, uma nova instância será criada e a simulação continuará.
Por fim, quando devemos calcular a mudança de velocidade e posição para cada bola? Importa se a bola é desenhada antes ou depois das variáveis serem atualizadas? O problema na programação é que há múltiplas respostas para ambas as perguntas. Para lidar com isso, introduzi um método ligeiramente diferente de rastrear colisões. Cabe a você escolher o que preferir, mas ambos os métodos podem e funcionam.
<head> <title>Multiple Balls</title> <meta description="Snake-like climbing gradient effect"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body>
<script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); var width = 320; var height = 200; var hue = 0; let ballX = 160; let velocityX = 0; let ballY = 100; let velocityY = 0; let accelerationY = 1; let effects = [];
function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
// 1. Increase the number of balls present in the effects array if (effects.length < 3){ effects.push(new Ball()); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
function Ball(){ this.bx = ballX; this.vx = 100 * Math.sin(Date.now()); this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; // 2. Add a unique ID to each ball so we can compare them to each other and not themselves. We have also removed "impactX" and "impactY" in favor of a better solution for more balls. this.id = Math.random();
this.draw = function(){ this.vy += this.ay; this.vx += Math.sin(Date.now());
if (this.by + this.radius + this.vy > height){ this.vy = -10; } else if (this.by - this.radius + this.vy < 0){ // 3. Add a "ceiling" to the canvas in case a ball collision causes the height to exceed 0y. this.vy = 1; } if (this.bx + this.radius + this.vx > width){ this.vx = -.9; } else if (this.bx - this.radius + this.vx < 0){ this.vx = -.9; } // 4. Iterate through each ball in the effects array to check for collisions for (let i = 0; i < effects.length; i++){ let ele = effects[i]; // If the ball we are checking against is this ball, skip this cycle if (this.id == ele.id){ continue; } // If the balls are clipping too much, remove this ball from the canvas if (Math.abs(this.bx - ele.bx) < this.radius && Math.abs(this.by - ele.by) < this.radius){ effects.splice(i, 1); } // If the balls will collide, reverse their velocities and subtract some force to account for elasticity if (Math.abs(this.bx + this.vx - ele.bx) < (this.radius * 2) && Math.abs(this.by + this.vy - ele.by) < (this.radius * 2)){ this.vx *= -.9; this.vy *= -.9; } }
this.by += this.vy; this.bx += this.vx;
// 5. Because complicated calcualtions for the velocities happen before this point, finalize the x- and y-coordinates by making sure they are bounded within the canvas. while (this.bx - this.radius < 0){ this.bx++ } while (this.bx + this.radius > width){ this.bx-- } while (this.by - this.radius < 0){ this.by++ } while (this.by + this.radius > height){ this.by-- }
ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script><head> <title>Multiple Balls</title> <meta description="Snake-like climbing gradient effect"/> <meta publisher="WhirlwindFX" /></head>
<body style="margin: 0; padding: 0;"> <canvas id="exCanvas" width="320" height="200"></canvas></body><script> // Get the canvas element from the DOM var c = document.getElementById("exCanvas"); var ctx = c.getContext("2d"); var width = 320; var height = 200; var hue = 0; let ballX = 160; let velocityX = 0; let ballY = 100; let velocityY = 0; let accelerationY = 1; let effects = [];
function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
// 1. Increase the number of balls present in the effects array if (effects.length < 3){ effects.push(new Ball()); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }
function Ball(){ this.bx = ballX; this.vx = 100 * Math.sin(Date.now()); this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; // 2. Add a unique ID to each ball so we can compare them to each other and not themselves. We have also removed "impactX" and "impactY" in favor of a better solution for more balls. this.id = Math.random();
this.draw = function(){ this.vy += this.ay; this.vx += Math.sin(Date.now());
if (this.by + this.radius + this.vy > height){ this.vy = -10; } else if (this.by - this.radius + this.vy < 0){ // 3. Add a "ceiling" to the canvas in case a ball collision causes the height to exceed 0y. this.vy = 1; } if (this.bx + this.radius + this.vx > width){ this.vx = -.9; } else if (this.bx - this.radius + this.vx < 0){ this.vx = -.9; } // 4. Iterate through each ball in the effects array to check for collisions for (let i = 0; i < effects.length; i++){ let ele = effects[i]; // If the ball we are checking against is this ball, skip this cycle if (this.id == ele.id){ continue; } // If the balls are clipping too much, remove this ball from the canvas if (Math.abs(this.bx - ele.bx) < this.radius && Math.abs(this.by - ele.by) < this.radius){ effects.splice(i, 1); } // If the balls will collide, reverse their velocities and subtract some force to account for elasticity if (Math.abs(this.bx + this.vx - ele.bx) < (this.radius * 2) && Math.abs(this.by + this.vy - ele.by) < (this.radius * 2)){ this.vx *= -.9; this.vy *= -.9; } }
this.by += this.vy; this.bx += this.vx;
// 5. Because complicated calcualtions for the velocities happen before this point, finalize the x- and y-coordinates by making sure they are bounded within the canvas. while (this.bx - this.radius < 0){ this.bx++ } while (this.bx + this.radius > width){ this.bx-- } while (this.by - this.radius < 0){ this.by++ } while (this.by + this.radius > height){ this.by-- }
ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
window.requestAnimationFrame(update);
</script>function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
// 1. Increase the number of balls present in the effects array if (effects.length < 3){ effects.push(new Ball()); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }function Ball(){ this.bx = ballX; this.vx = 100 * Math.sin(Date.now()); this.by = ballY; this.vy = velocityY; this.ay = accelerationY; this.radius = 15; // 2. Add a unique ID to each ball so we can compare them to each other and not themselves. We have also removed "impactX" and "impactY" in favor of a better solution for more balls. this.id = Math.random();
this.draw = function(){ this.vy += this.ay; this.vx += Math.sin(Date.now());
if (this.by + this.radius + this.vy > height){ this.vy = -10; } else if (this.by - this.radius + this.vy < 0){ // 3. Add a "ceiling" to the canvas in case a ball collision causes the height to exceed 0y. this.vy = 1; } if (this.bx + this.radius + this.vx > width){ this.vx = -.9; } else if (this.bx - this.radius + this.vx < 0){ this.vx = -.9; } // 4. Iterate through each ball in the effects array to check for collisions for (let i = 0; i < effects.length; i++){ let ele = effects[i]; // If the ball we are checking against is this ball, skip this cycle if (this.id == ele.id){ continue; } // If the balls are clipping too much, remove this ball from the canvas if (Math.abs(this.bx - ele.bx) < this.radius && Math.abs(this.by - ele.by) < this.radius){ effects.splice(i, 1); } // If the balls will collide, reverse their velocities and subtract some force to account for elasticity if (Math.abs(this.bx + this.vx - ele.bx) < (this.radius * 2) && Math.abs(this.by + this.vy - ele.by) < (this.radius * 2)){ this.vx *= -.9; this.vy *= -.9; } }
this.by += this.vy; this.bx += this.vx;
// 5. Because complicated calculations for the velocities happen before this point, finalize the x- and y-coordinates by making sure they are bounded within the canvas. while (this.bx - this.radius < 0){ this.bx++ } while (this.bx + this.radius > width){ this.bx-- } while (this.by - this.radius < 0){ this.by++ } while (this.by + this.radius > height){ this.by-- }
ctx.beginPath(); ctx.arc(this.bx, this.by, this.radius, 0, Math.PI * 2); ctx.fillStyle = "black" ctx.fill(); } }
Controles do Usuário
Seção intitulada “Controles do Usuário”Por fim, vamos adicionar alguns controles básicos de usuário para tornar as coisas mais interessantes. Já discuti as mudanças de matiz em detalhes, então para evitar que o código fique ainda mais complicado, vamos focar em ajustar o número e o raio das bolas. Isso requer apenas algumas modificações nas funções existentes.
<head> <title>User Controls</title> <meta description="Snake-like climbing gradient effect"/> <meta publisher="WhirlwindFX" /> <!-- Add two numerical sliders: radius and amount ---> <meta property="radius" label="Size" type="number" min="5" max="100" default="20"> <meta property="amount" label="Amount" type="number" min="1" max="50" default="2"></head>function update() {
ctx.fillStyle = "white"; ctx.fillRect(0, 0, 320,200);
// Set the minimum effects to be the user-controlled amount. If the length of effects is greater than amount, pop an element off the end. if (effects.length < amount){ effects.push(new Ball()); } else if (effects.length > amount){ effects.pop(); }
effects.forEach(ele => { ele.draw(); })
window.requestAnimationFrame(update); }this.draw = function(){ // Finally, set the radius to be the user-controlled radius inside of the draw function each frame. this.radius = radius; this.vy += this.ay; this.vx += Math.sin(Date.now());
// . . . rest of function . . .}
Espero que você tenha curtido nosso tutorial de animações com classes JavaScript! Mesmo com esse nível de complexidade, esse efeito é apenas a ponta do iceberg em termos do que você pode alcançar com o canvas HTML. Além disso, eu não o consideraria finalizado ainda. Se buscarmos a perfeição, você vai notar que as colisões entre bolas são detectadas um frame antes de acontecerem, o que significa que frequentemente elas não chegam a colidir de verdade. As colisões também são simplificadas em termos de transferência de energia, pois a velocidade resultante considera apenas a velocidade atual da bola e não leva em conta a energia da outra bola. Por último, verificar a posição de cada bola várias vezes por loop é ineficiente em termos de complexidade de tempo e poderia ser otimizado com memoização básica.
No final, sempre seja crítico com seus efeitos. Eles geralmente podem ser melhorados de alguma forma, e a diferença entre um efeito medíocre e um ótimo muitas vezes vem de simplesmente olhar para ele e se permitir não gostar pelo menos uma vez.
Para um último tutorial passo a passo, confira nossa seção de Callbacks, onde percorro o processo de capturar dados de medidores para ativar e desativar efeitos.