跳转到内容

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 可以实现的冰山一角。此外,我不认为它已经完成。如果我们追求完美,您会注意到球的碰撞在发生前一帧被检测到,这意味着它们通常实际上并没有碰撞。碰撞在能量传递方面也被简化了,因为最终速度只考虑球当前的速度,并不考虑另一个球的能量。最后,每次循环多次检查每个球的位置在时间复杂度方面是低效的,可以通过基本的记忆化来优化。

最终,要对您的效果保持批判性。它们通常可以以这种或那种方式改进,而平庸效果和优秀效果之间的区别通常来自于简单地盯着它看,并允许自己至少不满意一次。

对于最后一个分步教程,请查阅我们的回调部分,我在其中介绍了捕获计量器数据以激活和停用效果的过程。