Kelas Ball
Tutorial ini menerangkan cara menggunakan kelas JavaScript dalam animasi kanvas dan tidak dimaksudkan untuk pemula. Saya mengesyorkan untuk melalui bahagian Enjin Pencahayaan kami terlebih dahulu dan mencuba tutorial Kitaran Warna.
Walau bagaimanapun, saya tidak akan menggunakan sintaks kelas yang ketat di sini kerana ia hanyalah “gula sintaktik” JavaScript, dan konsep asasnya tetap sama. Jika Anda berminat untuk mempelajari animasi yang lebih kompleks, saya cadangkan menyemak halaman ini untuk pengenalan yang menyeluruh. Jika tidak, mari kita berpegang kepada asas-asasnya.
Untuk memulakan, kelas hanyalah fungsi JavaScript biasa. Ia berfungsi dengan cara yang sama, tetapi kita menambah kemampuan untuk mengikat nilai kepada contoh fungsi tertentu. Dalam contoh ini, saya telah mencipta bola yang berkelakuan mengikut fizik asas. Ia menjejaki kedudukan dan halajunya, serta “graviti” yang bertindak ke atasnya.
Pantulan Mudah
Section titled “Pantulan Mudah”<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>
Pantulan Penuh
Section titled “Pantulan Penuh”Setelah kita mengatasi kerumitan awal, lebih mudah untuk menambah tingkah laku yang lebih maju. Dalam contoh seterusnya, saya telah menggabungkan perlanggaran dinding dan memperkenalkan beberapa variasi rawak dalam arah-x untuk memastikan bola terus bergerak.
<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>
Berbilang Bola
Section titled “Berbilang Bola”Saya akan jujur dengan Anda, contoh ini adalah Rumit dengan huruf besar R, dan ia berkemungkinan memerlukan lebih banyak usaha daripada yang Anda perlukan untuk kebanyakan kesan asas. Untuk mengelakkan penambahan terlalu banyak komen dalam kod, kita akan membincangkan beberapa konsep yang rumit di sini.
Pertama, bagaimana bola tahu ia akan berlanggar dengan yang lain? Setiap bingkai, setiap contoh kelas bola mesti memeriksa kedudukan setiap bola lain. Dengan membandingkan kedudukan mereka, kita boleh menentukan apakah dua bola berada dalam jejari antara satu sama lain, dan kemudian membalikkan halaju x- dan y-mereka dengan sewajarnya.
Kadang-kadang, bola mungkin mendapat halaju yang cukup untuk melepasi bola lain, menjadikan mereka hampir berada di ruang yang sama. Matematik yang diperlukan untuk melaraskan kedudukan kedua-dua bola akan terlalu kompleks, jadi saya hanya mengalih keluar contoh bola yang sedang kita semak. Memandangkan fungsi kemaskini memastikan bilangan bola minimum, contoh bola baru akan dicipta, dan simulasi akan diteruskan.
Akhirnya, bilakah kita perlu mengira perubahan dalam halaju dan kedudukan untuk setiap bola? Adakah penting sama ada bola dilukis sebelum atau selepas pemboleh ubah dikemas kini? Isu dalam pengaturcaraan ialah terdapat pelbagai jawapan untuk kedua-dua soalan. Untuk menangani ini, saya telah memperkenalkan kaedah yang sedikit berbeza untuk mengesan perlanggaran. Terpulang kepada Anda untuk memilih yang Anda sukai, tetapi kedua-dua kaedah boleh dan memang berfungsi.
<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>
Kawalan Pengguna
Section titled “Kawalan Pengguna”Akhirnya, kita akan menambah beberapa kawalan pengguna asas untuk menjadikan perkara lebih menarik. Saya sudah membincangkan perubahan hue secara terperinci, jadi untuk mengelakkan kod menjadi lebih rumit lagi, kita akan fokus pada melaraskan bilangan dan jejari bola. Ini hanya memerlukan beberapa pengubahsuaian pada fungsi sedia ada.
<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 . . .}
Semoga Anda menikmati tutorial animasi kelas JavaScript kami! Walaupun pada tahap kerumitan ini, kesan ini hanyalah puncak gunung ais dari segi apa yang boleh Anda capai dengan kanvas HTML. Selain itu, saya tidak akan menganggapnya selesai lagi. Jika kita bertujuan untuk kesempurnaan, Anda akan menyedari bahawa perlanggaran bola dikesan satu bingkai sebelum berlaku, yang bermakna ia sering tidak benar-benar berlanggar. Perlanggaran juga disederhanakan dari segi pemindahan tenaga, kerana halaju yang terhasil hanya mempertimbangkan halaju semasa bola dan tidak mengambil kira tenaga bola yang lain. Akhirnya, memeriksa kedudukan setiap bola beberapa kali setiap gelung adalah tidak cekap dari segi kerumitan masa dan boleh dioptimumkan dengan memoization asas.
Pada akhirnya, sentiasa kritikal terhadap kesan Anda. Ia biasanya boleh ditingkatkan dalam satu cara atau yang lain, dan perbezaan antara kesan sederhana dan yang hebat sering datang daripada sekadar menatapnya dan membenarkan diri Anda untuk tidak menyukainya sekurang-kurangnya sekali.
Untuk satu langkah demi langkah tutorial terakhir, lihat bahagian Callbacks kami, di mana saya membimbing melalui proses menangkap data meter untuk mengaktifkan dan menyahaktifkan kesan.