Spaces:
Running
Running
Download index.html from User-2468/anycoder-5b02769a: direct link, hf CLI and curl.
- Browser
- Download file 44.3 kB
-
https://huggingface.co/spaces/User-2468/anycoder-5b02769a/resolve/main/index.html
- Command line
-
hf download hf://spaces/User-2468/anycoder-5b02769a/index.html
-
curl -L -o index.html https://huggingface.co/spaces/User-2468/anycoder-5b02769a/resolve/main/index.html
44.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Focus Position Mapper</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --secondary: #ec4899; | |
| --bg-dark: #0f172a; | |
| --bg-card: #1e293b; | |
| --bg-elevated: #334155; | |
| --text-primary: #f8fafc; | |
| --text-secondary: #94a3b8; | |
| --border: #475569; | |
| --success: #22c55e; | |
| --warning: #f59e0b; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: var(--bg-dark); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Animated Background */ | |
| .bg-animation { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| overflow: hidden; | |
| } | |
| .bg-animation::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: | |
| radial-gradient(circle at 20% 80%, rgba(99, 102, 241, 0.15) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 20%, rgba(236, 72, 153, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 40% 40%, rgba(99, 102, 241, 0.08) 0%, transparent 40%); | |
| animation: bgMove 20s ease-in-out infinite; | |
| } | |
| @keyframes bgMove { | |
| 0%, 100% { transform: translate(0, 0) rotate(0deg); } | |
| 50% { transform: translate(-5%, -5%) rotate(5deg); } | |
| } | |
| /* Header */ | |
| header { | |
| background: rgba(30, 41, 59, 0.8); | |
| backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--border); | |
| padding: 1rem 2rem; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .header-content { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .logo-icon { | |
| width: 40px; | |
| height: 40px; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.25rem; | |
| } | |
| .logo-text { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .anycoder-link { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| padding: 0.5rem 1rem; | |
| border-radius: 8px; | |
| background: rgba(99, 102, 241, 0.1); | |
| border: 1px solid rgba(99, 102, 241, 0.2); | |
| transition: all 0.3s ease; | |
| } | |
| .anycoder-link:hover { | |
| background: rgba(99, 102, 241, 0.2); | |
| border-color: var(--primary); | |
| } | |
| /* Main Content */ | |
| main { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| } | |
| /* Tabs */ | |
| .tabs-container { | |
| display: flex; | |
| justify-content: center; | |
| margin-bottom: 2rem; | |
| } | |
| .tabs { | |
| display: flex; | |
| background: var(--bg-card); | |
| border-radius: 16px; | |
| padding: 6px; | |
| border: 1px solid var(--border); | |
| } | |
| .tab { | |
| padding: 1rem 2.5rem; | |
| border: none; | |
| background: transparent; | |
| color: var(--text-secondary); | |
| font-size: 1rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| border-radius: 12px; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .tab:hover { | |
| color: var(--text-primary); | |
| background: rgba(99, 102, 241, 0.1); | |
| } | |
| .tab.active { | |
| background: linear-gradient(135deg, var(--primary), var(--primary-hover)); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); | |
| } | |
| .tab-icon { | |
| font-size: 1.25rem; | |
| } | |
| /* Panels */ | |
| .panel { | |
| display: none; | |
| } | |
| .panel.active { | |
| display: block; | |
| animation: fadeIn 0.4s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Workspace */ | |
| .workspace { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| @media (max-width: 1024px) { | |
| .workspace { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .media-panel { | |
| background: var(--bg-card); | |
| border-radius: 20px; | |
| border: 1px solid var(--border); | |
| overflow: hidden; | |
| } | |
| .panel-header { | |
| padding: 1.25rem 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .panel-title { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .panel-badge { | |
| font-size: 0.75rem; | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 20px; | |
| font-weight: 500; | |
| } | |
| .badge-source { | |
| background: rgba(99, 102, 241, 0.2); | |
| color: var(--primary); | |
| } | |
| .badge-reference { | |
| background: rgba(236, 72, 153, 0.2); | |
| color: var(--secondary); | |
| } | |
| /* Canvas Container */ | |
| .canvas-container { | |
| position: relative; | |
| width: 100%; | |
| aspect-ratio: 16/9; | |
| background: var(--bg-dark); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| } | |
| .canvas-container canvas, | |
| .canvas-container video { | |
| max-width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| } | |
| .placeholder { | |
| text-align: center; | |
| color: var(--text-secondary); | |
| padding: 2rem; | |
| } | |
| .placeholder-icon { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| opacity: 0.5; | |
| } | |
| .placeholder-text { | |
| font-size: 0.9rem; | |
| } | |
| /* Upload Area */ | |
| .upload-area { | |
| padding: 1.5rem; | |
| border-top: 1px solid var(--border); | |
| } | |
| .upload-btn { | |
| width: 100%; | |
| padding: 1rem; | |
| border: 2px dashed var(--border); | |
| background: transparent; | |
| color: var(--text-secondary); | |
| border-radius: 12px; | |
| cursor: pointer; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| } | |
| .upload-btn:hover { | |
| border-color: var(--primary); | |
| color: var(--primary); | |
| background: rgba(99, 102, 241, 0.1); | |
| } | |
| .file-input { | |
| display: none; | |
| } | |
| /* Controls */ | |
| .controls { | |
| background: var(--bg-card); | |
| border-radius: 20px; | |
| border: 1px solid var(--border); | |
| padding: 1.5rem; | |
| } | |
| .controls-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| margin-bottom: 1.5rem; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| } | |
| .controls-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .control-label { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| } | |
| .control-value { | |
| font-size: 0.9rem; | |
| color: var(--text-primary); | |
| font-weight: 600; | |
| } | |
| /* Buttons */ | |
| .btn { | |
| padding: 0.75rem 1.5rem; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary), var(--primary-hover)); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); | |
| } | |
| .btn-secondary { | |
| background: var(--bg-elevated); | |
| color: var(--text-primary); | |
| border: 1px solid var(--border); | |
| } | |
| .btn-secondary:hover { | |
| background: var(--border); | |
| } | |
| .btn-danger { | |
| background: rgba(239, 68, 68, 0.2); | |
| color: #ef4444; | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| } | |
| .btn-danger:hover { | |
| background: rgba(239, 68, 68, 0.3); | |
| } | |
| /* Status Display */ | |
| .status-display { | |
| background: var(--bg-card); | |
| border-radius: 20px; | |
| border: 1px solid var(--border); | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .status-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| margin-bottom: 1rem; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| } | |
| .status-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .status-item { | |
| background: var(--bg-elevated); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| text-align: center; | |
| } | |
| .status-label { | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.25rem; | |
| } | |
| .status-value { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| color: var(--primary); | |
| } | |
| .status-value.reference { | |
| color: var(--secondary); | |
| } | |
| /* Focus Point Visualization */ | |
| .focus-point { | |
| position: absolute; | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| background: var(--primary); | |
| border: 3px solid white; | |
| box-shadow: 0 0 20px var(--primary), 0 0 40px var(--primary); | |
| transform: translate(-50%, -50%); | |
| pointer-events: none; | |
| z-index: 10; | |
| animation: pulse 1.5s ease-in-out infinite; | |
| } | |
| .focus-point.reference { | |
| background: var(--secondary); | |
| box-shadow: 0 0 20px var(--secondary), 0 0 40px var(--secondary); | |
| } | |
| .focus-point::after { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 8px; | |
| height: 8px; | |
| background: white; | |
| border-radius: 50%; | |
| transform: translate(-50%, -50%); | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; } | |
| 50% { transform: translate(-50%, -50%) scale(1.2); opacity: 0.8; } | |
| } | |
| /* Crosshair Lines */ | |
| .crosshair { | |
| position: absolute; | |
| background: rgba(255, 255, 255, 0.3); | |
| pointer-events: none; | |
| z-index: 5; | |
| } | |
| .crosshair-h { | |
| width: 100%; | |
| height: 1px; | |
| top: 50%; | |
| left: 0; | |
| } | |
| .crosshair-v { | |
| width: 1px; | |
| height: 100%; | |
| top: 0; | |
| left: 50%; | |
| } | |
| /* Video Controls */ | |
| .video-controls { | |
| display: flex; | |
| gap: 0.5rem; | |
| padding: 1rem 1.5rem; | |
| border-top: 1px solid var(--border); | |
| align-items: center; | |
| } | |
| .video-controls .btn { | |
| padding: 0.5rem 1rem; | |
| font-size: 0.85rem; | |
| } | |
| .video-progress { | |
| flex: 1; | |
| height: 6px; | |
| background: var(--bg-elevated); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| cursor: pointer; | |
| } | |
| .video-progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| width: 0%; | |
| transition: width 0.1s linear; | |
| } | |
| .video-time { | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| min-width: 80px; | |
| text-align: center; | |
| } | |
| /* Coordinate Display Overlay */ | |
| .coord-display { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 0.5rem 0.75rem; | |
| border-radius: 8px; | |
| font-size: 0.75rem; | |
| font-family: monospace; | |
| z-index: 10; | |
| } | |
| /* Mapping Line */ | |
| .mapping-line { | |
| position: absolute; | |
| height: 2px; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| transform-origin: left center; | |
| z-index: 8; | |
| pointer-events: none; | |
| opacity: 0.6; | |
| } | |
| /* Toast Notifications */ | |
| .toast-container { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .toast { | |
| padding: 1rem 1.5rem; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); | |
| animation: slideIn 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .toast.success { | |
| border-color: var(--success); | |
| } | |
| .toast.error { | |
| border-color: #ef4444; | |
| } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| /* Info Box */ | |
| .info-box { | |
| background: rgba(99, 102, 241, 0.1); | |
| border: 1px solid rgba(99, 102, 241, 0.2); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| margin-bottom: 1.5rem; | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 0.75rem; | |
| } | |
| .info-box-icon { | |
| font-size: 1.25rem; | |
| } | |
| .info-box-text { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| line-height: 1.5; | |
| } | |
| .info-box-text strong { | |
| color: var(--text-primary); | |
| } | |
| /* Action Bar */ | |
| .action-bar { | |
| display: flex; | |
| gap: 1rem; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| margin-top: 2rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-animation"></div> | |
| <header> | |
| <div class="header-content"> | |
| <div class="logo"> | |
| <div class="logo-icon">🎯</div> | |
| <span class="logo-text">Focus Position Mapper</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder | |
| </a> | |
| </div> | |
| </header> | |
| <main> | |
| <!-- Tabs --> | |
| <div class="tabs-container"> | |
| <div class="tabs"> | |
| <button class="tab active" data-tab="video"> | |
| <span class="tab-icon">🎬</span> | |
| Video Mode | |
| </button> | |
| <button class="tab" data-tab="image"> | |
| <span class="tab-icon">🖼️</span> | |
| Image Mode | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Video Panel --> | |
| <div class="panel active" id="video-panel"> | |
| <div class="info-box"> | |
| <span class="info-box-icon">💡</span> | |
| <div class="info-box-text"> | |
| <strong>Video Mode:</strong> Upload a source video and a reference video. The focus point from the source will be mapped to the reference video position. | |
| Click on the source video to set focus points manually, or use auto-tracking. | |
| </div> | |
| </div> | |
| <div class="workspace"> | |
| <!-- Source Video --> | |
| <div class="media-panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title"> | |
| <span>📹</span> Source Video | |
| </span> | |
| <span class="panel-badge badge-source">Input</span> | |
| </div> | |
| <div class="canvas-container" id="source-container"> | |
| <div class="placeholder"> | |
| <div class="placeholder-icon">📹</div> | |
| <div class="placeholder-text">Upload a source video</div> | |
| </div> | |
| <canvas id="source-canvas" style="display: none;"></canvas> | |
| <video id="source-video" style="display: none;" muted playsinline></video> | |
| <div class="coord-display" id="source-coords" style="display: none;">X: 0% | Y: 0%</div> | |
| <div class="focus-point" id="source-focus" style="display: none;"></div> | |
| <div class="crosshair crosshair-h" style="display: none;"></div> | |
| <div class="crosshair crosshair-v" style="display: none;"></div> | |
| </div> | |
| <div class="upload-area"> | |
| <button class="upload-btn" onclick="document.getElementById('source-video-input').click()"> | |
| <span>📁</span> Upload Source Video | |
| </button> | |
| <input type="file" id="source-video-input" class="file-input" accept="video/*"> | |
| </div> | |
| <div class="video-controls" id="source-video-controls" style="display: none;"> | |
| <button class="btn btn-secondary" id="play-pause-btn">▶</button> | |
| <div class="video-progress" id="video-progress"> | |
| <div class="video-progress-bar" id="video-progress-bar"></div> | |
| </div> | |
| <span class="video-time" id="video-time">0:00 / 0:00</span> | |
| </div> | |
| </div> | |
| <!-- Reference Video --> | |
| <div class="media-panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title"> | |
| <span>🎬</span> Reference Video | |
| </span> | |
| <span class="panel-badge badge-reference">Output</span> | |
| </div> | |
| <div class="canvas-container" id="reference-container"> | |
| <div class="placeholder"> | |
| <div class="placeholder-icon">🎬</div> | |
| <div class="placeholder-text">Upload a reference video</div> | |
| </div> | |
| <canvas id="reference-canvas" style="display: none;"></canvas> | |
| <video id="reference-video" style="display: none;" muted playsinline></video> | |
| <div class="coord-display" id="reference-coords" style="display: none;">X: 0% | Y: 0%</div> | |
| <div class="focus-point reference" id="reference-focus" style="display: none;"></div> | |
| <div class="crosshair crosshair-h" style="display: none;"></div> | |
| <div class="crosshair crosshair-v" style="display: none;"></div> | |
| </div> | |
| <div class="upload-area"> | |
| <button class="upload-btn" onclick="document.getElementById('reference-video-input').click()"> | |
| <span>📁</span> Upload Reference Video | |
| </button> | |
| <input type="file" id="reference-video-input" class="file-input" accept="video/*"> | |
| </div> | |
| <div class="video-controls" id="reference-video-controls" style="display: none;"> | |
| <button class="btn btn-secondary" id="ref-play-pause-btn">▶</button> | |
| <div class="video-progress" id="ref-video-progress"> | |
| <div class="video-progress-bar" id="ref-video-progress-bar"></div> | |
| </div> | |
| <span class="video-time" id="ref-video-time">0:00 / 0:00</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Image Panel --> | |
| <div class="panel" id="image-panel"> | |
| <div class="info-box"> | |
| <span class="info-box-icon">💡</span> | |
| <div class="info-box-text"> | |
| <strong>Image Mode:</strong> Upload a source image and a reference image. Click on the source image to set focus points, | |
| and see them mapped to the reference image position in real-time. | |
| </div> | |
| </div> | |
| <div class="workspace"> | |
| <!-- Source Image --> | |
| <div class="media-panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title"> | |
| <span>🖼️</span> Source Image | |
| </span> | |
| <span class="panel-badge badge-source">Input</span> | |
| </div> | |
| <div class="canvas-container" id="source-img-container"> | |
| <div class="placeholder"> | |
| <div class="placeholder-icon">🖼️</div> | |
| <div class="placeholder-text">Upload a source image</div> | |
| </div> | |
| <canvas id="source-img-canvas" style="display: none;"></canvas> | |
| <img id="source-img" style="display: none;" alt="Source"> | |
| <div class="coord-display" id="source-img-coords" style="display: none;">X: 0% | Y: 0%</div> | |
| <div class="focus-point" id="source-img-focus" style="display: none;"></div> | |
| <div class="crosshair crosshair-h" style="display: none;"></div> | |
| <div class="crosshair crosshair-v" style="display: none;"></div> | |
| </div> | |
| <div class="upload-area"> | |
| <button class="upload-btn" onclick="document.getElementById('source-img-input').click()"> | |
| <span>📁</span> Upload Source Image | |
| </button> | |
| <input type="file" id="source-img-input" class="file-input" accept="image/*"> | |
| </div> | |
| </div> | |
| <!-- Reference Image --> | |
| <div class="media-panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title"> | |
| <span>🖼️</span> Reference Image | |
| </span> | |
| <span class="panel-badge badge-reference">Output</span> | |
| </div> | |
| <div class="canvas-container" id="reference-img-container"> | |
| <div class="placeholder"> | |
| <div class="placeholder-icon">🖼️</div> | |
| <div class="placeholder-text">Upload a reference image</div> | |
| </div> | |
| <canvas id="reference-img-canvas" style="display: none;"></canvas> | |
| <img id="reference-img" style="display: none;" alt="Reference"> | |
| <div class="coord-display" id="reference-img-coords" style="display: none;">X: 0% | Y: 0%</div> | |
| <div class="focus-point reference" id="reference-img-focus" style="display: none;"></div> | |
| <div class="crosshair crosshair-h" style="display: none;"></div> | |
| <div class="crosshair crosshair-v" style="display: none;"></div> | |
| </div> | |
| <div class="upload-area"> | |
| <button class="upload-btn" onclick="document.getElementById('reference-img-input').click()"> | |
| <span>📁</span> Upload Reference Image | |
| </button> | |
| <input type="file" id="reference-img-input" class="file-input" accept="image/*"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Status Display --> | |
| <div class="status-display"> | |
| <div class="status-header"> | |
| <span>📊</span> Position Mapping Status | |
| </div> | |
| <div class="status-grid"> | |
| <div class="status-item"> | |
| <div class="status-label">Source Position</div> | |
| <div class="status-value" id="status-source">--</div> | |
| </div> | |
| <div class="status-item"> | |
| <div class="status-label">Reference Position</div> | |
| <div class="status-value reference" id="status-reference">--</div> | |
| </div> | |
| <div class="status-item"> | |
| <div class="status-label">Mapping Mode</div> | |
| <div class="status-value" id="status-mode">Manual</div> | |
| </div> | |
| <div class="status-item"> | |
| <div class="status-label">Active Points</div> | |
| <div class="status-value" id="status-points">0</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Controls --> | |
| <div class="controls"> | |
| <div class="controls-header"> | |
| <span>⚙️</span> Controls | |
| </div> | |
| <div class="controls-grid"> | |
| <div class="control-group"> | |
| <span class="control-label">Source Focus X</span> | |
| <span class="control-value" id="focus-x">--</span> | |
| </div> | |
| <div class="control-group"> | |
| <span class="control-label">Source Focus Y</span> | |
| <span class="control-value" id="focus-y">--</span> | |
| </div> | |
| <div class="control-group"> | |
| <span class="control-label">Reference Focus X</span> | |
| <span class="control-value" id="ref-focus-x">--</span> | |
| </div> | |
| <div class="control-group"> | |
| <span class="control-label">Reference Focus Y</span> | |
| <span class="control-value" id="ref-focus-y">--</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Action Bar --> | |
| <div class="action-bar"> | |
| <button class="btn btn-primary" id="start-mapping"> | |
| <span>🎯</span> Start Mapping | |
| </button> | |
| <button class="btn btn-secondary" id="auto-track"> | |
| <span>🤖</span> Auto Track | |
| </button> | |
| <button class="btn btn-secondary" id="sync-videos"> | |
| <span>🔗</span> Sync Videos | |
| </button> | |
| <button class="btn btn-danger" id="clear-all"> | |
| <span>🗑️</span> Clear All | |
| </button> | |
| </div> | |
| </main> | |
| <div class="toast-container" id="toast-container"></div> | |
| <script> | |
| // Application State | |
| const state = { | |
| mode: 'video', // 'video' or 'image' | |
| sourceFocus: { x: 50, y: 50 }, | |
| referenceFocus: { x: 50, y: 50 }, | |
| isPlaying: false, | |
| isReferencePlaying: false, | |
| sourceLoaded: false, | |
| referenceLoaded: false, | |
| autoTrack: false, | |
| focusPoints: [] | |
| }; | |
| // DOM Elements | |
| const elements = { | |
| tabs: document.querySelectorAll('.tab'), | |
| panels: document.querySelectorAll('.panel'), | |
| sourceVideo: document.getElementById('source-video'), | |
| sourceCanvas: document.getElementById('source-canvas'), | |
| referenceVideo: document.getElementById('reference-video'), | |
| referenceCanvas: document.getElementById('reference-canvas'), | |
| sourceImg: document.getElementById('source-img'), | |
| sourceImgCanvas: document.getElementById('source-img-canvas'), | |
| referenceImg: document.getElementById('reference-img'), | |
| referenceImgCanvas: document.getElementById('reference-img-canvas'), | |
| sourceContainer: document.getElementById('source-container'), | |
| referenceContainer: document.getElementById('reference-container'), | |
| sourceFocus: document.getElementById('source-focus'), | |
| referenceFocus: document.getElementById('reference-focus'), | |
| sourceImgFocus: document.getElementById('source-img-focus'), | |
| referenceImgFocus: document.getElementById('reference-img-focus'), | |
| sourceCoords: document.getElementById('source-coords'), | |
| referenceCoords: document.getElementById('reference-coords'), | |
| sourceImgCoords: document.getElementById('source-img-coords'), | |
| referenceImgCoords: document.getElementById('reference-img-coords') | |
| }; | |
| // Tab Switching | |
| elements.tabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| const tabId = tab.dataset.tab; | |
| state.mode = tabId; | |
| elements.tabs.forEach(t => t.classList.remove('active')); | |
| elements.panels.forEach(p => p.classList.remove('active')); | |
| tab.classList.add('active'); | |
| document.getElementById(`${tabId}-panel`).classList.add('active'); | |
| showToast(`Switched to ${tabId === 'video' ? 'Video' : 'Image'} Mode`, 'success'); | |
| }); | |
| }); | |
| // File Upload Handlers | |
| document.getElementById('source-video-input').addEventListener('change', handleVideoUpload); | |
| document.getElementById('reference-video-input').addEventListener('change', handleReferenceVideoUpload); | |
| document.getElementById('source-img-input').addEventListener('change', handleImageUpload); | |
| document.getElementById('reference-img-input').addEventListener('change', handleReferenceImageUpload); | |
| function handleVideoUpload(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const url = URL.createObjectURL(file); | |
| elements.sourceVideo.src = url; | |
| elements.sourceVideo.style.display = 'block'; | |
| elements.sourceVideo.parentElement.querySelector('.placeholder').style.display = 'none'; | |
| elements.sourceVideo.addEventListener('loadeddata', () => { | |
| state.sourceLoaded = true; | |
| setupVideoControls('source'); | |
| showToast('Source video loaded!', 'success'); | |
| setupClickHandler('video', 'source'); | |
| }); | |
| } | |
| function handleReferenceVideoUpload(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const url = URL.createObjectURL(file); | |
| elements.referenceVideo.src = url; | |
| elements.referenceVideo.style.display = 'block'; | |
| elements.referenceVideo.parentElement.querySelector('.placeholder').style.display = 'none'; | |
| elements.referenceVideo.addEventListener('loadeddata', () => { | |
| state.referenceLoaded = true; | |
| setupVideoControls('reference'); | |
| showToast('Reference video loaded!', 'success'); | |
| }); | |
| } | |
| function handleImageUpload(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const url = URL.createObjectURL(file); | |
| elements.sourceImg.src = url; | |
| elements.sourceImg.style.display = 'block'; | |
| elements.sourceImg.parentElement.querySelector('.placeholder').style.display = 'none'; | |
| elements.sourceImg.onload = () => { | |
| state.sourceLoaded = true; | |
| showToast('Source image loaded!', 'success'); | |
| setupClickHandler('image', 'source'); | |
| }; | |
| } | |
| function handleReferenceImageUpload(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const url = URL.createObjectURL(file); | |
| elements.referenceImg.src = url; | |
| elements.referenceImg.style.display = 'block'; | |
| elements.referenceImg.parentElement.querySelector('.placeholder').style.display = 'none'; | |
| elements.referenceImg.onload = () => { | |
| state.referenceLoaded = true; | |
| showToast('Reference image loaded!', 'success'); | |
| setupClickHandler('image', 'reference'); | |
| }; | |
| } | |
| // Video Controls | |
| function setupVideoControls(type) { | |
| const video = type === 'source' ? elements.sourceVideo : elements.referenceVideo; | |
| const controlsId = type === 'source' ? 'source-video-controls' : 'reference-video-controls'; | |
| const progressBarId = type === 'source' ? 'video-progress-bar' : 'ref-video-progress-bar'; | |
| const timeId = type === 'source' ? 'video-time' : 'ref-video-time'; | |
| const progressId = type === 'source' ? 'video-progress' : 'ref-video-progress'; | |
| const playBtnId = type === 'source' ? 'play-pause-btn' : 'ref-play-pause-btn'; | |
| document.getElementById(controlsId).style.display = 'flex'; | |
| video.addEventListener('timeupdate', () => { | |
| const progress = (video.currentTime / video.duration) * 100; | |
| document.getElementById(progressBarId).style.width = `${progress}%`; | |
| document.getElementById(timeId).textContent = | |
| `${formatTime(video.currentTime)} / ${formatTime(video.duration)}`; | |
| }); | |
| document.getElementById(progressId).addEventListener('click', (e) => { | |
| const rect = e.target.getBoundingClientRect(); | |
| const pos = (e.clientX - rect.left) / rect.width; | |
| video.currentTime = pos * video.duration; | |
| }); | |
| document.getElementById(playBtnId).addEventListener('click', () => { | |
| if (video.paused) { | |
| video.play(); | |
| document.getElementById(playBtnId).textContent = '⏸'; | |
| } else { | |
| video.pause(); | |
| document.getElementById(playBtnId).textContent = '▶'; | |
| } | |
| }); | |
| } | |
| function formatTime(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| // Click Handler for Focus Points | |
| function setupClickHandler(mode, containerType) { | |
| const container = containerType === 'source' | |
| ? (mode === 'video' ? elements.sourceContainer : document.getElementById('source-img-container')) | |
| : (mode === 'video' ? elements.referenceContainer : document.getElementById('reference-img-container')); | |
| const focusEl = containerType === 'source' | |
| ? (mode === 'video' ? elements.sourceFocus : elements.sourceImgFocus) | |
| : (mode === 'video' ? elements.referenceFocus : elements.referenceImgFocus); | |
| const coordsEl = containerType === 'source' | |
| ? (mode === 'video' ? elements.sourceCoords : elements.sourceImgCoords) | |
| : (mode === 'video' ? elements.referenceCoords : elements.referenceImgCoords); | |
| const crosshairs = container.querySelectorAll('.crosshair'); | |
| container.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('video-controls') || | |
| e.target.closest('.video-controls') || | |
| e.target.classList.contains('upload-area') || | |
| e.target.closest('.upload-area')) { | |
| return; | |
| } | |
| const rect = container.getBoundingClientRect(); | |
| const x = ((e.clientX - rect.left) / rect.width) * 100; | |
| const y = ((e.clientY - rect.top) / rect.height) * 100; | |
| if (containerType === 'source') { | |
| state.sourceFocus = { x, y }; | |
| updateFocusDisplay(focusEl, coordsEl, x, y, crosshairs); | |
| updateStatus('source', x, y); | |
| // Map to reference | |
| if (state.referenceLoaded) { | |
| mapPositionToReference(x, y); | |
| } | |
| } else { | |
| state.referenceFocus = { x, y }; | |
| const refFocusEl = mode === 'video' ? elements.referenceFocus : elements.referenceImgFocus; | |
| const refCoordsEl = mode === 'video' ? elements.referenceCoords : elements.referenceImgCoords; | |
| updateFocusDisplay(refFocusEl, refCoordsEl, x, y, crosshairs); | |
| updateStatus('reference', x, y); | |
| } | |
| showToast(`Focus set at ${Math.round(x)}%, ${Math.round(y)}%`, 'success'); | |
| }); | |
| } | |
| function updateFocusDisplay(focusEl, coordsEl, x, y, crosshairs) { | |
| focusEl.style.display = 'block'; | |
| focusEl.style.left = `${x}%`; | |
| focusEl.style.top = `${y}%`; | |
| coordsEl.style.display = 'block'; | |
| coordsEl.textContent = `X: ${Math.round(x)}% | Y: ${Math.round(y)}%`; | |
| crosshairs.forEach(ch => ch.style.display = 'block'); | |
| } | |
| function mapPositionToReference(sourceX, sourceY) { | |
| // Calculate mapped position (same relative position) | |
| const mappedX = sourceX; | |
| const mappedY = sourceY; | |
| state.referenceFocus = { x: mappedX, y: mappedY }; | |
| const mode = state.mode; | |
| const focusEl = mode === 'video' ? elements.referenceFocus : elements.referenceImgFocus; | |
| const coordsEl = mode === 'video' ? elements.referenceCoords : elements.referenceImgCoords; | |
| const container = mode === 'video' ? elements.referenceContainer : document.getElementById('reference-img-container'); | |
| const crosshairs = container.querySelectorAll('.crosshair'); | |
| updateFocusDisplay(focusEl, coordsEl, mappedX, mappedY, crosshairs); | |
| updateStatus('reference', mappedX, mappedY); | |
| } | |
| function updateStatus(type, x, y) { | |
| if (type === 'source') { | |
| document.getElementById('status-source').textContent = `${Math.round(x)}%, ${Math.round(y)}%`; | |
| document.getElementById('focus-x').textContent = `${Math.round(x)}%`; | |
| document.getElementById('focus-y').textContent = `${Math.round(y)}%`; | |
| } else { | |
| document.getElementById('status-reference').textContent = `${Math.round(x)}%, ${Math.round(y)}%`; | |
| document.getElementById('ref-focus-x').textContent = `${Math.round(x)}%`; | |
| document.getElementById('ref-focus-y').textContent = `${Math.round(y)}%`; | |
| } | |
| document.getElementById('status-points').textContent = | |
| (state.sourceLoaded ? 1 : 0) + (state.referenceLoaded ? 1 : 0); | |
| } | |
| // Action Buttons | |
| document.getElementById('start-mapping').addEventListener('click', () => { | |
| if (!state.sourceLoaded) { | |
| showToast('Please upload a source first!', 'error'); | |
| return; | |
| } | |
| if (state.referenceLoaded) { | |
| mapPositionToReference(state.sourceFocus.x, state.sourceFocus.y); | |
| showToast('Position mapping complete!', 'success'); | |
| } else { | |
| showToast('Please upload a reference to map to!', 'error'); | |
| } | |
| }); | |
| document.getElementById('auto-track').addEventListener('click', () => { | |
| state.autoTrack = !state.autoTrack; | |
| document.getElementById('status-mode').textContent = state.autoTrack ? 'Auto Track' : 'Manual'; | |
| if (state.autoTrack && state.mode === 'video' && state.sourceLoaded) { | |
| showToast('Auto tracking enabled! Click on video to set initial point.', 'success'); | |
| startAutoTracking(); | |
| } else { | |
| showToast('Auto tracking disabled!', 'success'); | |
| } | |
| }); | |
| function startAutoTracking() { | |
| if (!state.autoTrack) return; | |
| const video = elements.sourceVideo; | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let lastPos = { x: state.sourceFocus.x, y: state.sourceFocus.y }; | |
| let frameCount = 0; | |
| function trackFrame() { | |
| if (!state.autoTrack || video.paused || video.ended) { | |
| requestAnimationFrame(trackFrame); | |
| return; | |
| } | |
| frameCount++; | |
| if (frameCount % 5 === 0) { // Process every 5th frame for performance | |
| canvas.width = video.videoWidth; | |
| canvas.height = video.videoHeight; | |
| ctx.drawImage(video, 0, 0); | |
| // Simple motion detection - find brightest/most active area | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| // This is a simplified motion detection | |
| // In a real app, you'd use more sophisticated algorithms | |
| let motionX = lastPos.x; | |
| let motionY = lastPos.y; |