通過將近四個月的開發與測試,站酷海洛的圖片編輯器終於發佈上線了!👏👏 編輯器和圖庫的整合,使得設計變得更加容易了。項目的初心也很明確,回饋給社區一份好的設計工具,提升設計圈的創造力。 目前的版本有裁剪、文本、濾鏡三種功能,後期還會繼續迭代,用來加強用戶體驗和豐富功能。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 }} /> ) } 複製代碼
如此,即可以在項目內部任何地方引用了。
要豐富畫布的內容,須要調用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