From 39afc25aba70e25ec987d52b401f263abffba2f5 Mon Sep 17 00:00:00 2001 From: "mrkad@rpi" Date: Sat, 17 Jan 2026 13:20:17 +0700 Subject: [PATCH] feat: enhance photo capturing and uploading features with auto-capture and image cropping --- src/views/PickupView.vue | 128 ++++++++++++++++++++++----- src/views/PrintStepView.vue | 7 +- src/views/ShootingView.vue | 166 ++++++++++++++++++++++++++++++------ src/views/UploadView.vue | 71 +++++++++++++-- vite.config.ts | 3 + 5 files changed, 322 insertions(+), 53 deletions(-) diff --git a/src/views/PickupView.vue b/src/views/PickupView.vue index 0129502..36e054d 100644 --- a/src/views/PickupView.vue +++ b/src/views/PickupView.vue @@ -37,14 +37,16 @@ const getGridStyle = computed(() => { display: 'grid', gridTemplateColumns: '1fr', gridTemplateRows: 'repeat(4, 1fr)', - gap: '8px' + gap: '8px', + aspectRatio: '1/2' // ทำให้แต่ละภาพยาวขึ้น (1:2 aspect ratio รวม) } } else { return { display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateRows: 'repeat(2, 1fr)', - gap: '8px' + gap: '8px', + aspectRatio: '1/1' // 2x2 เป็น square } } }) @@ -67,24 +69,113 @@ const generateShareUrl = () => { qrCodeUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(shareUrl.value)}` } -const downloadImage = () => { - // ในโปรเจกต์จริงควรสร้าง canvas และ download เป็นไฟล์ภาพ - // ตัวอย่างนี้ download เป็น JSON ชั่วคราว - const data = { - photos: photos.value, - layout: layout.value, - frame: frame.value +const downloadImage = async () => { + if (photos.value.length === 0) return + + // สร้าง canvas สำหรับรวมรูป + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d') + + if (!context) return + + // ขนาดรูปแต่ละภาพ (720x960 = 3:4) + const photoWidth = 720 + const photoHeight = 960 + const gap = 20 // ระยะห่างระหว่างรูป + + let canvasWidth, canvasHeight + + if (layout.value === '1x4') { + // 1x4: 1 คอลัมน์ 4 แถว + canvasWidth = photoWidth + canvasHeight = (photoHeight * 4) + (gap * 3) + } else { + // 2x2: 2 คอลัมน์ 2 แถว + canvasWidth = (photoWidth * 2) + gap + canvasHeight = (photoHeight * 2) + gap } - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `photobooth-${Date.now()}.json` - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) + canvas.width = canvasWidth + canvas.height = canvasHeight + + // วาด background ตาม frame + drawFrameBackground(context, canvasWidth, canvasHeight, frame.value) + + // วาดรูปภาพ + for (let i = 0; i < photos.value.length; i++) { + const img = new Image() + img.src = photos.value[i] + + await new Promise((resolve) => { + img.onload = () => { + let x, y + + if (layout.value === '1x4') { + // 1x4 layout + x = 0 + y = i * (photoHeight + gap) + } else { + // 2x2 layout + const col = i % 2 + const row = Math.floor(i / 2) + x = col * (photoWidth + gap) + y = row * (photoHeight + gap) + } + + // วาดรูปภาพ + context.drawImage(img, x, y, photoWidth, photoHeight) + resolve(void 0) + } + }) + } + + // สร้าง PNG และ download + canvas.toBlob((blob) => { + if (blob) { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `photobooth-${Date.now()}.png` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + }, 'image/png') +} + +const drawFrameBackground = (context: CanvasRenderingContext2D, width: number, height: number, frameType: number) => { + // วาด background ตาม frame type + switch (frameType) { + case 0: // Classic - white + context.fillStyle = '#ffffff' + break + case 1: // Modern - gradient + const gradient1 = context.createLinearGradient(0, 0, width, height) + gradient1.addColorStop(0, '#667eea') + gradient1.addColorStop(1, '#764ba2') + context.fillStyle = gradient1 + break + case 2: // Vintage - gradient + const gradient2 = context.createLinearGradient(0, 0, width, height) + gradient2.addColorStop(0, '#f093fb') + gradient2.addColorStop(1, '#f5576c') + context.fillStyle = gradient2 + break + case 3: // Colorful - gradient + const gradient3 = context.createLinearGradient(0, 0, width, height) + gradient3.addColorStop(0, '#4facfe') + gradient3.addColorStop(1, '#00f2fe') + context.fillStyle = gradient3 + break + case 4: // Minimal - light gray + context.fillStyle = '#f8f9fa' + break + default: + context.fillStyle = '#ffffff' + } + + context.fillRect(0, 0, width, height) } const shareImage = async () => { @@ -223,7 +314,6 @@ const startOver = () => { background: #f8f9fa; border-radius: 8px; padding: 12px; - aspect-ratio: 3/4; } .photo-cell { diff --git a/src/views/PrintStepView.vue b/src/views/PrintStepView.vue index 719523d..ed1365b 100644 --- a/src/views/PrintStepView.vue +++ b/src/views/PrintStepView.vue @@ -52,14 +52,16 @@ const getGridStyle = () => { display: 'grid', gridTemplateColumns: '1fr', gridTemplateRows: 'repeat(4, 1fr)', - gap: '8px' + gap: '8px', + aspectRatio: '1/5' // ทำให้แต่ละภาพยาวขึ้น (1:2 aspect ratio รวม) } } else { return { display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateRows: 'repeat(2, 1fr)', - gap: '8px' + gap: '8px', + aspectRatio: '1/1' // 2x2 เป็น square } } } @@ -194,7 +196,6 @@ const getGridStyle = () => { background: #f8f9fa; border-radius: 8px; padding: 12px; - aspect-ratio: 3/4; } .photo-cell { diff --git a/src/views/ShootingView.vue b/src/views/ShootingView.vue index 0e96335..5939610 100644 --- a/src/views/ShootingView.vue +++ b/src/views/ShootingView.vue @@ -8,6 +8,8 @@ const canvasRef = ref() const photos = ref([]) const currentPhotoIndex = ref(0) const isCapturing = ref(false) +const isAutoCapturing = ref(false) +const countdown = ref(0) const stream = ref(null) const totalPhotos = 4 @@ -25,8 +27,8 @@ const startCamera = async () => { const constraints = { video: { facingMode: 'user', - width: { ideal: 1280 }, - height: { ideal: 720 } + width: { ideal: 720 }, + height: { ideal: 960 } } } @@ -47,7 +49,7 @@ const stopCamera = () => { } } -const capturePhoto = () => { +const captureSinglePhoto = () => { if (!videoRef.value || !canvasRef.value || isCapturing.value) return isCapturing.value = true @@ -58,26 +60,88 @@ const capturePhoto = () => { if (!context) return - // ตั้งค่าขนาด canvas ให้ตรงกับ video - canvas.width = video.videoWidth - canvas.height = video.videoHeight + const videoWidth = video.videoWidth + const videoHeight = video.videoHeight - // วาดภาพจาก video ไปยัง canvas - context.drawImage(video, 0, 0, canvas.width, canvas.height) + // คำนวณขนาดสำหรับ 3:4 aspect ratio + const targetAspectRatio = 3/4 // width:height = 3:4 + let cropWidth, cropHeight, cropX, cropY + + if (videoWidth / videoHeight > targetAspectRatio) { + // ภาพกว้างกว่าที่ต้องการ - ครอบด้านข้าง + cropHeight = videoHeight + cropWidth = videoHeight * targetAspectRatio + cropX = (videoWidth - cropWidth) / 2 + cropY = 0 + } else { + // ภาพสูงกว่าที่ต้องการ - ครอบด้านบน/ล่าง + cropWidth = videoWidth + cropHeight = videoWidth / targetAspectRatio + cropX = 0 + cropY = (videoHeight - cropHeight) / 2 + } + + // ตั้งค่าขนาด canvas เป็น 3:4 (720x960) + const finalWidth = 720 + const finalHeight = 960 + + canvas.width = finalWidth + canvas.height = finalHeight + + // วาดภาพที่ครอบแล้วไปยัง canvas + context.drawImage( + video, + cropX, cropY, cropWidth, cropHeight, // ตำแหน่งและขนาดที่ครอบจาก video + 0, 0, finalWidth, finalHeight // ตำแหน่งและขนาดใน canvas + ) // แปลงเป็น base64 const photoDataUrl = canvas.toDataURL('image/jpeg', 0.9) photos.value.push(photoDataUrl) currentPhotoIndex.value++ - // ถ่ายครบ 4 รูปแล้ว ไปหน้าถัดไป - if (currentPhotoIndex.value >= totalPhotos) { - proceedToNext() - } - setTimeout(() => { isCapturing.value = false - }, 500) + }, 200) +} + +const startAutoCapture = () => { + if (isAutoCapturing.value) return + + isAutoCapturing.value = true + photos.value = [] + currentPhotoIndex.value = 0 + + const captureNextPhoto = () => { + if (currentPhotoIndex.value >= totalPhotos) { + // ถ่ายครบแล้ว ไปหน้าถัดไป + isAutoCapturing.value = false + proceedToNext() + return + } + + // นับถอยหลังสำหรับรูปปัจจุบัน + countdown.value = 3 + + const countdownInterval = setInterval(() => { + countdown.value-- + if (countdown.value <= 0) { + clearInterval(countdownInterval) + countdown.value = 0 + + // ถ่ายภาพ + captureSinglePhoto() + + // ถ่ายรูปต่อไปหลังจาก delay สั้นๆ + setTimeout(() => { + captureNextPhoto() + }, 500) + } + }, 1000) + } + + // เริ่มถ่ายรูปแรก + captureNextPhoto() } const proceedToNext = () => { @@ -95,6 +159,8 @@ const goBack = () => { const retakePhoto = () => { photos.value = [] currentPhotoIndex.value = 0 + isAutoCapturing.value = false + countdown.value = 0 } @@ -121,22 +187,29 @@ const retakePhoto = () => {
+ + +
+
รูปที่ {{ currentPhotoIndex + 1 }}
+
{{ countdown }}
+