canvas에서 ctrl+z
같은 undo
단축키를 사용할 수 있는 드로잉 히스토리는 canvas를 ImageData
로 저장하고 이를 리스트로 보관하여 구현할 수 있다.
const canvasHistory = useRef<ImageData[]>([]);
const historyIndex = useRef<number>(-1);
useRef를 이용해 먼저 ImageData를 푸시할 리스트를 만든다.
그리고 히스토리의 기록과 undo
로 불러올 히스토리를 지정하기 위해 index를 도입하였는데, 이러한 인덱스가 없이 그냥 push
, pop
만으로 구현할 경우 현재 canvas의 ImageData를 별도의 공간에 저장해야하거나, undo
를 할 때 pop하려는 ImageData가 현재 ImageData가 아닌지 구분하는 로직이 따로 필요하다.
다음으로, push
와, undo
를 수행하는 함수를 만든다.
const pushHistory = () => {
const context = contextRef.current;
if (context === null) return;
const { dpr, width, height } = size;
const img = context.getImageData(0, 0, width * dpr, height * dpr);
historyIndex.current += 1;
canvasHistory.current[historyIndex.current] = img;
}
const undo = () => {
const context = contextRef.current;
if (context === null) return;
const { dpr, width, height } = size;
const img = canvasHistory.current[historyIndex.current - 1];
if (img === undefined) return;
context.putImageData(img, 0, 0, 0, 0, width * dpr, height * dpr);
historyIndex.current -= 1;
}
히스토리를 보관할때는,
getImageData
-> index 증가 -> 히스토리에 푸시
반대로 히스토리를 꺼낼때는,
lastIndex -1
참조 -> putImageData
-> index 감소
undo
함수를 필요로하는 버튼에 onClick
추가해주거나, 아니면 페이지에 이벤트리스너를 추가해서 단축키를 인식하도록 한다.
useWindowEventListener('keydown', (e) => {
if (e.key === 'z' && (e.ctrlKey || e.metaKey)) undo();
});
export const useWindowEventListener = <K extends keyof WindowEventMap>(
type: K,
listener: (this: Window, ev: WindowEventMap[K]) => void,
options?: boolean | AddEventListenerOptions,
) => {
useEffect(() => {
window.addEventListener(type, listener, options);
return () => {
window.removeEventListener(type, listener, options);
};
}, [type, listener, options]);
};