<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script> <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script> <div class="container"> <div class="header"> <h1>360° Image Viewer</h1> <p class="subtitle">Drag to rotate • Mouse wheel to zoom</p> </div> <div class="viewer-wrapper"> <div class="viewer-container" id="viewer"> <div class="loading-overlay" id="loadingOverlay"> <div class="spinner"></div> <div class="loading-text">Loading 360° view...</div> <div class="progress-bar"> <div class="progress-fill" id="progressFill"></div> </div> </div> </div> </div> <div class="controls"> <div class="control-group"> <div class="control-label"> <ion-icon name="sync-outline"></ion-icon> Rotation </div> <div class="slider-container"> <input type="range" class="slider" id="frameSlider" min="1" max="36" value="1"> <span class="slider-value" id="frameValue">1/36</span> </div> </div> <div class="control-group"> <div class="control-label"> <ion-icon name="speedometer-outline"></ion-icon> Auto-Rotate Speed </div> <div class="slider-container"> <input type="range" class="slider" id="speedSlider" min="10" max="100" value="30"> <span class="slider-value" id="speedValue">30ms</span> </div> </div> <div class="control-group"> <div class="button-group"> <button class="btn btn-primary" id="autoRotateBtn"> <ion-icon name="play-outline" id="autoRotateIcon"></ion-icon> <span id="autoRotateText">Auto Rotate</span> </button> <button class="btn btn-secondary" id="resetBtn"> <ion-icon name="refresh-outline"></ion-icon> Reset View </button> </div> </div> <div class="info-box"> <ion-icon name="information-circle-outline"></ion-icon> <span>Click and drag to rotate the view. Use your mouse wheel or trackpad to zoom in and out.</span> </div> </div> </div>
* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .container { max-width: 900px; width: 100%; background: white; border-radius: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden; } .header { padding: 30px; text-align: center; background: #f8fafc; border-bottom: 1px solid #e2e8f0; } h1 { font-size: 28px; color: #1a202c; margin-bottom: 8px; } .subtitle { color: #64748b; font-size: 14px; } .viewer-wrapper { position: relative; background: #000; } .viewer-container { position: relative; width: 100%; height: 500px; overflow: hidden; cursor: grab; user-select: none; background: #1a1a1a; } .viewer-container.dragging { cursor: grabbing; } .viewer-container img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; pointer-events: none; } .loading-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #1a1a1a; display: flex; flex-direction: column; align-items: center; justify-content: center; color: white; z-index: 10; transition: opacity 0.3s; } .loading-overlay.hidden { opacity: 0; pointer-events: none; } .spinner { width: 50px; height: 50px; border: 4px solid rgba(255, 255, 255, 0.2); border-top-color: #667eea; border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 20px; } @keyframes spin { to { transform: rotate(360deg); } } .loading-text { font-size: 14px; color: #94a3b8; } .progress-bar { width: 200px; height: 4px; background: rgba(255, 255, 255, 0.1); border-radius: 2px; margin-top: 16px; overflow: hidden; } .progress-fill { height: 100%; background: #667eea; width: 0%; transition: width 0.3s; } .controls { padding: 24px 30px; background: white; } .control-group { margin-bottom: 20px; } .control-group:last-child { margin-bottom: 0; } .control-label { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; color: #334155; margin-bottom: 12px; } .control-label ion-icon { color: #667eea; } .slider-container { display: flex; align-items: center; gap: 16px; } .slider { flex: 1; height: 6px; border-radius: 3px; background: #e2e8f0; outline: none; -webkit-appearance: none; cursor: pointer; } .slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #667eea; cursor: pointer; transition: all 0.3s; } .slider::-webkit-slider-thumb:hover { transform: scale(1.2); box-shadow: 0 0 0 8px rgba(102, 126, 234, 0.1); } .slider::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: #667eea; cursor: pointer; border: none; } .slider-value { min-width: 45px; text-align: right; font-size: 14px; color: #667eea; font-weight: 600; } .button-group { display: flex; gap: 12px; } .btn { flex: 1; padding: 12px 20px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 8px; } .btn-primary { background: #667eea; color: white; } .btn-primary:hover { background: #5568d3; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } .btn-secondary { background: #f1f5f9; color: #475569; } .btn-secondary:hover { background: #e2e8f0; } .info-box { display: flex; align-items: center; gap: 12px; padding: 16px; background: #f0f9ff; border-radius: 8px; font-size: 13px; color: #0369a1; } .info-box ion-icon { font-size: 20px; flex-shrink: 0; } @media (max-width: 768px) { .viewer-container { height: 350px; } h1 { font-size: 24px; } .button-group { flex-direction: column; } }
const viewer = document.getElementById('viewer'); const frameSlider = document.getElementById('frameSlider'); const frameValue = document.getElementById('frameValue'); const speedSlider = document.getElementById('speedSlider'); const speedValue = document.getElementById('speedValue'); const autoRotateBtn = document.getElementById('autoRotateBtn'); const autoRotateIcon = document.getElementById('autoRotateIcon'); const autoRotateText = document.getElementById('autoRotateText'); const resetBtn = document.getElementById('resetBtn'); const loadingOverlay = document.getElementById('loadingOverlay'); const progressFill = document.getElementById('progressFill'); const totalFrames = 36; let currentFrame = 1; let isDragging = false; let startX = 0; let rotationOffset = 0; let isAutoRotating = false; let autoRotateInterval = null; let rotationSpeed = 30; let images = []; let imagesLoaded = 0; // Generate images using placeholder service const baseUrl = 'https://picsum.photos/800/600?random='; // Preload all images function preloadImages() { for (let i = 1; i <= totalFrames; i++) { const img = new Image(); img.src = `${baseUrl}${i}`; img.style.display = 'none'; img.onload = () => { imagesLoaded++; const progress = (imagesLoaded / totalFrames) * 100; progressFill.style.width = progress + '%'; if (imagesLoaded === totalFrames) { setTimeout(() => { loadingOverlay.classList.add('hidden'); }, 500); } }; images.push(img); viewer.appendChild(img); } // Show first frame if (images.length > 0) { images[0].style.display = 'block'; } } function showFrame(frameNumber) { const index = ((frameNumber - 1) % totalFrames + totalFrames) % totalFrames; images.forEach((img, i) => { img.style.display = i === index ? 'block' : 'none'; }); currentFrame = index + 1; frameSlider.value = currentFrame; frameValue.textContent = `${currentFrame}/${totalFrames}`; } // Mouse drag controls viewer.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; viewer.classList.add('dragging'); stopAutoRotate(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const sensitivity = 0.2; const frameDelta = Math.floor(deltaX * sensitivity); if (frameDelta !== 0) { const newFrame = currentFrame + frameDelta; showFrame(newFrame); startX = e.clientX; } }); document.addEventListener('mouseup', () => { isDragging = false; viewer.classList.remove('dragging'); }); // Touch controls viewer.addEventListener('touchstart', (e) => { isDragging = true; startX = e.touches[0].clientX; stopAutoRotate(); }); viewer.addEventListener('touchmove', (e) => { if (!isDragging) return; const deltaX = e.touches[0].clientX - startX; const sensitivity = 0.2; const frameDelta = Math.floor(deltaX * sensitivity); if (frameDelta !== 0) { const newFrame = currentFrame + frameDelta; showFrame(newFrame); startX = e.touches[0].clientX; } }); viewer.addEventListener('touchend', () => { isDragging = false; }); // Frame slider frameSlider.addEventListener('input', (e) => { stopAutoRotate(); showFrame(parseInt(e.target.value)); }); // Speed slider speedSlider.addEventListener('input', (e) => { rotationSpeed = parseInt(e.target.value); speedValue.textContent = rotationSpeed + 'ms'; if (isAutoRotating) { stopAutoRotate(); startAutoRotate(); } }); // Auto rotate function startAutoRotate() { isAutoRotating = true; autoRotateIcon.name = 'pause-outline'; autoRotateText.textContent = 'Stop Rotate'; autoRotateBtn.classList.remove('btn-primary'); autoRotateBtn.classList.add('btn-secondary'); autoRotateInterval = setInterval(() => { showFrame(currentFrame + 1); }, rotationSpeed); } function stopAutoRotate() { isAutoRotating = false; autoRotateIcon.name = 'play-outline'; autoRotateText.textContent = 'Auto Rotate'; autoRotateBtn.classList.add('btn-primary'); autoRotateBtn.classList.remove('btn-secondary'); if (autoRotateInterval) { clearInterval(autoRotateInterval); autoRotateInterval = null; } } autoRotateBtn.addEventListener('click', () => { if (isAutoRotating) { stopAutoRotate(); } else { startAutoRotate(); } }); // Reset button resetBtn.addEventListener('click', () => { stopAutoRotate(); showFrame(1); }); // Initialize preloadImages();
Login to leave a comment
No comments yet. Be the first!
View Project
No comments yet. Be the first!