PageSourceSearch

https://double-exposure.onrender.com/

html double-exposure.onrender.com collected 2026-10-03 11:39:50 UTC 50,627 bytes, 1,201 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5    <meta charset="UTF-8">
6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
8    <!-- SEO Meta Tags -->
9    <title>Double Exposure - Free Online Video Effect Maker (True Detective Style)</title>
10    <meta name="description"
11        content="Create cinematic double exposure videos in seconds. Free online tool. No sign-up required. Merge photos and videos instantly in your browser.">
12    <meta name="keywords"
13        content="double exposure video, true detective intro maker, silhouette video effect, motion portrait, free online video editor, double exposure effect, video inside silhouette">
14    <meta name="author" content="Double Exposure">
15    <meta name="robots" content="index, follow">
16
17    <!-- Open Graph / Social Media -->
18    <meta property="og:type" content="website">
19    <meta property="og:title" content="Double Exposure - Free Online Video Effect Maker">
20    <meta property="og:description"
21        content="Create cinematic double exposure videos in seconds. Free online tool. No sign-up required.">
22    <meta property="og:url" content="https://doubleexposure.app">
23    <meta property="og:site_name" content="Double Exposure">
24
25    <!-- Twitter Card -->
26    <meta name="twitter:card" content="summary_large_image">
27    <meta name="twitter:title" content="Double Exposure - Free Online Video Effect Maker">
28    <meta name="twitter:description" content="Create cinematic double exposure videos in seconds. Free online tool.">
29
30    <!-- Structured Data (JSON-LD) -->
31    
31<script type="application/ld+json">
32    {
33        "@context": "https://schema.org",
34        "@type": "SoftwareApplication",
35        "name": "Double Exposure",
36        "applicationCategory": "MultimediaApplication",
37        "offers": {
38            "@type": "Offer",
39            "price": "0",
40            "priceCurrency": "USD"
41        },
42        "operatingSystem": "Any (Browser-based)",
43        "permissions": "No registration required",
44        "description": "Create cinematic double exposure videos with AI-powered background removal. Free online tool that runs entirely in your browser.",
45        "featureList": [
46            "AI-powered background removal",
47            "Real-time video preview",
48            "Client-side processing (privacy-first)",
49            "No sign-up required",
50            "Free to use",
51            "Export as video"
52        ],
53        "browserRequirements": "Requires JavaScript. Works in Chrome, Firefox, Safari, Edge."
54    }
55    </script>
55
56
57    <!-- Tailwind CSS CDN -->
58    
58<script src="https://cdn.tailwindcss.com"></script>
58
59
60    <!-- Google Fonts -->
61    <link rel="preconnect" href="https://fonts.googleapis.com">
62    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
63    <link
64        href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@600;700&display=swap"
65        rel="stylesheet">
66
67    <!-- TensorFlow.js and BodyPix for better background removal -->
68    
68<script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]"></script>
68
69    
69<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/[email protected]"></script>
69
70
71    <style>
72        * {
73            margin: 0;
74            padding: 0;
75            box-sizing: border-box;
76        }
77
78        body {
79            font-family: 'Inter', sans-serif;
80            background: linear-gradient(180deg, #f8f9ff 0%, #ffffff 100%);
81            min-height: 100vh;
82            color: #1a1a2e;
83        }
84
85        .logo-text {
86            font-family: 'Space Grotesk', sans-serif;
87            background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
88            -webkit-background-clip: text;
89            -webkit-text-fill-color: transparent;
90            background-clip: text;
91            font-weight: 700;
92        }
93
94        .gradient-text {
95            background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
96            -webkit-background-clip: text;
97            -webkit-text-fill-color: transparent;
98            background-clip: text;
99        }
100
101        .hero-section {
102            background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%);
103            border-radius: 24px;
104            padding: 60px 40px;
105            text-align: center;
106            margin-bottom: 60px;
107            position: relative;
108            overflow: hidden;
109        }
110
111        .hero-section::before {
112            content: '';
113            position: absolute;
114            top: -50%;
115            left: -50%;
116            width: 200%;
117            height: 200%;
118            background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 70%);
119            animation: float 20s ease-in-out infinite;
120        }
121
122        @keyframes float {
123
124            0%,
125            100% {
126                transform: translate(0, 0);
127            }
128
129            50% {
130                transform: translate(20px, 20px);
131            }
132        }
133
134        .upload-zone {
135            border: 2px dashed #e0e7ff;
136            background: #ffffff;
137            border-radius: 20px;
138            padding: 40px;
139            text-align: center;
140            cursor: pointer;
141            transition: all 0.3s ease;
142            position: relative;
143            overflow: hidden;
144        }
145
146        .upload-zone:hover {
147            border-color: #6366f1;
148            background: #f8f9ff;
149            transform: translateY(-4px);
150            box-shadow: 0 12px 40px rgba(99, 102, 241, 0.15);
151        }
152
153        .upload-zone.has-file {
154            border-color: #6366f1;
155            background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%);
156        }
157
158        .btn-primary {
159            background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
160            color: white;
161            padding: 16px 32px;
162            border-radius: 12px;
163            font-weight: 600;
164            border: none;
165            cursor: pointer;
166            transition: all 0.3s ease;
167            box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3);
168        }
169
170        .btn-primary:hover:not(:disabled) {
171            transform: translateY(-2px);
172            box-shadow: 0 8px 30px rgba(99, 102, 241, 0.4);
173        }
174
175        .btn-primary:disabled {
176            opacity: 0.5;
177            cursor: not-allowed;
178        }
179
180        .btn-secondary {
181            background: #ffffff;
182            color: #6366f1;
183            padding: 16px 32px;
184            border-radius: 12px;
185            font-weight: 600;
186            border: 2px solid #6366f1;
187            cursor: pointer;
188            transition: all 0.3s ease;
189        }
190
191        .btn-secondary:hover:not(:disabled) {
192            background: #6366f1;
193            color: white;
194            transform: translateY(-2px);
195        }
196
197        .btn-record {
198            background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
199            color: white;
200            padding: 16px 32px;
201            border-radius: 12px;
202            font-weight: 600;
203            border: none;
204            cursor: pointer;
205            transition: all 0.3s ease;
206            box-shadow: 0 4px 20px rgba(239, 68, 68, 0.3);
207        }
208
209        .btn-record:hover:not(:disabled) {
210            transform: translateY(-2px);
211            box-shadow: 0 8px 30px rgba(239, 68, 68, 0.4);
212        }
213
214        .btn-record.recording {
215            animation: pulse 1.5s ease-in-out infinite;
216        }
217
218        @keyframes pulse {
219
220            0%,
221            100% {
222                opacity: 1;
223            }
224
225            50% {
226                opacity: 0.8;
227            }
228        }
229
230        .canvas-container {
231            background: #ffffff;
232            border-radius: 20px;
233            overflow: hidden;
234            box-shadow: 0 8px 40px rgba(0, 0, 0, 0.08);
235            margin-bottom: 40px;
236        }
237
238        .control-card {
239            background: #ffffff;
240            border-radius: 16px;
241            padding: 24px;
242            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
243            transition: all 0.3s ease;
244        }
245
246        .control-card:hover {
247            box-shadow: 0 4px 20px rgba(99, 102, 241, 0.15);
248            transform: translateY(-2px);
249        }
250
251        .slider {
252            -webkit-appearance: none;
253            appearance: none;
254            width: 100%;
255            height: 6px;
256            border-radius: 3px;
257            background: #e0e7ff;
258            outline: none;
259        }
260
261        .slider::-webkit-slider-thumb {
262            -webkit-appearance: none;
263            appearance: none;
264            width: 20px;
265            height: 20px;
266            border-radius: 50%;
267            background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
268            cursor: pointer;
269            box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
270            transition: all 0.2s ease;
271        }
272
273        .slider::-webkit-slider-thumb:hover {
274            transform: scale(1.2);
275            box-shadow: 0 4px 12px rgba(99, 102, 241, 0.6);
276        }
277
278        .slider::-moz-range-thumb {
279            width: 20px;
280            height: 20px;
281            border-radius: 50%;
282            background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
283            cursor: pointer;
284            border: none;
285            box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
286            transition: all 0.2s ease;
287        }
288
289        .slider::-moz-range-thumb:hover {
290            transform: scale(1.2);
291            box-shadow: 0 4px 12px rgba(99, 102, 241, 0.6);
292        }
293
294        .preview-thumbnail {
295            width: 100%;
296            height: 200px;
297            object-fit: cover;
298            border-radius: 12px;
299            margin-bottom: 16px;
300        }
301
302        .status-badge {
303            background: linear-gradient(135deg, #10b981 0%, #059669 100%);
304            color: white;
305            padding: 6px 16px;
306            border-radius: 20px;
307            font-size: 12px;
308            font-weight: 600;
309            display: inline-flex;
310            align-items: center;
311            gap: 6px;
312        }
313
314        .step-card {
315            background: #ffffff;
316            border-radius: 16px;
317            padding: 24px;
318            text-align: center;
319            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
320        }
321
322        .step-number {
323            width: 40px;
324            height: 40px;
325            border-radius: 50%;
326            background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
327            color: white;
328            display: flex;
329            align-items: center;
330            justify-content: center;
331            font-weight: 700;
332            margin: 0 auto 16px;
333        }
334
335        .loading-overlay {
336            background: rgba(255, 255, 255, 0.95);
337            backdrop-filter: blur(10px);
338        }
339
340        .spinner {
341            border: 4px solid #e0e7ff;
342            border-top: 4px solid #6366f1;
343            border-radius: 50%;
344            width: 60px;
345            height: 60px;
346            animation: spin 1s linear infinite;
347        }
348
349        @keyframes spin {
350            0% {
351                transform: rotate(0deg);
352            }
353
354            100% {
355                transform: rotate(360deg);
356            }
357        }
358
359        #segmentCanvas {
360            display: none;
361        }
362
363        .icon-upload {
364            width: 64px;
365            height: 64px;
366            margin: 0 auto 16px;
367            opacity: 0.6;
368        }
369    </style>
370</head>
371
372<body class="p-6">
373
374    <!-- Header -->
375    <header class="max-w-7xl mx-auto mb-8 text-center">
376        <h1 class="logo-text text-3xl">Double Exposure</h1>
377
378        <!-- Trust Badges -->
379        <div class="flex flex-wrap justify-center gap-4 mt-6">
380            <div
381                class="flex items-center gap-2 bg-white/10 backdrop-blur-sm px-4 py-2 rounded-full border border-white/20">
382                <span class="text-2xl">🚫</span>
383                <span class="text-sm font-semibold">No Sign Up Required</span>
384            </div>
385            <div
386                class="flex items-center gap-2 bg-white/10 backdrop-blur-sm px-4 py-2 rounded-full border border-white/20">
387                <span class="text-2xl">🔒</span>
388                <span class="text-sm font-semibold">100% Client-Side</span>
389            </div>
390            <div
391                class="flex items-center gap-2 bg-white/10 backdrop-blur-sm px-4 py-2 rounded-full border border-white/20">
392                <span class="text-2xl">⚡</span>
393                <span class="text-sm font-semibold">Powered by AI</span>
394            </div>
395        </div>
396    </header>
397
398    <!-- Hero Section -->
399    <div class="max-w-7xl mx-auto">
400        <div class="hero-section relative">
401            <h2 class="text-5xl md:text-6xl font-bold mb-4 relative z-10">
402                Create <span class="gradient-text">Double Exposure</span><br>Designs Easily
403            </h2>
404            <p class="text-xl text-gray-600 mb-8 relative z-10 max-w-2xl mx-auto">
405                Turn Your Portraits into Cinematic Art.<br>
406                Create beautiful double exposure effects with AI-powered background removal.<br>
407                Upload your portrait and video to get started!
408            </p>
409        </div>
410
411        <!-- Upload Section -->
412        <div class="grid md:grid-cols-2 gap-8 mb-12">
413            <!-- Portrait Upload -->
414            <div class="upload-zone" id="portraitZone">
415                <input type="file" id="portraitInput" accept="image/*" class="hidden">
416                <div id="portraitPreview" class="hidden">
417                    <img id="portraitImg" class="preview-thumbnail">
418                    <span class="status-badge">✓ Portrait Loaded</span>
419                </div>
420                <div id="portraitPlaceholder">
421                    <svg class="icon-upload" fill="none" stroke="currentColor" viewBox="0 0 24 24">
422                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
423                            d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
424                        </path>
425                    </svg>
426                    <h3 class="text-xl font-semibold mb-2">Upload Portrait</h3>
427                    <p class="text-gray-500 text-sm">Click to select your portrait image</p>
428                </div>
429            </div>
430
431            <!-- Texture Upload (Image or Video) -->
432            <div class="upload-zone" id="textureZone">
433                <input type="file" id="textureInput" accept="image/*,video/*" class="hidden">
434                <div id="texturePreview" class="hidden">
435                    <img id="textureImgPreview" class="preview-thumbnail hidden">
436                    <video id="textureVideoPreview" class="preview-thumbnail hidden" muted loop></video>
437                    <span class="status-badge">✓ Texture Loaded</span>
438                </div>
439                <div id="texturePlaceholder">
440                    <svg class="icon-upload" fill="none" stroke="currentColor" viewBox="0 0 24 24">
441                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
442                            d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
443                        </path>
444                    </svg>
445                    <h3 class="text-xl font-semibold mb-2">Upload Texture</h3>
446                    <p class="text-gray-500 text-sm">Image or Video (mountains, ocean, city, etc.)</p>
447                </div>
448            </div>
449        </div>
450
451        <!-- Process Button -->
452        <div class="text-center mb-12">
453            <button id="processBtn" class="btn-primary text-lg px-12 py-4" disabled>
454                Start Creating Now →
455            </button>
456        </div>
457
458        <!-- Canvas Preview -->
459        <div class="canvas-container mb-12" id="canvasSection" style="display: none;">
460            <canvas id="mainCanvas" class="w-full" width="1920" height="1080"></canvas>
461        </div>
462
463        <!-- Controls -->
464        <div class="grid md:grid-cols-6 gap-6 mb-12" id="controlsSection" style="display: none;">
465            <div class="control-card">
466                <label class="block text-sm font-semibold mb-3 text-gray-700">Video Blend</label>
467                <input type="range" id="videoBlend" min="0" max="100" value="70" class="slider">
468                <div class="text-center mt-3 text-sm font-semibold text-gray-600"><span id="blendValue">70</span>%</div>
469            </div>
470
471            <div class="control-card">
472                <label class="block text-sm font-semibold mb-3 text-gray-700">Texture Fade</label>
473                <input type="range" id="textureFade" min="0" max="100" value="0" class="slider">
474                <div class="text-center mt-3 text-sm font-semibold text-gray-600"><span id="fadeValue">0</span>%</div>
475            </div>
476
477            <div class="control-card">
478                <label class="block text-sm font-semibold mb-3 text-gray-700">
478Background Blur</label>
479                <input type="range" id="bgBlur" min="0" max="20" value="0" class="slider">
480                <div class="text-center mt-3 text-sm font-semibold text-gray-600"><span id="blurValue">0</span>px</div>
481            </div>
482
483            <div class="control-card">
484                <label class="block text-sm font-semibold mb-3 text-gray-700">Video Scale</label>
485                <input type="range" id="videoScale" min="50" max="200" value="100" class="slider">
486                <div class="text-center mt-3 text-sm font-semibold text-gray-600"><span id="scaleValue">100</span>%
487                </div>
488            </div>
489
490            <div class="control-card">
491                <label class="block text-sm font-semibold mb-3 text-gray-700">Position X</label>
492                <input type="range" id="videoPosX" min="-100" max="100" value="0" class="slider">
493                <div class="text-center mt-3 text-sm font-semibold text-gray-600"><span id="posXValue">0</span>%</div>
494            </div>
495
496            <div class="control-card">
497                <label class="block text-sm font-semibold mb-3 text-gray-700">Position Y</label>
498                <input type="range" id="videoPosY" min="-100" max="100" value="0" class="slider">
499                <div class="text-center mt-3 text-sm font-semibold text-gray-600"><span id="posYValue">0</span>%</div>
500            </div>
501        </div>
502
503        <!-- Action Buttons -->
504        <div class="flex flex-wrap gap-4 justify-center mb-12" id="actionsSection" style="display: none;">
505            <!-- Download Image Button (for static textures) -->
506            <button id="downloadImageBtn" class="btn-primary text-lg px-10 py-4" style="display: none;">
507                ⬇ Download Image
508            </button>
509
510            <!-- Record Video Button (for video textures) -->
511            <button id="recordBtn" class="btn-record text-lg px-10 py-4" style="display: none;">
512                ● Record (5s)
513            </button>
514
515            <!-- Download Video Button (appears after recording) -->
516            <button id="downloadVideoBtn" class="btn-primary text-lg px-10 py-4" style="display: none;">
517                ⬇ Download Video
518            </button>
519        </div>
520
521        <!-- Featured Designs Gallery -->
522        <div class="mt-20 mb-12">
523            <p class="text-center text-sm text-gray-500 mb-4 uppercase tracking-wider">DISCOVER WHAT PEOPLE MADE WITH
524                DOUBLE EXPOSURE</p>
525            <h3 class="text-3xl font-bold text-center mb-4">Stand Out with Eye-Catching<br>Double Exposure Designs</h3>
526            <p class="text-center text-gray-600 mb-8">Featured Designs</p>
527
528            <div class="grid md:grid-cols-2 gap-6">
529                <div
530                    class="rounded-xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:scale-105">
531                    <img src="images/F0CMIIKKYR9WTYL.webp" alt="Double Exposure Example 1"
532                        class="w-full h-80 object-cover">
533                </div>
534                <div
535                    class="rounded-xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:scale-105">
536                    <img src="images/man+in+city+final.webp" alt="Double Exposure Example 2"
537                        class="w-full h-80 object-cover">
538                </div>
539                <div
540                    class="rounded-xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:scale-105">
541                    <img src="images/o1AJ9qDyyJNSpZWhUgGYc3MngFqoAMy9tfTNBoKhf763LYQai.webp"
542                        alt="Double Exposure Example 3" class="w-full h-80 object-cover">
543                </div>
544                <div
545                    class="rounded-xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:scale-105">
546                    <img src="images/pngtree-double-exposure-effect-a-man-face-and-city-skyline-picture-image_15462523.jpg"
547                        alt="Double Exposure Example 4" class="w-full h-80 object-cover">
548                </div>
549            </div>
550        </div>
551
552        <!-- How It Works -->
553        <div class="mt-20 mb-12">
554            <h3 class="text-3xl font-bold text-center mb-12">Create Your Design in 4 Simple Steps</h3>
555            <div class="grid md:grid-cols-4 gap-6">
556                <div class="step-card">
557                    <div class="step-number">1</div>
558                    <h4 class="font-semibold mb-2">Upload Photo</h4>
559                    <p class="text-sm text-gray-600">Upload your portrait image</p>
560                </div>
561                <div class="step-card">
562                    <div class="step-number">2</div>
563                    <h4 class="font-semibold mb-2">Add Video</h4>
564                    <p class="text-sm text-gray-600">Select your texture video</p>
565                </div>
566                <div class="step-card">
567                    <div class="step-number">3</div>
568                    <h4 class="font-semibold mb-2">Customize</h4>
569                    <p class="text-sm text-gray-600">Adjust blend and position</p>
570                </div>
571                <div class="step-card">
572                    <div class="step-number">4</div>
573                    <h4 class="font-semibold mb-2">Download</h4>
574                    <p class="text-sm text-gray-600">Record and save your video</p>
575                </div>
576            </div>
577        </div>
578
579        <!-- SEO Content: How To Guide -->
580        <div class="mt-20 mb-12 max-w-4xl mx-auto">
581            <h2 class="text-3xl font-bold mb-8">How to Create a Double Exposure Video</h2>
582            <div class="bg-white rounded-xl p-8 shadow-lg">
583                <ol class="space-y-4 text-gray-700">
584                    <li class="flex gap-4">
585                        <span
586                            class="flex-shrink-0 w-8 h-8 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-full flex items-center justify-center font-bold">1</span>
587                        <div>
588                            <strong>Upload Your Portrait:</strong> Choose a high-quality portrait photo (JPG or PNG).
589                            The AI will automatically detect and isolate the person from the background.
590                        </div>
591                    </li>
592                    <li class="flex gap-4">
593                        <span
594                            class="flex-shrink-0 w-8 h-8 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-full flex items-center justify-center font-bold">2</span>
595                        <div>
596                            <strong>Add Texture:</strong> Upload a video or image (ocean waves, city skyline, mountains,
597                            etc.) that will appear inside the person's silhouette.
598                        </div>
599                    </li>
600                    <li class="flex gap-4">
601                        <span
602                            class="flex-shrink-0 w-8 h-8 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-full flex items-center justify-center font-bold">3</span>
603                        <div>
604                            <strong>Customize:</strong> Adjust the blend intensity, add vignette fade, blur the
605                            background, and position the texture perfectly.
606                        </div>
607                    </li>
608                    <li class="flex gap-4">
609                        <span
610                            class="flex-shrink-0 w-8 h-8 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-full flex items-center justify-center font-bold">4</span>
611                        <div>
612                            <strong>Download:</strong> For static images, click "Download Image" to save as PNG. For
613                            videos, click "Record" to capture a 5-second clip and download as WebM.
614                        </div>
615                    </li>
616                </ol>
617            </div>
618        </div>
619
620        <!-- SEO Content: Features -->
621        <div class="mt-20 mb-12">
622            <h2 class="text-3xl font-bold text-center mb-12">Why Choose Double Exposure?</h2>
623            <div class="grid md:grid-cols-3 gap-8">
624                <div class="bg-white rounded-xl p-8 shadow-lg text-center">
625                    <div class="text-5xl mb-4">🔒</div>
626                    <h3 class="text-xl font-bold mb-3">Privacy First</h3>
627                    <p class="text-gray-600">All processing happens in your browser. Your photos and videos never leave
628                        your device. No servers, no uploads, no tracking.</p>
629                </div>
630                <div class="bg-white rounded-xl p-8 shadow-lg text-center">
631                    <div class="text-5xl mb-4">⚡</div>
632                    <h3 class="text-xl font-bold mb-3">Lightning Fast</h3>
633                    <p class="text-gray-600">Powered by TensorFlow.js and BodyPix AI. Get professional results in
634                        seconds without waiting for cloud processing.</p>
635                </div>
636                <div class="bg-white rounded-xl p-8 shadow-lg text-center">
637                    <div class="text-5xl mb-4">🎨</div>
638                    <h3 class="text-xl font-bold mb-3">Full Control</h3>
639                    <p class="text-gray-600">Adjust blend, fade, blur, scale, and position. Create exactly the look you
640                        want with professional-grade controls.</p>
641                </div>
642            </div>
643        </div>
644
645        <!-- SEO Content: FAQ -->
646        <div class="mt-20 mb-12 max-w-4xl mx-auto">
647            <h2 class="text-3xl font-bold mb-8 text-center">Frequently Asked Questions</h2>
648            <div class="space-y-4">
649                <div class="bg-white rounded-xl p-6 shadow-lg">
650                    <h3 class="text-lg font-bold mb-2">Is Double Exposure really free?</h3>
651                    <p class="text-gray-600">Yes! Double Exposure is 100% free to use. No hidden costs, no
652                        subscriptions, no watermarks. Create unlimited double exposure effects.</p>
653                </div>
654                <div class="bg-white rounded-xl p-6 shadow-lg">
655                    <h3 class="text-lg font-bold mb-2">Do I need to create an account?</h3>
656                    <p class="text-gray-600">No sign-up required. Simply visit the page, upload your files, and start
657                        creating. Your privacy is our priority.</p>
658                </div>
659                <div class="bg-white rounded-xl p-6 shadow-lg">
660                    <h3 class="text-lg font-bold mb-2">What file formats are supported?</h3>
661                    <p class="text-gray-600">For portraits: JPG, PNG. For textures: JPG, PNG (static images) or MP4,
662                        WebM (videos). Output: PNG for images, WebM for videos.</p>
663                </div>
664                <div class="bg-white rounded-xl p-6 shadow-lg">
665                    <h3 class="text-lg font-bold mb-2">How do I save my creation?</h3>
666                    <p class="text-gray-600">For static textures, click "Download Image" to save as PNG. For video
667                        textures, click "Record (5s)" to capture a clip, then "Download Video" to save as WebM.</p>
668                </div>
669                <div class="bg-white rounded-xl p-6 shadow-lg">
670                    <h3 class="text-lg font-bold mb-2">Is my data safe?</h3>
671                    <p class="text-gray-600">Absolutely. All processing happens locally in your browser using JavaScript
672                        and AI models. Your files never leave your device or get uploaded to any server.</p>
673                </div>
674                <div class="bg-white rounded-xl p-6 shadow-lg">
675                    <h3 class="text-lg font-bold mb-2">What browsers are supported?</h3>
676                    <p class="text-gray-600">Works best in modern browsers: Chrome, Firefox, Safari, and Edge. Make sure
677                        JavaScript is enabled for the AI features to work.</p>
678                </div>
679            </div>
680        </div>
681    </div>
682
683    <!-- Loading Overlay -->
684    <div id="loadingOverlay"
685        class="loading-overlay fixed inset-0 flex flex-col items-center justify-center z-50 hidden">
686        <div class="spinner mb-6"></div>
687        <p class="text-2xl font-semibold text-gray-800 mb-2">Processing Background Removal</p>
688        <p class="text-gray-600">This may take a moment...</p>
689    </div>
690
691    <!-- Hidden Elements -->
692    <video id="hiddenTextureVideo" style="display: none;" muted loop></video>
693    <canvas id="segmentCanvas" width="1920" height="1080"></canvas>
694
695    
695<script>
696        // State
697        let portraitImage = null;
698        let maskCanvas = null;
699        let textureMedia = null; // Can be image or video
700        let textureType = null; // 'image' or 'video'
701        let animationId = null;
702        let mediaRecorder = null;
703        let recordedChunks = [];
704        let isProcessed = false;
705        let bodyPixNet = null; // BodyPix model
706
707        // Elements
708        const portraitZone = document.getElementById('portraitZone');
709        const portraitInput = document.getElementById('portraitInput');
710        const portraitPreview = document.getElementById('portraitPreview');
711        const portraitPlaceholder = document.getElementById('portraitPlaceholder');
712        const portraitImg = document.getElementById('portraitImg');
713
714        const textureZone = document.getElementById('textureZone');
715        const textureInput = document.getElementById('textureInput');
716        const texturePreview = document.getElementById('texturePreview');
717        const texturePlaceholder = document.getElementById('texturePlaceholder');
718        const textureImgPreview = document.getElementById('textureImgPreview');
719        const textureVideoPreview = document.getElementById('textureVideoPreview');
720
721        const mainCanvas = document.getElementById('mainCanvas');
722        const ctx = mainCanvas.getContext('2d', { willReadFrequently: true });
723
724        const segmentCanvas = document.getElementById('segmentCanvas');
725        const segmentCtx = segmentCanvas.getContext('2d', { willReadFrequently: true });
726
727        const hiddenTextureVideo = document.getElementById('hiddenTextureVideo');
728
729        const videoBlendInput = document.getElementById('videoBlend');
730        const blendValue = document.getElementById('blendValue');
731        const textureFadeInput = document.getElementById('textureFade');
732        const fadeValue = document.getElementById('fadeValue');
733        const bgBlurInput = document.getElementById('bgBlur');
734        const blurValue = document.getElementById('blurValue');
735        const videoScaleInput = document.getElementById('videoScale');
736        const scaleValue = document.getElementById('scaleValue');
737        const videoPosXInput = document.getElementById('videoPosX');
738        const posXValue = document.getElementById('posXValue');
739        const videoPosYInput = document.getElementById('videoPosY');
740        const posYValue = document.getElementById('posYValue');
741
742        const processBtn = document.getElementById('processBtn');
743        const recordBtn = document.getElementById('recordBtn');
744        const downloadImageBtn = document.getElementById('downloadImageBtn');
745        const downloadVideoBtn = document.getElementById('downloadVideoBtn');
746        const loadingOverlay = document.getElementById('loadingOverlay');
747
748        const canvasSection = document.getElementById('canvasSection');
749        const controlsSection = document.getElementById('controlsSection');
750        const actionsSection = document.getElementById('actionsSection');
751
752        // Initialize BodyPix
753        async function initBodyPix() {
754            if (!bodyPixNet) {
755                bodyPixNet = await bodyPix.load({
756                    architecture: 'MobileNetV1',
757                    outputStride: 16,
758                    multiplier: 0.75,
759                    quantBytes: 2
760                });
761            }
762            return bodyPixNet;
763        }
764
765        // Upload Handlers
766        portraitZone.addEventListener('click', () => portraitInput.click());
767        portraitInput.addEventListener('change', handlePortraitUpload);
768
769        textureZone.addEventListener('click', () => textureInput.click());
770        textureInput.addEventListener('change', handleTextureUpload);
771
772        function handlePortraitUpload(e) {
773            const file = e.target.files[0];
774            if (!file) return;
775
776            const reader = new FileReader();
777            reader.onload = (event) => {
778                portraitImage = new Image();
779                portraitImage.onload = () => {
780                    portraitImg.src = event.target.result;
781                    portraitPlaceholder.classList.add('hidden');
782                    portraitPreview.classList.remove('hidden');
783                    portraitZone.classList.add('has-file');
784                    checkReadyToProcess();
785                };
786                portraitImage.src = event.target.result;
787            };
788            reader.readAsDataURL(file);
789        }
790
791        function handleTextureUpload(e) {
792            const file = e.target.files[0];
793            if (!file) return;
794
795            const url = URL.createObjectURL(file);
796            const isVideo = file.type.startsWith('video/');
797
798            texturePlaceholder.classList.add('hidden');
799            texturePreview.classList.remove('hidden');
800            textureZone.classList.add('has-file');
801
802            if (isVideo) {
803                // Handle video
804                textureType = 'video';
805                textureImgPreview.classList.add('hidden');
806                textureVideoPreview.classList.remove('hidden');
807                textureVideoPreview.src = url;
808                textureVideoPreview.play();
809
810                textureMedia = hiddenTextureVideo; // Use the hidden video element
811                textureMedia.src = url;
812                textureMedia.loop = true;
813                textureMedia.muted = true;
814                // Don't play textureMedia here, it will be played in processBtn handler
815            } else {
816                // Handle image
817                textureType = 'image';
818                textureVideoPreview.classList.add('hidden');
819                textureImgPreview.classList.remove('hidden');
820                textureImgPreview.src = url;
821
822                textureMedia = new Image();
823                textureMedia.onload = () => {
824                    checkReadyToProcess();
825                };
826                textureMedia.src = url;
827            }
828
829            checkReadyToProcess();
830        }
831
832        function checkReadyToProcess() {
833            if (portraitImage && textureMedia) {
834                processBtn.disabled = false;
835            }
836        }
837
838        // Process Effect
839        processBtn.addEventListener('click', async () => {
840            if (!portraitImage || !textureMedia) return;
841
842            loadingOverlay.classList.remove('hidden');
843            processBtn.disabled = true;
844
845            try {
846                // Load BodyPix model
847                const net = await initBodyPix();
848
849                // Create a temporary canvas for the portrait
850                const tempCanvas = document.createElement('canvas');
851                tempCanvas.width = portraitImage.width;
852                tempCanvas.height = portraitImage.height;
853                const tempCtx = tempCanvas.getContext('2d');
854                tempCtx.drawImage(portraitImage, 0, 0);
855
856                // Perform segmentation with high quality settings
857                const segmentation = await net.segmentPerson(tempCanvas, {
858                    flipHorizontal: false,
859                    internalResolution: 'medium',
860                    segmentationThreshold: 0.7,
861                    maxDetections: 1,
862                    scoreThreshold: 0.2,
863                    nmsRadius: 20
864                });
865
866                // Create mask canvas
867                segmentCanvas.width = portraitImage.width;
868                segmentCanvas.height = portraitImage.height;
869
870                segmentCtx.clearRect(0, 0, segmentCanvas.width, segmentCanvas.height);
871
872                // Draw the original image
873                segmentCtx.drawImage(portraitImage, 0, 0);
874
875                // Apply mask using segmentation data
876                const imageData = segmentCtx.getImageData(0, 0, segmentCanvas.width, segmentCanvas.height);
877                const pixels = imageData.data;
878
879                // Apply the mask - make background transparent
880                for (let i = 0; i < segmentation.data.length; i++) {
881                    const shouldKeep = segmentation.data[i];
882                    if (!shouldKeep) {
883                        // Make this pixel transparent (it's background)
884                        pixels[i * 4 + 3] = 0; // Alpha channel
885                    }
886                }
887
888                segmentCtx.putImageData(imageData, 0, 0);
889                maskCanvas = segmentCanvas;
890
891                loadingOverlay.classList.add('hidden');
892                isProcessed = true;
893
894                canvasSection.style.display = 'block';
895                controlsSection.style.display = 'grid';
896                actionsSection.style.display = 'flex';
897
898                // Show appropriate button based on texture type
899                if (textureType === 'video') {
900                    recordBtn.style.display = 'inline-block';
901                    recordBtn.disabled = false; // Enable record button for video
902                    downloadImageBtn.style.display = 'none';
903                } else {
904                    downloadImageBtn.style.display = 'inline-block';
905                    downloadImageBtn.disabled = false; // Enable download image button for image
906                    recordBtn.style.display = 'none';
907                }
908
909                // Play video if texture is video
910                if (textureType === 'video') {
911                    textureMedia.play();
912                }
913
914                startAnimation();
915
916                // Scroll to canvas
917                canvasSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
918
919            } catch (error) {
920                console.error('Background removal failed:', error);
921                loadingOverlay.classList.add('hidden');
922                alert('Background removal failed. Please try again.');
923                processBtn.disabled = false;
924            }
925        });
926
927        // Animation Loop
928        let offCanvas = null;
929        let offCtx = null;
930        let lastFrameTime = 0;
931        const targetFPS = 30;
932        const frameInterval = 1000 / targetFPS;
933
934        function startAnimation() {
935            if (animationId) {
936                cancelAnimationFrame(animationId);
937            }
938
939            // Create off-screen canvas once
940            if (!offCanvas) {
941                offCanvas = document.createElement('canvas');
942                offCanvas.width = mainCanvas.width;
943                offCanvas.height = mainCanvas.height;
944                offCtx = offCanvas.getContext('2d', {
945                    willReadFrequently: false,
946                    alpha: true,
947                    desynchronized: true
948                });
949            }
950
951            function render(currentTime) {
952                if (!isProcessed || !maskCanvas || !textureMedia || !portraitImage) return;
953
954                // Throttle to target FPS
955                const elapsed = currentTime - lastFrameTime;
956                if (elapsed < frameInterval) {
957                    if (textureType === 'video') {
958                        animationId = requestAnimationFrame(render);
959                    }
960                    return;
961                }
962                lastFrameTime = currentTime - (elapsed % frameInterval);
963
964                const canvasWidth = mainCanvas.width;
965                const canvasHeight = mainCanvas.height;
966
967                // Get blur value
968                const blurAmount = parseFloat(bgBlurInput.value);
969
970                // STEP 1: Draw the original background with blur
971                if (blurAmount > 0) {
972                    // Create a temporary canvas for blurred background
973                    const bgCanvas = document.createElement('canvas');
974                    bgCanvas.width = canvasWidth;
975                    bgCanvas.height = canvasHeight;
976                    const bgCtx = bgCanvas.getContext('2d', { alpha: true });
977
978                    // Draw full image with blur
979                    bgCtx.filter = `blur(${blurAmount}px)`;
980                    bgCtx.drawImage(portraitImage, 0, 0, canvasWidth, canvasHeight);
981                    bgCtx.filter = 'none';
982
983                    // Remove person from blurred image (keep only background)
984                    const bgImageData = bgCtx.getImageData(0, 0, canvasWidth, canvasHeight);
985                    const bgPixels = bgImageData.data;
986
987                    // Get mask data
988                    const maskImageData = segmentCtx.getImageData(0, 0, canvasWidth, canvasHeight);
989                    const maskPixels = maskImageData.data;
990
991                    // Make person area transparent in blurred background
992                    for (let i = 0; i < maskPixels.length; i += 4) {
993                        if (maskPixels[i + 3] > 0) {
994                            // This pixel is part of the person, make it transparent
995                            bgPixels[i + 3] = 0;
996                        }
997                    }
998
999                    bgCtx.putImageData(bgImageData, 0, 0);
1000
1001                    // Draw blurred background to main canvas
1002                    ctx.drawImage(bgCanvas, 0, 0);
1003
1004                    // Draw original person (no blur) on top
1005                    ctx.drawImage(maskCanvas, 0, 0, canvasWidth, canvasHeight);
1006                } else {
1007                    // No blur, just draw original
1008                    ctx.drawImage(portraitImage, 0, 0, canvasWidth, canvasHeight);
1009                }
1010
1011                // STEP 2: Clear and prepare off-screen canvas for texture-filled silhouette
1012                offCtx.clearRect(0, 0, canvasWidth, canvasHeight);
1013
1014                // Calculate texture dimensions
1015                const scale = parseFloat(videoScaleInput.value) / 100;
1016                const posX = parseFloat(videoPosXInput.value) / 100;
1017                const posY = parseFloat(videoPosYInput.value) / 100;
1018
1019                let textureWidth, textureHeight;
1020
1021                if (textureType === 'video') {
1022                    textureWidth = textureMedia.videoWidth;
1023                    textureHeight = textureMedia.videoHeight;
1024                } else {
1025                    textureWidth = textureMedia.width;
1026                    textureHeight = textureMedia.height;
1027                }
1028
1029                const textureAspect = textureWidth / textureHeight;
1030                const canvasAspect = canvasWidth / canvasHeight;
1031
1032                let drawWidth, drawHeight, drawX, drawY;
1033
1034                if (textureAspect > canvasAspect) {
1035                    drawHeight = canvasHeight * scale;
1036                    drawWidth = drawHeight * textureAspect;
1037                } else {
1038                    drawWidth = canvasWidth * scale;
1039                    drawHeight = drawWidth / textureAspect;
1040                }
1041
1042                drawX = (canvasWidth - drawWidth) / 2 + (posX * canvasWidth);
1043                drawY = (canvasHeight - drawHeight) / 2 + (posY * canvasHeight);
1044
1045                // Draw texture
1046                offCtx.drawImage(textureMedia, drawX, drawY, drawWidth, drawHeight);
1047
1048                // Apply vignette/fade effect if enabled
1049                const fadeAmount = parseFloat(textureFadeInput.value) / 100;
1050                if (fadeAmount > 0) {
1051                    // Create radial gradient for vignette
1052                    const centerX = canvasWidth / 2;
1053                    const centerY = canvasHeight / 2;
1054                    const radius = Math.sqrt(centerX * centerX + centerY * centerY);
1055
1056                    const gradient = offCtx.createRadialGradient(
1057                        centerX, centerY, radius * (1 - fadeAmount),
1058                        centerX, centerY, radius
1059                    );
1060
1061                    gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
1062                    gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
1063
1064                    // Apply gradient as mask
1065                    offCtx.globalCompositeOperation = 'destination-in';
1066                    offCtx.fillStyle = gradient;
1067                    offCtx.fillRect(0, 0, canvasWidth, canvasHeight);
1068                    offCtx.globalCompositeOperation = 'source-over';
1069                }
1070
1071                // Apply person mask
1072                offCtx.globalCompositeOperation = 'destination-in';
1073                offCtx.drawImage(maskCanvas, 0, 0, canvasWidth, canvasHeight);
1074                offCtx.globalCompositeOperation = 'source-over';
1075
1076                // STEP 3: Draw texture-filled person with opacity
1077                const blendIntensity = parseFloat(videoBlendInput.value) / 100;
1078                ctx.globalAlpha = blendIntensity;
1079                ctx.drawImage(offCanvas, 0, 0);
1080                ctx.globalAlpha = 1.0;
1081
1082                // Only continue animation if texture is video
1083                if (textureType === 'video') {
1084                    animationId = requestAnimationFrame(render);
1085                }
1086            }
1087
1088            render(0);
1089
1090            // If texture is image, re-render only when controls change
1091            if (textureType === 'image') {
1092                const reRender = () => {
1093                    if (isProcessed) render(performance.now());
1094                };
1095
1096                videoBlendInput.removeEventListener('input', reRender);
1097                textureFadeInput.removeEventListener('input', reRender);
1098                bgBlurInput.removeEventListener('input', reRender);
1099                videoScaleInput.removeEventListener('input', reRender);
1100                videoPosXInput.removeEventListener('input', reRender);
1101                videoPosYInput.removeEventListener('input', reRender);
1102
1103                videoBlendInput.addEventListener('input', reRender);
1104                textureFadeInput.addEventListener('input', reRender);
1105                bgBlurInput.addEventListener('input', reRender);
1106                videoScaleInput.addEventListener('input', reRender);
1107                videoPosXInput.addEventListener('input', reRender);
1108                videoPosYInput.addEventListener('input', reRender);
1109            }
1110        }
1111
1112        // Control Updates
1113        videoBlendInput.addEventListener('input', (e) => {
1114            blendValue.textContent = e.target.value;
1115        });
1116
1117        textureFadeInput.addEventListener('input', (e) => {
1118            fadeValue.textContent = e.target.value;
1119        });
1120
1121        bgBlurInput.addEventListener('input', (e) => {
1122            blurValue.textContent = e.target.value;
1123        });
1124
1125        videoScaleInput.addEventListener('input', (e) => {
1126            scaleValue.textContent = e.target.value;
1127        });
1128
1129        videoPosXInput.addEventListener('input', (e) => {
1130            posXValue.textContent = e.target.value;
1131        });
1132
1133        videoPosYInput.addEventListener('input', (e) => {
1134            posYValue.textContent = e.target.value;
1135        });
1136
1137        // Download Image (for static textures)
1138        downloadImageBtn.addEventListener('click', () => {
1139            if (!isProcessed) return;
1140
1141            // Convert canvas to blob and download
1142            mainCanvas.toBlob((blob) => {
1143                const url = URL.createObjectURL(blob);
1144                const a = document.createElement('a');
1145                a.href = url;
1146                a.download = `double-exposure-${Date.now()}.png`;
1147                a.click();
1148                URL.revokeObjectURL(url);
1149            }, 'image/png');
1150        });
1151
1152        // Recording (for video textures)
1153        recordBtn.addEventListener('click', () => {
1154            if (!isProcessed) return;
1155
1156            recordBtn.classList.add('recording');
1157            recordBtn.disabled = true;
1158            recordBtn.textContent = '● Recording...';
1159            recordedChunks = [];
1160
1161            const stream = mainCanvas.captureStream(30);
1162            mediaRecorder = new MediaRecorder(stream, {
1163                mimeType: 'video/webm;codecs=vp9',
1164                videoBitsPerSecond: 5000000
1165            });
1166
1167            mediaRecorder.ondataavailable = (event) => {
1168                if (event.data.size > 0) {
1169                    recordedChunks.push(event.data);
1170                }
1171            };
1172
1173            mediaRecorder.onstop = () => {
1174                const blob = new Blob(recordedChunks, { type: 'video/webm' });
1175                const url = URL.createObjectURL(blob);
1176
1177                downloadVideoBtn.onclick = () => {
1178                    const a = document.createElement('a');
1179                    a.href = url;
1180                    a.download = `double-exposure-${Date.now()}.webm`;
1181                    a.click();
1182                };
1183
1184                downloadVideoBtn.style.display = 'inline-block';
1185                recordBtn.classList.remove('recording');
1186                recordBtn.disabled = false;
1187                recordBtn.textContent = '● Record (5s)';
1188            };
1189
1190            mediaRecorder.start();
1191
1192            setTimeout(() => {
1193                if (mediaRecorder && mediaRecorder.state === 'recording') {
1194                    mediaRecorder.stop();
1195                }
1196            }, 5000);
1197        });
1198    </script>
1198
1199</body>
1200
1201</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.