초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
This commit is contained in:
967
js/drawingModule.js
Normal file
967
js/drawingModule.js
Normal file
@@ -0,0 +1,967 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user