【进阶1】使用Fabric.js玩转canvas画布

·  阅读 674
【进阶1】使用Fabric.js玩转canvas画布

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了

前言

继上一篇文章「使用Fabric.js玩转canvas」 发布后,不少朋友对canvasfabricjs产生了兴趣,接下里在原基础上给大家分享些有趣的进阶功能。

撤销与恢复画布的操作

撤销恢复.png

原理:定义两个栈,将每次对画布操作后的状态保存到其中一个栈,撤销时从栈中取出历史最新的状态放出另一个栈,恢复同理。

你对画布的每一次编辑,如新增、挪动、旋转和缩放图层等等,都将画布状态记录在数组undoList

let canvasState = null;
let undoList = [];
let redoList = [];

// 保存画布状态
const saveState = () => {
  // 当有新操作时,清空恢复栈
  redoList = [];
  if (canvasState) {
    undoList.push(canvasState);
    document.getElementById('undo').removeAttribute('disabled');
  }

  // 保存当前画布信息
  canvasState = card.toJSON([
    'hasControls',
    'borderColor',
    'scaleX',
    'scaleY',
    'angle',
    'top',
    'left',
    'crossOrigin'
  ]);
}
saveState();
复制代码

当你进行一次撤销时,从已经完成的操作栈undoList里取出最后一个画布json状态信息,放入redoList中并重绘画布

// 将当前最新的操作放至恢复栈
redoList.push(canvasState);
const lastState = {...undoList[undoList.length - 1]}; // 上一次状态
canvasState = lastState;
undoList.pop(); // 去除上一次状态
card.loadFromJSON(lastState, () => {
  // 绘制
  card.renderAll();
  if (undoList.length === 0) {
    // 如果撤销栈里没有历史状态了,撤销按钮不可用
    document.getElementById('undo').setAttribute('disabled', true);
  }
  // 每次撤销,恢复按钮必可用
  document.getElementById('redo').removeAttribute('disabled');
});
复制代码

同理,恢复时则相反

undoList.push(canvasState);
const lastState = {...redoList[redoList.length - 1]};
canvasState = lastState;
redoList.pop();
card.loadFromJSON(lastState, () => {
  card.renderAll();
  if (redoList.length === 0) {
    document.getElementById('redo').setAttribute('disabled', true);
  }
  document.getElementById('undo').removeAttribute('disabled');
});
复制代码

通过雪碧图实现画布动画

原理:雪碧图大家应该都很熟悉,我们可以将一张长条的png图片,等宽分成多组动作帧放入数组,定时循环渲染来实现动画效果。

示例中我使用了热门动画海贼王路飞,图片如下:

luffy-hit-sprite.png

如果大家对fabricjs有进一步了解后,就能知道可以自定义一个雪碧图类,即fabric.Sprite

fabric.Sprite = fabric.util.createClass(fabric.Image, {
  type: 'sprite', // 自定义类型
  spriteWidth: 320, // 定义雪碧图每帧的图片宽度
  spriteHeight: 200, // 每帧的高度(等高)
  spriteIndex: 0, // 动画从哪帧开始
  frameTime: 180, // 动画帧速度
  
  initialize: function (element, options) {
    options || (options = {});

    options.width = this.spriteWidth;
    options.height = this.spriteHeight;

    this.callSuper('initialize', element, options);

    this.createTmpCanvas();
    this.createSpriteImages();
  },
  
 ...类的内部逻辑方法
});

// 定义fromURL方法,实现雪碧图的加载
fabric.Sprite.fromURL = function (url, callback, imgOptions) {
  fabric.util.loadImage(url, function (img) {
    callback(new fabric.Sprite(img, imgOptions));
  }, null, { crossOrigin: 'anonymous' });
};

// 自定义class时,需重写fabricjs导出Object方法
fabric.Sprite.fromObject = function (el, options) {
  return new fabric.Sprite(el, options);
}   
复制代码

其中比较重要的就是将整个雪碧图等宽裁切后,放入数组中

createSpriteImage: function (i) {
  var tmpCtx = this.tmpCanvasEl.getContext('2d');
  tmpCtx.clearRect(0, 0, this.tmpCanvasEl.width, this.tmpCanvasEl.height);
  tmpCtx.drawImage(this._element, -i * this.spriteWidth, 0);

  var dataURL = this.tmpCanvasEl.toDataURL('image/png');
  var tmpImg = fabric.util.createImage();

  tmpImg.src = dataURL;

  this.spriteImages.push(tmpImg);
},
复制代码

有了多帧的图片数组后,就可以通过setInterval来循环绘制每一帧图片,相关代码如下:

// fabricjs图层类的通用渲染方法
_render: function (ctx) {
  ctx.drawImage(
    this.spriteImages[this.spriteIndex], // 某一个帧图片
    -this.width / 2,
    -this.height / 2
  );
},

play: function () {
  var _this = this;
  this.animInterval = setInterval(function () {
    // 定时器循环切换每一帧
    _this.onPlay && _this.onPlay();
    _this.dirty = true;
    _this.spriteIndex++;
    if (_this.spriteIndex === _this.spriteImages.length) {
      _this.spriteIndex = 0;
    }
  }, this.frameTime);
},

stop: function () {
  // 停止动画方法
  clearInterval(this.animInterval);
}
复制代码

更多详细代码可以体验并查看代码片段哦~

代码片段

结语

关于canvas画布还有很多很多有趣的业务场景,如

  • T恤图案定制
  • 手机壳定制
  • 矢量图画板等等

未来有机会还会分享更多精彩内容,谢谢!

分类:
前端
收藏成功!
已添加到「」, 点击更改