【实现自己的可视化引擎04】图像元素动画

554 阅读6分钟

基本动画

前几章节,我们已经通过图元来绘制图形,但是图元一旦绘制出来就保持那样了。当然我们可以通过可以用window.setInterval(), window.setTimeout(),和window.requestAnimationFrame()来设定定期执行一个指定函数修改图元的属性来实现动画效果。下面我们通过一个例子实现通过图元实现动画。

let rectangle = new Rectangle(this.canvas, {
  position: new Point(this.canvas.width / 2, 400),
  width: 500,
  height: 100,
  type: Rectangle.TYPE.FILL,
  linearGradient: [
    [0, '#DEDEDE'],
    [0.3, '#999999'],
    [0.5, '#777777'],
    [0.7, '#444444'],
    [1, '#000000'],
  ],
});
setInterval(() => {
  if (rectangle.rotation >= 360) {
    rectangle.rotation = 0;
  }
  // 每次旋转10度
  rectangle.rotation += 10;
  this.canvas.paint();
}, 100 / 6);
this.canvas.addChild(rectangle);
this.canvas.paint();

上面我们通过新建了一个矩形,并通过定时器setInterval每16.667ms使矩形旋转10度。由上面的例子可以看出,通过修改图元属性可以很方便的实现动画效果。

虽然通过setInterval可以很方便的实现动画效果,然而如果需要动画的图元很多,那么如何管理这些动画,以及动画效果如何复用也是一个需要考虑的问题。下面,我们通过构建动作类来对动画进行管理。

构建动作类Action

首先我们来确定一下一个动画所包含的属性:
-动画时长 duration
-动画状态 status: 1、运行前,2、运行中,3、运行结束,4、运行终止, 5、暂停
-动画帧率 fps
根据上述属性,构建动画类

export default class Action {
  static STATUS = {
    BEFORE_RUN: 1,  // 动画运行前
    RUNNING: 2,     // 动画运行中
    PAUSE: 3,       // 动画暂停
    STOP: 4,        // 动画终止
  }

  /**
   * @param _duration 运行时长 以秒为单位
   */
  constructor(_duration, fps = 60) {
    this.status = Action.STATUS.BEFORE_RUN;     // 动作状态
    this.duration = _duration;
    this.allFrames = Math.round(_duration * 1000 * fps / 60); // 根据运行时间与帧率,计算该动作所有帧率
    this.currentFrame = 0;                // 当前帧
    this.fpsRatio = 60 / fps;            // 帧率控制 浏览器帧率为60,如果fps为30,那么以为着浏览器每两次刷新运行一次动作
    this.fpsCount = 0;                   // 帧率计数器
    this.runCallback = undefined;        // 动作运行回调函数
  }

  /**
   * 运行函数
   * @param node
   * @param callback 每一帧回调函数,暴露给外围使用
   */
  run(node, callback) {
    if (this.fpsCount < this.fpsRatio) {
      this.fpsCount++;
      requestAnimationFrame((timestamp) => {
        //console.log(timestamp);
        this.run(node, callback);
      });
      return;
    }
    if(!this.runCallback) {
      this.runCallback = callback;
    }
    // 调用动画更新函数
    this.update(node, this.currentFrame, this.allFrames);
    callback && callback(node, this);
    if (this.status === Action.STATUS.RUNNING) {
      // 当动作为运行状态
      if (this.duration < 0 || this.currentFrame < this.allFrames) {
        // 如果总帧数小于0 或者 总帧数大于0 且当前帧小于总帧数, 动作进入下一帧
        this.currentFrame += 1;
        this.fpsCount = 0; // 帧率计数器置0
        requestAnimationFrame(() => {
          this.run(node, callback);
        });
      } else {
        // 运行结束
        // 设置动画为停止
        this.status = Action.STATUS.STOP;
        // 调用动画生命周期停止回调函数
        this.onStop && this.onStop(node);
      }
    }
  }

  /**
   * 抽象方法,由子类继承
   * @param node
   * @param frame
   */
  update(node, frame, frames) {

  }

  /**
   * 动作控制函数,停止操作
   */
  stop() {
    this.status = Action.STATUS.STOP;
    if (this.allFrames > 0) {
      // 确保当前帧为最后一帧
      this.currentFrame = this.allFrames;
    }
    // 调用动画生命周期停止回调函数
    this.onStop && this.onStop(node);
  }

  /**
   * 动作控制函数, 暂停操作
   */
  pause() {
    this.status = Action.STATUS.PAUSE;
    // 调用动画生命周期暂停回调函数
    this.onPause && this.onPause();
  }


