overlay layer와 control layer 분리
Figma 같은 에디터 화면에는 실제 문서 도형만 있는 것이 아닙니다. 선택 박스, hover outline, resize handle, ruler, toolbar, inspector가 함께 있습니다.
이것들을 모두 같은 방식으로 그리면 금방 혼란스러워집니다.
세 레이어를 따로 렌더링하는 코드
content는 GPU renderer가 그리고, overlay는 같은 camera를 기준으로 screen 좌표에 그리며, controls는 DOM UI로 둡니다.
function renderEditorFrame(editor) {
const snapshot = editor.getSnapshot();
contentRenderer.render({
scene: snapshot.scene,
camera: snapshot.camera
});
overlayRenderer.clear();
overlayRenderer.drawSelection({
selection: snapshot.selection,
scene: snapshot.scene,
camera: snapshot.camera
});
overlayRenderer.drawGuides(snapshot.tool.guides);
controls.render({
tool: snapshot.tool.current,
selection: snapshot.selection,
documentName: snapshot.document.name
});
}
selection handle처럼 화면 크기가 고정되어야 하는 요소는 world 크기가 아니라 screen pixel 크기로 그립니다.
function drawHandle(ctx, screenPoint) {
const size = 8;
ctx.fillRect(screenPoint.x - size / 2, screenPoint.y - size / 2, size, size);
}
selection bounds를 screen으로 투영한다
overlay는 문서 geometry를 읽지만 최종 draw는 screen 좌표에서 하는 편이 단순합니다.
function worldBoundsToScreen(bounds, camera) {
const a = worldToScreen({ x: bounds.x, y: bounds.y }, camera);
const b = worldToScreen({
x: bounds.x + bounds.width,
y: bounds.y + bounds.height
}, camera);
return {
x: Math.min(a.x, b.x),
y: Math.min(a.y, b.y),
width: Math.abs(b.x - a.x),
height: Math.abs(b.y - a.y)
};
}
function drawSelectionOverlay(ctx, selectionBounds, camera) {
const screen = worldBoundsToScreen(selectionBounds, camera);
ctx.strokeStyle = "#0d9488";
ctx.lineWidth = 1;
ctx.strokeRect(screen.x, screen.y, screen.width, screen.height);
for (const point of selectionHandlePoints(screen)) {
drawHandle(ctx, point);
}
}
handle 위치는 screen bounds에서 바로 계산합니다.
function selectionHandlePoints(bounds) {
const x0 = bounds.x;
const x1 = bounds.x + bounds.width / 2;
const x2 = bounds.x + bounds.width;
const y0 = bounds.y;
const y1 = bounds.y + bounds.height / 2;
const y2 = bounds.y + bounds.height;
return [
{ x: x0, y: y0 },
{ x: x1, y: y0 },
{ x: x2, y: y0 },
{ x: x2, y: y1 },
{ x: x2, y: y2 },
{ x: x1, y: y2 },
{ x: x0, y: y2 },
{ x: x0, y: y1 }
];
}
content layer는 문서 자체다
content layer에는 사용자가 만든 도형을 그립니다.
rect
image
text
frame
group
이 레이어는 camera의 영향을 받습니다. 사용자가 zoom하면 도형도 같이 커지고, pan하면 같이 움직입니다.
overlay layer는 편집 보조선이다
selection outline, smart guide, resize handle 같은 것은 문서 도형이 아닙니다. 문서를 편집하기 위해 화면 위에 얹는 정보입니다.
overlay는 보통 world 정보를 기반으로 계산하지만, 최종적으로는 screen 좌표에 그리는 편이 다루기 쉽습니다.
selected node world bounds
-> camera
-> screen bounds
-> draw outline and handles
handle 크기는 zoom에 따라 커지면 안 됩니다. 도형을 400% 확대해도 resize handle이 화면에서 32px짜리 거대한 점이 되면 이상합니다. 그래서 handle은 screen pixel 기준 크기를 유지합니다.
function drawOverlayFrame(ctx, snapshot) {
ctx.clearRect(0, 0, snapshot.viewport.width, snapshot.viewport.height);
if (snapshot.selection.bounds) {
drawSelectionOverlay(ctx, snapshot.selection.bounds, snapshot.camera);
}
for (const guide of snapshot.tool.guides) {
drawGuide(ctx, guide, snapshot.camera);
}
}
여기서 selection.bounds는 world 기준이고, handle size는 screen 기준입니다. 이 둘을 섞지 않는 것이 overlay 구현의 핵심입니다.
control layer는 앱 UI다
toolbar, layer panel, property inspector는 문서 좌표계와 무관합니다. 이들은 일반 DOM UI로 두는 편이 좋습니다.
content: WebGL/WebGPU canvas
overlay: canvas/SVG/DOM overlay
controls: DOM app UI
처음부터 모든 것을 GPU로 그릴 필요는 없습니다. 그래픽 문서는 GPU로 그리고, 앱 UI는 DOM으로 만드는 혼합 구조가 현실적입니다.
오늘의 핵심
에디터 화면은 하나의 canvas처럼 보여도 내부적으로는 역할이 다릅니다.
content layer: camera 적용, 문서 도형
overlay layer: 편집 보조, screen pixel 기준 요소 포함
control layer: 앱 UI, DOM으로 유지
이 분리를 해두면 selection, handle, toolbar가 서로의 좌표계를 망가뜨리지 않습니다.