Classe Ball
Este tutorial explica como usar classes JavaScript em animações de canvas e não é destinado a principiantes. Recomendo percorrer a nossa secção de Motor de Iluminação primeiro e experimentar o tutorial de Ciclo de Cores.
Dito isto, não usarei sintaxe de classe estrita aqui porque é apenas “açúcar sintático” do JavaScript, e os conceitos subjacentes permanecem os mesmos. Se tem interesse em aprender sobre animações mais complexas, sugiro consultar esta página para uma introdução completa. Caso contrário, vamos manter-nos no básico.
Para começar, uma classe é simplesmente uma função JavaScript comum. Funciona da mesma forma, mas adicionamos a capacidade de associar 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. Rastreia a 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. if (this.impact){ this.vy = -10; this.impact = false; }
// 9. Detect a future impact. if (this.by + this.radius + this.vy > 200){ this.vy = 0; this.impact = true; 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>
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> 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 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 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 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>
Múltiplas Bolas
Seção intitulada “Múltiplas Bolas”Vou ser honesto consigo: 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 é que 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. Ao comparar as suas posições, podemos determinar se duas bolas estão dentro do raio uma da outra e, em seguida, inverter as suas velocidades x e y em conformidade.
À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 demasiado complexa, portanto simplesmente removo a instância da bola que estamos a verificar. 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 isto, introduzi um método ligeiramente diferente de rastrear colisões. Cabe-lhe a si 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> 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 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 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 (this.id == ele.id){ continue; } if (Math.abs(this.bx - ele.bx) < this.radius && Math.abs(this.by - ele.by) < this.radius){ effects.splice(i, 1); } 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. Finalize the x- and y-coordinates within the canvas bounds. 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>
Controlos do Utilizador
Seção intitulada “Controlos do Utilizador”Por fim, vamos adicionar alguns controlos básicos de utilizador para tornar as coisas mais interessantes. Já discuti as mudanças de matiz em detalhes, portanto, para evitar que o código fique ainda mais complicado, vamos focar em ajustar o número e o raio das bolas. Isto 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);
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(){ // 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 tenha gostado do nosso tutorial de animações com classes JavaScript! Mesmo com este nível de complexidade, este efeito é apenas a ponta do icebergue em termos do que pode alcançar com o canvas HTML. Além disso, não o consideraria finalizado ainda. Se procurarmos a perfeição, vai notar que as colisões entre bolas são detetadas um frame antes de acontecerem, o que significa que frequentemente 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 tem 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, seja sempre crítico com os seus efeitos. 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 permitir-se não gostar pelo menos uma vez.
Para um último tutorial passo a passo, consulte a nossa secção de Callbacks, onde percorro o processo de capturar dados de medidores para ativar e desativar efeitos.