  /**
   * 动作控制函数,暂停重新启动
   * @param node
   */
  restart(node) {
    if (this.status === Action.STATUS.PAUSE) {
      this.fpsCount = 0;
      this.status = Action.STATUS.RUNNING;
      this.run(node, this.runCallback);
      // 生命周期函数 暂停启动回调
      this.onRestart && this.onRestart();
    }
  }

  /**
   * 动画重新启动
   * @param node
   */
  reset(node) {
    this.currentFrame = 0;
    this.fpsCount = 0;
    this.status = Action.STATUS.RUNNING;
    this.run(node, this.runCallback);
    // 生命周期函数 重启回调
    this.onReset && this.onReset()
  }
}

构造器传入2个参数,分别为动作运行时间与帧率。在运行函数中,我们可以看到,我们通过调用window.requestAnimationFrame来实现递归运行。window.requestAnimationFrame函数提供了更加平缓并更加有效率的方式来执行动画,当系统准备好了重绘条件的时候,才调用绘制动画帧。一般每秒钟回调函数执行60次,也有可能会被降低。这里我们假定每秒钟执行60次,具体优化可以参考可以在Game development zone 参考这篇文章 Anatomy of a video game。Action构造器函数中的参数fps向外提供了分频器的作用,通过修改fps可以实现分频的功能。假定构造器参数fps为30,那么对于正常浏览器帧率60来说,该动作在浏览器每两次刷频中执行一次run函数,控制代码如下:

 if (this.fpsCount < this.fpsRatio) {
  this.fpsCount++;
  requestAnimationFrame(() => {
    this.run(node, callback);
  });
  return;
}

Action类最主要的函数为run函数,该函数调用自身的update方法进行动作执行图元的更新操作,而update方法为抽象函数供子类继承实现。具体的动作子类通过实现update方法更新图元属性,从而实现动画效果,而动作的时序时间的控制则交给Action类控制,保持对子类的透明。
update函数为抽象函数,由子类实现,该函数提供三个参数,分别为执行动作的图元node,当前帧frame以及所有帧frames。
在实现动作类后,我们需要给图元函数增加执行动作的方法,在Node类中,加入如下代码:

export default class Node {
  //...其他代码
  runAction(action, callback) {
    if (action instanceof Action) {
      action.reset(this);
      action.run(this, callback);
    } else {
      throw new Error('Error Arguments: action is not a instance of class Action');
    }
  }

  stopAction(action) {
    if (action instanceof Action) {
      action.stop();
    } else {
      throw new Error('Error Arguments: action is not a instance of class Action');
    }
  }
 
}

扩展动作类及实践

下面我们通过简单的时钟示例来看看如何使用Action类。首先我们创建一个旋转动作RotateAction继承Action。

export default class RotateAction extends Action {
  constructor(duration, fps, angle) {
    super(duration, fps);
    this.angle = angle;
  }
  update(_sprite, frame) {
    _sprite.rotation += this.angle
    _sprite.canvas.paint();
  }
}

RotateAction代码很简单,构造函数中传入每帧旋转的角度angle,实现抽象函数update,在update每一帧中对图元旋转属性加上旋转角度。
接下来,我们创建两个线框为10单位的直线,并执行相应的动作。

// 秒针线
let secondHand = new Line(this.canvas, {
  lineWidth: 10,
  position: new Point(this.canvas.width / 2, this.canvas.height / 2),
  to: new Point(this.canvas.width / 2, this.canvas.height / 2 + 100),
});
// 分针线
let miniHand = new Line(this.canvas, {
  lineWidth: 10,
  position: new Point(this.canvas.width / 2, this.canvas.height / 2),
  to: new Point(this.canvas.width / 2, this.canvas.height / 2 + 60),
});
// 创建秒针线动作,运行实现-1不停止,帧率为1,每秒旋转角度6度
let secondHandAction = new RotateAction(-1, 1, 6);
let miniHandAction = new RotateAction(-1, 1 / 60, 6);

this.canvas.addChild(secondHand, miniHand);
// 运行旋转动作
secondHand.runAction(secondHandAction);
miniHand.runAction(miniHandAction);
this.canvas.paint();

目录

【实现自己的可视化引擎01】认识Canvas
【实现自己的可视化框架引擎02】抽象图像元素
【实现自己的可视化引擎03】构建基础图元库
【实现自己的可视化引擎04】图像元素动画
【实现自己的可视化引擎05】交互与事件
【实现自己的可视化引擎06】折线图
【实现自己的可视化引擎07】柱状图
【实现自己的可视化引擎08】条形图
【实现自己的可视化引擎09】饼图
【实现自己的可视化引擎10】散点图
【实现自己的可视化引擎11】雷达图
【实现自己的可视化引擎12】K线图
【实现自己的可视化引擎13】仪表盘
【实现自己的可视化引擎14】地图
【实现自己的可视化引擎15】关系图