Ball 类
本教程解释了如何在 canvas 动画中使用 JavaScript 类,不适合初学者。我建议先阅读我们的灯光引擎部分并尝试颜色循环教程。
话虽如此,我在这里不会使用严格的类语法,因为它只是 JavaScript 的”语法糖”,底层概念保持不变。如果您有兴趣了解更复杂的动画,我建议查阅此页面进行详细介绍。否则,让我们坚持基础知识。
首先,类只是一个普通的 JavaScript 函数。它以相同的方式工作,但我们添加了将值绑定到函数特定实例的能力。在这个例子中,我创建了一个根据基本物理行为的球。它跟踪其位置和速度,以及作用于它的”重力”。
<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>
一旦我们完成了初始复杂性,添加更高级的行为就更容易了。在下一个示例中,我加入了墙壁碰撞,并在 x 方向引入了一些随机变化以保持球的运动。
<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 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>
老实说,这个示例复杂,首字母大写的那种,它可能需要比大多数基本效果更多的工作。为了避免在代码中添加过多注释,我们将在这里介绍一些棘手的概念。
首先,一个球如何知道它会与另一个碰撞?每一帧,每个球类实例都必须检查每个其他球实例的位置。通过比较它们的位置,我们可以确定两个球是否在彼此的半径内,然后相应地反转它们的 x 和 y 速度。
有时,一个球可能获得足够的速度穿过另一个球,使它们几乎占据相同的空间。调整两个球位置所需的数学计算过于复杂,所以我只是删除当前正在检查的球实例。由于更新函数确保了最少数量的球,将创建一个新的球实例,模拟将继续。
最后,我们应该何时计算每个球的速度和位置变化?球是在变量更新之前还是之后绘制重要吗?编程中的问题是两个问题都有多个答案。为了处理这个问题,我引入了一种略微不同的跟踪碰撞的方法。由您来选择您喜欢的方法,但两种方法都可以且确实有效。
<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>
最后,我们将添加一些基本的用户控件使事情更有趣。我已经详细讨论了色相变化,所以为了防止代码变得更加复杂,我们将专注于调整球的数量和半径。这只需要对现有函数进行少量修改。
<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 . . .}
希望您喜欢我们的 JavaScript 类动画教程!即使在这种复杂程度下,这个效果也只是使用 HTML canvas 可以实现的冰山一角。此外,我不认为它已经完成。如果我们追求完美,您会注意到球的碰撞在发生前一帧被检测到,这意味着它们通常实际上并没有碰撞。碰撞在能量传递方面也被简化了,因为最终速度只考虑球当前的速度,并不考虑另一个球的能量。最后,每次循环多次检查每个球的位置在时间复杂度方面是低效的,可以通过基本的记忆化来优化。
最终,要对您的效果保持批判性。它们通常可以以这种或那种方式改进,而平庸效果和优秀效果之间的区别通常来自于简单地盯着它看,并允许自己至少不满意一次。
对于最后一个分步教程,请查阅我们的回调部分,我在其中介绍了捕获计量器数据以激活和停用效果的过程。