Canvas 상호작용을 통한 그래픽 요소 이벤트 처리 기법

기본 원리: 캔버스 내 요소의 클릭 감지

캔버스는 단순히 2D 픽셀 그리기 도구이지만, 사용자 인터랙션을 위해선 그 위에 그려진 각 그래픽 요소를 개별적으로 식별하고 이벤트를 처리할 수 있어야 합니다. 기본적으로 <canvas> 요소 자체만이 자바스크립트에서 접근 가능한 객체이며, 내부의 도형이나 이미지는 "그림물"일 뿐입니다. 따라서 이를 해결하기 위해선 Path2DisPointInPath 메서드를 활용한 지점 확인 로직이 핵심입니다.

이미지 렌더링 및 스케일링

CanvasAnnotate 클래스는 캔버스 요소와 이미지 주소를 받아, 이미지를 적절한 비율로 조정하여 캔버스에 표시합니다. 이미지가 캔버스보다 크면 최대 한계 내에서 축소되며, 오류 발생 시에도 안전하게 처리됩니다.

class CanvasAnnotate {
  canvas: HTMLCanvasElement | null = null;
  ctx: CanvasRenderingContext2D | null = null;
  img: HTMLImageElement;

  constructor(canvasId: string, imgUrl: string) {
    this.canvas = document.getElementById(canvasId) as HTMLCanvasElement;
    this.ctx = this.canvas.getContext('2d');
    this.img = new Image();

    if (!this.canvas || !this.ctx) {
      console.error('캔버스 또는 컨텍스트가 유효하지 않습니다.');
      return;
    }

    this.img.onload = this.initStage.bind(this);
    this.img.onerror = () => console.error('이미지 로딩 실패');
    this.img.src = imgUrl;
  }

  initStage() {
    const scale = Math.min(
      this.canvas.width / (this.img.width || 1),
      this.canvas.height / (this.img.height || 1)
    );

    const drawWidth = this.img.width * scale;
    const drawHeight = this.img.height * scale;

    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.ctx.drawImage(this.img, 0, 0, drawWidth, drawHeight);
  }
}

도형 추상화: 베이스 클래스 설계

모든 도형은 공통된 속성과 행동을 가집니다. 이를 위한 BaseShape 클래스를 정의합니다. 각 도형은 고유한 id, ctx, 색상, 그리고 이벤트 리스너를 갖습니다. 중요한 점은 path 프로퍼티가 Path2D 타입으로 구현되어야 하며, 이는 isPointInRegion 메서드에서 사용될 수 있기 때문입니다.

abstract class BaseShape {
  listeners: { [key: string]: ((e: MouseEvent) => void)[] } = {};

  constructor(
    public id: string,
    protected context: CanvasRenderingContext2D,
    public color: string
  ) {}

  abstract get path(): Path2D;

  draw() {
    this.context.fillStyle = this.color;
    this.context.fill(this.path);
  }

  on(eventType: string, callback: (e: MouseEvent) => void) {
    if (!this.listeners[eventType]) this.listeners[eventType] = [];
    this.listeners[eventType]?.push(callback);
  }

  isPointInRegion(x: number, y: number): boolean {
    return this.context.isPointInPath(this.path, x, y);
  }
}

구체적인 도형 구현

예제에서는 사각형과 원을 구현합니다. 두 클래스 모두 BaseShape를 확장하며, get path()에서 해당 도형의 경로를 생성합니다.

사각형 클래스:

class Rect extends BaseShape {
  constructor(
    id: string,
    ctx: CanvasRenderingContext2D,
    color: string,
    public x: number,
    public y: number,
    public width: number,
    public height: number
  ) {
    super(id, ctx, color);
  }

  get path() {
    const path = new Path2D();
    path.rect(this.x, this.y, this.width, this.height);
    return path;
  }
}

원 클래스:

class Circle extends BaseShape {
  constructor(
    id: string,
    ctx: CanvasRenderingContext2D,
    color: string,
    public centerX: number,
    public centerY: number,
    public radius: number,
    public startAngle: number = 0,
    public endAngle: number = Math.PI * 2
  ) {
    super(id, ctx, color);
  }

  get path() {
    const path = new Path2D();
    path.arc(this.centerX, this.centerY, this.radius, this.startAngle, this.endAngle);
    return path;
  }
}

도형 렌더링 및 이벤트 연결

캔버스 초기화 후, 생성된 도형들을 배열에 저장하고, drawGraphics() 메서드를 통해 일괄 렌더링합니다. 또한 각 도형에 클릭 이벤트를 바인딩해, 마우스 위치가 도형 내부인지 확인합니다.

initStage() {
  // ... 이미지 그리기

  const rectA = new Rect('rect-01', this.ctx, '#3366ff', 50, 50, 80, 60);
  const circleA = new Circle('circle-01', this.ctx, '#ff6633', 200, 150, 40);

  this.shapes.push(rectA, circleA);
  this.drawGraphics();
}

drawGraphics() {
  this.shapes.forEach(shape => shape.draw());
}

addClickHandler(shape: BaseShape) {
  shape.on('click', (e) => {
    const mouseX = e.offsetX;
    const mouseY = e.offsetY;

    if (shape.isPointInRegion(mouseX, mouseY)) {
      console.log(`클릭된 도형: ${shape.id}`);
      // 색상 변경 등 추가 액션 가능
    }
  });
}

전역 이벤트 처리

최종적으로, 캔버스 요소에 클릭 이벤트 리스너를 등록하고, 모든 도형의 이벤트 핸들러를 순차적으로 실행합니다. 이 과정에서 offsetX, offsetY를 사용해 마우스 좌표를 정확히 계산합니다.

attachEventListeners() {
  this.canvas.addEventListener('click', (e) => {
    this.shapes.forEach(shape => {
      const handlers = shape.listeners['click'];
      if (handlers) {
        handlers.forEach(handler => handler(e));
      }
    });
  });
}

결론

캔버스의 제약을 극복하기 위해, 도형의 경로 정보를 Path2D로 관리하고, isPointInPath를 통해 마우스 포인터의 위치를 검사하는 방식은 효과적입니다. 이 방법은 복잡한 인터랙티브 문서 렌더링, 메모 작성, 툴팁 시스템 등 다양한 시나리오에 적용 가능합니다.

태그: canvas Path2D event handling interactive graphics mouse detection

5월 21일 20:12에 게시됨