使用 canvas 实现精灵动画

文章首发于个人博客:http://heavenru.com

在最近项目中需要实现一个精灵动画,素材方只提供了一个短视频素材,所以在实现精灵动画之前先介绍两个工具来帮助我们更好的实现需求。在这篇文章中,主要是介绍两个命令行工具来实现将一个短视频文件转化成一张 sprite 图片与如何使用 canvas 绘制精灵动画

两个工具官方地址如下:

1、ffmpeg 视频转图片工具

ffmpeg 是「一个完整的跨平台解决方案,用于记录,转换和流式传输音频和视频的工具」,它的作用原不止于这篇文章中所介绍的,有兴趣的同学可以自己去官方网站了解更多。

将视频转成图片输出

基本用法

./ffmpeg -i jellyfish.mp4 -vf scale=138:-1 -r 8 %04d.png
  • -i 视频流输入 URL

  • -vf 创建由过滤器指定的过滤器,并使用它过滤流,过滤器是要应用于流的过滤器的描述,并且必须具有相同类型流的单个输入和单个输出。对应的过滤器参数必须跟在这个之后,不然无法生效

  • scale 视频缩放,scale=width:height 其中,如果 height=-1 ,则表示自适应高度,按照视频的宽高比输出,后面紧接这 scale=width:height,setar=16:9 则可以指定输出宽高比

  • -r 视频输出 fps 值, 值越大,则以越高的 fps 切片视频,别名 -framerate,比如我们想以 60fps 去裁剪视频导出图片,则使用 -r 60

  • -aspect 视频输出宽高比,比如常用的 4:316:9 都是规范的参数用法

  • -ss 裁剪开始位置,表示从视频的某个时间开始裁剪,是一个非常有用的参数,该参数使用位置放在 -i 前面,参数格式 hh:mm:ss 表示时分秒

  • -t 持续时间,表示需要裁剪的视频长度,通常配合 -ss 一起使用,就能实现裁剪任意视频时间段的内容了,比如我们需要裁剪 5-10 秒的视频导出,可以这么配合使用 ffmgeg -ss 00:00:05 -t 00:00:10

  • -vframes 设定输出视频帧数,它是 -frames:v 的别名

  • -qscale:v 2 指定输出图片质量,取值范围2-31,值越大,质量越差,建议取值 2-5

综合应用:

// 截取 60 秒处的一张图片
ffmpeg -ss 60 -i input.mp4 -qscale:v 2 -vframes 1 output.jpg

// 将视频按照 60fps 的速度导出所有图片
ffmpeg -i input.mp4 -r 60 %04d.png

2、合并多个图片为一张图片 montage

通过上面介绍的工具,我们能很轻易的将一个视频转化为一系列的图片文件,那么这个时候,我们就可以使用 montage 工具将前面导出的 n 张图片合并为一张图片

基本用法:

montage -border 0 -geometry 138x -tile 89x -quality 100% *.png myvideo.jpg
  • -tile 代表需要合并的一行图片数量,当超出这个数字的时候,将换行合并

  • -quality 代表合成图片质量,取值范围 0 - 100%

3、绘制 canvas 精灵动画

在开始编辑代码之前,我们整理一下需求:

  • 动画需要能循环播放

  • 动画需要能指定从某一帧开始渲染

  • 指定渲染多少帧动画

  • 动画需要能控制渲染帧率

  • 当精灵图片不是单行的时候,要能实现自动换行渲染

OK,明白了我们的需求之后,我们开始编写代码。先来一个简易的参数合并工具方法

var _extends = Object.assign || function (target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i];
    for (var key in source) { // 遍历传入的对象的属性
      if (Object.prototype.hasOwnProperty.call(source, key)) { // 只操作该实例上的属性和方法, 避免循环原型
        target[key] = source[key];
      }
    }
  }
  return target;
}

接下来是我们的 canvas 精灵对象

