【问题标题】:How to optimize undo/redo for canvas drawing in react如何在反应中优化画布绘图的撤消/重做
【发布时间】:2023-01-07 03:16:05
【问题描述】:

我正在实现撤消/重做功能 (with this hook)html-canvas绘图在医学(.nii)图像上做出反应。这些图像是一系列图像,表示存储在 Uint8ClampedArray 中的切片。该阵列通常约为 500(列)x 500(行)x 250(切片),换句话说,一个相当大的阵列。

我当前的解决方案只是在 mouseup 事件上从当前数组创建一个新的 Uint8ClampedArray,并将其添加到撤消/重做数组。然而,这很慢并且会造成明显的打嗝在 mouseup 事件上。我正在考虑实施更复杂的撤消/重做,它只保存受影响的体素,而不是鼠标松开时的整个阵列,但在我超越自己之前,我想知道是否有更简单的方法来优化当前的解决方案?

这是我当前的代码:

// 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,
  };
}

【问题讨论】:

    标签: javascript reactjs canvas undo typed-arrays


    【解决方案1】:

    以下是三种撤消方法及其性能。

    首先,this fiddle 包含调用绘制函数 10,000 次的基线,在我的计算机上提供的平均绘制时间为 0.0018 毫秒。

    This fiddle 都调用绘图函数并将调用记录存储在历史数组中,在我的计算机上绘制和存储函数调用的平均时间为 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 ms,比仅调用 draw 函数慢大约 3,800 倍。

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

    将存储的图像数据绘制回画布只需要大约 0.002 毫秒,这比重新运行一切都快。

    最后,this fiddle 演示了在每次调用绘制函数之前使用 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 次,后面的运行时间可能是前面运行的两倍。

    最佳方法可能是结合模式和函数存储方法,偶尔存储模式以允许截断函数调用列表。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-03-30
      • 1970-01-01
      • 2014-01-16
      • 1970-01-01
      • 2020-08-11
      • 2015-07-28
      • 2020-08-12
      相关资源
      最近更新 更多