Files
photobooth/src/views/PrintStepView.vue

552 lines
14 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const countdown = ref(3)
const showResult = ref(false)
const photos = ref<string[]>([])
const layout = ref<'1x4' | '2x2'>('1x4')
const frame = ref<number>(0)
const finalImage = ref<string | null>(null)
onMounted(() => {
// โหลดข้อมูลจาก localStorage
const savedPhotos = localStorage.getItem('photobooth-photos')
const savedLayout = localStorage.getItem('photobooth-layout')
const savedFrame = localStorage.getItem('photobooth-frame')
if (savedPhotos) {
photos.value = JSON.parse(savedPhotos)
}
if (savedLayout) {
layout.value = savedLayout as '1x4' | '2x2'
}
if (savedFrame) {
frame.value = parseInt(savedFrame)
}
// เริ่มนับถอยหลัง
startCountdown()
// Load the final image from localStorage
finalImage.value = localStorage.getItem('photobooth-final-image');
})
const startCountdown = () => {
const timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
showResult.value = true
// หลังจากแสดงผล 3 วินาที ให้ไปหน้า pickup
setTimeout(() => {
// router.push('/pickup')
}, 3000)
}
}, 1000)
}
const getGridStyle = () => {
if (layout.value === '1x4') {
return {
display: 'grid',
gridTemplateColumns: '1fr',
gridTemplateRows: 'repeat(4, 1fr)',
gap: '8px',
aspectRatio: '1/1' // ทำให้แต่ละภาพยาวขึ้น (1:2 aspect ratio รวม)
}
} else {
return {
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gridTemplateRows: 'repeat(2, 1fr)',
gap: '8px',
aspectRatio: '1/1' // 2x2 เป็น square
}
}
}
const saveToLocalStorage = (key: string, data: any) => {
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
};
const drawFrameBackground = (context: CanvasRenderingContext2D, width: number, height: number, frameType: number) => {
context.clearRect(0, 0, width, height);
switch (frameType) {
case 0: // Classic frame
context.fillStyle = '#000000';
context.fillRect(0, 0, width, height);
break;
case 1: // Modern frame
const gradient1 = context.createLinearGradient(0, 0, width, height);
gradient1.addColorStop(0, '#667eea');
gradient1.addColorStop(1, '#764ba2');
context.fillStyle = gradient1;
context.fillRect(0, 0, width, height);
break;
case 2: // Vintage frame
const gradient2 = context.createLinearGradient(0, 0, width, height);
gradient2.addColorStop(0, '#f093fb');
gradient2.addColorStop(1, '#f5576c');
context.fillStyle = gradient2;
context.fillRect(0, 0, width, height);
break;
case 3: // Colorful frame
const gradient3 = context.createLinearGradient(0, 0, width, height);
gradient3.addColorStop(0, '#4facfe');
gradient3.addColorStop(1, '#00f2fe');
context.fillStyle = gradient3;
context.fillRect(0, 0, width, height);
break;
case 4: // Minimal frame
context.fillStyle = '#f8f9fa';
context.fillRect(0, 0, width, height);
context.strokeStyle = '#dee2e6';
context.lineWidth = 4;
context.strokeRect(0, 0, width, height);
break;
default:
console.warn('Unknown frame type:', frameType);
context.fillStyle = '#ffffff';
context.fillRect(0, 0, width, height);
}
};
const drawHeaderFooterImage = async (context: CanvasRenderingContext2D, imagePath: string, x: number, y: number, width: number, height: number) => {
return new Promise<void>((resolve) => {
const img = new Image()
img.onload = () => {
context.drawImage(img, x, y, width, height)
resolve()
}
img.onerror = () => {
console.warn(`Failed to load header/footer image: ${imagePath}, drawing placeholder`)
// วาด placeholder color แทน
if (imagePath.includes('header-1x4')) {
context.fillStyle = '#007bff' // น้ำเงินสำหรับ 1x4 header
} else if (imagePath.includes('footer-1x4')) {
context.fillStyle = '#dc3545' // แดงสำหรับ 1x4 footer
} else if (imagePath.includes('header-2x2')) {
context.fillStyle = '#28a745' // เขียวสำหรับ 2x2 header
} else if (imagePath.includes('footer-2x2')) {
context.fillStyle = '#ffc107' // เหลืองสำหรับ 2x2 footer
}
context.fillRect(x, y, width, height)
resolve()
}
img.src = imagePath
})
}
const generateAndCacheImage = async () => {
try {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Could not get canvas context')
}
// ขนาดรูปแต่ละภาพ (360x480 = 3:4) - ลดขนาดลงเพื่อป้องกันปัญหา canvas ใหญ่เกินไป
const photoWidth = 360
const photoHeight = 480
const gap = 20 // ระยะห่างระหว่างรูป
let canvasWidth, canvasHeight
let headerHeight = 0, footerHeight = 0
let sidePadding = 0
if (layout.value === '1x4') {
// 1x4: 1 คอลัมน์ 4 แถว + header/footer
headerHeight = 50
footerHeight = 50
sidePadding = 30 // padding ซ้ายขวาเพื่อให้ total width = 420
canvasWidth = 420
canvasHeight = headerHeight + (photoHeight * 4) + (gap * 3) + footerHeight
} else {
// 2x2: 2 คอลัมน์ 2 แถว + header/footer
headerHeight = 60
footerHeight = 60
sidePadding = 20 // padding ซ้ายขวาเพื่อให้ total width = 780
canvasWidth = 780
canvasHeight = headerHeight + (photoHeight * 2) + gap + footerHeight
}
console.log('Canvas dimensions:', canvasWidth, 'x', canvasHeight)
canvas.width = canvasWidth
canvas.height = canvasHeight
// วาด background ตาม frame
drawFrameBackground(context, canvasWidth, canvasHeight, frame.value)
// วาด header image
const headerImagePath = layout.value === '1x4' ? '/assets/header-1x4.png' : '/assets/header-2x2.png'
await drawHeaderFooterImage(context, headerImagePath, 0, 0, canvasWidth, headerHeight)
// วาด footer image
const footerImagePath = layout.value === '1x4' ? '/assets/footer-1x4.png' : '/assets/footer-2x2.png'
await drawHeaderFooterImage(context, footerImagePath, 0, canvasHeight - footerHeight, canvasWidth, footerHeight)
// วาดรูปภาพ (มี padding ซ้ายขวา)
for (let i = 0; i < photos.value.length; i++) {
const photoSrc = photos.value[i]
if (!photoSrc) continue
const img = new Image()
img.src = photoSrc
await new Promise((resolve) => {
img.onload = () => {
let x, y
if (layout.value === '1x4') {
// 1x4 layout - วาดรูปตรงกลาง (มี padding ซ้ายขวา)
x = sidePadding
y = headerHeight + i * (photoHeight + gap)
} else {
// 2x2 layout - วาดรูปตรงกลาง (มี padding ซ้ายขวา)
const col = i % 2
const row = Math.floor(i / 2)
x = sidePadding + col * (photoWidth + gap)
y = headerHeight + row * (photoHeight + gap)
}
// วาดรูปภาพ
context.drawImage(img, x, y, photoWidth, photoHeight)
resolve(void 0)
}
img.onerror = () => {
console.error(`Failed to load image ${i}`)
resolve(void 0)
}
})
}
// สร้าง PNG data URL และบันทึกใน localStorage
const dataUrl = canvas.toDataURL('image/png')
try {
saveToLocalStorage('photobooth-final-image', dataUrl);
finalImage.value = dataUrl;
} catch (error) {
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
console.error('localStorage quota exceeded for cached image')
alert('ไม่สามารถบันทึกภาพได้เนื่องจากพื้นที่เก็บข้อมูลไม่เพียงพอ')
} else {
console.error('Error saving cached image:', error)
throw error
}
}
} catch (error) {
console.error('Error in generateAndCacheImage:', error)
}
}
const compressImage = (dataUrl: string, quality: number): Promise<string> => {
return new Promise((resolve) => {
const img = new Image();
img.src = dataUrl;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Canvas context not available for compression');
resolve(dataUrl);
return;
}
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL('image/jpeg', quality));
};
img.onerror = () => {
console.error('Failed to compress image');
resolve(dataUrl);
};
});
};
onMounted(() => {
// ...existing code...
generateAndCacheImage();
});
</script>
<template>
<div class="print-container">
<div v-if="!showResult" class="countdown-section">
<div class="countdown-display">
<div class="countdown-number">{{ countdown }}</div>
<div class="processing-text">กำลงประมวลผล...</div>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${((4 - countdown) / 4) * 100}%` }"></div>
</div>
</div>
<div v-else class="result-section">
<div class="result-header">
<h1>เสรจแล!</h1>
<p>กำลงเตรยมไฟลสำหรบดาวนโหลด...</p>
</div>
<div class="photo-result" style="display: none;">
<div class="photo-frame" :class="`frame-${frame}`">
<div class="photo-grid" :style="getGridStyle()">
<div
v-for="(photo, index) in photos"
:key="index"
class="photo-cell" >
<img :src="photo" :alt="`Photo ${index + 1}`" />
</div>
</div>
</div>
</div>
<div class="loading-indicator">
<div class="spinner"></div>
<span>กำลงสรางไฟล...</span>
</div>
</div>
<div v-if="showResult" class="final-image-section">
<h2>ภาพทรวมแล</h2>
<div class="final-image-container">
<img :src="finalImage" alt="Final Combined Image" />
</div>
</div>
</div>
</template>
<style scoped>
.print-container {
min-height: 100vh;
background: #000;
color: white;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.countdown-section {
text-align: center;
animation: fadeIn 0.5s ease-in-out;
}
.countdown-display {
margin-bottom: 2rem;
}
.countdown-number {
font-size: 8rem;
font-weight: bold;
color: #ff6b6b;
margin-bottom: 1rem;
animation: scalePulse 1s ease-in-out infinite;
}
.processing-text {
font-size: 1.5rem;
color: #ccc;
}
.progress-bar {
width: 300px;
height: 8px;
background: rgba(255,255,255,0.2);
border-radius: 4px;
margin: 0 auto;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #ff6b6b, #4ecdc4);
border-radius: 4px;
transition: width 1s ease-in-out;
}
.result-section {
text-align: center;
animation: slideUp 0.8s ease-out;
}
.result-header h1 {
font-size: 3rem;
margin-bottom: 0.5rem;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.result-header p {
font-size: 1.2rem;
color: #ccc;
margin-bottom: 2rem;
}
.photo-result {
margin: 2rem 0;
display: flex;
justify-content: center;
}
.photo-frame {
background: white;
padding: 20px;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(255,255,255,0.1);
max-width: 400px;
width: 100%;
}
.photo-grid {
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
}
.photo-cell {
overflow: hidden;
border-radius: 6px;
}
.photo-cell img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid rgba(255,255,255,0.3);
border-top: 3px solid #ff6b6b;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-indicator span {
color: #ccc;
font-size: 1.1rem;
}
.final-image-section {
margin-top: 2rem;
text-align: center;
animation: fadeIn 1s ease-out forwards;
}
.final-image-container {
margin-top: 1rem;
display: flex;
justify-content: center;
align-items: center;
}
.final-image-container img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scalePulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Frame styles */
.frame-0 {
/* Classic frame - default white */
}
.frame-1 {
/* Modern frame */
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
}
.frame-2 {
/* Vintage frame */
background: linear-gradient(45deg, #f093fb, #f5576c);
}
.frame-3 {
/* Colorful frame */
background: linear-gradient(45deg, #4facfe, #00f2fe);
}
.frame-4 {
/* Minimal frame */
background: #f8f9fa;
border: 2px solid #dee2e6;
}
@media (max-width: 768px) {
.countdown-number {
font-size: 6rem;
}
.result-header h1 {
font-size: 2rem;
}
.photo-frame {
max-width: 300px;
}
}
</style>