function Sprite(canvas, opts) {
  var defaults = {
    loop: false,  // 是否循环播放
    frameIndex: 0,  // 当前第几帧
    startFrameIndex: 0, // 其实渲染位置
    tickCount: 0, // 每个时间段内计数器
    ticksPerFrame: 1, // 每个渲染时间段帧数,通过这个来控制动画的渲染速度
    numberOfFrames: 1, // 动画总帧数
    numberOfPerLine: undefined, // 每行动画帧数
    width: 0, // 画布宽度
    height: 0, // 画屏高度
    sprite: undefined  // 图片 image 对象
  };

  var params = opts || {};
  this.canvas = canvas;
  this.ctx = canvas.getContext('2d');
  this.options = _extends({}, defaults, params);

  if (this.image) throw new Error('请传入图片对象');

  // 这里的取 Math.min() 的原因是,在 safari 下面,如果图片的大小超过了画布的大小,那么将不会渲染任何图像
  // 所以在这里,我们去画布和图片中的小者。
  this.options.width = Math.min(this.canvas.width, this.options.sprite.width);
  this.options.height = Math.min(this.canvas.height, this.options.sprite.height);
  if (!this.options.numberOfPerLine) {
    this.options.numberOfPerLine = this.options.numberOfFrames || 9999;
  }
}

Sprite.prototype.render = function () {
  this.ctx.clearRect(0, 0, this.options.width, this.options.height);
  // 核心绘制代码,主要使用了 canvas.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) API
  // this.options.frameIndex % this.options.numberOfPerLine 每次求余数,判断是否换行
  // Math.floor(this.options.frameIndex / this.options.numberOfPerLine)
  this.ctx.drawImage(this.options.sprite, this.options.width * (this.options.frameIndex % this.options.numberOfPerLine), this.options.height * Math.floor(this.options.frameIndex / this.options.numberOfPerLine), this.options.width, this.options.height, 0, 0, this.options.width, this.options.height);
}

Sprite.prototype.update = function () {
  this.options.tickCount++;
  // 控制帧率的核心部分,在每个绘制时间点,判断当前的计数器是否大于我们传入的值
  if (this.options.tickCount > this.options.ticksPerFrame) {
    this.options.tickCount = 0;

    // 动画循环判断
    if (this.options.frameIndex < this.options.numberOfFrames - 1) {
      this.options.frameIndex++;
    } else if (this.options.loop) {
      // 每次循环都从给定的 startFrameIndex 开始
      this.options.frameIndex = this.options.startFrameIndex;
    }
  }
}

到这里,我们的精灵类基本完成了,接下来看下具体在业务代码中如何使用它

var spriteCanvas = document.getElementById('spriteCanvas');
spriteCanvas.width = 138;
spriteCanvas.height = 308;
var isSpriteLoaded = false;
var spriteImage = new Image();
var sprite;

// 这里有个 IE 下的 BUG,如果我们的 sprite 在图片没有加载完全就执行
// 那么在 IE 下面会抛出一个 DOM Exception
// 因此我们将 Sprite 初始化放在了 image.onlaod 回调函数中执行
sprite.onload = function () {
  sprite = new Sprite(spriteCanvas, {
    sprite: spriteImage,
    loop: true,
    numberOfFrames: 92,
    ticksPerFrame: 3
  });

  spriteAnimate();
}

sprite.src = 'xxxxx/sprite.jpg';

function spriteAnimate() {
  requestAnimationFrame(spriteAnimate);
  sprite.render();
  sprite.update();
}

文章到这里基本完成了,想要看具体效果的同学,可以去这里查看
传送门: 水母动画蜂鸟动画

参考资料

https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/drawImage
http://www.williammalone.com/articles/create-html5-canvas-javascript-sprite-animation/

阅读 3.7k

推荐阅读
前沿开发团队
用户专栏

Make the world be a better place by coding!

690 人关注
101 篇文章
专栏主页
目录