CSS 개발자를 위한 GPU 2D 에디터 만들기
Guide
CSS Matrix
Guide
CSS Matrix
  • Part 0. CSS에서 GPU로 사고 전환

    • CSS 박스가 사라지면 무엇이 남는가
    • Canvas는 DOM이 아니라 픽셀 버퍼다
    • CSS pixel, device pixel, backing store
    • scene model과 renderer 분리하기
    • retained mode vs immediate mode
    • DOM event target 없이 hit testing 하기
  • Part 1. Canvas editor의 기본 뼈대

    • render loop와 frame lifecycle
    • viewport와 camera
    • screen/world/local 좌표계 복습
    • cursor anchored zoom
    • grid와 ruler를 canvas에 그리기
    • overlay layer와 control layer 분리
  • Part 2. WebGL 2D renderer

    • WebGL context와 첫 triangle
    • clip space와 화면 좌표
    • shader를 CSS transform 관점으로 읽기
    • rectangle을 두 triangle로 그리기
    • matrix uniform으로 camera 적용하기
    • 색상, alpha, blending
    • 여러 rectangle을 buffer에 담기
    • batching과 draw call 줄이기
    • instanced rectangle renderer
    • line/outline renderer
  • Part 3. Editor tool math

    • pointer 좌표를 world 좌표로 바꾸기
    • CPU hit testing
    • selection bounds
    • hover outline
    • move tool
    • resize handles
    • rotation handle
    • marquee selection
    • snapping과 smart guides
    • group transform
  • Part 4. Figma-like scene graph

    • node tree 설계
    • transform inheritance
    • layer order와 z sorting
    • frame과 clipping
    • fill, stroke, effect 모델
    • command model과 undo/redo
    • JSON export/import
    • renderer-independent editor core
  • Part 5. Image, text, vector

    • texture로 image node 그리기
    • texture atlas 기본
    • text rendering 전략
    • SVG/text를 GPU editor에서 다루는 경계
    • vector path는 어디까지 직접 구현할 것인가
  • Part 6. WebGPU로 옮기기

    • WebGPU adapter/device/context
    • WGSL과 render pipeline
    • WebGL renderer를 WebGPU renderer로 바꾸기
    • uniform buffer와 bind group
    • WebGPU instancing
    • WebGL/WebGPU fallback 전략
  • Part 7. Capstone

    • editor shell 만들기
    • toolbar / layer panel / inspector 연결
    • mini Figma-like editor 완성
    • 성능 점검과 디버깅
    • 배포와 브라우저 호환성 체크
  • Part 8. Three.js로 WebGL 개발하기

    • Three.js를 WebGL renderer로 쓰는 기준
    • Scene, Camera, Renderer, render loop
    • OrthographicCamera로 2D editor 좌표계 만들기
    • BufferGeometry, Material, ShaderMaterial
    • Raycaster와 editor picking
    • Three.js renderer를 editor core 뒤에 붙이기
    • Three.js 프로젝트 세팅과 renderer lifecycle
    • Object3D transform과 editor scene graph 매핑
    • InstancedMesh로 많은 rectangle 그리기
    • Texture, CanvasTexture, Sprite로 이미지/텍스트 다루기
    • Three.js에서 outline, selection, overlay 만들기
    • dispose, cache, renderer.info로 성능 관리하기
    • WebGPURenderer와 TSL로 넘어가는 길
    • RenderTarget을 이용한 picking buffer
    • Three.js를 쓰면 안 좋은 경우
  • Appendix A. GPU editor debugging

    • WebGL/WebGPU 디버그 overlay 만들기
    • 좌표계, matrix, bounds readout 설계
    • frame time, draw call, buffer upload 측정하기
    • Spector.js / Chrome DevTools로 WebGL 프레임 보기
  • Appendix B. Browser and GPU compatibility

    • WebGL/WebGPU feature detection 체크리스트
    • DPR, resize, context lost 처리
    • Safari/Chrome/Firefox 차이와 fallback 정책
    • GPU memory와 texture size 제한
  • Appendix C. Asset pipeline

    • 이미지 로딩, ImageBitmap, texture upload
    • SVG를 texture로 쓸지 vector로 유지할지
    • 폰트 로딩과 text metrics
    • export용 PNG/SVG/JSON 생성 전략
  • Appendix D. Interaction polish and motion

    • inertial pan과 smooth zoom
    • snapping feedback animation
    • selection/hover transition
    • timeline 없이 필요한 최소 모션 수학
  • Appendix E. Production architecture

    • renderer worker / OffscreenCanvas를 고려하는 기준
    • document model versioning과 migration
    • plugin architecture와 command API
    • test 가능한 renderer abstraction 만들기
  • Appendix F. 2D renderer engine patterns

    • renderable type 선택: shape, sprite, mesh
    • static subtree를 texture cache로 굽기
    • render layer와 render group 설계
    • viewport culling과 spatial index
    • clipping 구현: scissor, stencil, mask texture
    • filters와 blend modes가 batch를 깨는 이유
    • interactivity budget: pickable, hitArea, skip children
    • texture GC와 idle resource eviction
    • dynamic text update 비용과 bitmap/glyph 전략
    • Canvas/WebGL editor의 accessibility layer
  • Appendix G. Rendering editor production gaps

    • render invalidation과 dirty flag
    • color space, premultiplied alpha, export 색상
    • stroke join/cap/dash/fill rule
    • editable text: DOM overlay, IME, caret, metrics
    • tool state machine과 pointer capture
    • pixel test와 renderer regression test

screen/world/local 좌표계 복습

