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.