Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Interactive Glass Loading Screen</title> | |
| <style> | |
| /* ======= Theme variables ======= | |
| - Tweak --gradient-start/--gradient-end to recolor the progress gradient. | |
| - Tweak --glass-bg to change the glass overlay tint. | |
| - --base-speed is the initial percent-per-second (default: 20 => 5s total). | |
| */ | |
| :root{ | |
| --gradient-start: #ff3b3b; /* start color (red-ish) */ | |
| --gradient-end: #22c55e; /* end color (green-ish) */ | |
| --glass-bg: rgba(15,15,22,0.62); | |
| --glass-border: rgba(255,255,255,0.15); | |
| --text: #e9eefc; | |
| --muted: #b8c0d9; | |
| --base-speed: 20; /* % per second */ | |
| --radius: 14px; | |
| } | |
| /* ======= Reset ======= */ | |
| * { box-sizing: border-box; } | |
| html, body { height: 100%; } | |
| body { | |
| margin: 0; | |
| font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, "Helvetica Neue", Arial, "Apple Color Emoji","Segoe UI Emoji"; | |
| color: var(--text); | |
| background: radial-gradient(1200px 800px at 70% 20%, #111424 0%, #0a0d18 45%, #080a14 100%); | |
| overflow: hidden; | |
| } | |
| /* ======= Loader Overlay ======= */ | |
| .overlay { | |
| position: fixed; | |
| inset: 0; | |
| z-index: 9999; | |
| display: grid; | |
| place-items: center; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(5px) saturate(140%); | |
| -webkit-backdrop-filter: blur(5px) saturate(140%); | |
| transition: opacity .45s ease, visibility .45s ease; | |
| } | |
| /* Fallback for browsers without backdrop-filter support */ | |
| @supports not ((backdrop-filter: blur(5px)) or (-webkit-backdrop-filter: blur(5px))) { | |
| .overlay { background: rgba(8,10,20,0.9); } | |
| } | |
| .overlay.hidden { opacity: 0; visibility: hidden; pointer-events: none; } | |
| .card { | |
| width: min(92vw, 560px); | |
| border-radius: var(--radius); | |
| border: 1px solid var(--glass-border); | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.04); | |
| background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02)); | |
| padding: 24px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| /* ======= Progress Area ======= */ | |
| .progress { | |
| position: relative; | |
| width: min(84vw, 480px); | |
| height: 16px; | |
| border-radius: 999px; | |
| background: rgba(255,255,255,0.12); | |
| overflow: hidden; | |
| box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); | |
| } | |
| .progress .bar { | |
| height: 100%; | |
| width: 0%; | |
| background: linear-gradient(90deg, var(--gradient-start), var(--gradient-end)); | |
| position: relative; | |
| border-radius: inherit; | |
| transition: width .12s linear; | |
| } | |
| /* Moving diagonal sheen over the bar */ | |
| .progress .bar::after { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| background: repeating-linear-gradient(45deg, | |
| rgba(255,255,255,0.12) 0px, rgba(255,255,255,0.12) 6px, | |
| transparent 6px, transparent 12px); | |
| mix-blend-mode: overlay; | |
| animation: moveSheen 1.6s linear infinite; | |
| pointer-events: none; | |
| } | |
| @keyframes moveSheen { | |
| from { background-position: 0 0; } | |
| to { background-position: 48px 0; } | |
| } | |
| .status { | |
| margin-top: 14px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| color: var(--muted); | |
| font-size: 14px; | |
| user-select: none; | |
| } | |
| .loading-dots::after { | |
| content: ""; | |
| display: inline-block; | |
| width: 1ch; | |
| text-align: left; | |
| animation: dots 1.2s steps(4,end) infinite; | |
| } | |
| @keyframes dots { | |
| 0% { content: ""; } | |
| 25% { content: "."; } | |
| 50% { content: ".."; } | |
| 75% { content: "..."; } | |
| 100% { content: ""; } | |
| } | |
| .skip { | |
| margin-top: 18px; | |
| border: 1px solid rgba(255,255,255,0.18); | |
| background: rgba(255,255,255,0.08); | |
| color: var(--text); | |
| padding: 10px 16px; | |
| font-size: 14px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: transform .15s ease, opacity .15s ease, background .2s ease; | |
| backdrop-filter: blur(6px); | |
| } | |
| .skip:hover { | |
| opacity: 0.95; | |
| transform: translateY(-1px); | |
| background: rgba(255,255,255,0.12); | |
| } | |
| .skip:active { transform: translateY(0) scale(0.98); } | |
| .skip:focus-visible { outline: 2px solid #8ab4ff; outline-offset: 2px; } | |
| /* ======= Particle Canvas ======= */ | |
| #particleCanvas { | |
| position: absolute; | |
| inset: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| filter: saturate(120%); | |
| } | |
| /* ======= Reduced motion: disable animations ======= */ | |
| @media (prefers-reduced-motion: reduce) { | |
| .progress .bar::after { animation: none; } | |
| .loading-dots::after { animation: none; } | |
| .skip { transition: none; } | |
| .overlay { transition: none; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Full-screen loading overlay with glassmorphism --> | |
| <div class="overlay" id="loader" aria-live="polite"> | |
| <canvas id="particleCanvas"></canvas> | |
| <div class="card" id="card"> | |
| <div | |
| id="progress" | |
| class="progress" | |
| role="progressbar" | |
| aria-label="Loading progress" | |
| aria-valuemin="0" | |
| aria-valuemax="100" | |
| aria-valuenow="0"> | |
| <div class="bar" id="bar"></div> | |
| </div> | |
| <div class="status"> | |
| <div id="statusText" class="loading-dots">Loading</div> | |
| <div id="percentText">0%</div> | |
| </div> | |
| <button id="skipBtn" class="skip" type="button" aria-label="Skip loading">Skip</button> | |
| <!-- Screen-reader live region for percentage announcements --> | |
| <div id="srPercent" style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;">0 percent</div> | |
| </div> | |
| </div> | |
| <script> | |
| /* ======= Interactive Glass Loader (Vanilla JS) ======= | |
| - Click anywhere (except Skip) or press Space to double loading speed. | |
| - Particles follow the pointer (disabled if prefers-reduced-motion). | |
| - Skip jumps to 100% and ends the sequence with confetti fade-out. | |
| - Progress color smoothly shifts from red to green as it advances. | |
| - All animations respect prefers-reduced-motion. | |
| Customization notes: | |
| - Change --gradient-start/--gradient-end in :root for different palette. | |
| - Adjust --base-speed to control base percent-per-second. | |
| - To disable particles entirely, set NO_PARTICLES = true below. | |
| */ | |
| (() => { | |
| const overlay = document.getElementById('loader'); | |
| const card = document.getElementById('card'); | |
| const progress = document.getElementById('progress'); | |
| const bar = document.getElementById('bar'); | |
| const percentText = document.getElementById('percentText'); | |
| const statusText = document.getElementById('statusText'); | |
| const skipBtn = document.getElementById('skipBtn'); | |
| const srPercent = document.getElementById('srPercent'); | |
| const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| const NO_PARTICLES = prefersReduced; // disable particles if user prefers reduced motion | |
| // Speed handling (percent per second) | |
| const root = document.documentElement; | |
| const BASE_SPEED = parseFloat(getComputedStyle(root).getPropertyValue('--base-speed')) || 20; // % per second | |
| let speedMultiplier = 1; | |
| let progressValue = 0; | |
| // Time & RAF | |
| let lastTime = 0; | |
| let rafId = 0; | |
| let running = true; | |
| // Pointer / particles | |
| const canvas = document.getElementById('particleCanvas'); | |
| const ctx = canvas.getContext('2d', { alpha: true }); | |
| let pointer = { x: window.innerWidth/2, y: window.innerHeight/2, active: false }; | |
| let particles = []; | |
| let emitAccumulator = 0; | |
| const EMIT_RATE = 80; // particles per second when pointer is active | |
| const MAX_PARTICLES = 320; | |
| let dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1)); // limit DPR for perf | |
| // Confetti | |
| let confetti = []; | |
| let confettiDone = false; | |
| // Utilities | |
| const clamp = (n, min, max) => Math.min(max, Math.max(min, n)); | |
| const lerp = (a, b, t) => a + (b - a) * t; | |
| function hexToRgb(hex) { | |
| hex = hex.trim(); | |
| if (hex.startsWith('hsl')) return { r:0, g:0, b:0 }; // not used, but safe | |
| if (hex.startsWith('#')) hex = hex.slice(1); | |
| if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); | |
| const num = parseInt(hex, 16); | |
| return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 }; | |
| } | |
| function rgbToHex(r, g, b) { | |
| return '#' + [r,g,b].map(v => v.toString(16).padStart(2,'0')).join(''); | |
| } | |
| function mixHex(c1, c2, t) { | |
| const a = hexToRgb(c1), b = hexToRgb(c2); | |
| return rgbToHex( | |
| Math.round(lerp(a.r, b.r, t)), | |
| Math.round(lerp(a.g, b.g, t)), | |
| Math.round(lerp(a.b, b.b, t)) | |
| ); | |
| } | |
| // Color mapping: red -> green as progress moves 0 -> 100 | |
| function progressColor(t) { | |
| // start red-ish, end green-ish (adjust if you change CSS variables) | |
| return mixHex('#ff3b3b', '#22c55e', t); | |
| } | |
| // Resize canvas to CSS pixels with DPR scaling | |
| function resizeCanvas() { | |
| const rect = canvas.getBoundingClientRect(); | |
| canvas.width = Math.max(1, Math.floor(rect.width * dpr)); | |
| canvas.height = Math.max(1, Math.floor(rect.height * dpr)); | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // scale once | |
| } | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas, { passive: true }); | |
| // Update progress bar and labels | |
| function updateUI() { | |
| const pct = Math.floor(progressValue); | |
| bar.style.width = pct + '%'; | |
| progress.setAttribute('aria-valuenow', String(pct)); | |
| percentText.textContent = pct + '%'; | |
| srPercent.textContent = pct + ' percent'; | |
| // Hue shift by current percent | |
| const t = clamp(progressValue / 100, 0, 1); | |
| const col = progressColor(t); | |
| bar.style.background = `linear-gradient(90deg, ${col}, ${mixHex(col, '#22c55e', 0.35)})`; | |
| } | |
| // Particles | |
| function spawnParticle(x, y) { | |
| if (NO_PARTICLES) return; | |
| if (particles.length > MAX_PARTICLES) particles.shift(); | |
| const hue = Math.floor(lerp(180, 320, Math.random())); // purple-ish hues | |
| particles.push({ | |
| x, y, | |
| vx: (Math.random() - 0.5) * 90, | |
| vy: (Math.random() - 0.8) * 120, | |
| life: 0, | |
| maxLife: 0.8 + Math.random() * 0.6, | |
| size: 2 + Math.random() * 2, | |
| hue | |
| }); | |
| } | |
| // Confetti burst at center of overlay | |
| function burstConfetti() { | |
| const rect = overlay.getBoundingClientRect(); | |
| const cx = rect.left + rect.width / 2; | |
| const cy = rect.top + rect.height / 2; | |
| const count = prefersReduced ? 30 : 140; | |
| for (let i = 0; i < count; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = 200 + Math.random() * 420; | |
| const hue = Math.floor(lerp(0, 360, Math.random())); | |
| confetti.push({ | |
| x: cx, y: cy, | |
| vx: Math.cos(angle) * speed, | |
| vy: Math.sin(angle) * speed, | |
| life: 0, | |
| maxLife: 1.2 + Math.random() * 0.8, | |
| size: 2 + Math.random() * 3, | |
| hue, | |
| rotation: Math.random() * Math.PI, | |
| vr: (Math.random() - 0.5) * 12 | |
| }); | |
| } | |
| } | |
| // Animation loop | |
| function frame(ts) { | |
| if (!running) return; | |
| if (!lastTime) lastTime = ts; | |
| const dt = Math.min(0.05, (ts - lastTime) / 1000); // seconds, clamp to 50ms | |
| lastTime = ts; | |
| // Progress update | |
| progressValue = clamp(progressValue + BASE_SPEED * speedMultiplier * dt, 0, 100); | |
| updateUI(); | |
| // Emit particles when pointer is active | |
| if (!NO_PARTICLES && pointer.active) { | |
| emitAccumulator += dt * EMIT_RATE; | |
| while (emitAccumulator >= 1) { | |
| emitAccumulator -= 1; | |
| spawnParticle(pointer.x, pointer.y); | |
| } | |
| } | |
| // Clear canvas (CSS pixels; transform handles DPR) | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Update & draw particles | |
| if (!NO_PARTICLES) { | |
| const drag = 0.98; | |
| const gravity = 280; | |
| for (let i = particles.length - 1; i >= 0; i--) { | |
| const p = particles[i]; | |
| p.life += dt; | |
| if (p.life >= p.maxLife) { particles.splice(i, 1); continue; } | |
| p.vx *= drag; | |
| p.vy = p.vy * drag + gravity * dt; | |
| p.x += p.vx * dt; | |
| p.y += p.vy * dt; | |
| const alpha = 1 - (p.life / p.maxLife); | |
| ctx.fillStyle = `hsla(${p.hue}, 80%, 60%, ${alpha.toFixed(3)})`; | |
| ctx.beginPath(); | |
| ctx.arc(p.x - overlay.getBoundingClientRect().left, | |
| p.y - overlay.getBoundingClientRect().top, | |
| p.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| // Draw & update confetti | |
| if (progressValue >= 100 && !confettiDone) { | |
| burstConfetti(); | |
| confettiDone = true; | |
| } | |
| if (confetti.length) { | |
| const drag = 0.985, gravity = 380; | |
| for (let i = confetti.length - 1; i >= 0; i--) { | |
| const c = confetti[i]; | |
| c.life += dt; | |
| if (c.life >= c.maxLife) { confetti.splice(i, 1); continue; } | |
| c.vx *= drag; | |
| c.vy = c.vy * drag + gravity * dt; | |
| c.x += c.vx * dt; | |
| c.y += c.vy * dt; | |
| c.rotation += c.vr * dt; | |
| const alpha = 1 - (c.life / c.maxLife); | |
| ctx.save(); | |
| ctx.translate(c.x - overlay.getBoundingClientRect().left, | |
| c.y - overlay.getBoundingClientRect().top); | |
| ctx.rotate(c.rotation); | |
| ctx.fillStyle = `hsla(${c.hue}, 85%, 60%, ${alpha.toFixed(3)})`; | |
| ctx.fillRect(-c.size/2, -c.size/2, c.size * (1.8 + Math.abs(Math.sin(c.rotation))*0.6), c.size); | |
| ctx.restore(); | |
| } | |
| } | |
| // Finish | |
| if (progressValue >= 100) { | |
| // Small delay to let confetti start, then fade overlay | |
| setTimeout(() => { | |
| overlay.classList.add('hidden'); | |
| running = false; | |
| if (rafId) cancelAnimationFrame(rafId); | |
| // Cleanup canvas to free memory | |
| setTimeout(() => { try { ctx.clearRect(0,0,canvas.width,canvas.height); } catch(e){} }, 600); | |
| }, 450); | |
| return; // stop scheduling new frames after finish | |
| } | |
| rafId = requestAnimationFrame(frame); | |
| } | |
| // Interactions | |
| function speedUp() { | |
| speedMultiplier = Math.min(speedMultiplier * 2, 64); // cap | |
| // Optional tiny feedback flash on the bar | |
| bar.style.transition = 'none'; | |
| bar.offsetHeight; // reflow | |
| bar.style.transition = 'width .12s linear'; | |
| } | |
| overlay.addEventListener('pointermove', (e) => { | |
| pointer.x = e.clientX; | |
| pointer.y = e.clientY; | |
| pointer.active = true; | |
| }, { passive: true }); | |
| overlay.addEventListener('pointerleave', () => { pointer.active = false; }, { passive: true }); | |
| // Double speed on click (not on skip) | |
| overlay.addEventListener('click', (e) => { | |
| if (e.target === skipBtn) return; // skip has its own handler | |
| speedUp(); | |
| }); | |
| // Keyboard: Space to double speed | |
| window.addEventListener('keydown', (e) => { | |
| if (e.code === 'Space' || e.key === ' ') { | |
| e.preventDefault(); | |
| speedUp(); | |
| } | |
| }); | |
| // Skip handler | |
| skipBtn.addEventListener('click', () => { | |
| progressValue = 100; | |
| updateUI(); | |
| // Do immediate burst + fade | |
| confettiDone = false; | |
| burstConfetti(); | |
| setTimeout(() => { | |
| overlay.classList.add('hidden'); | |
| running = false; | |
| if (rafId) cancelAnimationFrame(rafId); | |
| }, 250); | |
| }); | |
| // Start | |
| updateUI(); | |
| rafId = requestAnimationFrame(frame); | |
| })(); | |
| </script> | |
| <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script> | |
| </body> | |
| </html> |