프로그래밍 · 

웹 기반 인테리어 설계 에디터를 구현하기 위해 필요한 것들 [3편]

카메라 추가에 따른 좌표 변환 구현

인테리어 설계 에디터를 개발하면서 어느순간 '카메라'라는 개념이 필요해졌습니다. 그러니까, 평면도를 옆으로 움직여서 더 넓은 공간에 그릴 수도 있어야 하고, 때로는 줌을 땡겨서 평면도를 더 크게 보면서 작업해야 하기도 했습니다. 그리고 대각선 벽체가 나오는 경우에는, 평면도를 회전해서 보고 싶기도 했죠.


2편에서는 수학좌표계를 캔버스좌표계로 변환시켰는데, 카메라 기능을 구현하기 위해서는 코드를 추가해야 했습니다. 유니티 같은 에디터에서도 객체의 Transform은 Position(위치), Rotation(회전), Scale(크기) 세 가지 요소로 표현하는데 저희도 똑같습니다. 다만 유니티는 3D 공간이기 때문에 각각 3차원 벡터로 표현하지만, 2D 인테리어 설계 에디터에서는 Positon은 2차원 좌표(x, y)로, Rotation과 Scale은 실수값으로 표현했습니다.

export class ArcEntity {
  render(ctx: CanvasRenderingContext2D) {
    ctx.beginPath();
    ctx.lineTo({ x: 0, y: 0 });
    ctx.lineTo({ x: 100, y: 100 });
    ctx.arc({ x: 0, y: 100 }, 100, 0, Math.PI / 2);
    ctx.stroke();
  }
}

이렇게 새로운 기능을 추가할 때는, 코드 적용을 바로 확인할 수 있는 예제 코드를 만들어서 화면을 보면서 코드를 변경하는 편입니다. 2편에서 다룬 Y축 반전, 평행이동, 원호 드로잉까지 모두 확인할 수 있는 예제 코드입니다. 결과는 아래과 같이 나옵니다.

화면 캡처 2026-05-28 162320.png

카메라 회전에 따른 좌표 변환

일단 회전부터 시작했습니다. 먼저 카메라의 Transform 요소에 해당하는 속성을 정의합니다.

class MyCanvasRenderingContext {
  canvas: HTMLCanvasElement
  context: CanvasRenderingContext2D;
  camera = {
    position: { x: 0, y: 0 },
    rotation: 0,
    scale: 1,
  }
  
  lineTo(x: number, y: number) {
    const canvasPoint = this.toCanvasPoint(x, y);
    
    this.context.lineTo(canvasPoint.x, canvasPoint.y);    
  }
  
  arc(x: number, y: number, radius: number, startAngle: number, endAngle: number) {
    const canvasPoint = this.toCanvasPoint(x, y);
    
    this.context.arc(canvasPoint.x, canvasPoint.y, radius, startAngle, endAngle); 
  }
  
  toCanvasPoint(x: number, y: number) {
    const canvasX = this.canvas.clientWidth * 0.5 + x;
    const canvasY = this.canvas.clientHeight * 0.5 - y;
    
    return {
      x: canvasX,
      y: canvasY,
    }
  }
}

이때, 카메라가 90도 반시계방향으로 회전한다고 가정해 봅시다(camera.rotation = Math.PI * 0.5). 그렇다면 드로잉 결과는 시계방향으로 90도 회전한 결과여야 할 겁니다. 이를 구현기 위해 toCanvasPoint를 아래와 같이 수정했습니다.

import { Point } from "@flatten-js/core";

class MyCanvasRenderingContext {
  camera = {
    position: new Point(0, 0),
    rotation: Math.PI * 0.5,
    scale: 1,
  }
  
  toCanvasPoint(x: number, y: number) {
    const point = new Point(x, y);
    
    const rotatedPoint = point.rotate(-this.camera.rotation);
    
    const scaledPoint = rotatedPoint.scale(1, -1);
    
    const canvasPoint = scaledPoint.translate(this.canvas.clientWidth * 0.5, this.canvas.clientHeight * 0.5);
      
    return canvasPoint;
  }
}

계산상 편의를 위해 flatten-js 라이브러리를 이용했습니다. y축 반전과 평행이동은 이전 코드와 동일하고 좌표를 회전하는 코드만 추가했습니다. 카메라 rotation 값은 90도로 설정했습니다. 결과는 다음과 같습니다.

화면 캡처 2026-05-28 162352.png

언뜻 보기에는 모양이 완전히 달라졌지만, context.lineTo({ x: 100, y: 100 }); 코드가 향하는 지점은 올바르게 회전시켰습니다. 명시적으로 보이지는 않지만, 원호의 중심점도 올바른 위치로 이동했습니다.


하지만 arc() 함수에 들어가는 시작각도와 끝각도에 대해서는 수정하지 않고 좌표만 변환했기 때문에 원호의 모양은 그대로 남아 있습니다. 따라서 멀리 떨어지게 된 원호의 시작점과 선을 이어서 stroke하느라고 새로운 선분이 생겼습니다. arc() 함수에 들어가는 시작 각도와 끝 각도를 수정해주면 올바르게 그려질 것이라고 생각했습니다. 눈으로 보면, arc() 함수에 들어가는 시작 각도와 끝 각도도 카메라의 rotation값 만큼 시계방향으로 회전해야 함을 알 수 있습니다. 코드로 작성하면 다음과 같습니다.

class MyCanvasRenderingContext {
  canvas: HTMLCanvasElement
  context: CanvasRenderingContext2D;
  
