【发布时间】:2016-10-11 01:56:19
【问题描述】:
有什么方法可以同时使用 Fabric.js 和 Redux 吗? Fabric.js 状态应该用作 store 的一部分,但它不是不可变的,并且可以通过用户画布交互来改变自身。任何想法?谢谢。
【问题讨论】:
-
你找到解决办法了吗?
-
@ChetanSachdev 我试图在回答中解释我的解决方案
有什么方法可以同时使用 Fabric.js 和 Redux 吗? Fabric.js 状态应该用作 store 的一部分,但它不是不可变的,并且可以通过用户画布交互来改变自身。任何想法?谢谢。
【问题讨论】:
我从我的 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>
【讨论】:
我找到了一些解决方案。我试图描述,但对英语感到抱歉。因为 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 然后我调用渲染。 希望对你有帮助。
【讨论】: