保存和加载 HTML5 Canvas 阶段的最佳实践

保存/加载完整阶段内容的最佳方法是什么?如何实现撤销/重做?

如果您想保存/加载简单的画布内容,可以使用内置的 Konva 方法:node.toJSON()Node.create(json)
请查看 简单示例复杂示例

但这些方法在非常小的应用程序中才有用。在较大的应用程序中,使用这些方法是非常困难的。为什么?因为在较大的应用程序中,树结构通常非常复杂,您可能有很多事件监听器、图像、滤镜等。这些数据不能序列化为 JSON(或者这样做非常困难)。

此外,树中的节点通常包含很多与应用程序状态无关的信息,而只是用于描述应用程序的视觉视图。

例如,假设我们有一个游戏,它在画布上绘制几个球。那些球不仅仅是圆形,而是包含阴影和内部文本(例如“中国制造”)的复杂视觉对象组合。现在假设你想序列化你应用程序的状态并在其他地方使用它,比如发送到另一台计算机或实现撤销/重做。几乎所有的视觉信息(阴影、文本、大小)都是非关键性的,可能您不需要保存它。因为所有的球都有相同的阴影、大小等。但什么是关键的?在这种情况下,关键就是球的数量和它们的坐标。您只需要保存/加载这些信息。它将只是一个简单的数组:

var state = [{x: 10, y: 10}, { x: 160, y: 1041}]

现在,当您拥有这些信息时,您需要一个函数可以创建整个画布结构。
如果您想更新画布,例如,您想创建一个新球,您不需要直接创建一个新的画布节点(就像创建新的 Konva.Circle 实例一样),您只需要将一个新对象推入状态中并更新(或重建)画布。

在这种情况下,您不需要在保存/加载阶段关心图像加载、滤镜、事件监听器等。因为您在 createupdate 函数中执行所有这些操作。

如果您知道现代框架如何工作(如 ReactVueAngular 和许多其他框架),您会更好地理解我在说什么。

此外,请查看这些演示以更好地了解:

  1. 使用 react 的撤销/重做
  2. 使用 Vue 的保存/加载

如何实现 createupdate 函数?这要依赖于情况。以我个人的观点来看,使用可以为您完成这项工作的框架会更简单,比如 react-konva

如果您不想使用这样的框架,您需要从您自己的应用程序的角度来思考。在这里,我将尝试做一个小演示来给您一个概念。

超级简单的方法是实现一个函数 create(state),它将完成加载的所有复杂工作。
如果您的应用中有些更改,您只需销毁画布并创建一个新画布。但这种方法的缺点可能是性能不佳。

稍微聪明一点的实现是创建两个函数 create(state)update(state)create 将实例化所有所需的对象,附加事件并加载图像。update 将更新节点的属性。如果对象数量发生变化 - 就销毁所有并从头开始创建。如果只有某些属性改变 - 调用 update

说明:在这个演示中,我们将拥有一堆带有滤镜的图像,您可以添加更多图像,移动它们,通过点击图像应用新滤镜,并使用撤销/重做功能。

Konva Load Complex Stage Demoview raw
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/konva@9.3.18/konva.min.js"></script>
<meta charset="utf-8" />
<title>Konva Load Complex Stage Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #f0f0f0;
}
</style>
</head>
<body>
<button id="create-yoda">Create yoda</button>
<button id="create-darth">Create darth</button>
<button id="undo">Undo</button>
<button id="redo">Redo</button>
<div id="container"></div>
<script>
var possibleFilters = ['', 'blur', 'invert'];

function createObject(attrs) {
return Object.assign({}, attrs, {
// define position
x: 0,
y: 0,
// here should be url to image
src: '',
// and define filter on it, let's define that we can have only
// "blur", "invert" or "" (none)
filter: 'blur',
});
}
function createYoda(attrs) {
return Object.assign(createObject(attrs), {
src: '/assets/yoda.jpg',
});
}

function createDarth(attrs) {
return Object.assign(createObject(attrs), {
src: '/assets/darth-vader.jpg',
});
}

// initial state
var state = [createYoda({ x: 50, y: 50 })];

// our history
var appHistory = [state];
var appHistoryStep = 0;

var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});

var layer = new Konva.Layer();
stage.add(layer);

// create function will destroy previous drawing
// then it will created required nodes and attach all events
function create() {
layer.destroyChildren();
state.forEach((item, index) => {
var node = new Konva.Image({
draggable: true,
name: 'item-' + index,
// make it smaller
scaleX: 0.5,
scaleY: 0.5,
});
layer.add(node);
node.on('dragend', () => {
// make new state
state = state.slice();
// update object data
state[index] = Object.assign({}, state[index], {
x: node.x(),
y: node.y(),
});
// save it into history
saveStateToHistory(state);
// don't need to call update here
// because changes already in node
});

node.on('click', () => {
// find new filter
var oldFilterIndex = possibleFilters.indexOf(state[index].filter);
var nextIndex = (oldFilterIndex + 1) % possibleFilters.length;
var filter = possibleFilters[nextIndex];

// apply state changes
state = state.slice();
state[index] = Object.assign({}, state[index], {
filter: filter,
});
// save state to history
saveStateToHistory(state);
// update canvas from state
update(state);
});

var img = new window.Image();
img.onload = function () {
node.image(img);
update(state);
};
img.src = item.src;
});
update(state);
}

function update() {
state.forEach(function (item, index) {
var node = stage.findOne('.item-' + index);
node.setAttrs({
x: item.x,
y: item.y,
});

if (!node.image()) {
return;
}
if (item.filter === 'blur') {
node.filters([Konva.Filters.Blur]);
node.blurRadius(10);
node.cache();
} else if (item.filter === 'invert') {
node.filters([Konva.Filters.Invert]);
node.cache();
} else {
node.filters([]);
node.clearCache();
}
});
}

//
function saveStateToHistory(state) {
appHistory = appHistory.slice(0, appHistoryStep + 1);
appHistory = appHistory.concat([state]);
appHistoryStep += 1;
}
create(state);

document
.querySelector('#create-yoda')
.addEventListener('click', function () {
// create new object
state.push(
createYoda({
x: width * Math.random(),
y: height * Math.random(),
})
);
// recreate canvas
create(state);
});

document
.querySelector('#create-darth')
.addEventListener('click', function () {
// create new object
state.push(
createDarth({
x: width * Math.random(),
y: height * Math.random(),
})
);
// recreate canvas
create(state);
});

document.querySelector('#undo').addEventListener('click', function () {
if (appHistoryStep === 0) {
return;
}
appHistoryStep -= 1;
state = appHistory[appHistoryStep];
// create everything from scratch
create(state);
});

document.querySelector('#redo').addEventListener('click', function () {
if (appHistoryStep === appHistory.length - 1) {
return;
}
appHistoryStep += 1;
state = appHistory[appHistoryStep];
// create everything from scratch
create(state);
});
</script>
</body>
</html>