GPU 에디터에서도 좌표계 문제는 사라지지 않습니다. 오히려 DOM이 도와주던 부분이 사라져서 더 명시적으로 다뤄야 합니다.

이번 레슨에서는 세 좌표계를 다시 고정합니다.

screen: 화면 기준
world: 문서 기준
local: 도형 내부 기준

screen 좌표는 입력과 렌더링의 표면이다

pointer event는 screen에 가까운 좌표를 줍니다. 정확히는 viewport 기준 clientX, clientY에서 Canvas rect를 빼서 canvas local screen 좌표를 만듭니다.

const screen = {
  x: event.clientX - rect.left,
  y: event.clientY - rect.top
};

selection handle, toolbar, inspector 같은 UI도 보통 screen 좌표에 가깝게 생각합니다.

function clientToCanvas(event, canvas) {
  const rect = canvas.getBoundingClientRect();
  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top
  };
}

world 좌표는 문서의 안정적인 기준이다

도형 위치는 world에 저장합니다. camera가 움직여도 world 좌표는 바뀌지 않습니다.

const world = {
  x: screen.x / camera.zoom + camera.x,
  y: screen.y / camera.zoom + camera.y
};

이 변환은 hit testing, drawing, snapping에서 계속 반복됩니다.

function screenToWorld(screen, camera) {
  return {
    x: screen.x / camera.zoom + camera.x,
    y: screen.y / camera.zoom + camera.y
  };
}

function worldToScreen(world, camera) {
  return {
    x: (world.x - camera.x) * camera.zoom,
    y: (world.y - camera.y) * camera.zoom
  };
}

local 좌표는 도형 내부 기준이다

회전된 도형을 클릭했는지 정확히 알고 싶으면 world point를 node local 좌표로 바꿉니다.

local = inverse(nodeWorldMatrix) * world

그다음 local point가 0 <= x <= width, 0 <= y <= height 안에 있는지 검사합니다. 이 방식은 사각형, 이미지, 텍스트 박스 같은 도형에서 기본이 됩니다.

2D affine matrix 최소 구현

Part 1부터 아래 정도의 matrix helper는 익숙해져야 합니다. 뒤에서 group transform, hit test, WebGL uniform이 모두 이 형태로 이어집니다.

function identity3() {
  return [1, 0, 0, 0, 1, 0, 0, 0, 1];
}

function translate3(tx, ty) {
  return [1, 0, 0, 0, 1, 0, tx, ty, 1];
}

function scale3(sx, sy) {
  return [sx, 0, 0, 0, sy, 0, 0, 0, 1];
}

function rotate3(rad) {
  const c = Math.cos(rad);
  const s = Math.sin(rad);
  return [c, s, 0, -s, c, 0, 0, 0, 1];
}

곱셈은 “오른쪽 변환을 먼저 적용하고, 그 결과에 왼쪽 변환을 적용한다”고 읽으면 됩니다.

function multiply3(a, b) {
  return [
    a[0] * b[0] + a[3] * b[1] + a[6] * b[2],
    a[1] * b[0] + a[4] * b[1] + a[7] * b[2],
    a[2] * b[0] + a[5] * b[1] + a[8] * b[2],
    a[0] * b[3] + a[3] * b[4] + a[6] * b[5],
    a[1] * b[3] + a[4] * b[4] + a[7] * b[5],
    a[2] * b[3] + a[5] * b[4] + a[8] * b[5],
    a[0] * b[6] + a[3] * b[7] + a[6] * b[8],
    a[1] * b[6] + a[4] * b[7] + a[7] * b[8],
    a[2] * b[6] + a[5] * b[7] + a[8] * b[8]
  ];
}

local point를 world로 보낼 때는 node matrix를 적용하고, world point를 local로 되돌릴 때는 inverse matrix를 적용합니다.

function invert3(m) {
  const a00 = m[0], a01 = m[1], a02 = m[2];
  const a10 = m[3], a11 = m[4], a12 = m[5];
  const a20 = m[6], a21 = m[7], a22 = m[8];

  const b01 = a22 * a11 - a12 * a21;
  const b11 = -a22 * a10 + a12 * a20;
  const b21 = a21 * a10 - a11 * a20;
  const det = a00 * b01 + a01 * b11 + a02 * b21;

  if (Math.abs(det) < 1e-8) {
    throw new Error("matrix is not invertible");
  }

  const invDet = 1 / det;
  return [
    b01 * invDet,
    (-a22 * a01 + a02 * a21) * invDet,
    (a12 * a01 - a02 * a11) * invDet,
    b11 * invDet,
    (a22 * a00 - a02 * a20) * invDet,
    (-a12 * a00 + a02 * a10) * invDet,
    b21 * invDet,
    (-a21 * a00 + a01 * a20) * invDet,
    (a11 * a00 - a01 * a10) * invDet
  ];
}
function pointInNode(worldPoint, node) {
  const local = transformPoint(invert3(node.worldMatrix), worldPoint);
  return local.x >= 0 && local.y >= 0 && local.x <= node.width && local.y <= node.height;
}

오늘의 핵심

좌표계는 이름표입니다. 이름표가 없으면 숫자는 금방 위험해집니다.

screen -> world: camera inverse
world -> local: node matrix inverse
local -> world: node matrix
world -> screen: camera

GPU 렌더러는 API가 달라져도 이 좌표계 구분을 그대로 사용합니다.

최근 수정: 26. 5. 16. PM 12:53
Contributors: jinho.park.s3
Prev
viewport와 camera
Next
cursor anchored zoom