Pular para o conteúdo

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.

<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>

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>

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>

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.