Gå til indhold

Bolde-klassen

Dette tutorial forklarer brugen af JavaScript-klasser i canvas-animationer og er ikke beregnet til begyndere. Jeg anbefaler at gennemgå vores afsnit om Lighting Engine og prøve Color Cycle-tutorialet først.

Med det sagt vil jeg ikke bruge streng klassesyntaks her, da det blot er “syntaktisk sukker” i JavaScript, og de underliggende koncepter forbliver de samme. Er du interesseret i mere komplekse animationer, anbefaler jeg at besøge denne side for en grundig introduktion. Ellers holder vi os til det grundlæggende.

For det første er en klasse blot en almindelig JavaScript-funktion. Den fungerer på samme måde, men vi tilføjer muligheden for at binde værdier til en bestemt instans af funktionen. I dette eksempel har jeg oprettet en bold der opfører sig i overensstemmelse med grundlæggende fysik. Den sporer sin position og hastighed samt den “tyngdekraft” der virker på den.

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

Når vi har mestret den indledende kompleksitet, er det nemmere at tilføje mere avanceret adfærd. I næste eksempel har jeg tilføjet vægkollisioner og indsat nogle tilfældige variationer i x-retningen for at holde bolden i bevægelse.

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

Jeg vil være ærlig: Dette eksempel er Komplekst med stort K og kræver sandsynligvis mere arbejde end du har brug for til de fleste grundlæggende effekter. For ikke at have for mange kommentarer i koden, gennemgår vi her nogle vanskelige koncepter.

For det første: Hvordan ved en bold at den vil kollidere med en anden? Hvert frame skal hver instans af Bolde-klassen tjekke positionen for hver anden boldinstans. Ved at sammenligne deres positioner kan vi afgøre om to bolde er inden for hinandens radius, og derefter vende deres x- og y-hastigheder tilsvarende.

Til tider kan en bold opnå nok hastighed til at gå igennem en anden bold og næsten besætte samme rum. Matematikken til at justere begge boldpositioner ville være for kompleks, så jeg fjerner simpelthen den boldinstans vi tjekker. Da update-funktionen sikrer et minimalt antal bolde, oprettes en ny boldinstans og simuleringen fortsætter.

Endelig: Hvornår skal vi beregne ændringen i hastighed og position for hver bold? Har det betydning om bolden tegnes før eller efter opdatering af variablerne? Problemet i programmering er at der er flere svar på begge spørgsmål. For at håndtere dette har jeg introduceret en lidt anderledes metode til sporing af kollisioner. Det er op til dig at vælge den foretrukne, men begge metoder kan og fungerer.

<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){ 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. Bound the coordinates 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>

Endelig tilføjer vi nogle grundlæggende brugerkontroller for at gøre det mere interessant. Jeg har allerede gennemgået farveskift grundigt, så for at forenkle koden fokuserer vi på justering af antal og radius for boldene. Dette kræver kun nogle få ændringer i de eksisterende funktioner.

<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(){
// 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 . . .
}

Jeg håber du har nydt vores JavaScript-klasse-animationstutorial! Selv på dette kompleksitetsniveau er denne effekt blot toppen af isbjerget af hvad der er opnåeligt med HTML canvas.

For et sidste trin-for-trin tutorial, se vores afsnit om Callbacks, hvor jeg gennemgår processen med at indsamle målerdata for at aktivere og deaktivere effekter.