樱花下落动效

  • 网页学习
  • 动画
  • animate

看《春宵苦短,少女前进吧》的动画官网时,觉得飘舞的樱花很好看,不禁看了下是怎么的实现的。

简单的说,就是在canvas上生成n个樱花花瓣,每一帧重新计算花瓣的位置。下面直接看代码

初始化

花瓣和风口

var _sakuras = [];
var windRoots = [];

function setup() {
  addSakura();
  canvas.addEventListener('mousemove', function(e) {
    windRoots.push({x: e.clientX, y:e.clientY, rest:0});
  });
}

function addSakura() {
  for (var i=0; i < SAKURA_COUNT; ++i) {
    var sakura = {};

    sakura.scaleX = sakura.scaleY = Math.random() * 1.2 + 0.3;

    sakura.rotationX = Math.random() * 360;
    sakura.rotationY = Math.random() * 360;
    sakura.rotationZ = Math.random() * 360;

    sakura.x = Math.random() * 500;
    sakura.y = Math.random() * 500 - 500;
    sakura.z = Math.random() * 500;

    sakura.vx = 0.3 + 0.2 * Math.random();
    sakura.vy = 0.0 + 0.5 * Math.random();
    sakura.vz = 0.3 + 0.2 * Math.random();

    sakura.rotationVx = 7 - 10 * Math.random();
    sakura.rotationVy = 7 - 10 * Math.random();
    sakura.rotationVz = 7 - 10 * Math.random();

    _sakuras.push(sakura);
  }
}

风有坐标和强度(rest),类似于光源,用于之后计算花瓣运动的位置。每当鼠标移动会在鼠标位置生成一个风口,加上节流函数(throttle)会更好。

花瓣对象包含了位置、放缩(scale)、旋转角度(rotation)、速度(v)的相关信息。

定时器

var IMAGE_URL = 'images/sakura.png';

var _img = new Image();
_img.src = IMAGE_URL;
_img.onload = play;

function play(){
  setInterval(function(){
    draw();
  }, 1000 / 60);
}

当花瓣图片加载后开始定时计算花瓣位置。这里采用一秒60帧的固定频率计算,其实用requestAnimationFrame会更好。1

主流程

function draw(data){
  ++_cnt;

  _ctx.clearRect(0,0, CANVAS_WIDTH+1,CANVAS_HEIGHT+1);

  var len = _sakuras.length;
  for (var i=0; i < len; ++i) {
    fall(_sakuras[i]);
  }
  drawSakuras();
}

重置画布,对每片花瓣计算位置后,重新作画。

樱花下落

位置计算

function fall(sakura) {
  sakura.rotationX += sakura.rotationVx + Math.random() * 5; 
  sakura.rotationY += sakura.rotationVy + Math.random() * 5;
  sakura.rotationZ += sakura.rotationVz + Math.random() * 5;
  var vx = sakura.vx + 1 * Math.abs(Math.sin(sakura.rotationZ * Math.PI / 180));
  var vy = sakura.vy + 1 * Math.abs(Math.cos(sakura.rotationX * Math.PI / 180));
  var vz = sakura.vz + 1 * Math.abs(Math.sin(sakura.rotationY * Math.PI / 180));

  var w = getNearWindRoot(sakura);
  if (w) {
    var kyori = getKyori(w.x, w.y, sakura.x, sakura.y);
    if (kyori <= 0) {
      vx += 3;
    } else {
      vx += (sakura.x - w.x) / kyori * (500 - sakura.z + 200) * 0.005 * Math.min(w.rest / 10, 1);
      vy += (sakura.y - w.y) / kyori * (500 - sakura.z + 200) * 0.005 * Math.min(w.rest / 10, 1);
    }
  }

  sakura.x += vx;
  sakura.y += vy;
  sakura.z -= vz;
  if(sakura.x > 500) {
    sakura.x = 0;
  }
  if(sakura.y > 500) {
    sakura.y = -100;
  }
  if(sakura.z < 0) {
    sakura.z = 500;
  }

  var scale = 1 / Math.max(sakura.z / 200, 0.001);
  sakura.scaleX = sakura.scaleY = scale;
}

樱花的大小为500*500,其位置限制在500*600*500的长方体中,放缩的范围是[0.4, 1000]。

因为风的强度均为0,这里x和y方向的速度只有随机的增量,只有风口和花瓣重叠时x方向有一个很明显的加速度。

作画

function drawSakuras() {
  var len = _sakuras.length;
  for (var i=0; i < len; ++i) {
    var s = _sakuras[i];
    var dispX = (s.x - 250) / Math.max(s.z / 200, 0.001) * 500 / 200 + 1000;
    var dispY = (s.y - 250) / Math.max(s.z / 200, 0.001) * 500 / 200 + 250;

    _ctx.translate(dispX, dispY);
    _ctx.scale(s.scaleX, s.scaleY);
    _ctx.rotate(s.rotationZ * Math.PI / 180);
    _ctx.transform(1, 0, 0, Math.sin(s.rotationX * Math.PI / 180), 0, 0);
    _ctx.translate(-dispX, -dispY);

    _ctx.drawImage(_img, dispX - IMG_SIZE / 2, dispY - IMG_SIZE / 2, IMG_SIZE, IMG_SIZE);

    _ctx.setTransform(1, 0, 0, 1, 0, 0);

  }
}

这里用到了很多Canvas API2

z轴的方向是向屏幕内,根据花瓣的z坐标放大了花瓣在画布上的坐标。

其他

看代码的注释,还有一些额外的效果并未体现。

风的强度

function changeWind() {
  for (var i=0; i < windRoots.length; ++i) {
    windRoots[i].rest -= 1;
    if (windRoots[i].rest < 0) {
      windRoots.splice(i, 1);
      i -= 1
    }
  }
}

随着时间推移,风的强度不断减弱,直至消失。

光照

function drawLight(s, alpha, dispX, dispY) {
  _ctx.translate(dispX, dispY);
  _ctx.scale(s.scaleX, s.scaleY);
  _ctx.rotate(s.rotationZ * Math.PI / 180);
  _ctx.transform(1, 0, 0, Math.sin(s.rotationX * Math.PI / 180), 0, 0);
  _ctx.translate(-dispX, -dispY);

  _ctx.globalAlpha = alpha * 0.2;
  _ctx.fillStyle = "rgb(255, 255, 255)";
  _ctx.beginPath();
  _ctx.arc(dispX, dispY, 7, 0, Math.PI * 2, true);
  _ctx.fill();
  _ctx.beginPath();
  _ctx.arc(dispX, dispY, 6, 0, Math.PI * 2, true);
  _ctx.fill();
  _ctx.beginPath();
  _ctx.arc(dispX, dispY, 5, 0, Math.PI * 2, true);
  _ctx.fill();

  _ctx.setTransform(1, 0, 0, 1, 0, 0);
}

用白色的透明扇面模拟花瓣上光的反射。

参考资料