* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #0f172a;
min-height: 100vh;
overflow: hidden;
}
.viewer-container {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
cursor: grab;
user-select: none;
}
.viewer-container.dragging {
cursor: grabbing;
}
.panorama-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.panorama-image {
position: absolute;
height: 100%;
min-width: 100%;
object-fit: cover;
left: 0;
top: 0;
will-change: transform;
transition: transform 0.1s ease-out;
}
.controls {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 12px;
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(10px);
padding: 16px;
border-radius: 50px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
}
.control-btn {
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(59, 130, 246, 0.2);
border: 2px solid rgba(59, 130, 246, 0.3);
color: #60a5fa;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
font-size: 24px;
}
.control-btn:hover {
background: rgba(59, 130, 246, 0.4);
border-color: rgba(59, 130, 246, 0.6);
color: #93c5fd;
transform: scale(1.1);
}
.control-btn:active {
transform: scale(0.95);
}
.control-btn.active {
background: rgba(59, 130, 246, 0.6);
border-color: #3b82f6;
color: white;
}
.info-panel {
position: absolute;
top: 30px;
left: 30px;
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(10px);
padding: 20px 24px;
border-radius: 16px;
color: white;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
max-width: 300px;
}
.info-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.info-title ion-icon {
font-size: 24px;
color: #60a5fa;
}
.info-subtitle {
font-size: 14px;
color: #94a3b8;
margin-bottom: 16px;
}
.info-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 13px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
color: #94a3b8;
}
.info-value {
color: #60a5fa;
font-weight: 600;
}
.instructions {
position: absolute;
top: 30px;
right: 30px;
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(10px);
padding: 16px 20px;
border-radius: 16px;
color: white;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
max-width: 250px;
}
.instructions-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
color: #60a5fa;
}
.instruction-item {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
font-size: 13px;
color: #cbd5e1;
}
.instruction-item ion-icon {
font-size: 18px;
color: #60a5fa;
}
.zoom-slider {
position: absolute;
bottom: 30px;
right: 30px;
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(10px);
padding: 16px;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.zoom-label {
color: #94a3b8;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.slider {
-webkit-appearance: none;
appearance: none;
width: 120px;
height: 6px;
border-radius: 3px;
background: rgba(59, 130, 246, 0.2);
outline: none;
transform: rotate(-90deg);
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
transition: all 0.3s;
}
.slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.6);
}
.slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
border: none;
}
.zoom-value {
color: #60a5fa;
font-size: 14px;
font-weight: 600;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0f172a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
transition: opacity 0.5s;
}
.loading-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.spinner {
width: 60px;
height: 60px;
border: 6px solid rgba(59, 130, 246, 0.2);
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: #94a3b8;
font-size: 16px;
}
.fullscreen-btn {
position: absolute;
top: 30px;
right: 30px;
width: 48px;
height: 48px;
border-radius: 12px;
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #60a5fa;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
font-size: 24px;
z-index: 11;
}
.fullscreen-btn:hover {
background: rgba(59, 130, 246, 0.3);
border-color: rgba(59, 130, 246, 0.6);
color: #93c5fd;
}
@media (max-width: 768px) {
.info-panel,
.instructions {
display: none;
}
.controls {
bottom: 20px;
padding: 12px;
gap: 8px;
}
.control-btn {
width: 44px;
height: 44px;
font-size: 20px;
}
.zoom-slider {
bottom: 20px;
right: 20px;
padding: 12px;
}
}
const viewer = document.getElementById('viewer');
const panoramaWrapper = document.getElementById('panoramaWrapper');
const panoramaImage = document.getElementById('panoramaImage');
const loadingOverlay = document.getElementById('loadingOverlay');
const rotationValue = document.getElementById('rotationValue');
const zoomValueDisplay = document.getElementById('zoomValueDisplay');
const modeValue = document.getElementById('modeValue');
const zoomSlider = document.getElementById('zoomSlider');
const zoomValue = document.getElementById('zoomValue');
const autoRotateBtn = document.getElementById('autoRotateBtn');
const autoRotateIcon = document.getElementById('autoRotateIcon');
const rotateLeftBtn = document.getElementById('rotateLeftBtn');
const rotateRightBtn = document.getElementById('rotateRightBtn');
const resetBtn = document.getElementById('resetBtn');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const fullscreenIcon = document.getElementById('fullscreenIcon');
let isDragging = false;
let startX = 0;
let currentX = 0;
let rotation = 0;
let zoom = 100;
let isAutoRotating = false;
let autoRotateInterval = null;
// Load image
panoramaImage.onload = () => {
setTimeout(() => {
loadingOverlay.classList.add('hidden');
}, 500);
};
// Mouse drag
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;
rotation += deltaX * 0.5;
startX = e.clientX;
updateRotation();
});
document.addEventListener('mouseup', () => {
isDragging = false;
viewer.classList.remove('dragging');
});
// Touch support
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;
rotation += deltaX * 0.5;
startX = e.touches[0].clientX;
updateRotation();
});
viewer.addEventListener('touchend', () => {
isDragging = false;
});
// Mouse wheel zoom
viewer.addEventListener('wheel', (e) => {
e.preventDefault();
zoom -= e.deltaY * 0.1;
zoom = Math.max(100, Math.min(200, zoom));
zoomSlider.value = zoom;
updateZoom();
}, { passive: false });
// Zoom slider
zoomSlider.addEventListener('input', (e) => {
zoom = parseInt(e.target.value);
updateZoom();
});
// Auto rotate
function startAutoRotate() {
isAutoRotating = true;
autoRotateBtn.classList.add('active');
autoRotateIcon.name = 'pause-outline';
modeValue.textContent = 'Auto';
autoRotateInterval = setInterval(() => {
rotation += 0.5;
updateRotation();
}, 20);
}
function stopAutoRotate() {
isAutoRotating = false;
autoRotateBtn.classList.remove('active');
autoRotateIcon.name = 'play-outline';
modeValue.textContent = 'Manual';
if (autoRotateInterval) {
clearInterval(autoRotateInterval);
autoRotateInterval = null;
}
}
autoRotateBtn.addEventListener('click', () => {
if (isAutoRotating) {
stopAutoRotate();
} else {
startAutoRotate();
}
});
// Manual rotation buttons
rotateLeftBtn.addEventListener('click', () => {
stopAutoRotate();
rotation -= 10;
updateRotation();
});
rotateRightBtn.addEventListener('click', () => {
stopAutoRotate();
rotation += 10;
updateRotation();
});
// Reset
resetBtn.addEventListener('click', () => {
stopAutoRotate();
rotation = 0;
zoom = 100;
zoomSlider.value = 100;
updateRotation();
updateZoom();
});
// Fullscreen
fullscreenBtn.addEventListener('click', () => {
if (!document.fullscreenElement) {
viewer.requestFullscreen();
fullscreenIcon.name = 'contract-outline';
} else {
document.exitFullscreen();
fullscreenIcon.name = 'expand-outline';
}
});
// Update functions
function updateRotation() {
rotation = rotation % 360;
panoramaImage.style.transform = `translateX(${rotation}px) scale(${zoom / 100})`;
rotationValue.textContent = Math.round(Math.abs(rotation % 360)) + '°';
}
function updateZoom() {
panoramaImage.style.transform = `translateX(${rotation}px) scale(${zoom / 100})`;
zoomValue.textContent = zoom + '%';
zoomValueDisplay.textContent = zoom + '%';
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
rotation -= 10;
updateRotation();
} else if (e.key === 'ArrowRight') {
rotation += 10;
updateRotation();
} else if (e.key === ' ') {
e.preventDefault();
if (isAutoRotating) {
stopAutoRotate();
} else {
startAutoRotate();
}
} else if (e.key === 'r' || e.key === 'R') {
resetBtn.click();
}
});
No comments yet. Be the first!