- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
967 lines
35 KiB
JavaScript
967 lines
35 KiB
JavaScript
/**
|
|
* Drawing Module - 독립적인 Canvas 그리기 모듈
|
|
* @version 1.0.0
|
|
* @author 김보곤
|
|
*
|
|
* 사용법:
|
|
* const drawer = new DrawingModule({
|
|
* container: 'myContainer',
|
|
* onSave: (imageData) => { console.log('저장된 이미지:', imageData); }
|
|
* });
|
|
* drawer.show();
|
|
*/
|
|
|
|
class DrawingModule {
|
|
constructor(options = {}) {
|
|
this.options = {
|
|
container: options.container || 'body',
|
|
width: options.width || 800,
|
|
height: options.height || 600,
|
|
canvasWidth: options.canvasWidth || 320,
|
|
canvasHeight: options.canvasHeight || 240,
|
|
onSave: options.onSave || null,
|
|
onCancel: options.onCancel || null,
|
|
initialImage: options.initialImage || null,
|
|
title: options.title || '그리기 도구'
|
|
};
|
|
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
this.drawing = false;
|
|
this.drawMode = 'polyline';
|
|
this.drawColor = '#000000';
|
|
this.lineWidth = 2;
|
|
this.eraserRadius = 15;
|
|
this.isEraser = false;
|
|
this.textMode = false;
|
|
this.straightMode = true;
|
|
this.polyPrev = null;
|
|
this.startX = 0;
|
|
this.startY = 0;
|
|
this.currentPath = [];
|
|
this.drawingData = [];
|
|
this.originalImage = null;
|
|
this.modalElement = null;
|
|
this.handlers = {};
|
|
}
|
|
|
|
// 모달 생성 및 표시
|
|
show() {
|
|
this.createModal();
|
|
this.initializeCanvas();
|
|
this.attachEventHandlers();
|
|
this.modalElement.style.display = 'block';
|
|
|
|
// 초기 이미지가 있으면 로드
|
|
if (this.options.initialImage) {
|
|
this.loadImage(this.options.initialImage);
|
|
}
|
|
}
|
|
|
|
// 모달 HTML 생성
|
|
createModal() {
|
|
// 기존 모달이 있으면 제거
|
|
const existingModal = document.getElementById('drawingModuleModal');
|
|
if (existingModal) {
|
|
existingModal.remove();
|
|
}
|
|
|
|
const modalHtml = `
|
|
<div id="drawingModuleModal" class="dm-modal">
|
|
<div class="dm-modal-content">
|
|
<div class="dm-modal-header">
|
|
<h3>${this.options.title}</h3>
|
|
<span class="dm-close">×</span>
|
|
</div>
|
|
<div class="dm-modal-body">
|
|
<div class="dm-container">
|
|
<div class="dm-canvas-area">
|
|
<canvas id="dmCanvas"></canvas>
|
|
</div>
|
|
<div class="dm-controls">
|
|
<div class="dm-control-group">
|
|
<label>그리기 모드:</label>
|
|
<select id="dmDrawMode" class="dm-select">
|
|
<option value="polyline">점연결</option>
|
|
<option value="line">직선</option>
|
|
<option value="free">자유선</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="dm-control-group">
|
|
<label>색상:</label>
|
|
<input type="color" id="dmDrawColor" value="#000000" class="dm-color-picker">
|
|
</div>
|
|
|
|
<div class="dm-control-group">
|
|
<label>선 굵기:</label>
|
|
<input type="range" id="dmLineWidth" min="1" max="10" value="2" class="dm-range">
|
|
<span id="dmLineWidthLabel">2px</span>
|
|
</div>
|
|
|
|
<div class="dm-control-group">
|
|
<button type="button" id="dmLineBtn" class="dm-btn dm-btn-primary">선 그리기</button>
|
|
<button type="button" id="dmTextBtn" class="dm-btn dm-btn-primary">텍스트</button>
|
|
<button type="button" id="dmEraserBtn" class="dm-btn dm-btn-warning">지우개</button>
|
|
</div>
|
|
|
|
<div class="dm-control-group" id="dmEraserSizeContainer" style="display: none;">
|
|
<label>지우개 크기:</label>
|
|
<input type="range" id="dmEraserSize" min="5" max="50" value="15" class="dm-range">
|
|
<span id="dmEraserSizeLabel">15px</span>
|
|
</div>
|
|
|
|
<div class="dm-control-group">
|
|
<label class="dm-checkbox">
|
|
<input type="checkbox" id="dmStraightMode" checked>
|
|
<span>직각 모드</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="dm-control-group dm-button-group">
|
|
<button type="button" id="dmClearBtn" class="dm-btn dm-btn-danger">초기화</button>
|
|
<button type="button" id="dmUndoBtn" class="dm-btn dm-btn-secondary">실행취소</button>
|
|
<button type="button" id="dmSaveBtn" class="dm-btn dm-btn-success">저장</button>
|
|
<button type="button" id="dmCancelBtn" class="dm-btn dm-btn-secondary">취소</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 컨테이너에 모달 추가
|
|
const container = typeof this.options.container === 'string'
|
|
? document.querySelector(this.options.container)
|
|
: this.options.container;
|
|
|
|
container.insertAdjacentHTML('beforeend', modalHtml);
|
|
this.modalElement = document.getElementById('drawingModuleModal');
|
|
}
|
|
|
|
// 캔버스 초기화
|
|
initializeCanvas() {
|
|
this.canvas = document.getElementById('dmCanvas');
|
|
this.canvas.width = this.options.canvasWidth;
|
|
this.canvas.height = this.options.canvasHeight;
|
|
|
|
// 캔버스를 포커스 가능하게 만들기
|
|
this.canvas.tabIndex = 1;
|
|
this.canvas.style.outline = 'none'; // 포커스 아웃라인 제거
|
|
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.ctx.lineWidth = this.lineWidth;
|
|
this.ctx.strokeStyle = this.drawColor;
|
|
this.ctx.lineCap = 'round';
|
|
this.ctx.lineJoin = 'round';
|
|
|
|
// 흰색 배경 설정
|
|
this.ctx.fillStyle = 'white';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
}
|
|
|
|
// 이벤트 핸들러 연결
|
|
attachEventHandlers() {
|
|
// 모달 닫기 버튼
|
|
const closeBtn = this.modalElement.querySelector('.dm-close');
|
|
closeBtn.addEventListener('click', () => this.close());
|
|
|
|
// 취소 버튼
|
|
document.getElementById('dmCancelBtn').addEventListener('click', () => this.close());
|
|
|
|
// 저장 버튼
|
|
document.getElementById('dmSaveBtn').addEventListener('click', () => this.save());
|
|
|
|
// 초기화 버튼
|
|
document.getElementById('dmClearBtn').addEventListener('click', () => this.clear());
|
|
|
|
// 실행취소 버튼
|
|
document.getElementById('dmUndoBtn').addEventListener('click', () => this.undo());
|
|
|
|
// 그리기 모드 변경
|
|
document.getElementById('dmDrawMode').addEventListener('change', (e) => {
|
|
this.drawMode = e.target.value;
|
|
|
|
// 텍스트 모드 비활성화
|
|
this.textMode = false;
|
|
document.getElementById('dmTextBtn').classList.remove('dm-active');
|
|
|
|
// 지우개 모드 비활성화
|
|
this.isEraser = false;
|
|
document.getElementById('dmEraserBtn').classList.remove('dm-active');
|
|
document.getElementById('dmEraserSizeContainer').style.display = 'none';
|
|
this.removeEraserCursor();
|
|
|
|
this.canvas.style.cursor = 'crosshair';
|
|
this.resetDrawingState();
|
|
});
|
|
|
|
// 색상 변경
|
|
document.getElementById('dmDrawColor').addEventListener('change', (e) => {
|
|
this.drawColor = e.target.value;
|
|
this.ctx.strokeStyle = this.drawColor;
|
|
});
|
|
|
|
// 선 굵기 변경
|
|
document.getElementById('dmLineWidth').addEventListener('input', (e) => {
|
|
this.lineWidth = parseInt(e.target.value);
|
|
this.ctx.lineWidth = this.lineWidth;
|
|
document.getElementById('dmLineWidthLabel').textContent = `${this.lineWidth}px`;
|
|
});
|
|
|
|
// 지우개 버튼
|
|
document.getElementById('dmEraserBtn').addEventListener('click', (e) => {
|
|
e.preventDefault(); // 기본 동작 방지
|
|
e.stopPropagation(); // 이벤트 버블링 방지
|
|
this.toggleEraser();
|
|
});
|
|
|
|
// 지우개 크기 변경
|
|
document.getElementById('dmEraserSize').addEventListener('input', (e) => {
|
|
this.eraserRadius = parseInt(e.target.value);
|
|
document.getElementById('dmEraserSizeLabel').textContent = `${this.eraserRadius}px`;
|
|
|
|
// 지우개 모드일 때 커서 크기 업데이트
|
|
if (this.isEraser) {
|
|
const cursor = document.getElementById('dmEraserCursor');
|
|
if (cursor) {
|
|
cursor.style.width = `${this.eraserRadius * 2}px`;
|
|
cursor.style.height = `${this.eraserRadius * 2}px`;
|
|
}
|
|
}
|
|
});
|
|
|
|
// 직각 모드
|
|
document.getElementById('dmStraightMode').addEventListener('change', (e) => {
|
|
this.straightMode = e.target.checked;
|
|
});
|
|
|
|
// 선 그리기 버튼
|
|
document.getElementById('dmLineBtn').addEventListener('click', () => {
|
|
this.drawMode = 'line';
|
|
document.getElementById('dmDrawMode').value = 'line';
|
|
|
|
// 텍스트 모드 비활성화
|
|
this.textMode = false;
|
|
document.getElementById('dmTextBtn').classList.remove('dm-active');
|
|
|
|
// 지우개 모드 비활성화
|
|
this.isEraser = false;
|
|
document.getElementById('dmEraserBtn').classList.remove('dm-active');
|
|
document.getElementById('dmEraserSizeContainer').style.display = 'none';
|
|
this.removeEraserCursor();
|
|
|
|
this.canvas.style.cursor = 'crosshair';
|
|
this.resetDrawingState();
|
|
});
|
|
|
|
// 텍스트 버튼
|
|
document.getElementById('dmTextBtn').addEventListener('click', () => {
|
|
this.textMode = !this.textMode;
|
|
if (this.textMode) {
|
|
// 텍스트 모드 활성화
|
|
document.getElementById('dmTextBtn').classList.add('dm-active');
|
|
this.canvas.style.cursor = 'text';
|
|
|
|
// 다른 모드 비활성화
|
|
this.isEraser = false;
|
|
this.drawing = false;
|
|
this.polyPrev = null;
|
|
document.getElementById('dmEraserBtn').classList.remove('dm-active');
|
|
document.getElementById('dmEraserSizeContainer').style.display = 'none';
|
|
this.removeEraserCursor();
|
|
} else {
|
|
// 텍스트 모드 비활성화
|
|
document.getElementById('dmTextBtn').classList.remove('dm-active');
|
|
this.canvas.style.cursor = 'crosshair';
|
|
}
|
|
});
|
|
|
|
// 캔버스 이벤트
|
|
this.attachCanvasEvents();
|
|
}
|
|
|
|
// 캔버스 이벤트 처리
|
|
attachCanvasEvents() {
|
|
// 마우스 이벤트
|
|
this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
|
|
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
|
this.canvas.addEventListener('mouseup', (e) => this.handleMouseUp(e));
|
|
this.canvas.addEventListener('mouseout', (e) => this.handleMouseOut(e));
|
|
|
|
// 터치 이벤트 (모바일 지원)
|
|
this.canvas.addEventListener('touchstart', (e) => this.handleTouchStart(e));
|
|
this.canvas.addEventListener('touchmove', (e) => this.handleTouchMove(e));
|
|
this.canvas.addEventListener('touchend', (e) => this.handleTouchEnd(e));
|
|
|
|
// ESC 키 이벤트 (점연결 모드 취소)
|
|
this.handleEscKey = (e) => {
|
|
if (e.key === 'Escape' && this.drawMode === 'polyline' && this.polyPrev) {
|
|
this.cancelPolyline();
|
|
}
|
|
};
|
|
document.addEventListener('keydown', this.handleEscKey);
|
|
}
|
|
|
|
// 마우스 다운 이벤트
|
|
handleMouseDown(e) {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
// 캔버스 밖에서 클릭한 경우 무시
|
|
if (x < 0 || x > this.canvas.width || y < 0 || y > this.canvas.height) {
|
|
return;
|
|
}
|
|
|
|
if (this.textMode) {
|
|
this.addText(x, y);
|
|
return; // 텍스트 모드는 유지됨
|
|
}
|
|
|
|
if (this.isEraser) {
|
|
this.drawing = true;
|
|
this.eraseAt(x, y);
|
|
return;
|
|
}
|
|
|
|
switch (this.drawMode) {
|
|
case 'polyline':
|
|
this.handlePolylineClick(x, y);
|
|
break;
|
|
case 'line':
|
|
this.startLine(x, y);
|
|
break;
|
|
case 'free':
|
|
this.startFreeDraw(x, y);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 마우스 이동 이벤트
|
|
handleMouseMove(e) {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
if (this.isEraser && this.drawing) {
|
|
this.eraseAt(x, y);
|
|
return;
|
|
}
|
|
|
|
if (this.drawing) {
|
|
switch (this.drawMode) {
|
|
case 'line':
|
|
this.previewLine(x, y);
|
|
break;
|
|
case 'free':
|
|
this.continueDraw(x, y);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// polyline 모드에서 미리보기
|
|
if (this.drawMode === 'polyline' && this.polyPrev) {
|
|
this.previewPolyline(x, y);
|
|
}
|
|
}
|
|
|
|
// 마우스 업 이벤트
|
|
handleMouseUp(e) {
|
|
if (this.isEraser) {
|
|
this.drawing = false;
|
|
this.saveState();
|
|
return;
|
|
}
|
|
|
|
if (this.drawing && this.drawMode === 'line') {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
this.endLine(x, y);
|
|
} else if (this.drawing && this.drawMode === 'free') {
|
|
this.endFreeDraw();
|
|
}
|
|
}
|
|
|
|
// 마우스 아웃 이벤트
|
|
handleMouseOut(e) {
|
|
if (this.drawing && this.drawMode === 'free') {
|
|
this.endFreeDraw();
|
|
}
|
|
}
|
|
|
|
// 터치 이벤트 (모바일)
|
|
handleTouchStart(e) {
|
|
e.preventDefault();
|
|
const touch = e.touches[0];
|
|
const mouseEvent = new MouseEvent('mousedown', {
|
|
clientX: touch.clientX,
|
|
clientY: touch.clientY
|
|
});
|
|
this.canvas.dispatchEvent(mouseEvent);
|
|
}
|
|
|
|
handleTouchMove(e) {
|
|
e.preventDefault();
|
|
const touch = e.touches[0];
|
|
const mouseEvent = new MouseEvent('mousemove', {
|
|
clientX: touch.clientX,
|
|
clientY: touch.clientY
|
|
});
|
|
this.canvas.dispatchEvent(mouseEvent);
|
|
}
|
|
|
|
handleTouchEnd(e) {
|
|
e.preventDefault();
|
|
const mouseEvent = new MouseEvent('mouseup', {});
|
|
this.canvas.dispatchEvent(mouseEvent);
|
|
}
|
|
|
|
// Polyline 그리기
|
|
handlePolylineClick(x, y) {
|
|
if (!this.polyPrev) {
|
|
// 첫 번째 점
|
|
this.polyPrev = { x, y };
|
|
this.currentPath = [{ x, y }];
|
|
this.saveState(); // 시작 시점 상태 저장
|
|
} else {
|
|
// 연결선 그리기
|
|
let endX = x;
|
|
let endY = y;
|
|
|
|
if (this.straightMode) {
|
|
const angle = Math.atan2(y - this.polyPrev.y, x - this.polyPrev.x);
|
|
const angleDeg = Math.abs(angle * 180 / Math.PI);
|
|
|
|
if (angleDeg < 22.5 || angleDeg > 157.5) {
|
|
endY = this.polyPrev.y; // 수평선
|
|
} else if (angleDeg > 67.5 && angleDeg < 112.5) {
|
|
endX = this.polyPrev.x; // 수직선
|
|
}
|
|
}
|
|
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y);
|
|
this.ctx.lineTo(endX, endY);
|
|
this.ctx.stroke();
|
|
|
|
this.currentPath.push({ x: endX, y: endY });
|
|
this.polyPrev = { x: endX, y: endY };
|
|
this.saveState(); // 각 선분 그린 후 상태 저장
|
|
}
|
|
}
|
|
|
|
// Polyline 미리보기
|
|
previewPolyline(x, y) {
|
|
// 이전 상태 복원 (잔상 제거)
|
|
if (this.drawingData.length > 0) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.drawImage(img, 0, 0);
|
|
|
|
// 미리보기 선 그리기
|
|
let endX = x;
|
|
let endY = y;
|
|
|
|
if (this.straightMode) {
|
|
const angle = Math.atan2(y - this.polyPrev.y, x - this.polyPrev.x);
|
|
const angleDeg = Math.abs(angle * 180 / Math.PI);
|
|
|
|
if (angleDeg < 22.5 || angleDeg > 157.5) {
|
|
endY = this.polyPrev.y;
|
|
} else if (angleDeg > 67.5 && angleDeg < 112.5) {
|
|
endX = this.polyPrev.x;
|
|
}
|
|
}
|
|
|
|
this.ctx.save();
|
|
this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
|
|
this.ctx.setLineDash([5, 5]);
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y);
|
|
this.ctx.lineTo(endX, endY);
|
|
this.ctx.stroke();
|
|
this.ctx.restore();
|
|
};
|
|
img.src = this.drawingData[this.drawingData.length - 1];
|
|
}
|
|
}
|
|
|
|
// 직선 그리기
|
|
startLine(x, y) {
|
|
this.drawing = true;
|
|
this.startX = x;
|
|
this.startY = y;
|
|
this.saveState();
|
|
}
|
|
|
|
previewLine(x, y) {
|
|
// 이전 상태 복원 (미리보기용)
|
|
if (this.drawingData.length > 0) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.drawImage(img, 0, 0);
|
|
|
|
// 미리보기 선 그리기
|
|
let endX = x;
|
|
let endY = y;
|
|
|
|
if (this.straightMode) {
|
|
const angle = Math.atan2(y - this.startY, x - this.startX);
|
|
const angleDeg = Math.abs(angle * 180 / Math.PI);
|
|
|
|
if (angleDeg < 22.5 || angleDeg > 157.5) {
|
|
endY = this.startY;
|
|
} else if (angleDeg > 67.5 && angleDeg < 112.5) {
|
|
endX = this.startX;
|
|
}
|
|
}
|
|
|
|
this.ctx.save();
|
|
this.ctx.strokeStyle = this.drawColor;
|
|
this.ctx.lineWidth = this.lineWidth;
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(this.startX, this.startY);
|
|
this.ctx.lineTo(endX, endY);
|
|
this.ctx.stroke();
|
|
this.ctx.restore();
|
|
};
|
|
img.src = this.drawingData[this.drawingData.length - 1];
|
|
}
|
|
}
|
|
|
|
endLine(x, y) {
|
|
// 최종 선 그리기
|
|
let endX = x;
|
|
let endY = y;
|
|
|
|
if (this.straightMode) {
|
|
const angle = Math.atan2(y - this.startY, x - this.startX);
|
|
const angleDeg = Math.abs(angle * 180 / Math.PI);
|
|
|
|
if (angleDeg < 22.5 || angleDeg > 157.5) {
|
|
endY = this.startY;
|
|
} else if (angleDeg > 67.5 && angleDeg < 112.5) {
|
|
endX = this.startX;
|
|
}
|
|
}
|
|
|
|
// 먼저 이전 상태 복원
|
|
if (this.drawingData.length > 0) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.drawImage(img, 0, 0);
|
|
|
|
// 최종 선 그리기
|
|
this.ctx.save();
|
|
this.ctx.strokeStyle = this.drawColor;
|
|
this.ctx.lineWidth = this.lineWidth;
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(this.startX, this.startY);
|
|
this.ctx.lineTo(endX, endY);
|
|
this.ctx.stroke();
|
|
this.ctx.restore();
|
|
|
|
// 그린 후 상태 저장
|
|
this.drawing = false;
|
|
this.saveState();
|
|
};
|
|
img.src = this.drawingData[this.drawingData.length - 1];
|
|
} else {
|
|
// 저장된 상태가 없는 경우 직접 그리기
|
|
this.ctx.save();
|
|
this.ctx.strokeStyle = this.drawColor;
|
|
this.ctx.lineWidth = this.lineWidth;
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(this.startX, this.startY);
|
|
this.ctx.lineTo(endX, endY);
|
|
this.ctx.stroke();
|
|
this.ctx.restore();
|
|
|
|
this.drawing = false;
|
|
this.saveState();
|
|
}
|
|
}
|
|
|
|
// 자유 그리기
|
|
startFreeDraw(x, y) {
|
|
this.drawing = true;
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(x, y);
|
|
}
|
|
|
|
continueDraw(x, y) {
|
|
this.ctx.lineTo(x, y);
|
|
this.ctx.stroke();
|
|
}
|
|
|
|
endFreeDraw() {
|
|
this.drawing = false;
|
|
this.saveState();
|
|
}
|
|
|
|
// 지우개 기능
|
|
toggleEraser() {
|
|
this.isEraser = !this.isEraser;
|
|
const eraserBtn = document.getElementById('dmEraserBtn');
|
|
const eraserContainer = document.getElementById('dmEraserSizeContainer');
|
|
|
|
if (this.isEraser) {
|
|
eraserBtn.classList.add('dm-active');
|
|
eraserContainer.style.display = 'block';
|
|
this.canvas.style.cursor = 'none';
|
|
// 다른 그리기 모드 비활성화
|
|
this.textMode = false;
|
|
this.drawing = false;
|
|
this.polyPrev = null;
|
|
document.getElementById('dmTextBtn').classList.remove('dm-active');
|
|
// 지우개 커서 생성 (포커스 관련 문제 방지를 위해 마지막에)
|
|
this.createEraserCursor();
|
|
} else {
|
|
eraserBtn.classList.remove('dm-active');
|
|
eraserContainer.style.display = 'none';
|
|
// 텍스트 모드가 아니면 crosshair 커서로
|
|
this.canvas.style.cursor = this.textMode ? 'text' : 'crosshair';
|
|
this.removeEraserCursor();
|
|
}
|
|
}
|
|
|
|
eraseAt(x, y) {
|
|
this.ctx.save();
|
|
this.ctx.globalCompositeOperation = 'destination-out';
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(x, y, this.eraserRadius, 0, Math.PI * 2);
|
|
this.ctx.fill();
|
|
this.ctx.restore();
|
|
}
|
|
|
|
// 지우개 커서
|
|
createEraserCursor() {
|
|
// 기존 커서가 있으면 먼저 제거
|
|
this.removeEraserCursor();
|
|
|
|
let cursor = document.createElement('div');
|
|
cursor.id = 'dmEraserCursor';
|
|
cursor.className = 'dm-eraser-cursor';
|
|
cursor.style.width = `${this.eraserRadius * 2}px`;
|
|
cursor.style.height = `${this.eraserRadius * 2}px`;
|
|
cursor.style.pointerEvents = 'none'; // 커서가 마우스 이벤트를 방해하지 않도록
|
|
cursor.style.position = 'absolute';
|
|
cursor.style.display = 'none';
|
|
cursor.style.zIndex = '999'; // 커서가 충분히 위에 나타나도록
|
|
cursor.style.borderRadius = '50%'; // 원형 모양
|
|
this.modalElement.querySelector('.dm-canvas-area').appendChild(cursor);
|
|
|
|
// 이벤트 리스너 바인딩 저장 (중복 방지를 위해 기존 것 제거 후 추가)
|
|
this.eraserCursorMove = (e) => this.updateEraserCursor(e);
|
|
this.eraserCursorEnter = (e) => {
|
|
const cursor = document.getElementById('dmEraserCursor');
|
|
if (cursor && this.isEraser) {
|
|
this.updateEraserCursor(e); // 진입 시 위치 업데이트
|
|
cursor.style.display = 'block';
|
|
}
|
|
};
|
|
this.eraserCursorLeave = () => {
|
|
const cursor = document.getElementById('dmEraserCursor');
|
|
if (cursor) {
|
|
cursor.style.display = 'none';
|
|
}
|
|
};
|
|
|
|
this.canvas.addEventListener('mousemove', this.eraserCursorMove, { passive: true });
|
|
this.canvas.addEventListener('mouseenter', this.eraserCursorEnter, { passive: true });
|
|
this.canvas.addEventListener('mouseleave', this.eraserCursorLeave, { passive: true });
|
|
}
|
|
|
|
updateEraserCursor(e) {
|
|
const cursor = document.getElementById('dmEraserCursor');
|
|
if (!cursor || !this.isEraser) return;
|
|
|
|
const canvasRect = this.canvas.getBoundingClientRect();
|
|
const canvasAreaRect = this.modalElement.querySelector('.dm-canvas-area').getBoundingClientRect();
|
|
|
|
// 캔버스 영역 상대 좌표 계산
|
|
const canvasX = e.clientX - canvasRect.left;
|
|
const canvasY = e.clientY - canvasRect.top;
|
|
|
|
// 캔버스 영역 대비 좌표 계산 (커서 위치용)
|
|
const areaX = e.clientX - canvasAreaRect.left;
|
|
const areaY = e.clientY - canvasAreaRect.top;
|
|
|
|
// 커서 크기 업데이트
|
|
cursor.style.width = `${this.eraserRadius * 2}px`;
|
|
cursor.style.height = `${this.eraserRadius * 2}px`;
|
|
|
|
// 캔버스 내부에 있을 때만 커서 표시
|
|
if (canvasX >= 0 && canvasX <= this.canvas.width && canvasY >= 0 && canvasY <= this.canvas.height) {
|
|
cursor.style.left = `${areaX - this.eraserRadius}px`;
|
|
cursor.style.top = `${areaY - this.eraserRadius}px`;
|
|
cursor.style.display = 'block';
|
|
} else {
|
|
cursor.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
removeEraserCursor() {
|
|
// 이벤트 리스너 제거
|
|
if (this.canvas) {
|
|
if (this.eraserCursorMove) {
|
|
this.canvas.removeEventListener('mousemove', this.eraserCursorMove);
|
|
}
|
|
if (this.eraserCursorEnter) {
|
|
this.canvas.removeEventListener('mouseenter', this.eraserCursorEnter);
|
|
}
|
|
if (this.eraserCursorLeave) {
|
|
this.canvas.removeEventListener('mouseleave', this.eraserCursorLeave);
|
|
}
|
|
}
|
|
|
|
// 커서 엘리먼트 제거
|
|
const cursor = document.getElementById('dmEraserCursor');
|
|
if (cursor) {
|
|
cursor.remove();
|
|
}
|
|
}
|
|
|
|
// 텍스트 추가
|
|
addText(x, y) {
|
|
const text = prompt('텍스트를 입력하세요:');
|
|
if (text) {
|
|
this.ctx.save();
|
|
this.ctx.font = '14px Arial';
|
|
this.ctx.fillStyle = this.drawColor;
|
|
this.ctx.fillText(text, x, y);
|
|
this.ctx.restore();
|
|
this.saveState();
|
|
}
|
|
// 텍스트 모드 유지 (사용자가 명시적으로 다른 모드를 선택할 때까지)
|
|
// this.textMode = false; // 이 줄을 제거하여 텍스트 모드 유지
|
|
// document.getElementById('dmTextBtn').classList.remove('dm-active'); // 버튼 활성 상태 유지
|
|
this.canvas.style.cursor = 'text'; // 텍스트 커서 유지
|
|
}
|
|
|
|
// 상태 저장/복원
|
|
saveState() {
|
|
const imageData = this.canvas.toDataURL();
|
|
this.drawingData.push(imageData);
|
|
if (this.drawingData.length > 20) {
|
|
this.drawingData.shift();
|
|
}
|
|
}
|
|
|
|
restoreState() {
|
|
if (this.drawingData.length > 0) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.drawImage(img, 0, 0);
|
|
};
|
|
img.src = this.drawingData[this.drawingData.length - 1];
|
|
}
|
|
}
|
|
|
|
redrawCanvas() {
|
|
if (this.drawingData.length > 0) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.drawImage(img, 0, 0);
|
|
};
|
|
img.src = this.drawingData[this.drawingData.length - 1];
|
|
}
|
|
}
|
|
|
|
// 실행취소
|
|
undo() {
|
|
if (this.drawingData.length > 1) {
|
|
this.drawingData.pop();
|
|
this.restoreState();
|
|
} else if (this.drawingData.length === 1) {
|
|
this.drawingData.pop();
|
|
this.clear();
|
|
}
|
|
}
|
|
|
|
// 초기화
|
|
clear() {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.fillStyle = 'white';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.drawingData = [];
|
|
this.resetDrawingState();
|
|
}
|
|
|
|
// 그리기 상태 초기화
|
|
resetDrawingState() {
|
|
this.drawing = false;
|
|
this.polyPrev = null;
|
|
this.currentPath = [];
|
|
this.isEraser = false;
|
|
this.textMode = false;
|
|
|
|
document.getElementById('dmEraserBtn').classList.remove('dm-active');
|
|
document.getElementById('dmTextBtn').classList.remove('dm-active');
|
|
document.getElementById('dmEraserSizeContainer').style.display = 'none';
|
|
this.canvas.style.cursor = 'crosshair';
|
|
this.removeEraserCursor();
|
|
|
|
// 잔상 제거를 위한 캔버스 다시 그리기
|
|
if (this.drawingData.length > 0) {
|
|
this.restoreState();
|
|
}
|
|
}
|
|
|
|
// Polyline 취소 (ESC 키)
|
|
cancelPolyline() {
|
|
this.polyPrev = null;
|
|
this.currentPath = [];
|
|
|
|
// 마지막 저장 상태로 복원
|
|
if (this.drawingData.length > 0) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.drawImage(img, 0, 0);
|
|
};
|
|
img.src = this.drawingData[this.drawingData.length - 1];
|
|
} else {
|
|
// 저장된 상태가 없으면 캔버스 초기화
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.fillStyle = 'white';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
}
|
|
|
|
console.log('Polyline 그리기 취소됨');
|
|
}
|
|
|
|
// 이미지 로드
|
|
loadImage(imageSrc) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
// 캔버스 크기에 맞게 이미지 조정
|
|
const scale = Math.min(
|
|
this.canvas.width / img.width,
|
|
this.canvas.height / img.height
|
|
);
|
|
const width = img.width * scale;
|
|
const height = img.height * scale;
|
|
const x = (this.canvas.width - width) / 2;
|
|
const y = (this.canvas.height - height) / 2;
|
|
|
|
this.ctx.drawImage(img, x, y, width, height);
|
|
this.originalImage = this.canvas.toDataURL();
|
|
this.saveState();
|
|
};
|
|
img.src = imageSrc;
|
|
}
|
|
|
|
// 저장
|
|
save() {
|
|
// JPG로 저장하기 위해 흰색 배경이 있는 임시 캔버스 생성
|
|
const tempCanvas = document.createElement('canvas');
|
|
tempCanvas.width = this.canvas.width;
|
|
tempCanvas.height = this.canvas.height;
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
|
|
// 흰색 배경 먼저 그리기
|
|
tempCtx.fillStyle = '#FFFFFF';
|
|
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
|
|
|
|
// 기존 캔버스 내용을 위에 그리기
|
|
tempCtx.drawImage(this.canvas, 0, 0);
|
|
|
|
// JPG 형식으로 데이터 생성 (품질 0.9)
|
|
const imageData = tempCanvas.toDataURL('image/jpeg', 0.9);
|
|
|
|
if (this.options.onSave) {
|
|
this.options.onSave({
|
|
imageData: imageData,
|
|
timestamp: new Date().toISOString(),
|
|
width: this.canvas.width,
|
|
height: this.canvas.height,
|
|
format: 'jpeg'
|
|
});
|
|
}
|
|
|
|
this.close();
|
|
}
|
|
|
|
// 닫기
|
|
close() {
|
|
if (this.options.onCancel && !this.saved) {
|
|
this.options.onCancel();
|
|
}
|
|
|
|
// ESC 키 이벤트 리스너 제거
|
|
if (this.handleEscKey) {
|
|
document.removeEventListener('keydown', this.handleEscKey);
|
|
}
|
|
|
|
this.removeEraserCursor();
|
|
this.modalElement.remove();
|
|
this.modalElement = null;
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
}
|
|
|
|
// 이미지 데이터 가져오기 (JPG 형식)
|
|
getImageData(format = 'jpeg', quality = 0.9) {
|
|
if (format === 'jpeg') {
|
|
// JPG의 경우 흰색 배경 추가
|
|
const tempCanvas = document.createElement('canvas');
|
|
tempCanvas.width = this.canvas.width;
|
|
tempCanvas.height = this.canvas.height;
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
|
|
// 흰색 배경 먼저 그리기
|
|
tempCtx.fillStyle = '#FFFFFF';
|
|
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
|
|
|
|
// 기존 캔버스 내용을 위에 그리기
|
|
tempCtx.drawImage(this.canvas, 0, 0);
|
|
|
|
return tempCanvas.toDataURL('image/jpeg', quality);
|
|
} else {
|
|
// PNG는 기존 방식
|
|
return this.canvas.toDataURL('image/png');
|
|
}
|
|
}
|
|
|
|
// 이미지 다운로드 (기본 JPG)
|
|
download(filename = 'drawing.jpg', format = 'jpeg', quality = 0.9) {
|
|
const link = document.createElement('a');
|
|
link.download = filename;
|
|
|
|
if (format === 'jpeg') {
|
|
// JPG의 경우 흰색 배경 추가
|
|
const tempCanvas = document.createElement('canvas');
|
|
tempCanvas.width = this.canvas.width;
|
|
tempCanvas.height = this.canvas.height;
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
|
|
// 흰색 배경 먼저 그리기
|
|
tempCtx.fillStyle = '#FFFFFF';
|
|
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
|
|
|
|
// 기존 캔버스 내용을 위에 그리기
|
|
tempCtx.drawImage(this.canvas, 0, 0);
|
|
|
|
link.href = tempCanvas.toDataURL('image/jpeg', quality);
|
|
} else {
|
|
// PNG는 기존 방식
|
|
link.href = this.canvas.toDataURL('image/png');
|
|
}
|
|
|
|
link.click();
|
|
}
|
|
}
|
|
|
|
// 전역 네임스페이스에 등록
|
|
window.DrawingModule = DrawingModule; |