2016-06-10 56 views
3

有什麼辦法可以將Fabric.js和Redux一起使用嗎? Fabric.js狀態應該用作商店的一部分,但它不是不可變的,並且可以通過用戶畫布交互來改變自己。任何想法?謝謝。如何交朋友Fabric.js和Redux?

+1

你有沒有發現任何解決這個? –

+0

@ChetanSachdev我試圖解釋我的答案 – equicolor

回答

5

我找到了一些解決方案。我試圖描述,但對於英語感到抱歉。因爲Fabric.js中不存在任何不可變性,所以使用redux很難實現狀態管理。據我瞭解,默認的解決方案是使用fabric.loadFromJson函數來推送新狀態和序列化以進行下拉和存儲以進行下一個操作,例如操作歷史記錄。但在這種情況下,如果您想使用圖像,JSON解析將會成爲瓶頸,因爲它們將存儲在Base64數據中。

這種方式有點怪異,但它適用於我。我正在替換fabric.js(fabric._objects)的內部對象陣列,並且每次在畫布上發生某些事情時調用渲染,例如,通過鼠標移動物體。

首先,我的狀態現在通過Immutable.js是不可變的,即我必須在我的reducer中返回不可變的List。但是這些列表中的元素並不是不可變的,它只是爲了渲染而存儲的fabric.js對象。我的狀態由對象列表,選擇列表和多個表示例如視口狀態(縮放,平移)的助手對象組成。對象狀態列表鍵用作動作中對象的ID。我的根場景減速器有結構。

const sceneReducer = composeReducers(
    whetherRecordCurrentState, 
    combineReducers({ 
    project: undoable(
     composeReducers(
     projectActions, 
     combineReducers({ 
      objects, 
      params, 
      counters 
     }), 
    ), 
     { 
     limit: historyLimit, 
     filter: combineFilters(
      recordFilter, 
      excludeAction([ 
      'CREATE_SELECTION', 
      'CLEAR_SELECTION', 
      'SET_WORKSPACE_NAME', 
      'SET_WORKSPACE_ID', 
      'SET_WORKSPACE_TYPE', 
      'SET_TAGS', 
      ]), 
     ) 
     } 
    ), 
    selection, 
    meta, 
    viewport, 
    recording 
    }), 
    selectJustCreatedObject 
); 

它實現了任何fabric.js的可能性,包括異步函數,如應用過濾器。另外我使用了redux-undoable包,它允許實現無限制的撤銷/重做歷史記錄。它也允許實現不存儲的操作,例如通過滑塊改變不透明度(所有中間狀態將不被存儲)。由於我使用不變性,我可以只用一個更改的對象來推送新的歷史狀態以節省內存。還有就是我的狀態

https://i.gyazo.com/fcef421e9ccfa965946a6e5930e42edf.png

見它的工作原理:在fabric.js我處理與新對象狀態的事件。然後,我將該狀態的動作分派爲有效載荷。在我的行動中,我可以創建新的結構對象或傳遞更新的對象。所有異步操作(過濾,更改圖像源)在動作中執行並傳遞給reducer準備好的新對象。在reducer中,可以訪問我的fabric.js對象工廠,該工廠通過一個區別創建對象的深層副本。我補丁fabric.js(猴子補丁,但你可以使用原型擴展),它不會序列化圖像base64了。我通過覆蓋方法Object.toDatalessObject()來實現它,它返回沒有圖像數據的相同json。而是通過手動設置Image._element來存儲鏈接到HTMLElement對象的源數據 - uri圖像數據。即改變圖像座標後,新的圖像對象將具有相同的_element。它可以節省內存並加速應用程序。

畢竟,我的fabric.js容器是React組件。它與redux連接,並在提交更改後調用componentWillRecievProps方法。在方法中,我捕獲新的狀態,與我的工廠創建副本(是的,有雙重複制,它應該優化,但它適用於我),並將其傳遞到fabric._objects,然後我調用渲染。 我希望它有幫助。

6

我已經從我的React-Redux和Fabric.js的實現中提取了一個小例子。

它通過簡單地通過fabric.toObject()獲取整個結構對象,將其保存到狀態並通過fabric.loadFromJSON()來撤銷。您可以通過使用Redux DevTools並在州內旅行玩耍。

enter image description here

