【进阶1】使用Fabric.js玩转canvas画布
前言
继上一篇文章「使用Fabric.js玩转canvas」
发布后,不少朋友对canvas
和fabricjs
产生了兴趣,接下里在原基础上给大家分享些有趣的进阶功能。
撤销与恢复画布的操作
原理:定义两个栈,将每次对画布操作后的状态保存到其中一个栈,撤销时从栈中取出历史最新的状态放出另一个栈,恢复同理。
你对画布的每一次编辑,如新增、挪动、旋转和缩放图层等等,都将画布状态记录在数组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图片,等宽分成多组动作帧放入数组,定时循环渲染来实现动画效果。
示例中我使用了热门动画海贼王路飞,图片如下:
如果大家对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恤图案定制
- 手机壳定制
- 矢量图画板等等
未来有机会还会分享更多精彩内容,谢谢!
转载自:https://juejin.cn/post/7140544065203863588