File size: 19,894 Bytes
ea4bd4f
 
 
 
 
3d57c27
ea4bd4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d57c27
ea4bd4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d57c27
ea4bd4f
3d57c27
ea4bd4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77dacd1
ea4bd4f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
<!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>