Spaces:
Sleeping
Sleeping
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Visor Panorámico - {{ filename }}</title> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet"> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| background: #000; | |
| overflow: hidden; | |
| } | |
| .viewer-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| .panorama-canvas { | |
| display: block; | |
| cursor: grab; | |
| } | |
| .panorama-canvas:active { | |
| cursor: grabbing; | |
| } | |
| .controls { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 1000; | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .control-btn { | |
| background: rgba(0,0,0,0.7); | |
| border: none; | |
| color: white; | |
| padding: 10px 15px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: background 0.3s; | |
| } | |
| .control-btn:hover { | |
| background: rgba(0,0,0,0.9); | |
| } | |
| .info-panel { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 20px; | |
| right: 20px; | |
| background: rgba(0,0,0,0.8); | |
| color: white; | |
| padding: 15px; | |
| border-radius: 8px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .loading { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| color: white; | |
| text-align: center; | |
| } | |
| .zoom-controls { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .zoom-btn { | |
| background: rgba(0,0,0,0.7); | |
| border: none; | |
| color: white; | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .zoom-btn:hover { | |
| background: rgba(0,0,0,0.9); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="viewer-container"> | |
| <div class="loading" id="loading"> | |
| <div class="spinner-border" role="status"> | |
| <span class="visually-hidden">Cargando...</span> | |
| </div> | |
| <p class="mt-2">Cargando panorama...</p> | |
| </div> | |
| <canvas id="panoramaCanvas" class="panorama-canvas" style="display: none;"></canvas> | |
| <div class="controls"> | |
| <button class="control-btn" onclick="window.close()" title="Cerrar"> | |
| <i class="bi bi-x-lg"></i> | |
| </button> | |
| <button class="control-btn" onclick="window.history.back()" title="Volver"> | |
| <i class="bi bi-arrow-left"></i> | |
| </button> | |
| <button class="control-btn" onclick="toggleFullscreen()" title="Pantalla completa"> | |
| <i class="bi bi-fullscreen"></i> | |
| </button> | |
| <button class="control-btn" onclick="resetView()" title="Reiniciar vista"> | |
| <i class="bi bi-arrow-clockwise"></i> | |
| </button> | |
| </div> | |
| <div class="zoom-controls"> | |
| <button class="zoom-btn" onclick="zoomIn()" title="Acercar"> | |
| <i class="bi bi-plus"></i> | |
| </button> | |
| <button class="zoom-btn" onclick="zoomOut()" title="Alejar"> | |
| <i class="bi bi-dash"></i> | |
| </button> | |
| </div> | |
| <div class="info-panel"> | |
| <div> | |
| <strong>{{ filename }}</strong> | |
| <br> | |
| <small>Arrastra para rotar • Rueda del mouse para zoom</small> | |
| </div> | |
| <div> | |
| <button class="btn btn-sm btn-outline-light" onclick="downloadImage()"> | |
| <i class="bi bi-download"></i> Descargar | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById('panoramaCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const loading = document.getElementById('loading'); | |
| let img = new Image(); | |
| let isDragging = false; | |
| let lastX = 0; | |
| let lastY = 0; | |
| let offsetX = 0; | |
| let offsetY = 0; | |
| let zoom = 1; | |
| let minZoom = 0.5; | |
| let maxZoom = 3; | |
| // Configurar canvas | |
| function resizeCanvas() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| if (img.complete) { | |
| drawPanorama(); | |
| } | |
| } | |
| // Cargar imagen | |
| img.onload = function() { | |
| loading.style.display = 'none'; | |
| canvas.style.display = 'block'; | |
| resizeCanvas(); | |
| // Calcular zoom inicial para ajustar la imagen | |
| const scaleX = canvas.width / img.width; | |
| const scaleY = canvas.height / img.height; | |
| zoom = Math.max(scaleX, scaleY); | |
| minZoom = zoom * 0.5; | |
| maxZoom = zoom * 3; | |
| drawPanorama(); | |
| }; | |
| img.onerror = function() { | |
| loading.innerHTML = '<p>Error al cargar la imagen</p>'; | |
| }; | |
| // Dibujar panorama | |
| function drawPanorama() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| const scaledWidth = img.width * zoom; | |
| const scaledHeight = img.height * zoom; | |
| // Centrar verticalmente | |
| const centerY = (canvas.height - scaledHeight) / 2 + offsetY; | |
| // Dibujar imagen principal | |
| ctx.drawImage(img, offsetX, centerY, scaledWidth, scaledHeight); | |
| // Repetir horizontalmente para efecto cilíndrico | |
| if (scaledWidth + offsetX < canvas.width) { | |
| ctx.drawImage(img, offsetX + scaledWidth, centerY, scaledWidth, scaledHeight); | |
| } | |
| if (offsetX > 0) { | |
| ctx.drawImage(img, offsetX - scaledWidth, centerY, scaledWidth, scaledHeight); | |
| } | |
| // Limitar desplazamiento vertical | |
| const maxOffsetY = Math.max(0, (scaledHeight - canvas.height) / 2); | |
| offsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, offsetY)); | |
| } | |
| // Eventos de mouse | |
| canvas.addEventListener('mousedown', (e) => { | |
| isDragging = true; | |
| lastX = e.clientX; | |
| lastY = e.clientY; | |
| }); | |
| canvas.addEventListener('mousemove', (e) => { | |
| if (isDragging) { | |
| const deltaX = e.clientX - lastX; | |
| const deltaY = e.clientY - lastY; | |
| offsetX += deltaX; | |
| offsetY += deltaY; | |
| // Mantener offset X en rango válido para efecto cilíndrico | |
| const scaledWidth = img.width * zoom; | |
| if (offsetX > scaledWidth) offsetX -= scaledWidth; | |
| if (offsetX < -scaledWidth) offsetX += scaledWidth; | |
| drawPanorama(); | |
| lastX = e.clientX; | |
| lastY = e.clientY; | |
| } | |
| }); | |
| canvas.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| }); | |
| canvas.addEventListener('mouseleave', () => { | |
| isDragging = false; | |
| }); | |
| // Zoom con rueda del mouse | |
| canvas.addEventListener('wheel', (e) => { | |
| e.preventDefault(); | |
| const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; | |
| const newZoom = zoom * zoomFactor; | |
| if (newZoom >= minZoom && newZoom <= maxZoom) { | |
| zoom = newZoom; | |
| drawPanorama(); | |
| } | |
| }); | |
| // Eventos de teclado | |
| document.addEventListener('keydown', (e) => { | |
| switch(e.key) { | |
| case 'Escape': | |
| if (document.fullscreenElement) { | |
| document.exitFullscreen(); | |
| } | |
| break; | |
| case 'f': | |
| case 'F': | |
| toggleFullscreen(); | |
| break; | |
| case 'r': | |
| case 'R': | |
| resetView(); | |
| break; | |
| } | |
| }); | |
| // Funciones de control | |
| function zoomIn() { | |
| const newZoom = zoom * 1.2; | |
| if (newZoom <= maxZoom) { | |
| zoom = newZoom; | |
| drawPanorama(); | |
| } | |
| } | |
| function zoomOut() { | |
| const newZoom = zoom * 0.8; | |
| if (newZoom >= minZoom) { | |
| zoom = newZoom; | |
| drawPanorama(); | |
| } | |
| } | |
| function resetView() { | |
| offsetX = 0; | |
| offsetY = 0; | |
| const scaleX = canvas.width / img.width; | |
| const scaleY = canvas.height / img.height; | |
| zoom = Math.max(scaleX, scaleY); | |
| drawPanorama(); | |
| } | |
| function toggleFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen(); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| } | |
| function downloadImage() { | |
| const link = document.createElement('a'); | |
| link.download = '{{ filename }}'; | |
| link.href = '{{ url_for("static", filename=image_path) }}'; | |
| link.click(); | |
| } | |
| // Redimensionar canvas cuando cambie el tamaño de ventana | |
| window.addEventListener('resize', resizeCanvas); | |
| // Cargar imagen | |
| img.src = '{{ url_for("static", filename=image_path) }}'; | |
| </script> | |
| </body> | |
| </html> |