我正在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,
};
}
1条答案
按热度按时间h5qlskok1#
以下是三种撤消方法沿着性能。
首先,this fiddle包含一个基线,它调用了一个绘制函数10,000次,在我的计算机上提供了0.0018毫秒的平均绘制时间。
This fiddle既调用了draw函数,又在历史数组中存储了调用记录,在我的计算机上,绘制和存储函数调用的平均时间为0.002毫秒,非常接近基线时间。
然后可以将历史重放到某一点。在我的计算机上,重放10,000个历史项花费了400毫秒,但这会根据执行了多少操作而有所不同。
This fiddle在每次调用draw函数之前存储ImageData对象,存储ImageData和调用draw函数的平均时间为7毫秒,比仅调用draw函数慢约3,800倍。
将存储的图像数据绘制回画布只需要大约0.002毫秒,这比重新运行所有内容要快。
最后,this fiddle演示了在每次调用draw函数之前使用createPattern将画布的当前状态存储为模式,在我的计算机上存储模式和绘制的平均时间为0.3毫秒,比基线慢167倍,但比使用getImageData快23倍。
然后可以像这样把它画回画布上,这在我的电脑上是如此之快,以至于无法测量:
虽然createPattern非常快,但是它确实有内存开销,这个例子运行了20次,每次运行调用它1,000次,后面的运行时间可能是前面运行时间的两倍多。
一种最佳的方法是将模式和函数存储方法结合起来,偶尔存储一个模式以允许截断函数调用列表。