前言
经过将近四个月的开发与测试,站酷海洛的图片编辑器终于发布上线了!?? 编辑器和图库的整合,使得设计变得更加容易了。项目的初心也很明确,回馈给社区一份好的设计工具,提高设计圈的创造力。 目前的版本有裁剪、文本、滤镜三种功能,后期还会继续迭代,用来增强用户体验和丰富功能。
概要
整个项目是围绕React + Fabric.js来构建的,此外还使用了Redux来接管状态管理,用来解决多交互的应用场景。同时配套的还有Immutable +Reselect,用来提升整个项目的性能。
Fabric是一个强大的图形处理库,是在Canvas的基础上封装的,它简化了实现各种图形的难度,同时扩展了事件系统、滤镜、拖跩、缩放、SVG解析、动画等功能,支持IE10及以上的浏览器。整体压缩后的文件大小为270KB左右,官方还提供了(定制)功能,可以选择过滤一部分功能来减小文件体积。
它的整体结构如下:
画布作为容器,所有的2D图形及组合效果都可以填充到画布上面,工具类则提供了少量的公共函数。
按照官方文档,实例化一个画布需要这样(下文中的画布都代表实例化后的对象)
const instance = new fabric.Canvas('c')
复制代码
因为React的组件化形式,我们需要等到对应组件渲染完毕后才能实例化,这就限制了画布的作用域,导致无法在其他地方填充2D图形。如果想对内部全局可用,需要稍微改动一下实例化的方式
大致的代码如下:
lib/fabric.js
import fabric from 'fabric'
const instance = new fabric.Canvas() // new Canvas() 实际上调用的是initialize
export { instance }
复制代码
src/editor.js
import { instance } from 'lib/fabric'
// ...
componentDidMount() {
instance.initialize(this.canvas, {
preserveObjectStacking: true
})
}
render() {
return (
<canvas ref={ref => { this.canvas = ref }} />
)
}
复制代码
如此,便可以在项目内部任何地方引用了。
搭配React
要丰富画布的内容,需要调用instance.add 来添加其他实例,比如
const text = new fabric.Text('hello world', { fontSize: 24 })
instance.add(text)
复制代码
当然,这是最基本的一种。如果要更改字体、颜色、描边、阴影等等,都可以通过可选的options来设置,目前支持的属性有["stroke"
, "strokeWidth"
, "fill"
, "fontFamily"
, "fontSize"
, "fontWeight"
, "fontStyle"
, "underline"
, "overline"
, "linethrough"
, "textBackgroundColor"
]
其他实例的添加方法也是类似,主要区别在于实例的配置项,具体细节可以去官方文档查阅。
那么用户操作的状态如何保存呢?换句话说有没有办法可以把画布的内容序列化成一个对象?
serialization正好符合要求。序列化之后的画布反映了当前画布包含哪些内容。只要每次更新画布后都调用toObject,将数据更新到store中即可,基于此,撤销重做、自动保存都能实现了。
滤镜
CanvasRenderingContext2D.getImageData() 返回一个ImageData对象,用来描述Canvas区域隐含的像素数据。它的data属性描述了一个一维数组,包含以 RGBA 顺序的数据,数据使用 0 至 255(包含)的整数表示。Fabric内置的滤镜是基于颜色矩阵算法来实现的。具体来说就是每一个滤镜对应一个 4*5 的矩阵,对于当前像素区域内的RGBA,应用矩阵算法后会得到新的R’G’B’A’。如此一来,再调用putImageData重新填充回Canvas之后,就可以看到应用滤镜后的效果了。
历史记录
对于用户的一些特定操作,我们通过保存其历史记录来实现撤销与重做。上文中已经介绍了画布是可以序列化的,因此撤销重做也是可以实现的。社区已经有比较成熟的redux-undo,其本质也是一个reducer。虽然它也有过滤功能,可以指定某些特定action,但是他会影响最终的store结构,这对于使用了Reselect库的项目来说,是很不爽的一件事情。而且每次触发action都会进行判断,然后在分发给下层的reducer,性能上也会有一定损失。出于以上两点,我们自己内部实现了一个undo类,在不影响store结构的前提下,它可以指定记录store中关键key值的变化。
大致情况就是首先定义两个栈来存放撤销与重做的内容,snapshotFields则用来存储需要记录变动的key值(比如store中的doc.fabric.data)
import { Stack, Map, List, fromJS, is } from 'immutable'
class Snapshot {
undoStack = Stack([])
redoStack = Stack([])
snapshotFields = List([])
takeSnapshot() {
// 取出当前的store
const snapshot = this.getSnapshot()
// 与undoStack栈顶的store做比较,如果不同则放入栈中
const isEqual = is(this.undoStack.peek(), snapshot)
if (!isEqual) {
this.getRedoLength() > 0 && this.resetRedoStack()
this.undoStack = this.undoStack.push(snapshot)
}
}
getSnapshot() {
// 返回store,此处的store是经过过滤的,也就是只有snapshotFields中的字段才会返回
}
loadSnapshot(state) {
/**
* 伪代码如下
* 1. 遍历snapshotFields
* 2. 取出state中对应Field的值
* 3. 更新store中对应的值
*/
}
includeKeyPathInSnapshots(e) {
this.snapshotFields = this.snapshotFields.push(
Array.isArray(e) ? fromJS(e) : e
)
}
undo() {
if (this.undoStack.size > 1) {
const snapshot = this.getSnapshot()
this.redoStack = this.redoStack.push(snapshot)
this.undoStack = this.undoStack.pop()
const peeked = this.undoStack.peek()
this.loadSnapshot(peeked)
}
}
redo() {
if (this.redoStack.size > 0) {
const peeked = this.redoStack.peek()
this.undoStack = this.undoStack.push(peeked)
this.redoStack = this.redoStack.pop()
this.loadSnapshot(peeked)
}
}
// ...
}
复制代码
有了Snapshot,实例化之后就可以通过includeKeyPathInSnapshots来指定需要记录哪个key值了。takeSnapshot方法可以放在画布更新后的回调中去用来记录每次的画布数据,undo与redo则可以绑定到对应的组件Click事件中,整个撤销与重做大致就完成了。
下载
目前支持png、jpg格式的下载。下载流程如下图所示,省略了业务逻辑和相关权限校验
获取原始图片链接并下载到本地后,如果用户选择的尺寸与原尺寸不一致,需要对其裁剪,用到的是pica,裁剪之后放入原生的canvas元素,目的是为了替换画布中原有的canvas元素,然后再应用滤镜(因为图片版权的原因,用户编辑的图片都是带水印的小图,只有氪金用户才能使用下载功能),这样一来,处理的就是刚刚剪裁后的原图。再然后,把处理过的Canvas转换成Blob对象,此时还需要blueimp-canvas-to-blob来兼容一部分浏览器不支持canvas.toBlob的情况。
最后通过window.URL.createObjectURL(blob)得到一个新的URL对象并赋值给动态创建的a标签,即可完成下载。
const objectURL = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.download = fileName
a.setAttribute('style', 'display: none;')
a.href = objectURL
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
复制代码
技术栈
React + React-Router + Redux + Immutable + Reselect + Fabric.js
结语
体验地址:站酷海洛
作者:cuining