HTML5入门习作1:烟火

最近准备学习一些HTML5方面的知识。之前演示小球和天平的页面用的完全是老式的html+jquery,原本想做些动画效果的,结果只能退而求其次,用颜色代替了。

接下来拟定了几个小演示程序,准备用HTML5来做,此前先针对性地做一些非系统性的学习,只要能实现预定目标即可。

本次目标:在canvas画布上绘制彩色小球并实现模拟烟花般的动画效果。

程序很简单,只需要理解canvas元素,在指定位置用指定颜色绘制圆球然后逐帧形成动画就可以了。

考虑到性能因素,在球离开视觉区域或颜色完全变淡后就移除对象,并在每帧补上一定数量的球,这样也能够保证视觉效果。

演示效果如下(浏览器需支持HTML5):

源码如下(由于结构并不复杂,代码中的注释应该能解决大部分疑惑,代码后面附有简单解析。感兴趣的话不妨复制下来自行调试):

1<!DOCTYPE html>
2<html>
3<head>
4    <title>HTML5烟火</title>
5    <script type="text/javascript" src="../js/jquery-1.9.0.min.js"></script>
6    <script type="text/javascript" src="../js/requestAnimationFrame.js"></script>
7    <script type="text/javascript">
8        //配置参数
9        var maxballcount = 300 //小球最大数量
10        var ballradius = 4//小球半径
11        var startspeedx = 2//横向初速度范围(-x到x)
12        var startspeedy = 6//纵向初速度范围(向上,-y到0)
13        var gravity = 0.1 //重力加速度
14        var colorweaken = 0.008//颜色衰减速度
15        var newballcount = 4//每轮补球最大数量
16        var tail = 0.2//小球拖尾效果(1为无,0为拖尾不消失)
17 
18        //全局对象
19        var canvas //绘图对象
20        var context //绘图内容
21        var imgData //当前图形
22        var balls = new Array() //小球数组
23        var framestarttime//计算帧数开始时间
24        var framecount = 0//上次计数清空以来经过的帧数
25        var lastframecount = 0//上一秒的帧数
26 
27        //页面加载
28        $(function () {
29            init()
30        })
31 
32        //初始化
33        function init() {
34            canvas = document.getElementById("image")
35 
36            if (!canvas.getContext) self.location = "/nohtml5.html"
37            else {
38                var container = $("#container")
39                container.width($(window).width())
40                container.height($(window).height())
41                canvas.width = container.width()
42                canvas.height = container.height()
43                context = canvas.getContext("2d")
44                context.font = "16px Arial"
45 
46                framestarttime = new Date()
47                //开始动画
48                animeframe()
49            }
50        }
51 
52        //运行动画
53        function animeframe() {
54            //绘制半透明遮罩,淡化上一帧的颜色以达到拖尾效果
55            context.fillStyle = "rgba(0,0,0," + tail + ")"
56            context.fillRect(0, 0, canvas.width, canvas.height)
57            var newballs = new Array()//下一帧小球数组
58            for (var i in balls) {
59                var ball = balls[i]
60                ball.speedy += gravity//重力
61                ball.x += ball.speedx
62                ball.y += ball.speedy
63                ball.alpha -= colorweaken
64                if (ball.x > 0 && ball.x < canvas.width && ball.y > 0 && ball.y < canvas.height && ball.alpha > 0) {
65                    //只有小球在界内并尚未完全透明时才显示并保留到下一帧
66                    newballs.push(ball)
67                    drawball(ball)
68                }
69            }
70            //如果数量不足(初始,或有球出界),则补球,但不能超过最大数量
71            if (newballs.length < maxballcount) {
72                for (var i = 0; i < Math.min(newballcount, maxballcount - newballs.length); i++) {
73                    newballs.push(generaterandomball())
74                }
75            }
76 
77            //交换帧
78            balls = newballs
79            newballs = null
80 
81            //计算并在左上角绘制帧数
82            var thisframetime = new Date()
83            if (thisframetime - framestarttime >= 1000) {
84                lastframecount = framecount
85                framecount = 0
86                framestarttime = thisframetime
87            }
88            framecount++
89            context.fillStyle = "#000"
90            context.fillRect(0, 0, 80, 40)
91            context.fillStyle = "#FF0"
92            context.fillText("FPS:" + lastframecount, 10, 20)
93 
94            requestAnimationFrame(animeframe)
95        }
96 
97        //绘制单个小球
98        function drawball(ball) {
99            if (!ball) return
100            context.beginPath()
101            context.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, true)
102            context.closePath()
103            context.fillStyle = "rgba(" + ball.color + "," + ball.alpha + ")"
104            context.fill()
105        }
106 
107        //生成随机颜色和速度的球
108        function generaterandomball() {
109            var ball = new Object()
110            //初始位置在中央区域
111            ball.x = Math.round(Math.random() * canvas.width / 10) + (canvas.width / 2 - canvas.width / 20)
112            ball.y = Math.round(Math.random() * canvas.height / 10) + (canvas.height / 2 - canvas.height / 20)
113            ball.r = ballradius
114            ball.color = randomcolor()
115            ball.alpha = 1
116            //小球初速度,横向随机,纵向默认向上
117            ball.speedx = Math.round(Math.random() * startspeedx * 2) - startspeedx
118            ball.speedy = -Math.round(Math.random() * startspeedy)
119            return ball
120        }
121 
122        //生成RGB字符串格式的颜色
123        function randomcolor() {
124            var yellow = Math.round(Math.random() * 255)
125            return "255," + yellow + ",0"
126        }
127    </script>
128    <style type="text/css">
129        body {
130            margin: 0px;
131        }
132 
133        #container {
134            width: 100%;
135            height: 100%;
136        }
137 
138        #image {
139            background-color: #000;
140        }
141    </style>
142</head>
143<body>
144<div id="container">
145    <canvas id="image">
146    </canvas>
147</div>
148 
149</body>
150</html>