對於任何情況下,也有提供的jsfiddle:https://jsfiddle.net/radomeer/74t5y1r0/

// don't be scared, just some initial objects to play with (fabric's serialized JSON) 
 
const initialState = { 
 
    canvasObject: { 
 
     "objects": [{ 
 
     "type": "circle", 
 
     "originX": "center", 
 
     "originY": "center", 
 
     "left": 50, 
 
     "top": 50, 
 
     "width": 100, 
 
     "height": 100, 
 
     "fill": "#FF00FF", 
 
     "stroke": null, 
 
     "strokeWidth": 1, 
 
     "strokeDashArray": null, 
 
     "strokeLineCap": "butt", 
 
     "strokeLineJoin": "miter", 
 
     "strokeMiterLimit": 10, 
 
     "scaleX": 1, 
 
     "scaleY": 1, 
 
     "angle": 0, 
 
     "flipX": false, 
 
     "flipY": false, 
 
     "opacity": 1, 
 
     "shadow": null, 
 
     "visible": true, 
 
     "clipTo": null, 
 
     "backgroundColor": "", 
 
     "fillRule": "nonzero", 
 
     "globalCompositeOperation": "source-over", 
 
     "transformMatrix": null, 
 
     "radius": 50, 
 
     "startAngle": 0, 
 
     "endAngle": 6.283185307179586 
 
     }, { 
 
     "type": "rect", 
 
     "originX": "center", 
 
     "originY": "center", 
 
     "left": 126, 
 
     "top": 210, 
 
     "width": 100, 
 
     "height": 100, 
 
     "fill": "#FF0000", 
 
     "stroke": null, 
 
     "strokeWidth": 1, 
 
     "strokeDashArray": null, 
 
     "strokeLineCap": "butt", 
 
     "strokeLineJoin": "miter", 
 
     "strokeMiterLimit": 10, 
 
     "scaleX": 1, 
 
     "scaleY": 1, 
 
     "angle": 0, 
 
     "flipX": false, 
 
     "flipY": false, 
 
     "opacity": 1, 
 
     "shadow": null, 
 
     "visible": true, 
 
     "clipTo": null, 
 
     "backgroundColor": "", 
 
     "fillRule": "nonzero", 
 
     "globalCompositeOperation": "source-over", 
 
     "transformMatrix": null, 
 
     "radius": 50, 
 
     "startAngle": 0, 
 
     "endAngle": 6.283185307179586 
 
     }, { 
 
     "type": "triangle", 
 
     "originX": "center", 
 
     "originY": "center", 
 
     "left": 250, 
 
     "top": 100, 
 
     "width": 100, 
 
     "height": 100, 
 
     "fill": "#00F00F", 
 
     "stroke": null, 
 
     "strokeWidth": 1, 
 
     "strokeDashArray": null, 
 
     "strokeLineCap": "butt", 
 
     "strokeLineJoin": "miter", 
 
     "strokeMiterLimit": 10, 
 
     "scaleX": 1, 
 
     "scaleY": 1, 
 
     "angle": 0, 
 
     "flipX": false, 
 
     "flipY": false, 
 
     "opacity": 1, 
 
     "shadow": null, 
 
     "visible": true, 
 
     "clipTo": null, 
 
     "backgroundColor": "", 
 
     "fillRule": "nonzero", 
 
     "globalCompositeOperation": "source-over", 
 
     "transformMatrix": null, 
 
     "radius": 50, 
 
     "startAngle": 0, 
 
     "endAngle": 6.283185307179586 
 
     }], 
 
     "background": "" 
 
    } 
 
}; 
 
// Redux part 
 
const canvasObjectReducer = function(state = initialState, action) { 
 
    switch (action.type) { 
 
     case "OBJECTS_CANVAS_CHANGE": 
 
     return Object.assign({}, state, { 
 
      canvasObject: action.payload.canvasObject, 
 
      selectedObject: action.payload.selectedObject 
 
     }); 
 
     default: 
 
     return state 
 
    } 
 
    return state; 
 
} 
 
// standard react-redux boilerplate 
 
const reducers = Redux.combineReducers({ 
 
    canvasObjectState: canvasObjectReducer 
 
}); 
 
const { createStore } = Redux; 
 
const store = createStore(reducers, window.devToolsExtension && window.devToolsExtension()); 
 

 
const { Provider } = ReactRedux; 
 
