/**
* QRCropSelector.js
* คลาสสำหรับจัดการการเลือกพื้นที่ QR Code ในรูปภาพ
* รองรับการลาก ปรับขนาด และบันทึกตำแหน่ง
*/
class QRCropSelector {
constructor(imageElement, selectionElement) {
this.image = imageElement;
this.selection = selectionElement;
this.cropBox = selectionElement.querySelector('.qr-selection-crop');
this.handles = selectionElement.querySelectorAll('.crop-handle');
this.isDragging = false;
this.isResizing = false;
this.currentHandle = null;
this.startPos = {x: 0, y: 0};
this.startBox = {x: 0, y: 0, width: 0, height: 0};
this.minSize = 50; // ขนาดต่ำสุดของกรอบ
this.savedPositions = this.loadSavedPositions();
this.init();
}
/**
* เริ่มต้นระบบ
*/
init() {
this.bindEvents();
this.loadLastPosition();
}
/**
* ผูก Event Listeners
*/
bindEvents() {
// Crop box dragging
this.cropBox.addEventListener('mousedown', this.startDrag.bind(this));
this.cropBox.addEventListener('touchstart', this.startDrag.bind(this));
// Handle resizing
this.handles.forEach(handle => {
handle.addEventListener('mousedown', this.startResize.bind(this));
handle.addEventListener('touchstart', this.startResize.bind(this));
});
// Global mouse/touch events
document.addEventListener('mousemove', this.onMove.bind(this));
document.addEventListener('touchmove', this.onMove.bind(this));
document.addEventListener('mouseup', this.endAction.bind(this));
document.addEventListener('touchend', this.endAction.bind(this));
// Prevent context menu
this.selection.addEventListener('contextmenu', e => e.preventDefault());
}
/**
* เริ่มการลากกรอบ
*/
startDrag(e) {
if (e.target.classList.contains('crop-handle')) return;
e.preventDefault();
this.isDragging = true;
const pos = this.getEventPos(e);
const rect = this.image.getBoundingClientRect();
this.startPos = {
x: pos.x - rect.left,
y: pos.y - rect.top
};
const cropRect = this.cropBox.getBoundingClientRect();
this.startBox = {
x: cropRect.left - rect.left,
y: cropRect.top - rect.top,
width: cropRect.width,
height: cropRect.height
};
this.cropBox.style.cursor = 'grabbing';
}
/**
* เริ่มการปรับขนาด
*/
startResize(e) {
e.preventDefault();
e.stopPropagation();
this.isResizing = true;
this.currentHandle = e.target;
const pos = this.getEventPos(e);
const rect = this.image.getBoundingClientRect();
this.startPos = {
x: pos.x - rect.left,
y: pos.y - rect.top
};
const cropRect = this.cropBox.getBoundingClientRect();
this.startBox = {
x: cropRect.left - rect.left,
y: cropRect.top - rect.top,
width: cropRect.width,
height: cropRect.height
};
}
/**
* จัดการการเคลื่อนไหว
*/
onMove(e) {
if (!this.isDragging && !this.isResizing) return;
e.preventDefault();
const pos = this.getEventPos(e);
const rect = this.image.getBoundingClientRect();
const currentPos = {
x: pos.x - rect.left,
y: pos.y - rect.top
};
if (this.isDragging) {
this.handleDrag(currentPos);
} else if (this.isResizing) {
this.handleResize(currentPos);
}
}
/**
* จัดการการลาก
*/
handleDrag(currentPos) {
const deltaX = currentPos.x - this.startPos.x;
const deltaY = currentPos.y - this.startPos.y;
let newX = this.startBox.x + deltaX;
let newY = this.startBox.y + deltaY;
// จำกัดขอบเขต
const imageRect = this.image.getBoundingClientRect();
newX = Math.max(0, Math.min(newX, imageRect.width - this.startBox.width));
newY = Math.max(0, Math.min(newY, imageRect.height - this.startBox.height));
this.updateCropBox(newX, newY, this.startBox.width, this.startBox.height);
}
/**
* จัดการการปรับขนาด
*/
handleResize(currentPos) {
const deltaX = currentPos.x - this.startPos.x;
const deltaY = currentPos.y - this.startPos.y;
let newX = this.startBox.x;
let newY = this.startBox.y;
let newWidth = this.startBox.width;
let newHeight = this.startBox.height;
const handleClass = this.currentHandle.className;
if (handleClass.includes('top-left')) {
newX = this.startBox.x + deltaX;
newY = this.startBox.y + deltaY;
newWidth = this.startBox.width - deltaX;
newHeight = this.startBox.height - deltaY;
} else if (handleClass.includes('top-right')) {
newY = this.startBox.y + deltaY;
newWidth = this.startBox.width + deltaX;
newHeight = this.startBox.height - deltaY;
} else if (handleClass.includes('bottom-left')) {
newX = this.startBox.x + deltaX;
newWidth = this.startBox.width - deltaX;
newHeight = this.startBox.height + deltaY;
} else if (handleClass.includes('bottom-right')) {
newWidth = this.startBox.width + deltaX;
newHeight = this.startBox.height + deltaY;
}
// จำกัดขนาดและขอบเขต
const imageRect = this.image.getBoundingClientRect();
newWidth = Math.max(this.minSize, newWidth);
newHeight = Math.max(this.minSize, newHeight);
newX = Math.max(0, Math.min(newX, imageRect.width - newWidth));
newY = Math.max(0, Math.min(newY, imageRect.height - newHeight));
if (newX + newWidth > imageRect.width) {
newWidth = imageRect.width - newX;
}
if (newY + newHeight > imageRect.height) {
newHeight = imageRect.height - newY;
}
this.updateCropBox(newX, newY, newWidth, newHeight);
}
/**
* อัปเดตตำแหน่งกรอบ
*/
updateCropBox(x, y, width, height) {
const imageRect = this.image.getBoundingClientRect();
// แปลงเป็น percentage
const percentX = (x / imageRect.width) * 100;
const percentY = (y / imageRect.height) * 100;
const percentWidth = (width / imageRect.width) * 100;
const percentHeight = (height / imageRect.height) * 100;
this.cropBox.style.left = `${percentX}%`;
this.cropBox.style.top = `${percentY}%`;
this.cropBox.style.width = `${percentWidth}%`;
this.cropBox.style.height = `${percentHeight}%`;
}
/**
* จบการกระทำ
*/
endAction(e) {
this.isDragging = false;
this.isResizing = false;
this.currentHandle = null;
this.cropBox.style.cursor = 'move';
}
/**
* ดึงตำแหน่งจาก event
*/
getEventPos(e) {
if (e.touches && e.touches[0]) {
return {x: e.touches[0].clientX, y: e.touches[0].clientY};
}
return {x: e.clientX, y: e.clientY};
}
/**
* ดึงขอบเขตที่เลือก
*/
getCropBounds() {
const imageRect = this.image.getBoundingClientRect();
const cropRect = this.cropBox.getBoundingClientRect();
// แปลงเป็น pixel coordinates
const x = cropRect.left - imageRect.left;
const y = cropRect.top - imageRect.top;
const width = cropRect.width;
const height = cropRect.height;
// แปลงเป็น percentage สำหรับการบันทึก
const percentBounds = {
x: (x / imageRect.width) * 100,
y: (y / imageRect.height) * 100,
width: (width / imageRect.width) * 100,
height: (height / imageRect.height) * 100
};
return {
pixel: {x, y, width, height},
percent: percentBounds,
imageSize: {
width: imageRect.width,
height: imageRect.height
}
};
}
/**
* ตั้งตำแหน่งกรอบ
*/
setCropBounds(bounds) {
this.updateCropBox(
bounds.x,
bounds.y,
bounds.width,
bounds.height
);
}
/**
* รีเซ็ตตำแหน่งเป็นค่าเริ่มต้น
*/
resetSelection() {
this.updateCropBox(
this.image.offsetWidth * 0.2,
this.image.offsetHeight * 0.2,
this.image.offsetWidth * 0.6,
this.image.offsetHeight * 0.6
);
}
/**
* บันทึกตำแหน่งปัจจุบัน
*/
saveCurrentPosition(key = 'default') {
const bounds = this.getCropBounds();
this.savedPositions[key] = bounds.percent;
localStorage.setItem('qrCropPositions', JSON.stringify(this.savedPositions));
}
/**
* โหลดตำแหน่งที่บันทึก
*/
loadSavedPositions() {
try {
const saved = localStorage.getItem('qrCropPositions');
return saved ? JSON.parse(saved) : {};
} catch (error) {
console.error('Error loading saved positions:', error);
return {};
}
}
/**
* โหลดตำแหน่งล่าสุด
*/
loadLastPosition(key = 'default') {
if (this.savedPositions[key]) {
const bounds = this.savedPositions[key];
const imageRect = this.image.getBoundingClientRect();
this.updateCropBox(
(bounds.x / 100) * imageRect.width,
(bounds.y / 100) * imageRect.height,
(bounds.width / 100) * imageRect.width,
(bounds.height / 100) * imageRect.height
);
} else {
this.resetSelection();
}
}
/**
* ทำลายทรัพยากร
*/
destroy() {
document.removeEventListener('mousemove', this.onMove.bind(this));
document.removeEventListener('touchmove', this.onMove.bind(this));
document.removeEventListener('mouseup', this.endAction.bind(this));
document.removeEventListener('touchend', this.endAction.bind(this));
}
}