代码解析:

  • 在设置各项基本参数的值之后,执行init()方法初始化页面;
  • 初始化画布尺寸和基本对象,然后执行animeframe()开始动画;
  • 每个animeframe()显示一帧动画,结束后递归自己显示下一帧;
  • 每帧要通过各小球的位置和速度计算运动轨迹,并计算重力加速度对纵向速度的影响;
  • 小球出界则从数组中移除。这里使用的是相反的方法:只有检测到在界内的才添加到新数组,循环完成后将新旧数组交换;
  • 运动中的小球颜色逐渐变淡(增加alpha透明度);
  • 使用drawball()方法绘制小球;
  • 如果小球数量太少则补球;
  • 生成随机球的方法是generaterandomball(),颜色在全红(255,0,0)到全黄(255,255,0)间随机选择。只需要第二个值取随机即可;
  • 每秒统计帧数并显示在左上角。

 

补注:旧版代码使用的是setTimeout实现动画。加入一段兼容性脚本后,改用requestAnimationFrame代替setTimeout来达到更好的性能,并在左上角加入帧数显示(用于测试不同浏览器和设备上的性能)

脚本如下(已保存到单独的requestAnimationFrame.js文件中):

1//requestAnimationFrame动画方法的兼容性
2(function() {
3    var lastTime = 0;
4    var vendors = ['webkit', 'moz'];
5    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
6        window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
7        window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||    // Webkit中此取消方法的名字变了
8            window[vendors[x] + 'CancelRequestAnimationFrame'];
9    }
10 
11    if (!window.requestAnimationFrame) {
12        window.requestAnimationFrame = function(callback, element) {
13            var currTime = new Date().getTime();
14            var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
15            var id = window.setTimeout(function() {
16                callback(currTime + timeToCall);
17            }, timeToCall);
18            lastTime = currTime + timeToCall;
19            return id;
20        };
21    }
22    if (!window.cancelAnimationFrame) {
23        window.cancelAnimationFrame = function(id) {
24            clearTimeout(id);
25        };
26    }
27}());

这只是一个很简单的演示程序,预计在下一个示例作品中实现:

  • 提取动画方法,不与程序逻辑混杂在一起;
  • 与用户操作互动;
  • 简单的碰撞检测。
+1

4 comments

  1. chaos says:

    楼主你好,我对JS编程也很感兴趣,但是一直找不到好用的IDE,想请问下你是用什么软件编码和调试JS(或者HTML5)呢?感谢!

    0

回复 chaos 取消回复

您的邮箱地址不会被公开。 必填项已用 * 标注