2014年4月17日更新:演示中修改了对粒子走出屏幕范围后的处理。
最初版本中是穿出屏幕后从另一端再进入。经过我观察发现与安卓的动态壁纸不一致。现在改成了穿出屏幕的粒子自动销毁,从屏幕边缘重新生成随机粒子。并修复了粒子的阴影效果。为了提高性能,使用了 canvas 双缓冲(但这种场景下貌似并没有提高多少)。
在看《轻音少女》,同时也觉得有一段时间没做什么实验性质的玩意了,所以呢,照着大部分安卓手机都会有的一个动态壁纸效果写了这个东西—— CGrid
,名字是 Canvas Grid
的意思。
效果:
实际上我在过年时就想写这个了,不过鉴于对手写动画效果有阴影,不知道从何下手。这次得以完成,是因为在另一个实验性质的项目进行时偶然看到了一个 HTML5 canvas 实验 Trail 的源码。
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
|
function init() {
canvas = document.getElementById( 'world' );
if (canvas && canvas.getContext) {
context = canvas.getContext('2d');
createParticles();
windowResizeHandler();
setInterval( loop, 1000 / 60 );
}
}
function loop() {
if( mouseIsDown ) {
// Scale upward to the max scale
RADIUS_SCALE += ( RADIUS_SCALE_MAX - RADIUS_SCALE ) * (0.02);
}
else {
// Scale downward to the min scale
RADIUS_SCALE -= ( RADIUS_SCALE - RADIUS_SCALE_MIN ) * (0.02);
}
RADIUS_SCALE = Math.min( RADIUS_SCALE, RADIUS_SCALE_MAX );
// Fade out the lines slowly by drawing a rectangle over the entire canvas
context.fillStyle = 'rgba(0,0,0,0.05)';
context.fillRect(0, 0, context.canvas.width, context.canvas.height);
for (i = 0, len = particles.length; i < len; i++) {
var particle = particles[i];
var lp = { x: particle.position.x, y: particle.position.y };
// Offset the angle to keep the spin going
particle.angle += particle.speed;
// Follow mouse with some lag
particle.shift.x += ( mouseX - particle.shift.x) * (particle.speed);
particle.shift.y += ( mouseY - particle.shift.y) * (particle.speed);
// Apply position
particle.position.x = particle.shift.x + Math.cos(i + particle.angle) * (particle.orbit*RADIUS_SCALE);
particle.position.y = particle.shift.y + Math.sin(i + particle.angle) * (particle.orbit*RADIUS_SCALE);
// Limit to screen bounds
particle.position.x = Math.max( Math.min( particle.position.x, SCREEN_WIDTH ), 0 );
particle.position.y = Math.max( Math.min( particle.position.y, SCREEN_HEIGHT ), 0 );
particle.size += ( particle.targetSize - particle.size ) * 0.05;
// If we're at the target size, set a new one. Think of it like a regular day at work.
if( Math.round( particle.size ) == Math.round( particle.targetSize ) ) {
particle.targetSize = 1 + Math.random() * 7;
}
context.beginPath();
context.fillStyle = particle.fillColor;
context.strokeStyle = particle.fillColor;
context.lineWidth = particle.size;
context.moveTo(lp.x, lp.y);
context.lineTo(particle.position.x, particle.position.y);
context.stroke();
context.arc(particle.position.x, particle.position.y, particle.size/2, 0, Math.PI*2, true);
context.fill();
}
}
|
里面的动画效果其实很简单的,每个粒子都是一个对象,存储了自身的位置和颜色等等信息,然后创建一个 interval ,每隔一段时间就去调用一个函数 loop 。loop 这个函数遍历所有的粒子对象,读取每个粒子的位置,计算出该粒子下一步的位置然后存储起来,调用 canvas API 绘制粒子,这样就完成了动画的一帧。前面设置的 interval 就是用来在很短时间内重复这个创建新帧的动作。以一定频率创建新帧,动画效果就产生了。
然后呢,如果只有这个创建新帧的动作的话,粒子轨迹就会一直延长而不会消失了,随着粒子的运动,轨迹就会占满整个画布。为了让粒子呈现出“拖尾”的效果,原作者在 loop 函数里加上了一个绘制半透明图层的动作。这样的话,越早创建的帧,其中的粒子就被越多的半透明图层覆盖,就越不明显,很多这样的粒子连接起来就形成了轨迹。
于是我轻松地写好了这个玩意。
顺便说一下,点击画布生成 4 个新粒子这个过程, 4 个新粒子颜色要求不一样,但不能每一方向每次点击都产生同样颜色的粒子,所以我用了一个不太好的方法。个人觉得这里有改进的空间,但是没有想到具体的做法。
这个 Demo 在火狐浏览器中表现非常奇怪,而且点击事件会拖慢整个页面的速度。 Chrome 就没问题啦。