2014年4月17日更新:演示中修改了对粒子走出屏幕范围后的处理。

最初版本中是穿出屏幕后从另一端再进入。经过我观察发现与安卓的动态壁纸不一致。现在改成了穿出屏幕的粒子自动销毁,从屏幕边缘重新生成随机粒子。并修复了粒子的阴影效果。为了提高性能,使用了 canvas 双缓冲(但这种场景下貌似并没有提高多少)。

在看《轻音少女》,同时也觉得有一段时间没做什么实验性质的玩意了,所以呢,照着大部分安卓手机都会有的一个动态壁纸效果写了这个东西—— CGrid ,名字是 Canvas Grid 的意思。

效果:

实际上我在过年时就想写这个了,不过鉴于对手写动画效果有阴影,不知道从何下手。这次得以完成,是因为在另一个实验性质的项目进行时偶然看到了一个 HTML5 canvas 实验 Trail 的源码。

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 就没问题啦。