기본 원리: 캔버스 내 요소의 클릭 감지
캔버스는 단순히 2D 픽셀 그리기 도구이지만, 사용자 인터랙션을 위해선 그 위에 그려진 각 그래픽 요소를 개별적으로 식별하고 이벤트를 처리할 수 있어야 합니다. 기본적으로 <canvas> 요소 자체만이 자바스크립트에서 접근 가능한 객체이며, 내부의 도형이나 이미지는 "그림물"일 뿐입니다. 따라서 이를 해결하기 위해선 Path2D와 isPointInPath 메서드를 활용한 지점 확인 로직이 핵심입니다.
이미지 렌더링 및 스케일링
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를 통해 마우스 포인터의 위치를 검사하는 방식은 효과적입니다. 이 방법은 복잡한 인터랙티브 문서 렌더링, 메모 작성, 툴팁 시스템 등 다양한 시나리오에 적용 가능합니다.