vintage-photobooth / index.html
Tingchenliang's picture
Add 1 files
77dacd1 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pixel Photo Booth</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Import Boldonse font -->
<link href="https://fonts.googleapis.com/css2?family=Boldonse&display=swap" rel="stylesheet">
<style>
/* Pixel art styling */
.pixel-border {
border: 4px solid #000;
image-rendering: pixelated;
object-fit: cover; /* Keep this for preview */
}
.pixel-button {
position: relative;
background: #ff3366;
color: white;
border: 4px solid #000;
box-shadow: 4px 4px 0 #000;
transition: all 0.1s ease;
font-family: 'Courier New', monospace;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
}
.pixel-button:hover {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 #000;
}
.pixel-button:active {
transform: translate(4px, 4px);
box-shadow: 0 0 0 #000;
}
.pixel-text {
font-family: 'Courier New', monospace;
text-shadow: 0px 0px 0 rgba(0,0,0,0.2);
}
/* Apply Boldonse font to H1 */
#landing h1 {
font-family: 'Boldonse', sans-serif; /* Use Boldonse font */
text-shadow: 0px 0px 0 rgba(0,0,0,0.3); /* Optional: Adjust shadow if needed */
}
.pixel-bg {
background-color: #ffcc00;
background-image:
linear-gradient(45deg, #ff9933 25%, transparent 25%),
linear-gradient(-45deg, #ff9933 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ff9933 75%),
linear-gradient(-45deg, transparent 75%, #ff9933 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.pixel-frame {
border: 8px solid #000;
position: relative;
background: white;
box-shadow: 8px 8px 0 rgba(0,0,0,0.3);
}
.bw-filter {
filter: grayscale(100%) contrast(120%);
}
@keyframes flash {
0% { opacity: 0; }
10% { opacity: 1; }
100% { opacity: 0; }
}
.flash-effect {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
z-index: 999;
pointer-events: none;
animation: flash 0.3s ease-out;
display: none;
}
.counter {
font-size: 5rem;
color: white;
text-shadow: 4px 4px 0 #000;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 998;
display: none;
}
#resultsView .photo-strip {
display: flex;
flex-direction: column;
gap: 16px; /* Keep the gap for visual spacing */
padding: 16px; /* Keep padding */
}
#resultsView .photo-container {
position: relative;
width: 100%; /* Use 100% for layout, aspect ratio determines height */
border: 4px solid #000;
aspect-ratio: 3/4; /* Portrait orientation */
overflow: hidden; /* Ensure preview cropping matches object-fit */
}
#resultsView img {
width: 100%;
height: 100%;
display: block;
object-fit: cover; /* Still use cover for preview */
image-rendering: pixelated; /* Apply pixelated rendering here too */
}
</style>
</head>
<body class="min-h-screen pixel-bg flex flex-col items-center justify-center p-4">
<!-- Flash effect -->
<div class="flash-effect" id="flash"></div>
<!-- Counter -->
<div class="counter" id="counter">3</div>
<!-- Landing Page -->
<div id="landing" class="text-center">
<!-- Removed pixel-text class from H1 -->
<h1 class="text-4xl md:text-6xl font-bold mb-8">PHOTO BOOTH</h1>
<div class="pixel-frame p-8 mb-8 max-w-md mx-auto">
<img src="https://firebasestorage.googleapis.com/v0/b/ekoguides-e657a.appspot.com/o/others%2Fphoto-booth-cover.jpg?alt=media&token=3279c221-a4ab-442d-900e-45ffa5fb5879" alt="Photo Booth Example" class="w-full h-auto mb-4 bw-filter">
<p class="pixel-text text-lg mb-4">Take 4 retro selfies with our pixel photo booth!</p>
</div>
<button id="startBtn" class="pixel-button px-8 py-4 text-xl md:text-2xl mb-4">
<i class="fas fa-camera mr-2"></i> START PHOTO BOOTH
</button>
<p class="pixel-text text-sm">(Uses your front camera)</p>
</div>
<!-- Camera View -->
<div id="cameraView" class="hidden w-full max-w-md">
<div class="pixel-frame overflow-hidden mb-4">
<!-- Apply pixelated rendering to video preview if desired -->
<video id="video" autoplay playsinline class="w-full h-auto" style="image-rendering: pixelated;"></video>
</div>
<button id="captureBtn" class="pixel-button px-8 py-4 text-xl w-full mb-2">
<i class="fas fa-camera-retro mr-2"></i> TAKE PHOTO (1/4)
</button>
<button id="cancelBtn" class="pixel-button px-8 py-4 text-xl w-full bg-gray-500">
<i class="fas fa-times mr-2"></i> CANCEL
</button>
</div>
<!-- Results View -->
<div id="resultsView" class="hidden w-full max-w-md">
<div class="pixel-frame p-4 mb-4"> <!-- Removed padding here, handle in canvas -->
<div class="photo-strip">
<div class="photo-container">
<img id="photo1" class="bw-filter" alt="Photo 1">
</div>
<div class="photo-container">
<img id="photo2" class="bw-filter" alt="Photo 2">
</div>
<div class="photo-container">
<img id="photo3" class="bw-filter" alt="Photo 3">
</div>
<div class="photo-container">
<img id="photo4" class="bw-filter" alt="Photo 4">
</div>
</div>
</div>
<div class="flex gap-2">
<button id="saveBtn" class="pixel-button px-4 py-3 text-lg flex-1">
<i class="fas fa-save mr-2"></i> SAVE
</button>
<button id="retakeBtn" class="pixel-button px-4 py-3 text-lg flex-1 bg-gray-500">
<i class="fas fa-redo mr-2"></i> RETAKE
</button>
</div>
</div>
<script>
// DOM elements (no changes needed here)
const landing = document.getElementById('landing');
const cameraView = document.getElementById('cameraView');
const resultsView = document.getElementById('resultsView');
const video = document.getElementById('video');
const captureBtn = document.getElementById('captureBtn');
const startBtn = document.getElementById('startBtn');
const cancelBtn = document.getElementById('cancelBtn');
const saveBtn = document.getElementById('saveBtn');
const retakeBtn = document.getElementById('retakeBtn');
const flash = document.getElementById('flash');
const counter = document.getElementById('counter');
const photo1 = document.getElementById('photo1');
const photo2 = document.getElementById('photo2');
const photo3 = document.getElementById('photo3');
const photo4 = document.getElementById('photo4');
// State (no changes needed here)
let stream = null;
let photosTaken = 0;
let photoData = [];
// Start photo booth (no changes needed here)
startBtn.addEventListener('click', async () => {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'user',
width: { ideal: 640 },
height: { ideal: 480 }
},
audio: false
});
video.srcObject = stream;
landing.classList.add('hidden');
cameraView.classList.remove('hidden');
} catch (err) {
console.error("Error accessing camera:", err);
alert("Could not access the camera. Please make sure you've granted camera permissions.");
}
});
// Capture photo logic (no changes needed here)
captureBtn.addEventListener('click', () => {
startCountdown();
});
function startCountdown() {
let count = 3;
counter.textContent = count;
counter.style.display = 'block';
const countdown = setInterval(() => {
count--;
counter.textContent = count;
if (count <= 0) {
clearInterval(countdown);
counter.style.display = 'none';
takePhoto();
}
}, 1000);
}
function takePhoto() {
flash.style.display = 'block';
setTimeout(() => {
flash.style.display = 'none';
}, 300);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas dimensions (3:4 portrait orientation)
canvas.width = 300; // width is the shorter dimension (3)
canvas.height = 400; // height is the longer dimension (4)
const videoAspect = video.videoWidth / video.videoHeight;
const targetAspect = 3/4;
let srcX = 0, srcY = 0, srcWidth = video.videoWidth, srcHeight = video.videoHeight;
if (videoAspect > targetAspect) {
srcWidth = video.videoHeight * targetAspect;
srcX = (video.videoWidth - srcWidth) / 2;
} else {
srcHeight = video.videoWidth / targetAspect;
srcY = (video.videoHeight - srcHeight) / 2;
}
// Draw cropped video frame to canvas
// Enable image smoothing for capture? Maybe false for pixelated look
ctx.imageSmoothingEnabled = false; // Try disabling smoothing
ctx.drawImage(
video,
srcX, srcY, srcWidth, srcHeight,
0, 0, canvas.width, canvas.height
);
// Apply grayscale manually (already done in preview via CSS filter, but doing it here ensures saved image is B&W)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
}
ctx.putImageData(imageData, 0, 0);
const photoUrl = canvas.toDataURL('image/png');
photoData.push(photoUrl);
photosTaken++;
if (photosTaken < 4) {
captureBtn.innerHTML = `<i class="fas fa-camera-retro mr-2"></i> TAKE PHOTO (${photosTaken + 1}/4)`;
} else {
showResults();
}
}
function showResults() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
photo1.src = photoData[0];
photo2.src = photoData[1];
photo3.src = photoData[2];
photo4.src = photoData[3];
cameraView.classList.add('hidden');
resultsView.classList.remove('hidden');
}
// Cancel photo session (no changes needed here)
cancelBtn.addEventListener('click', () => {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
photosTaken = 0;
photoData = [];
captureBtn.innerHTML = `<i class="fas fa-camera-retro mr-2"></i> TAKE PHOTO (1/4)`;
cameraView.classList.add('hidden');
resultsView.classList.add('hidden'); // Hide results if cancelling from there
landing.classList.remove('hidden');
});
// Retake photos (no changes needed here)
retakeBtn.addEventListener('click', () => {
photosTaken = 0;
photoData = [];
captureBtn.innerHTML = `<i class="fas fa-camera-retro mr-2"></i> TAKE PHOTO (1/4)`;
resultsView.classList.add('hidden');
landing.classList.add('hidden'); // Ensure landing stays hidden
// Camera view is made visible by startBtn click simulation
// Re-initialize camera
startBtn.click(); // Simulate click to restart camera
});
// Save photos - *** REVISED LOGIC ***
saveBtn.addEventListener('click', async () => {
const pixelFrame = document.querySelector('#resultsView .pixel-frame');
// Get computed styles for accurate rendering dimensions
const computedStyle = window.getComputedStyle(pixelFrame);
const frameBorderWidth = parseInt(computedStyle.borderTopWidth, 10); // e.g., 8px
const framePadding = parseInt(computedStyle.paddingTop, 10); // e.g., 16px (since p-4 = 1rem = 16px)
// Constants from CSS
const photoBorderWidth = 4; // Border around individual photos
const gapBetweenPhotos = 16; // Gap between photos in the strip
// Calculate dimensions based on the rendered frame
const pixelFrameRect = pixelFrame.getBoundingClientRect();
// Create canvas for the final output
const canvas = document.createElement('canvas');
const dpr = window.devicePixelRatio || 1;
// Set canvas dimensions to match the rendered pixel-frame exactly
canvas.width = pixelFrameRect.width * dpr;
canvas.height = pixelFrameRect.height * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr); // Scale context for high-res displays
ctx.imageSmoothingEnabled = false; // Crucial for pixelated look on canvas
// 1. Draw white background for the entire frame area
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, pixelFrameRect.width, pixelFrameRect.height);
// 2. Draw the main black frame border (using frameBorderWidth)
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, pixelFrameRect.width, frameBorderWidth); // Top
ctx.fillRect(0, 0, frameBorderWidth, pixelFrameRect.height); // Left
ctx.fillRect(pixelFrameRect.width - frameBorderWidth, 0, frameBorderWidth, pixelFrameRect.height); // Right
ctx.fillRect(0, pixelFrameRect.height - frameBorderWidth, pixelFrameRect.width, frameBorderWidth); // Bottom
// 3. Define the content area (inside frame border and padding)
const contentX = frameBorderWidth + framePadding;
const contentY = frameBorderWidth + framePadding;
const contentWidth = pixelFrameRect.width - 2 * (frameBorderWidth + framePadding);
const contentHeight = pixelFrameRect.height - 2 * (frameBorderWidth + framePadding);
// 4. Calculate dimensions for each photo *within* the content area
const totalGapHeight = (photoData.length - 1) * gapBetweenPhotos;
const totalAvailableHeightForPhotos = contentHeight - totalGapHeight;
const singlePhotoHeightWithBorder = totalAvailableHeightForPhotos / photoData.length;
const singlePhotoWidthWithBorder = singlePhotoHeightWithBorder * (3 / 4); // Maintain aspect ratio *including* border
// Calculate the content dimensions *inside* the photo's border
const singlePhotoContentHeight = singlePhotoHeightWithBorder - (2 * photoBorderWidth);
const singlePhotoContentWidth = singlePhotoWidthWithBorder - (2 * photoBorderWidth);
// Calculate starting X to center the photos horizontally
const photoStartX = contentX + (contentWidth - singlePhotoWidthWithBorder) / 2;
let currentY = contentY; // Starting Y position
// 5. Draw each photo with its border
for (let i = 0; i < photoData.length; i++) {
const img = new Image();
img.src = photoData[i];
// Wait for the image to load before drawing
await new Promise(resolve => {
img.onload = () => {
// A. Draw the individual photo's black border (4px)
ctx.fillStyle = '#000000';
ctx.fillRect(
photoStartX,
currentY,
singlePhotoWidthWithBorder,
singlePhotoHeightWithBorder
);
// B. Draw the actual photo content *inside* its border
// Make sure source image aspect ratio (3:4) matches destination aspect ratio
ctx.drawImage(
img,
photoStartX + photoBorderWidth, // x position inside border
currentY + photoBorderWidth, // y position inside border
singlePhotoContentWidth, // width of content area
singlePhotoContentHeight // height of content area
);
// Move Y position down for the next photo + gap
currentY += singlePhotoHeightWithBorder + gapBetweenPhotos;
resolve();
};
img.onerror = () => {
console.error("Failed to load image for saving:", img.src);
resolve(); // Resolve anyway to not block the process
}
});
}
// 6. Trigger download
const link = document.createElement('a');
link.download = 'pixel-photo-booth-' + new Date().toISOString().slice(0, 10) + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=Tingchenliang/vintage-photobooth" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
</html>