const { Component } = React; 
 
const MyProvider = React.createClass({ 
 
    render: function() { 
 
     return ( 
 
\t \t \t <div> 
 
\t \t \t \t <Provider store={store}> 
 
\t \t \t \t \t <FabricCanvasReduxed/> 
 
\t \t \t \t </Provider> 
 
\t \t \t </div> 
 
    ); 
 
    } 
 
}); 
 

 
// Fabric part 
 
var fabricCanvas = new fabric.Canvas(); 
 

 
// class which takes care about instantiating fabric and passing state to component with actual canvas 
 
const FabricCanvas = React.createClass({ 
 
    componentDidMount() { 
 
\t \t \t // we need to get canvas element by ref to initialize fabric 
 
     var el = this.refs.canvasContainer.refs.objectsCanvas; 
 
     fabricCanvas.initialize(el, { 
 
      height: 400, 
 
      width: 400, 
 
     }); 
 
\t \t \t // initial call to load objects in store and render canvas 
 
     this.refs.canvasContainer.loadAndRender(); 
 
\t \t \t 
 
     fabricCanvas.on('mouse:up',() => { 
 
      store.dispatch({ 
 
       type: 'OBJECTS_CANVAS_CHANGE', 
 
       payload: { 
 
\t \t \t \t \t \t // send complete fabric canvas object to store 
 
        canvasObject: fabricCanvas.toObject(), 
 
\t \t \t \t \t \t // also keep lastly active (selected) object 
 
        selectedObject: fabricCanvas.getObjects().indexOf(fabricCanvas.getActiveObject()) 
 
       } 
 
      }); 
 
      this.refs.canvasContainer.loadAndRender(); 
 
     }); 
 
     }, 
 
     render: function() { 
 
     return (
 
\t \t \t \t <div> 
 
\t \t \t \t \t {/* send store and fabricInstance viac refs (maybe not the cleanest way, but I was not able to create global instance of fabric due to use of ES6 modules) */} 
 
      \t <CanvasContainer ref="canvasContainer" canvasObjectState={this.props.objects} fabricInstance={fabricCanvas}/> 
 
\t \t \t \t </div> 
 
     ) 
 
     } 
 
}); 
 
const mapStateToProps = function(store) { 
 
    return { 
 
     objects: store.canvasObjectState 
 
    }; 
 
}; 
 

 
// we can not use export default on jsfiddle so we need react class with mapped state in separate constant 
 
const FabricCanvasReduxed = ReactRedux.connect(mapStateToProps)(FabricCanvas); 
 

 
const CanvasContainer = React.createClass({ 
 
    loadAndRender: function() { 
 
     var fabricCanvas = this.props.fabricInstance; 
 
\t \t fabricCanvas.loadFromJSON(this.props.canvasObjectState.canvasObject); 
 
\t \t fabricCanvas.renderAll(); 
 
\t \t // if there is any previously active object, we need to re-set it after rendering canvas 
 
\t \t var selectedObject = this.props.canvasObjectState.selectedObject; 
 
\t \t if (selectedObject > -1) { 
 
\t \t \t fabricCanvas.setActiveObject(fabricCanvas.getObjects()[this.props.canvasObjectState.selectedObject]); 
 
\t \t } 
 

 
    }, 
 
    render: function() { 
 
     this.loadAndRender(); 
 
     return ( 
 
\t \t \t <canvas ref="objectsCanvas"> 
 
     </canvas> 
 
    ); 
 
    } 
 
}); 
 

 
var App = React.createClass({ 
 
     render: function() { 
 
      return ( 
 
\t \t \t \t \t <div> 
 
\t    <MyProvider/> 
 
       </div> 
 
\t \t \t \t); 
 
\t \t \t } 
 
\t }); 
 
\t 
 
ReactDOM.render(<App/>, document.getElementById('container'));
<!-- 
 
\t Please use Redux DevTools for Chrome or Firefox to see the store changes and time traveling 
 
\t https://github.com/zalmoxisus/redux-devtools-extension 
 
\t Inspired by https://jsfiddle.net/STHayden/2pncoLb5/ 
 
--> 
 
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script> 
 
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script> 
 
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.4/fabric.min.js"></script> 
 
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.5/react-redux.min.js"></script> 
 
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.js"></script> 
 
<div id="container"> 
 
</div>