Canvas는 DOM이 아니라 픽셀 버퍼다
Canvas를 처음 만질 때 가장 흔한 착각은 Canvas를 아주 큰 div처럼 생각하는 것입니다. 겉으로는 비슷합니다. HTML에 <canvas> 하나를 놓고, CSS로 크기를 주고, 그 안에 무언가를 그립니다.
하지만 Canvas 안쪽은 DOM이 아닙니다. Canvas 안에는 child element가 없습니다. 브라우저가 관리하는 layout box도 없습니다. Canvas는 화면에 표시되는 픽셀 버퍼입니다.
CSS는 구조를 남긴다
CSS 기반 에디터에서 사각형 세 개를 만들면 DOM에도 보통 세 개의 요소가 남습니다.
<div data-node-id="a"></div>
<div data-node-id="b"></div>
<div data-node-id="c"></div>
브라우저는 이 요소들을 알고 있습니다. 어떤 요소가 앞에 있는지, 어떤 요소가 pointer event를 받을 수 있는지, transform이 적용된 bounding box가 어디인지 계산할 수 있습니다.
Canvas는 다릅니다.
drawRect(a);
drawRect(b);
drawRect(c);
이렇게 그려도 Canvas 내부에 a, b, c라는 객체가 남지 않습니다. 그리는 순간 픽셀만 바뀝니다.
그래서 매 프레임 다시 그린다
Canvas/WebGL/WebGPU renderer는 보통 이런 흐름을 가집니다.
begin frame
clear
draw background
draw scene nodes
draw overlays
present
end frame
CSS에서는 도형의 style을 바꾸면 브라우저가 알아서 필요한 만큼 다시 그립니다. GPU renderer에서는 우리가 프레임을 구성합니다. 무엇을 먼저 그리고, 무엇을 나중에 그릴지 직접 정합니다.
이 흐름을 render loop라고 부릅니다.
function frame() {
renderer.clear();
renderer.drawScene(editor.scene);
renderer.drawOverlay(editor.selection);
requestAnimationFrame(frame);
}
처음에는 매번 전체를 다시 그리는 방식으로 시작합니다. 최적화는 나중입니다. 편집기에서는 먼저 모델과 좌표계를 올바르게 잡는 것이 더 중요합니다.
입력은 Canvas 밖에서 들어온다
사용자가 Canvas 위를 클릭하면 이벤트는 <canvas> 요소에 도착합니다. 하지만 그 안의 어떤 도형을 눌렀는지는 브라우저가 모릅니다.
그래서 입력 처리는 이런 식으로 나뉩니다.
pointer event
-> canvas local coordinate
-> world coordinate
-> hit test scene model
-> update selection or tool state
hit testing은 renderer가 아니라 editor core의 책임입니다. renderer는 픽셀을 그립니다. 어떤 도형이 클릭되었는지 판단하려면 scene model의 geometry를 검사해야 합니다.
이 구분을 초반에 잡아두지 않으면 코드가 금방 엉킵니다. WebGL draw call 안에서 selection 상태를 뒤지고, pointer handler가 GPU buffer를 직접 고치고, undo/redo가 renderer 상태에 묶이기 시작합니다.
오늘의 핵심
Canvas는 그래픽을 담는 DOM subtree가 아닙니다. Canvas는 우리가 매 프레임 갱신하는 픽셀 표면입니다.
따라서 GPU 기반 편집기는 다음 세 덩어리로 나눠 생각합니다.
editor core: scene, selection, tools, commands
input system: pointer 좌표를 editor action으로 변환
renderer: scene을 픽셀로 그림
이 분리가 있어야 WebGL renderer를 만들고, 나중에 WebGPU renderer로 바꿔도 편집기 자체가 무너지지 않습니다.