运动的小球Canvas特效代码

创建一个运动的小球动画在Canvas上是一个有趣的任务,对于初学者来说是一个很好的学习实践。下面是一个简单的示例,说明如何实现这个动画。

示例效果与源代码:

运行效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<!DOCTYPE html>
<html>

<head>
<title>动画(运动的小球)</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="图形系统开发实战:基础篇 示例">
<meta name="author" content="hjq">
<meta name="keywords" content="canvas,ladder,javascript">
<script src="/examples/canvas-qa/canvas_1b/js/helper.js"></script>
</head>

<body style="overflow: hidden; margin:10px;">
<canvas id="canvas" width="800" height="400" style="border:solid 1px #CCCCCC;"></canvas>
<div class="control">
<button id="btnPlay">开始动画</button>
<button id="btnPause">暂停动画</button>
</div>
</body>
<script>
/**
* 球
*/
class Ball {
constructor() {
this.x = getRandomNum(30, width - 30);
this.y = getRandomNum(30, height - 30);
this.radius = getRandomNum(15, 30);
this.color = colorSet[getRandomNum(0, colorSet.length - 1)];
this.speedX = getRandomNum(-maxSpeedX, maxSpeedX);
this.speedY = getRandomNum(-maxSpeedY, maxSpeedY);
this.angle = 0;
while (Math.abs(this.speedX) <= 2) {
this.speedX = getRandomNum(-maxSpeedX, maxSpeedX);
}
while (Math.abs(this.speedY) <= 1) {
this.speedY = getRandomNum(-maxSpeedY, maxSpeedY);
}
this.speedR = this.speedX + this.speedY;
}

/**
* 检测碰撞并更新速度
*/
update() {
// 根据运动方向判断是否要赋予新的速度
if (this.x + this.radius >= width || this.x - this.radius <= 0) {
this.speedX = - this.speedX;
}
if (this.y + this.radius >= height || this.y - this.radius <= 0) {
this.speedY = - this.speedY;
}

// 计算新的位置
this.x += fps > 0 ? this.speedX * (60 / fps) : 0;
this.y += fps > 0 ? this.speedY * (60 / fps) : 0;
this.angle += this.speedR;

// 边界检测
if(this.x + this.radius > width) {
this.x = width - this.radius;
}
if(this.x < this.radius) {
this.x = this.radius;
}
if(this.y + this.radius > height) {
this.y = height - this.radius;
}
if(this.y < this.radius) {
this.y = this.radius;
}

return [this.x, this.y]
}

/**
* 绘制小球
*/
draw(ctx) {
ctx.save();
ctx.beginPath();
ctx.translate(this.x, this.y);
// ctx.rotate(toRadians(this.angle));

const gradient = ctx.createRadialGradient(this.radius * 0.3, this.radius * 0.3, 0, 0, 0, this.radius);
gradient.addColorStop(0, "#CCCCCCBB");
gradient.addColorStop(0.95, this.color + "CC");
gradient.addColorStop(0.97, this.color + "AA");
gradient.addColorStop(1, this.color + "55");
ctx.fillStyle = gradient;
// ctx.fillStyle = this.color;

ctx.arc(0, 0, this.radius, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
}
}

// 从页面中获取画板对象
let canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
width = canvas.width,
height = canvas.height;

// 最后一次绘制时钟的秒数
let play = false,
fps = 0,
times = 0,
lastTime = Date.now();

let colorSet = ["#FF0000", "#00AA00", "#0000FF"],
ballNum = 20, //getRandomNum(50, 100),
maxSpeedX = 10,
maxSpeedY = 5
balls = [];

/**
* 系统初始化
*/
function init() {
// 随机产生小球
for (let i = 0; i < ballNum; i++) {
balls.push(new Ball());
}
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 绘制网格线
drawGrid('lightgray', 10, 10, ctx);
ctx.font = "bold 50px 黑体";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("运动的小球", width / 2, height / 2);

// 绘制小球
drawBalls();
}

// 重绘小球
function drawBalls() {
ctx.save();
for (let i = 0, len = balls.length; i < len; i++) {
let ball = balls[i];
ball.draw(ctx);
ball.update();
}
ctx.restore();
}

// 计算帧率
function calculateFps() {
times++;
// 当前秒数大于上一次绘制时钟的秒数时,重新计算帧率
if (Date.now() >= lastTime + 1000) {
fps = times;
times = 0;
lastTime = Date.now();
}
}

/**
* 绘制帧
*/
function frame() {
// 计算帧率
calculateFps();
if (play === true) {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制网格线
drawGrid('lightgray', 10, 10, ctx);
// 绘制进度栏
drawBalls();
}
// raf
animationFrame = window.requestAnimationFrame(frame);
}

// 开始动画
init();
window.requestAnimationFrame(frame);

document.getElementById("btnPlay").addEventListener("click", function () {
play = true;
});
document.getElementById("btnPause").addEventListener("click", function () {
play = false;
});
</script>

</html>

尝试一下 »

在这个示例中,我们首先获取了Canvas的上下文(context),然后定义了小球的初始位置和速度。drawBall函数用于在指定位置绘制小球,而animate函数则负责更新小球的位置并在每一帧上重新绘制它。如果小球碰到画布的边界,我们通过改变其速度来实现反弹效果。最后,我们使用requestAnimationFrame来循环调用animate函数,从而创建动画效果。