【问题标题】:How to make friends Fabric.js and Redux?如何交朋友 Fabric.js 和 Redux?
【发布时间】:2016-10-11 01:56:19
【问题描述】:

有什么方法可以同时使用 Fabric.js 和 Redux 吗? Fabric.js 状态应该用作 store 的一部分,但它不是不可变的,并且可以通过用户画布交互来改变自身。任何想法?谢谢。

【问题讨论】:

  • 你找到解决办法了吗?
  • @ChetanSachdev 我试图在回答中解释我的解决方案

标签: fabricjs redux


【解决方案1】:

我从我的 React-Redux 和 Fabric.js 实现中提取了一个小例子。

它的工作原理是简单地通过fabric.toObject() 获取整个织物对象,将其保存到状态并通过fabric.loadFromJSON() 撤销。您可以使用Redux DevTools 并在该州旅行。

对于任何情况,也有可用的 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'));
<!-- 
	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>

【讨论】:

    【解决方案2】:

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

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

    首先,我的状态现在通过 Immutable.js 是不可变的,即我必须在我的 reducer 中返回不可变列表。但是这些列表的元素不是不可变的,它只是存储了 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 对象的链接的源 data-uri 图像数据。 IE。更改图像坐标后,新图像对象将具有相同的 _element。它可以节省内存并加速应用程序。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-12-23
      • 1970-01-01
      • 2011-08-10
      • 1970-01-01
      • 1970-01-01
      • 2015-10-21
      相关资源
      最近更新 更多