如何结交Fabric.js和Redux?

时间:2016-06-10 07:33:52

标签: fabricjs redux

有没有办法将Fabric.js和Redux一起使用? Fabric.js状态应该用作存储的一部分,但它不是不可变的,并且可以通过用户画布交互来改变自身。任何的想法?感谢。

2 个答案:

答案 0 :(得分:10)

我从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 ( 
			<div>
				<Provider store={store}>
					<FabricCanvasReduxed/>
				</Provider>
			</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() {
			// 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,
         });
			// initial call to load objects in store and render canvas
         this.refs.canvasContainer.loadAndRender();
			
         fabricCanvas.on('mouse:up', () => {
            store.dispatch({
               type: 'OBJECTS_CANVAS_CHANGE',
               payload: {
						// send complete fabric canvas object to store
                  canvasObject: fabricCanvas.toObject(),
						// also keep lastly active (selected) object
                  selectedObject: fabricCanvas.getObjects().indexOf(fabricCanvas.getActiveObject())
               }
            });
            this.refs.canvasContainer.loadAndRender();
         });
      },
      render: function() {
         return (
				<div>
					{/* 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) */}
            	<CanvasContainer ref="canvasContainer" canvasObjectState={this.props.objects} fabricInstance={fabricCanvas}/>
				</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;
		fabricCanvas.loadFromJSON(this.props.canvasObjectState.canvasObject);
		fabricCanvas.renderAll();
		// if there is any previously active object, we need to re-set it after rendering canvas
		var selectedObject = this.props.canvasObjectState.selectedObject;
		if (selectedObject > -1) {
			fabricCanvas.setActiveObject(fabricCanvas.getObjects()[this.props.canvasObjectState.selectedObject]);
		}

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

var App = React.createClass({
         render: function() {
            return ( 
					<div>
	               <MyProvider/>
               </div>
				);
			}
	});
	
ReactDOM.render( <App/>, document.getElementById('container'));
&#13;
<!-- 
	Please use Redux DevTools for Chrome or Firefox to see the store changes and time traveling
	https://github.com/zalmoxisus/redux-devtools-extension
	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>
&#13;
&#13;
&#13;

答案 1 :(得分:5)

我找到了一些解决方案。我试着描述,但对不起英语。因为Fabric.js中没有不可靠性,所以使用redux实现状态管理很困难。据我所知,默认解决方案是使用fabric.loadFromJson函数来推送新状态和序列化以进行拉取和存储以用于下一个操作,例如操作历史记录。但在这种情况下,如果您想处理图像,JSON解析将成为瓶颈,因为它们将存储在Base64 data-uri中。

方式有点hacky,但它对我有用。我正在替换fabric.js(fabric._objects)的内部对象数组,并且每当画布上发生某些事情时调用渲染,例如用鼠标移动物体。

首先,我的状态现在通过Immutable.js是不可变的,即我必须在我的Redurs中返回不可变的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 ready新对象。在reducers中,可以访问我的fabric.js对象工厂,它创建了一个区别的对象的深层副本。我修补了fabric.js(猴子修补,但你可以使用原型扩展)并且它不再将图像序列化为base64。我通过重写方法Object.toDatalessObject()来实现它,它返回没有图像数据的相同json。而是通过手动设置Image._element来源数据 - uri图像数据,它存储链接到HTMLElement对象。即更改图像坐标后,新图像对象将具有相同的_element。它允许节省内存并加速应用程序。

毕竟,我的fabric.js容器是React组件。它与redux连接,在提交更改后调用componentWillRecievProps方法。在方法I中捕获新状态,使用我的工厂创建副本(是的,有双重复制,它应该被优化,但它对我来说工作正常)并将其传递给fabric._objects然后我调用渲染。 我希望它有所帮助。