Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CSS3 3D Model Viewer - AnyCoder</title> | |
| <!-- Import Fonts and Icons --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #00f2ff; | |
| --secondary: #7000ff; | |
| --bg-dark: #0f0f13; | |
| --panel-bg: rgba(255, 255, 255, 0.05); | |
| --border-color: rgba(255, 255, 255, 0.1); | |
| --text-main: #ffffff; | |
| --text-muted: #888888; | |
| /* Smartphone Variables */ | |
| --phone-w: 180px; | |
| --phone-h: 360px; | |
| --phone-d: 15px; | |
| --phone-radius: 25px; | |
| /* Glasses Variables */ | |
| --glass-frame-w: 70px; | |
| --glass-lens-h: 50px; | |
| --glass-d: 10px; | |
| --glass-bridge: 20px; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background-color: var(--bg-dark); | |
| background-image: | |
| radial-gradient(circle at 10% 20%, rgba(112, 0, 255, 0.15) 0%, transparent 20%), | |
| radial-gradient(circle at 90% 80%, rgba(0, 242, 255, 0.15) 0%, transparent 20%); | |
| color: var(--text-main); | |
| font-family: 'Inter', sans-serif; | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| padding: 20px 30px; | |
| z-index: 100; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| pointer-events: none; /* Let clicks pass through to canvas */ | |
| } | |
| .logo { | |
| font-weight: 800; | |
| font-size: 1.2rem; | |
| letter-spacing: 1px; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| pointer-events: auto; | |
| cursor: pointer; | |
| } | |
| .brand-link { | |
| text-decoration: none; | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| pointer-events: auto; | |
| transition: color 0.3s; | |
| } | |
| .brand-link:hover { | |
| color: var(--primary); | |
| } | |
| /* --- Main Scene Container --- */ | |
| .scene-container { | |
| flex: 1; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| perspective: 1000px; /* The depth of the 3D scene */ | |
| overflow: hidden; | |
| cursor: grab; | |
| } | |
| .scene-container:active { | |
| cursor: grabbing; | |
| } | |
| /* --- 3D World Wrapper --- */ | |
| .world { | |
| position: relative; | |
| width: 0; | |
| height: 0; | |
| transform-style: preserve-3d; | |
| transform: rotateX(0deg) rotateY(0deg); | |
| transition: transform 0.1s linear; /* Smooth drag */ | |
| } | |
| /* --- Common Face Styles --- */ | |
| .face { | |
| position: absolute; | |
| transform-style: preserve-3d; | |
| backface-visibility: hidden; /* Performance optimization */ | |
| border: 1px solid rgba(255,255,255,0.05); | |
| } | |
| /* --- MODEL 1: SMARTPHONE --- */ | |
| .phone-wrapper { | |
| display: none; /* Hidden by default */ | |
| transform-style: preserve-3d; | |
| } | |
| .phone-face { | |
| width: var(--phone-w); | |
| height: var(--phone-h); | |
| background: #111; | |
| border-radius: var(--phone-radius); | |
| box-shadow: inset 0 0 20px rgba(255,255,255,0.1); | |
| } | |
| /* Phone Geometry */ | |
| .p-front { transform: translateZ(calc(var(--phone-d) / 2)); } | |
| .p-back { transform: rotateY(180deg) translateZ(calc(var(--phone-d) / 2)); background: #222; } | |
| .p-right { width: var(--phone-d); height: var(--phone-h); transform: rotateY(90deg) translateZ(calc(var(--phone-w) / 2)); background: #050505; } | |
| .p-left { width: var(--phone-d); height: var(--phone-h); transform: rotateY(-90deg) translateZ(calc(var(--phone-w) / 2)); background: #050505; } | |
| .p-top { width: var(--phone-w); height: var(--phone-d); transform: rotateX(90deg) translateZ(calc(var(--phone-h) / 2)); background: #111; } | |
| .p-bottom { width: var(--phone-w); height: var(--phone-d); transform: rotateX(-90deg) translateZ(calc(var(--phone-h) / 2)); background: #111; } | |
| /* Phone Details */ | |
| .screen { | |
| width: 100%; | |
| height: 100%; | |
| border-radius: var(--phone-radius); | |
| background: url('https://images.unsplash.com/photo-1616348436168-de43ad0db179?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80') no-repeat center/cover; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .screen::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 50%); | |
| pointer-events: none; | |
| } | |
| /* Notch / Dynamic Island */ | |
| .notch { | |
| position: absolute; | |
| top: 10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 60px; | |
| height: 25px; | |
| background: #000; | |
| border-radius: 15px; | |
| z-index: 2; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .notch::before { | |
| content: ''; | |
| width: 6px; | |
| height: 6px; | |
| background: #222; | |
| border-radius: 50%; | |
| box-shadow: 0 0 0 2px #333; | |
| } | |
| /* Back Camera Bump */ | |
| .camera-bump { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| width: 80px; | |
| height: 80px; | |
| background: #1a1a1a; | |
| border-radius: 20px; | |
| transform: translateZ(1px); /* Slightly off back face */ | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 5px; | |
| padding: 10px; | |
| } | |
| .cam-lens { | |
| background: #000; | |
| border-radius: 50%; | |
| border: 1px solid #333; | |
| box-shadow: inset 0 0 5px rgba(0,0,0,0.8); | |
| } | |
| /* --- MODEL 2: GLASSES --- */ | |
| .glasses-wrapper { | |
| display: none; /* Hidden by default */ | |
| transform-style: preserve-3d; | |
| } | |
| /* Glasses Group Logic */ | |
| .lens-group { | |
| position: absolute; | |
| transform-style: preserve-3d; | |
| } | |
| /* Lenses */ | |
| .lens { | |
| width: var(--glass-frame-w); | |
| height: var(--glass-lens-h); | |
| background: rgba(100, 200, 255, 0.1); | |
| border: 4px solid #333; | |
| border-radius: 10px 10px 30px 30px; | |
| box-shadow: inset 0 0 20px rgba(0, 242, 255, 0.2); | |
| backdrop-filter: blur(2px); | |
| position: relative; | |
| } | |
| .lens-reflective { | |
| position: absolute; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| background: linear-gradient(135deg, rgba(255,255,255,0.3) 0%, transparent 50%); | |
| border-radius: 6px 6px 26px 26px; | |
| pointer-events: none; | |
| } | |
| .g-left { transform: translateX(calc(-1 * (var(--glass-frame-w) / 2))); } | |
| .g-right { transform: translateX(calc(var(--glass-frame-w) / 2)); } | |
| /* Bridge */ | |
| .bridge { | |
| position: absolute; | |
| width: var(--glass-bridge); | |
| height: 10px; | |
| background: #222; | |
| top: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| } | |
| /* Temples (Arms) */ | |
| .temple { | |
| position: absolute; | |
| width: 80px; | |
| height: 12px; | |
| background: #2a2a2a; | |
| top: 15px; | |
| transform-origin: left center; | |
| border-radius: 5px; | |
| } | |
| .t-left { left: calc(-1 * var(--glass-frame-w) / 2); transform: rotateY(-20deg) translateZ(calc(var(--glass-d) / 2)); } | |
| .t-right { right: calc(-1 * var(--glass-frame-w) / 2); transform: rotateY(20deg) translateZ(calc(var(--glass-d) / 2)); } | |
| /* Frame Rim Depth */ | |
| .rim-depth { | |
| position: absolute; | |
| width: var(--glass-frame-w); | |
| height: var(--glass-lens-h); | |
| border: 4px solid #222; | |
| border-radius: 10px 10px 30px 30px; | |
| transform: translateZ(calc(var(--glass-d) / 2)); | |
| pointer-events: none; | |
| } | |
| .rim-depth.back { transform: translateZ(calc(var(--glass-d) / -2)); } | |
| /* --- UI Controls Panel --- */ | |
| .controls { | |
| position: absolute; | |
| bottom: 30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 90%; | |
| max-width: 400px; | |
| background: var(--panel-bg); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid var(--border-color); | |
| border-radius: 20px; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| z-index: 10; | |
| } | |
| .tabs { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .tab-btn { | |
| flex: 1; | |
| background: rgba(255,255,255,0.05); | |
| border: none; | |
| color: var(--text-muted); | |
| padding: 10px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| font-family: inherit; | |
| font-weight: 600; | |
| transition: 0.3s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .tab-btn.active { | |
| background: var(--primary); | |
| color: #000; | |
| box-shadow: 0 0 15px rgba(0, 242, 255, 0.4); | |
| } | |
| .sliders { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 15px; | |
| } | |
| .slider-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .slider-group label { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| input[type=range] { | |
| width: 100%; | |
| accent-color: var(--primary); | |
| background: transparent; | |
| } | |
| /* --- Responsive Adjustments --- */ | |
| @media (max-width: 768px) { | |
| .controls { | |
| bottom: 10px; | |
| width: 95%; | |
| padding: 15px; | |
| } | |
| :root { | |
| /* Scale down models on mobile */ | |
| --phone-w: 140px; | |
| --phone-h: 280px; | |
| } | |
| } | |
| /* Grid floor decoration */ | |
| .grid-floor { | |
| position: absolute; | |
| width: 2000px; | |
| height: 2000px; | |
| background-image: | |
| linear-gradient(var(--border-color) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--border-color) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| transform: rotateX(90deg) translateZ(-200px) translateX(-500px) translateY(-500px); | |
| pointer-events: none; | |
| opacity: 0.2; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo">Fiorc3D</div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link">Built with anycoder</a> | |
| </header> | |
| <!-- 3D Scene --> | |
| <div class="scene-container" id="scene"> | |
| <div class="world" id="world"> | |
| <!-- Decorative Floor --> | |
| <div class="grid-floor"></div> | |
| <!-- Model: Smartphone --> | |
| <div class="phone-wrapper" id="model-phone"> | |
| <div class="face p-front phone-face"> | |
| <div class="screen"> | |
| <div class="notch"></div> | |
| </div> | |
| </div> | |
| <div class="face p-back phone-face"> | |
| <div class="camera-bump"> | |
| <div class="cam-lens"></div> | |
| <div class="cam-lens"></div> | |
| <div class="cam-lens"></div> | |
| <div class="cam-lens"></div> | |
| </div> | |
| </div> | |
| <div class="face p-right phone-face"></div> | |
| <div class="face p-left phone-face"></div> | |
| <div class="face p-top phone-face"></div> | |
| <div class="face p-bottom phone-face"></div> | |
| </div> | |
| <!-- Model: Glasses --> | |
| <div class="glasses-wrapper" id="model-glasses"> | |
| <div class="lens-group"> | |
| <!-- Bridge --> | |
| <div class="bridge"></div> | |
| <!-- Left Lens --> | |
| <div class="lens g-left"> | |
| <div class="lens-reflective"></div> | |
| <div class="rim-depth"></div> <!-- Front Rim --> | |
| </div> | |
| <div class="rim-depth back"></div> <!-- Back Rim (simplified positioning) --> | |
| <!-- Right Lens --> | |
| <div class="lens g-right"> | |
| <div class="lens-reflective"></div> | |
| <div class="rim-depth"></div> | |
| </div> | |
| <div class="rim-depth back" style="transform: translateX(50%) translateZ(-5px);"></div> | |
| <!-- Temples --> | |
| <div class="temple t-left"></div> | |
| <div class="temple t-right"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Controls --> | |
| <div class="controls"> | |
| <div class="tabs"> | |
| <button class="tab-btn active" onclick="switchModel('phone')"> | |
| <i class="fas fa-mobile-screen-button"></i> Smartphone | |
| </button> | |
| <button class="tab-btn" onclick="switchModel('glasses')"> | |
| <i class="fas fa-glasses"></i> Glasses | |
| </button> | |
| </div> | |
| <div class="sliders"> | |
| <div class="slider-group"> | |
| <label>Rotate X</label> | |
| <input type="range" min="0" max="360" value="0" id="rotateX"> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Rotate Y</label> | |
| <input type="range" min="0" max="360" value="0" id="rotateY"> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- State Management --- | |
| const state = { | |
| rotX: 15, | |
| rotY: -25, | |
| currentModel: 'phone' | |
| }; | |
| // --- DOM Elements --- | |
| const world = document.getElementById('world'); | |
| const modelPhone = document.getElementById('model-phone'); | |
| const modelGlasses = document.getElementById('model-glasses'); | |
| const scene = document.getElementById('scene'); | |
| const inputRotateX = document.getElementById('rotateX'); | |
| const inputRotateY = document.getElementById('rotateY'); | |
| const tabBtns = document.querySelectorAll('.tab-btn'); | |
| // --- Initialization --- | |
| function init() { | |
| updateTransform(); | |
| showModel(state.currentModel); | |
| // Add Auto-rotation animation | |
| requestAnimationFrame(animate); | |
| } | |
| // --- Animation Loop --- | |
| let autoRotate = true; | |
| let lastTime = 0; | |
| function animate(time) { | |
| // Slowly rotate if not dragging/interacting | |
| if(autoRotate) { | |
| state.rotY += 0.2; | |
| // Update inputs visually | |
| inputRotateY.value = state.rotY % 360; | |
| inputRotateX.value = state.rotX; // Keep X relatively stable | |
| updateTransform(); | |
| } | |
| requestAnimationFrame(animate); | |
| } | |
| // --- Core Logic --- | |
| function updateTransform() { | |
| // Apply CSS transform to the 3D world | |
| world.style.transform = `rotateX(${state.rotX}deg) rotateY(${state.rotY}deg)`; | |
| } | |
| function showModel(modelName) { | |
| state.currentModel = modelName; | |
| // Hide all | |
| modelPhone.style.display = 'none'; | |
| modelGlasses.style.display = 'none'; | |
| // Show active | |
| if(modelName === 'phone') { | |
| modelPhone.style.display = 'block'; | |
| // Adjust scale for phone | |
| modelPhone.style.transform = `scale(1.2)`; | |
| } else { | |
| modelGlasses.style.display = 'block'; | |
| // Adjust scale for glasses (make it bigger) | |
| modelGlasses.style.transform = `scale(1.8) translateY(-20px)`; | |
| } | |
| // Reset rotation slightly for better initial view | |
| state.rotX = 15; | |
| state.rotY = -25; | |
| inputRotateX.value = 15; | |
| inputRotateY.value = -25; | |
| } | |
| // --- Event Listeners --- | |
| // 1. Sliders | |
| inputRotateX.addEventListener('input', (e) => { | |
| autoRotate = false; // Stop auto rotation on interaction | |
| state.rotX = e.target.value; | |
| updateTransform(); | |
| }); | |
| inputRotateY.addEventListener('input', (e) => { | |
| autoRotate = false; | |
| state.rotY = e.target.value; | |
| updateTransform(); | |
| }); | |
| // 2. Mouse/Touch Drag on Scene (Orbit Control Simulation) | |
| let isDragging = false; | |
| let startX, startY; | |
| scene.addEventListener('mousedown', (e) => { | |
| isDragging = true; | |
| autoRotate = false; | |
| startX = e.clientX; | |
| startY = e.clientY; | |
| }); | |
| window.addEventListener('mousemove', (e) => { | |
| if (!isDragging) return; | |
| const deltaX = e.clientX - startX; | |
| const deltaY = e.clientY - startY; | |
| state.rotY += deltaX * 0.5; | |
| state.rotX -= deltaY * 0.5; | |
| // Clamp X rotation to avoid flipping | |
| state.rotX = Math.max(-90, Math.min(90, state.rotX)); | |
| // Update Inputs | |
| inputRotateX.value = state.rotX; | |
| inputRotateY.value = state.rotY; | |
| updateTransform(); | |
| startX = e.clientX; | |
| startY = e.clientY; | |
| }); | |
| window.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| }); | |
| // Touch support | |
| scene.addEventListener('touchstart', (e) => { | |
| isDragging = true; | |
| autoRotate = false; | |
| startX = e.touches[0].clientX; | |
| startY = e.touches[0].clientY; | |
| }, {passive: false}); | |
| window.addEventListener('touchmove', (e) => { | |
| if (!isDragging) return; | |
| e.preventDefault(); // Prevent scrolling | |
| const deltaX = e.touches[0].clientX - startX; | |
| const deltaY = e.touches[0].clientY - startY; | |
| state.rotY += deltaX * 0.5; | |
| state.rotX -= deltaY * 0.5; | |
| state.rotX = Math.max(-90, Math.min(90, state.rotX)); | |
| updateTransform(); | |
| startX = e.touches[0].clientX; | |
| startY = e.touches[0].clientY; | |
| }, {passive: false}); | |
| window.addEventListener('touchend', () => { | |
| isDragging = false; | |
| }); | |
| // 3. Tab Switching | |
| window.switchModel = (model) => { | |
| tabBtns.forEach(btn => btn.classList.remove('active')); | |
| // Find the button that calls this function (hacky but works for simple inline) | |
| if(model === 'phone') { | |
| tabBtns[0].classList.add('active'); | |
| } else { | |
| tabBtns[1].classList.add('active'); | |
| } | |
| showModel(model); | |
| } | |
| // Start app | |
| init(); | |
| </script> | |
| </body> | |
| </html> |