2026-01-17 11:41:46 +07:00
|
|
|
<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)
|
2026-01-18 01:27:38 +07:00
|
|
|
const finalImage = ref<string | null>(null)
|
2026-01-17 11:41:46 +07:00
|
|
|
|
|
|
|
|
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()
|
2026-01-18 01:27:38 +07:00
|
|
|
|
|
|
|
|
// Load the final image from localStorage
|
|
|
|
|
finalImage.value = localStorage.getItem('photobooth-final-image');
|
2026-01-17 11:41:46 +07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const startCountdown = () => {
|
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
|
countdown.value--
|
|
|
|
|
if (countdown.value <= 0) {
|
|
|
|
|
clearInterval(timer)
|
|
|
|
|
showResult.value = true
|
|
|
|
|
|
|
|
|
|
// หลังจากแสดงผล 3 วินาที ให้ไปหน้า pickup
|
|
|
|
|
setTimeout(() => {
|
2026-01-18 02:27:05 +07:00
|
|
|
router.push('/pickup')
|
2026-01-17 11:41:46 +07:00
|
|
|
}, 3000)
|
|
|
|
|
}
|
|
|
|
|
}, 1000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getGridStyle = () => {
|
|
|
|
|
if (layout.value === '1x4') {
|
|
|
|
|
return {
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: '1fr',
|
|
|
|
|
gridTemplateRows: 'repeat(4, 1fr)',
|
2026-01-17 13:20:17 +07:00
|
|
|
gap: '8px',
|
2026-01-18 01:27:38 +07:00
|
|
|
aspectRatio: '1/1' // ทำให้แต่ละภาพยาวขึ้น (1:2 aspect ratio รวม)
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return {
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
|
|
|
gridTemplateRows: 'repeat(2, 1fr)',
|
2026-01-17 13:20:17 +07:00
|
|
|
gap: '8px',
|
|
|
|
|
aspectRatio: '1/1' // 2x2 เป็น square
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 01:27:38 +07:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
2026-01-17 11:41:46 +07:00
|
|
|
</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>
|
|
|
|
|
|
2026-01-18 02:27:05 +07:00
|
|
|
|
2026-01-17 11:41:46 +07:00
|
|
|
<div v-else class="result-section">
|
|
|
|
|
<div class="result-header">
|
|
|
|
|
<h1>เสร็จแล้ว!</h1>
|
|
|
|
|
<p>กำลังเตรียมไฟล์สำหรับดาวน์โหลด...</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:27:05 +07:00
|
|
|
<div class="final-image-display">
|
2026-01-18 11:12:09 +07:00
|
|
|
<img v-if="finalImage" :src="finalImage" alt="Final Combined Image" />
|
2026-01-17 11:41:46 +07:00
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:27:05 +07:00
|
|
|
|
2026-01-17 11:41:46 +07:00
|
|
|
<div class="loading-indicator">
|
2026-01-18 02:27:05 +07:00
|
|
|
<!-- <div class="spinner"></div> -->
|
|
|
|
|
<!-- <span>กำลังสร้างไฟล์...</span> -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-18 01:27:38 +07:00
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-18 02:27:05 +07:00
|
|
|
|
|
|
|
|
|
2026-01-17 11:41:46 +07:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-01-18 02:27:05 +07:00
|
|
|
@import url('https://fonts.googleapis.com/css2?family=Crete+Round&family=Old+Standard+TT&display=swap');
|
|
|
|
|
|
2026-01-17 11:41:46 +07:00
|
|
|
.print-container {
|
|
|
|
|
min-height: 100vh;
|
2026-01-18 02:27:05 +07:00
|
|
|
background: #fdfaf6; /* Light vintage paper color */
|
|
|
|
|
color: #3e2723; /* Dark brown for text */
|
|
|
|
|
font-family: 'Old Standard TT', serif;
|
2026-01-17 11:41:46 +07:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-01-18 02:27:05 +07:00
|
|
|
padding: 1.5rem; /* Adjusted padding */
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-01-18 02:27:05 +07:00
|
|
|
color: #ff5e3a; /* Vintage orange-red for countdown */
|
2026-01-17 11:41:46 +07:00
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
animation: scalePulse 1s ease-in-out infinite;
|
2026-01-18 02:27:05 +07:00
|
|
|
font-family: 'Crete Round', serif; /* Vintage font for number */
|
|
|
|
|
text-shadow: 2px 2px 5px rgba(255, 94, 58, 0.5); /* Subtle glow */
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.processing-text {
|
|
|
|
|
font-size: 1.5rem;
|
2026-01-18 02:27:05 +07:00
|
|
|
color: #6d4c41; /* Muted brown for text */
|
|
|
|
|
font-family: 'Crete Round', serif;
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-bar {
|
|
|
|
|
width: 300px;
|
2026-01-18 02:27:05 +07:00
|
|
|
height: 10px; /* Slightly thicker progress bar */
|
|
|
|
|
background: #e0d8cf; /* Lighter vintage tone */
|
|
|
|
|
border-radius: 5px; /* Softer border-radius */
|
2026-01-17 11:41:46 +07:00
|
|
|
margin: 0 auto;
|
|
|
|
|
overflow: hidden;
|
2026-01-18 02:27:05 +07:00
|
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-fill {
|
|
|
|
|
height: 100%;
|
2026-01-18 02:27:05 +07:00
|
|
|
background: linear-gradient(90deg, #a1887f, #8d6e63); /* Vintage brown gradient */
|
|
|
|
|
border-radius: 5px;
|
2026-01-17 11:41:46 +07:00
|
|
|
transition: width 1s ease-in-out;
|
2026-01-18 02:27:05 +07:00
|
|
|
box-shadow: 0 0 5px rgba(0,0,0,0.1);
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.result-section {
|
|
|
|
|
text-align: center;
|
|
|
|
|
animation: slideUp 0.8s ease-out;
|
2026-01-18 02:27:05 +07:00
|
|
|
background: #e0d8cf; /* Lighter vintage background for result section */
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
|
|
|
|
border: 2px solid #bcaaa4;
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.result-header h1 {
|
|
|
|
|
font-size: 3rem;
|
|
|
|
|
margin-bottom: 0.5rem;
|
2026-01-18 02:27:05 +07:00
|
|
|
background: linear-gradient(45deg, #5d4037, #8d6e63); /* Darker vintage brown gradient */
|
2026-01-17 11:41:46 +07:00
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
|
background-clip: text;
|
2026-01-18 02:27:05 +07:00
|
|
|
font-family: 'Crete Round', serif;
|
|
|
|
|
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.result-header p {
|
|
|
|
|
font-size: 1.2rem;
|
2026-01-18 02:27:05 +07:00
|
|
|
color: #6d4c41; /* Muted brown */
|
|
|
|
|
margin-bottom: 1.5rem; /* Adjusted margin */
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:27:05 +07:00
|
|
|
.final-image-display {
|
|
|
|
|
margin-top: 2rem;
|
|
|
|
|
display: inline-block; /* Allows auto width */
|
|
|
|
|
border: 4px solid #a1887f; /* Vintage frame border */
|
2026-01-17 11:41:46 +07:00
|
|
|
border-radius: 8px;
|
2026-01-18 02:27:05 +07:00
|
|
|
padding: 8px; /* Inner padding for the frame */
|
|
|
|
|
background: #fdfaf6; /* Background inside the frame */
|
|
|
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.2); /* Deeper shadow for the whole frame */
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:27:05 +07:00
|
|
|
.final-image-display img {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
height: auto;
|
|
|
|
|
display: block; /* Remove extra space below image */
|
|
|
|
|
border-radius: 4px; /* Slightly rounded corners for the photo itself */
|
|
|
|
|
filter: sepia(0.1) brightness(0.95); /* Subtle vintage photo effect */
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-indicator {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
margin-top: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spinner {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
2026-01-18 02:27:05 +07:00
|
|
|
border: 3px solid rgba(177,162,152,0.5); /* Muted gray/brown */
|
|
|
|
|
border-top: 3px solid #8d6e63; /* Vintage brown spinner */
|
2026-01-17 11:41:46 +07:00
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-indicator span {
|
2026-01-18 02:27:05 +07:00
|
|
|
color: #6d4c41; /* Muted brown */
|
2026-01-17 11:41:46 +07:00
|
|
|
font-size: 1.1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@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); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.countdown-number {
|
|
|
|
|
font-size: 6rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.result-header h1 {
|
|
|
|
|
font-size: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:27:05 +07:00
|
|
|
.final-image-display {
|
|
|
|
|
padding: 6px;
|
|
|
|
|
border-width: 3px;
|
2026-01-17 11:41:46 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|