Fabric - 构建在线图片编辑器

前言

通过将近四个月的开发与测试,站酷海洛的图片编辑器终于发布上线了!👏👏 编辑器和图库的整合,使得设计变得更加容易了。项目的初心也很明确,回馈给社区一份好的设计工具,提升设计圈的创造力。 目前的版本有裁剪、文本、滤镜三种功能,后期还会继续迭代,用来加强用户体验和丰富功能。html

概要

整个项目是围绕React + Fabric.js来构建的,此外还使用了Redux来接管状态管理,用来解决多交互的应用场景。同时配套的还有Immutable +Reselect,用来提高整个项目的性能。node

Fabric是一个强大的图形处理库,是在Canvas的基础上封装的,它简化了实现各类图形的难度,同时扩展了事件系统、滤镜、拖跩、缩放、SVG解析、动画等功能,支持IE10及以上的浏览器。总体压缩后的文件大小为270KB左右,官方还提供了(定制)功能,能够选择过滤一部分功能来减少文件体积。android

它的总体结构以下:git

画布做为容器,全部的2D图形及组合效果均可以填充到画布上面,工具类则提供了少许的公共函数。github

按照官方文档,实例化一个画布须要这样(下文中的画布都表明实例化后的对象)算法

const instance = new fabric.Canvas('c')
复制代码

由于React的组件化形式,咱们须要等到对应组件渲染完毕后才能实例化,这就限制了画布的做用域,致使没法在其余地方填充2D图形。若是想对内部全局可用,须要稍微改动一下实例化的方式redux

大体的代码以下:canvas

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

相关文章
相关标签/搜索