  camera = {
    position: new Point(0, 0),
    rotation: Math.PI * 0.5,
    scale: 1,
  }
  
   arc(x: number, y: number, radius: number, startAngle: number, endAngle: number) {
    const canvasPoint = this.toCanvasPoint(x, y);
    const canvasStartAngle = this.toCanvasAngle(startAngle);
    const cnavasEndAngle = this.toCanvasAngle(endAngle);
    
    this.context.arc(canvasPoint.x, canvasPoint.y, radius, canvasStartAngle, canvasEndAngle, true); 
  }
  
  toCanvasAngle(angle: number) {
    return -angle + this.camera.rotation;
  }
}
화면 캡처 2026-05-28 163148.png

결과는 올바르게 나옵니다.

카메라 줌에 따른 좌표 변환

다음으로 넘어가서, 카메라 줌을 구현했습니다. 카메라 줌을 구현하는 방법은 비교적 간단했습니다. 스케일링을 적용하면 됩니다. 이미 수학좌표계를 캔버스좌표계로 변환시키기 위해서 y축을 반전시키고 있는데, 이게 일종의 스케일링입니다. 따라서 카메라의 스케일 프로퍼티를 적용한 좌표 변환 코드는 다음과 같습니다.

import { Point } from "@flatten-js/core";

class MyCanvasRenderingContext {
  camera = {
    position: new Point(0, 0),
    rotation: Math.PI * 0.5,
    scale: 0.5,
  }
  
  toCanvasPoint(x: number, y: number) {
    const point = new Point(x, y);
    
    const rotatedPoint = point.rotate(-this.camera.rotation);
    
    const scaledPoint = rotatedPoint.scale(1/this.camera.scale, -1/this.camera.scale);
    
    const canvasPoint = scaledPoint.translate(this.canvas.clientWidth * 0.5, this.canvas.clientHeight * 0.5);
      
    return canvasPoint;
  }
}
화면 캡처 2026-05-28 165334.png

일반적으로 카메라 크기가 커지면 보이는 넓이가 넓어지는 것으로 생각할 수 있으므로 카메라 스케일의 역수를 적용했습니다. 하지만 이번에도 arc() 함수에서 반지름이 넓어지지 않아서 올바른 결과가 나오지 않았습니다.


class MyCanvasRenderingContext {
  canvas: HTMLCanvasElement
  context: CanvasRenderingContext2D;
  
  camera = {
    position: new Point(0, 0),
    rotation: Math.PI * 0.5,
    scale: 1,
  }
  
  arc(x: number, y: number, radius: number, startAngle: number, endAngle: number) {
    const canvasPoint = this.toCanvasPoint(x, y);
    const canvasRadius = this.toCanvasLength(radius);
    const canvasStartAngle = this.toCanvasAngle(startAngle);
    const cnavasEndAngle = this.toCanvasAngle(endAngle);
    
    this.context.arc(canvasPoint.x, canvasPoint.y, canvasRadius, canvasStartAngle, canvasEndAngle, true); 
  }
  
  toCanvasLength(length: number) {
    return length / this.camera.scale
  }
}
화면 캡처 2026-05-28 165659.png

결과가 올바르게 나왔습니다.

카메라 위치에 따른 좌표 변환

마지막으로 카메라 위치 이동을 구현했습니다. 카메라 위치 이동 또한 단순합니다. 카메라가 만약 오른쪽으로 x만큼 움직인다면, 도형은 왼쪽으로 x만큼 움직여야 합니다. 마찬가지로 위로 y만큼 움직인다면, 도형은 아래로 y만큼 움직여야 하죠.

import { Point } from "@flatten-js/core";

class MyCanvasRenderingContext {
  camera = {
    position: new Point(100, 100), // 카메라를 오른쪽으로 100, 위로 100만큼 이동
    rotation: 0, // 리셋
    scale: 1, // 리셋
  }
  
  toCanvasPoint(x: number, y: number) {
    const point = new Point(x, y);
    
    const translatedPoint = point.translate(-this.camera.position.x,-this.camera.position.y,);
    
    const rotatedPoint = point.rotate(-this.camera.rotation);
    
    const scaledPoint = rotatedPoint.scale(1/this.camera.scale, -1/this.camera.scale);
    
    const canvasPoint = scaledPoint.translate(this.canvas.clientWidth * 0.5, this.canvas.clientHeight * 0.5);
      
    return canvasPoint;
  }
}

예상대로라면 도형은 처음 이미지에서 왼쪽 아래로 움직였어야 합니다. 결과는 다음과 같습니다.

화면 캡처 2026-05-29 181643.png

네, 이렇게 평행이동까지 원하는 결과를 만들어냈습니다.


이제 좌표계를 사용하면서 camera의 position, rotation, scale만 변경하면 됩니다. 카메라를 만드니 무한 캔버스도 자동으로 구현되었습니다.

프로필 이미지오진수

Frontend Developer

댓글

    프로그래밍 카테고리 다른 글

    1. Next.js 상세 페이지 로딩 속도 87% 개선으로 조회수 30% 더 얻기
    2. AWS EC2 요금 폭탄
    3. 자바스크립트 instanceof 안 쓰고 클래스 타입 판별하는 방법
    4. 웹 기반 인테리어 설계 에디터를 구현하기 위해 필요한 것들 [5편]
    5. 실시간 협업을 위한 마우스 트래킹 구현
    6. Flutter 상태관리 Provider면 충분하다