javascript 如何优化react中画布绘制的撤消/重做

iqjalb3h  于 2023-01-07  发布在  Java
关注(0)|答案(1)|浏览(242)

我正在react中实现撤销/重做功能(with this hook),用于在医学(. nii)图像上进行html-canvas绘图。这些图像是一系列图像,表示存储在Uint8ClampedArray中的切片。该数组通常大约为500(列)x 500(行)x 250(切片),换句话说,这是一个相当大的数组。
我目前的解决方案只是在鼠标释放事件时从当前数组创建一个新的Uint8ClampedArray,并将其添加到撤销/重做数组。然而,这很慢,并在鼠标释放事件时创建了一个明显的小问题。我正在考虑实现一个更复杂的撤销/重做,它只保存受影响的体素,而不是鼠标释放时保存整个数组。但是在我开始之前,我想知道是否有更简单的方法来优化当前的解决方案?
这是我的当前代码:

// State that stores the array of voxels for the image series.
// This updates on every brush stroke
const canvasRef = useRef(undefined);
const initialArray = canvasRef?.current?.getContext("2d")?.getImageData(canvas.width, canvas.height);
const [currentArray, setCurrentArray] = useState<Uint8ClampedArray | undefined>(initialArray);

// undo & redo states
const {
  state,
  setState,
  resetState,
  index,
  lastIndex,
  goBack,
  goForward,
} = useUndoableState();

// Update currentArray on index change (undo/redo and draw)
useEffect(() => {
  setCurrentArray(state);
}, [index]);

// Activates on mouse movement combined with left-click on canvas
function handleDrawing(){
    // Logic for drawing onto the canvas
    // ...

    // Adds the stroke from the canvas onto the corresponding slice in the array-state
    const newArray = addCanvasStrokeToArrayState(imageData, slice);
    setCurrentArray(newArray);
}

function handleMouseUp() {
   // This causes a hiccup every time the current state of the array is saved to the undoable array
   setState(Uint8ClampedArray.from(currentArray));
}

下面是撤销/重做钩子的代码:

export default function useUndoableState(init?: TypedArray | undefined) {
  const historySize = 10; // How many states to store at max
  const [states, setStates] = useState([init]); // Used to store history of all states
  const [index, setIndex] = useState<number>(0); // Index of current state within `states`
  const state = useMemo(() => states[index], [states, index]); // Current state

  const setState = (value: TypedArray) => {
    // remove oldest state if history size is exceeded
    let startIndex = 0;
    if (states.length >= historySize) {
      startIndex = 1;
    }

    const copy = states.slice(startIndex, index + 1); // This removes all future (redo) states after current index
    copy.push(value);
    setStates(copy);
    setIndex(copy.length - 1);
  };
  // Clear all state history
  const resetState = (init: TypedArray) => {
    setIndex(0);
    setStates([init]);
  };
  // Allows you to go back (undo) N steps
  const goBack = (steps = 1) => {
    setIndex(Math.max(0, index - steps));
  };
  // Allows you to go forward (redo) N steps
  const goForward = (steps = 1) => {
    setIndex(Math.min(states.length - 1, index + steps));
  };
  return {
    state,
    setState,
    resetState,
    index,
    lastIndex: states.length - 1,
    goBack,
    goForward,
  };
}
h5qlskok

h5qlskok1#

以下是三种撤消方法沿着性能。
首先,this fiddle包含一个基线,它调用了一个绘制函数10,000次,在我的计算机上提供了0.0018毫秒的平均绘制时间。
This fiddle既调用了draw函数,又在历史数组中存储了调用记录,在我的计算机上,绘制和存储函数调用的平均时间为0.002毫秒,非常接近基线时间。

history.push({function: drawFunction, parameters: [i]});
drawFunction(i);

然后可以将历史重放到某一点。在我的计算机上,重放10,000个历史项花费了400毫秒,但这会根据执行了多少操作而有所不同。

for (let i = 0; i < history.length - 1; i++) {
  history[i].function(...history[i].parameters);
}

This fiddle在每次调用draw函数之前存储ImageData对象,存储ImageData和调用draw函数的平均时间为7毫秒,比仅调用draw函数慢约3,800倍。

history.push(context.getImageData(0, 0, 500, 500));
if (history.length > historyLimit) {
  history.splice(0, 1);
}

将存储的图像数据绘制回画布只需要大约0.002毫秒,这比重新运行所有内容要快。
最后,this fiddle演示了在每次调用draw函数之前使用createPattern将画布的当前状态存储为模式,在我的计算机上存储模式和绘制的平均时间为0.3毫秒,比基线慢167倍,但比使用getImageData快23倍。

history.push(context.createPattern(canvas, 'no-repeat'));

然后可以像这样把它画回画布上,这在我的电脑上是如此之快,以至于无法测量:

context.fillStyle = history[history.length - 1];
context.fillRect(0, 0, 500, 500);

虽然createPattern非常快,但是它确实有内存开销,这个例子运行了20次,每次运行调用它1,000次,后面的运行时间可能是前面运行时间的两倍多。
一种最佳的方法是将模式和函数存储方法结合起来,偶尔存储一个模式以允许截断函数调用列表。

相关问题