有没有办法将Fabric.js和Redux一起使用? Fabric.js状态应该用作存储的一部分,但它不是不可变的,并且可以通过用户画布交互来改变自身。任何的想法?感谢。
答案 0 :(得分:10)
我从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'));
&#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;
答案 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然后我调用渲染。 我希望它有所帮助。