跳到主要内容

保存和加载 HTML5 Canvas 舞台的最佳实践

保存和加载完整舞台内容并实现撤销和重做的最佳方式是什么?

如果要保存或加载简单的 Canvas 内容,可以使用内置的 Konva 方法:node.toJSON()Node.create(json)。 请参阅简单加载复杂加载示例。

但这些方法只适用于很小的应用。在较大的应用中,这些方法很难使用。因为大型应用的树结构通常很复杂,其中可能包含大量事件监听器、图像和滤镜等内容。这些数据无法序列化为 JSON,或很难完成序列化。

树中的节点通常还包含大量与应用状态没有直接关系的信息。这些信息仅用于描述应用的视觉外观。

例如,假设一个游戏在 Canvas 中绘制了多个球。每个球不仅是一个圆形,还是一个复杂的视觉对象组,其中包含阴影和文本(例如“中国制造”)。现在,假设你要序列化应用状态并在其他位置使用,例如发送到另一台计算机或实现撤销和重做。几乎所有视觉信息(阴影、文本和尺寸)都不是关键信息,可能不需要保存,因为所有球都使用相同的阴影和尺寸等设置。真正关键的信息只有球的数量及其坐标。你只需要保存和加载这些信息。它们只是一个简单的数组:

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

获得这些信息后,你需要一个可以创建完整 Canvas 结构的函数。 如果要更新 Canvas,例如创建一个新球,不需要直接创建新的 Canvas 节点(例如创建 Konva.Circle 的新实例)。只需向状态中添加一个新对象,然后更新或重新创建 Canvas。

这样,在保存和加载阶段就不需要处理图像加载、滤镜和事件监听器等内容。因为这些操作都在 createupdate 函数中完成。

如果你了解 ReactVueAngular 等现代框架的工作方式,就能更好地理解这种方法。

还可以查看以下示例以进一步了解这种方法:

  1. 使用 React 实现撤销和重做
  2. 使用 Vue 实现保存和加载

如何实现 createupdate 函数取决于具体情况。使用 react-konva 等能处理这些工作的框架会更容易。

如果不想使用此类框架,需要根据自己的应用来设计。下面的小型示例介绍一种实现方式。

最简单的方法是只实现一个 create(state) 函数,由它完成所有复杂的加载工作。 应用发生更改时,只需销毁 Canvas 并创建一个新 Canvas。但这种方法可能导致性能下降。

更合理的实现是创建 create(state)update(state) 两个函数。create 创建所有必需对象的实例、绑定事件并加载图像。update 更新节点的属性。如果对象数量发生变化,则销毁所有对象并从头创建。如果只有部分属性发生变化,则调用 update

**操作说明:**此示例包含多个带滤镜的图像。你可以添加和移动图像,单击图像以应用新滤镜,并使用撤销和重做功能。

import Konva from 'konva';

// Initial state
let state = {
  images: [
    { x: 50, y: 50, filter: 'none' },
    { x: 150, y: 50, filter: 'blur' }
  ]
};

// History for undo/redo
const history = [JSON.stringify(state)];
let historyStep = 0;

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

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

// Create container
const container = document.createElement('div');
container.style.position = 'relative';
document.body.appendChild(container);

// Create button container
const buttonContainer = document.createElement('div');
buttonContainer.style.position = 'absolute';
buttonContainer.style.top = '10px';
buttonContainer.style.left = '10px';
buttonContainer.style.zIndex = '10';
container.appendChild(buttonContainer);

// Create UI buttons
const addButton = document.createElement('button');
addButton.textContent = 'Add Image';
addButton.style.margin = '0 5px';
buttonContainer.appendChild(addButton);

const undoButton = document.createElement('button');
undoButton.textContent = 'Undo';
undoButton.style.margin = '0 5px';
buttonContainer.appendChild(undoButton);

const redoButton = document.createElement('button');
redoButton.textContent = 'Redo';
redoButton.style.margin = '0 5px';
buttonContainer.appendChild(redoButton);

// Move stage container into our container
const stageContainer = document.getElementById('container');
container.appendChild(stageContainer);
stageContainer.style.position = 'absolute';
stageContainer.style.top = '0';
stageContainer.style.left = '0';

// Load image
const imageObj = new Image();
imageObj.src = 'https://konvajs.org/assets/lion.png';

function createImage(imageConfig) {
  const image = new Konva.Image({
    image: imageObj,
    x: imageConfig.x,
    y: imageConfig.y,
    width: 100,
    height: 100,
    draggable: true
  });

  if (imageConfig.filter === 'blur') {
    image.filters([Konva.Filters.Blur]);
    image.blurRadius(10);
  }

  return image;
}

function create(state) {
  layer.destroyChildren();
  
  state.images.forEach(imgConfig => {
    const image = createImage(imgConfig);
    
    image.on('dragend', () => {
      const pos = image.position();
      const index = layer.children.indexOf(image);
      state.images[index] = {
        ...state.images[index],
        x: pos.x,
        y: pos.y
      };
      saveHistory();
    });

    image.on('click', () => {
      const index = layer.children.indexOf(image);
      state.images[index] = {
        ...state.images[index],
        filter: state.images[index].filter === 'none' ? 'blur' : 'none'
      };
      saveHistory();
      create(state);
    });

    layer.add(image);
  });
}

function saveHistory() {
  historyStep++;
  history.length = historyStep;
  history.push(JSON.stringify(state));
}

// Add event listeners
addButton.addEventListener('click', () => {
  state.images.push({
    x: Math.random() * stage.width(),
    y: Math.random() * stage.height(),
    filter: 'none'
  });
  saveHistory();
  create(state);
});

undoButton.addEventListener('click', () => {
  if (historyStep === 0) return;
  historyStep--;
  state = JSON.parse(history[historyStep]);
  create(state);
});

redoButton.addEventListener('click', () => {
  if (historyStep === history.length - 1) return;
  historyStep++;
  state = JSON.parse(history[historyStep]);
  create(state);
});

imageObj.onload = () => {
  create(state);
};