diff --git a/index.html b/index.html index e2c7108..f44bb17 100644 --- a/index.html +++ b/index.html @@ -11,5 +11,7 @@
+ + diff --git a/public/Locil Forecast - Elevator.mp3 b/public/Locil Forecast - Elevator.mp3 new file mode 100644 index 0000000..88ba68f Binary files /dev/null and b/public/Locil Forecast - Elevator.mp3 differ diff --git a/public/background-audio.js b/public/background-audio.js new file mode 100644 index 0000000..1290b66 --- /dev/null +++ b/public/background-audio.js @@ -0,0 +1,183 @@ +(() => { + const SHOW_AUDIO_CONTROL = true + const AUDIO_SOURCE = '/Locil%20Forecast%20-%20Elevator.mp3' + + const style = document.createElement('style') + style.textContent = ` + #mclive-floating-controls { + --mclive-control-night: #041117; + --mclive-control-ice: #d7f1e8; + --mclive-control-mist: #8fafb2; + --mclive-control-gold: #c6a15b; + --mclive-control-online: #72aa86; + --mclive-control-line: rgba(166, 209, 203, 0.22); + position: fixed; + z-index: 2147483647; + top: 94px; + right: clamp(20px, 3vw, 44px); + width: 190px; + display: flex; + flex-direction: column; + align-items: stretch; + border: 1px solid var(--mclive-control-line); + background: rgba(4, 17, 23, 0.92); + box-shadow: 0 18px 52px rgba(0, 0, 0, 0.3); + -webkit-backdrop-filter: blur(14px); + backdrop-filter: blur(14px); + pointer-events: none; + } + + #mclive-floating-controls:has(.mclive-countdown-restore:not(.is-visible)) .mclive-audio-control { + border-bottom: 0; + } + + .mclive-floating-control { + width: 100%; + min-height: 46px; + padding: 0 13px; + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 9px; + border: 0; + border-bottom: 1px solid var(--mclive-control-line); + color: var(--mclive-control-ice); + background: transparent; + cursor: pointer; + font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-size: 8px; + letter-spacing: 0.14em; + text-transform: uppercase; + pointer-events: auto; + } + + .mclive-floating-control:hover { + background: rgba(198, 161, 91, 0.08); + } + + .mclive-floating-control:focus-visible { + outline: 2px solid var(--mclive-control-gold); + outline-offset: 4px; + } + + .mclive-floating-control svg { + width: 14px; + height: 14px; + fill: currentColor; + } + + .mclive-control-status { + width: 6px; + height: 6px; + flex: 0 0 auto; + background: var(--mclive-control-mist); + box-shadow: 0 0 0 transparent; + transition: background 0.2s ease, box-shadow 0.2s ease; + } + + .mclive-audio-control.is-playing .mclive-control-status { + background: var(--mclive-control-online); + box-shadow: 0 0 10px rgba(114, 170, 134, 0.7); + } + + .mclive-control-name { + min-width: 0; + flex: 1; + color: var(--mclive-control-mist); + text-align: left; + } + + .mclive-control-value { + color: var(--mclive-control-ice); + font-variant-numeric: tabular-nums; + text-align: right; + } + + .mclive-audio-icon { + width: 14px; + height: 14px; + display: grid; + place-items: center; + color: var(--mclive-control-gold); + } + + @media (max-width: 620px) { + #mclive-floating-controls { + top: auto; + right: 16px; + bottom: 16px; + width: 168px; + } + + .mclive-floating-control { + min-height: 44px; + padding: 0 11px; + } + } + ` + + let controls = document.getElementById('mclive-floating-controls') + if (!controls) { + controls = document.createElement('div') + controls.id = 'mclive-floating-controls' + document.body.appendChild(controls) + } + + const audioButton = document.createElement('button') + audioButton.className = 'mclive-floating-control mclive-audio-control' + audioButton.type = 'button' + audioButton.hidden = !SHOW_AUDIO_CONTROL + audioButton.setAttribute('aria-label', '播放背景音乐') + audioButton.innerHTML = ` + + AUDIO + OFF + + ` + controls.appendChild(audioButton) + document.head.appendChild(style) + + const audioIcon = audioButton.querySelector('.mclive-audio-icon') + const audioLabel = audioButton.querySelector('.mclive-audio-label') + const audio = new Audio(AUDIO_SOURCE) + audio.loop = true + audio.preload = 'auto' + + const playIcon = '' + const pauseIcon = '' + + function updateAudioControl() { + const playing = !audio.paused + audioIcon.innerHTML = playing ? pauseIcon : playIcon + audioLabel.textContent = playing ? 'ON' : 'OFF' + audioButton.classList.toggle('is-playing', playing) + audioButton.setAttribute('aria-label', playing ? '暂停背景音乐' : '播放背景音乐') + } + + async function startAudio() { + if (!audio.paused) return + try { + await audio.play() + } catch { + // Browsers may reject playback until a qualifying user gesture occurs. + } + updateAudioControl() + } + + function handleFirstInteraction(event) { + if (audioButton.contains(event.target)) return + startAudio() + } + + audioButton.addEventListener('click', async () => { + if (audio.paused) await startAudio() + else audio.pause() + updateAudioControl() + }) + audio.addEventListener('play', updateAudioControl) + audio.addEventListener('pause', updateAudioControl) + document.addEventListener('pointerdown', handleFirstInteraction, { passive: true }) + document.addEventListener('touchstart', handleFirstInteraction, { passive: true }) + document.addEventListener('keydown', handleFirstInteraction) + updateAudioControl() +})() diff --git a/public/countdown-overlay.js b/public/countdown-overlay.js new file mode 100644 index 0000000..8904057 --- /dev/null +++ b/public/countdown-overlay.js @@ -0,0 +1,359 @@ +(() => { + const TARGET_TIME = '2026-08-01T20:00:00+08:00' + + const targetTimestamp = new Date(TARGET_TIME).getTime() + + if (Number.isNaN(targetTimestamp)) { + console.error('[MCLIVE countdown] Invalid TARGET_TIME:', TARGET_TIME) + return + } + + const style = document.createElement('style') + style.textContent = ` + #mclive-countdown-overlay { + --countdown-night: #041117; + --countdown-ice: #d7f1e8; + --countdown-mist: #8fafb2; + --countdown-gold: #c6a15b; + --countdown-line: rgba(166, 209, 203, 0.22); + position: fixed; + z-index: 2147483646; + inset: 0; + display: grid; + place-items: center; + min-height: 100svh; + padding: 84px 24px 42px; + overflow: hidden; + color: var(--countdown-ice); + background: rgba(4, 17, 23, 0.3); + -webkit-backdrop-filter: blur(20px) saturate(0.72); + backdrop-filter: blur(20px) saturate(0.72); + font-family: 'Noto Sans SC', system-ui, sans-serif; + touch-action: pan-y; + } + + #mclive-countdown-overlay::before { + content: ''; + position: absolute; + inset: 0; + opacity: 0.32; + background-image: + linear-gradient(var(--countdown-line) 1px, transparent 1px), + linear-gradient(90deg, var(--countdown-line) 1px, transparent 1px); + background-size: 54px 54px; + mask-image: radial-gradient(circle at center, black 10%, transparent 72%); + pointer-events: none; + } + + #mclive-countdown-overlay::after { + content: ''; + position: absolute; + width: min(72vw, 760px); + aspect-ratio: 1; + border: 1px solid rgba(215, 241, 232, 0.13); + border-radius: 50%; + box-shadow: + 0 0 0 68px rgba(79, 143, 120, 0.035), + 0 0 0 136px rgba(79, 143, 120, 0.018); + pointer-events: none; + } + + .mclive-countdown-terminal { + position: relative; + z-index: 1; + width: min(960px, 100%); + padding: clamp(28px, 5vw, 64px); + border: 1px solid var(--countdown-line); + background: rgba(4, 17, 23, 0.78); + box-shadow: 0 36px 120px rgba(0, 0, 0, 0.45); + text-align: center; + } + + .mclive-countdown-terminal::before, + .mclive-countdown-terminal::after { + content: ''; + position: absolute; + width: 14px; + height: 14px; + border-color: var(--countdown-gold); + } + + .mclive-countdown-terminal::before { + top: -5px; + left: -5px; + border-top: 1px solid; + border-left: 1px solid; + } + + .mclive-countdown-terminal::after { + right: -5px; + bottom: -5px; + border-right: 1px solid; + border-bottom: 1px solid; + } + + .mclive-countdown-kicker, + .mclive-countdown-target, + .mclive-countdown-unit, + .mclive-countdown-status, + .mclive-countdown-toggle { + font-family: 'IBM Plex Mono', ui-monospace, monospace; + text-transform: uppercase; + letter-spacing: 0.14em; + } + + .mclive-countdown-kicker { + margin: 0 0 18px; + color: var(--countdown-gold); + font-size: 10px; + } + + .mclive-countdown-title { + margin: 0; + font-family: 'ZCOOL QingKe HuangYou', 'Noto Sans SC', sans-serif; + font-size: clamp(40px, 7vw, 82px); + font-weight: 400; + line-height: 1; + letter-spacing: -0.02em; + } + + .mclive-countdown-title span { + color: var(--countdown-gold); + } + + .mclive-countdown-target { + margin: 18px 0 0; + color: var(--countdown-mist); + font-size: 9px; + } + + .mclive-countdown-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + margin-top: clamp(30px, 5vw, 54px); + border-top: 1px solid var(--countdown-line); + border-bottom: 1px solid var(--countdown-line); + } + + .mclive-countdown-cell { + min-width: 0; + padding: clamp(18px, 4vw, 34px) 8px; + border-right: 1px solid var(--countdown-line); + } + + .mclive-countdown-cell:last-child { + border-right: 0; + } + + .mclive-countdown-value { + display: block; + color: var(--countdown-ice); + font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-size: clamp(34px, 7vw, 72px); + font-weight: 500; + font-variant-numeric: tabular-nums; + line-height: 1; + text-shadow: 0 0 34px rgba(215, 241, 232, 0.13); + } + + .mclive-countdown-unit { + display: block; + margin-top: 12px; + color: var(--countdown-mist); + font-size: 8px; + } + + .mclive-countdown-status { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin: 24px 0 0; + color: var(--countdown-mist); + font-size: 8px; + } + + .mclive-countdown-status i { + width: 7px; + height: 7px; + background: #e05a47; + box-shadow: 0 0 12px #e05a47; + animation: mclive-countdown-pulse 1.8s infinite; + } + + .mclive-countdown-collapse { + min-height: 42px; + padding: 0 17px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + border: 1px solid var(--countdown-line); + color: var(--countdown-ice); + background: rgba(4, 17, 23, 0.82); + cursor: pointer; + font-size: 8px; + margin-top: 22px; + border-color: rgba(166, 209, 203, 0.14); + color: var(--countdown-mist); + background: transparent; + } + + .mclive-countdown-restore { + display: none; + border-bottom: 0; + } + + .mclive-countdown-collapse:hover { + border-color: var(--countdown-gold); + } + + .mclive-countdown-collapse:focus-visible { + outline: 2px solid var(--countdown-gold); + outline-offset: 4px; + } + + .mclive-countdown-collapse svg { + width: 15px; + height: 15px; + stroke: currentColor; + fill: none; + stroke-width: 1.8; + } + + #mclive-countdown-overlay.is-collapsed { + display: none; + } + + .mclive-countdown-restore.is-visible { + display: inline-flex; + } + + .mclive-countdown-restore .mclive-archive-mark { + width: 9px; + height: 9px; + flex: 0 0 auto; + border: 1px solid var(--mclive-control-gold, #c6a15b); + transform: rotate(45deg); + } + + .mclive-countdown-restore .mclive-control-value { + color: var(--mclive-control-gold, #c6a15b); + } + + @keyframes mclive-countdown-pulse { + 50% { opacity: 0.4; box-shadow: 0 0 3px #e05a47; } + } + + @media (max-width: 620px) { + #mclive-countdown-overlay { padding: 76px 16px 28px; } + .mclive-countdown-terminal { padding: 32px 14px; } + .mclive-countdown-cell { padding-left: 3px; padding-right: 3px; } + .mclive-countdown-unit { letter-spacing: 0.08em; } + .mclive-countdown-collapse { margin-top: 18px; } + } + + @media (prefers-reduced-motion: reduce) { + .mclive-countdown-status i { animation: none; } + } + ` + + const overlay = document.createElement('div') + overlay.id = 'mclive-countdown-overlay' + overlay.setAttribute('role', 'dialog') + overlay.setAttribute('aria-label', 'MCLIVE 世界实例开放倒计时') + overlay.innerHTML = ` +
+

WORLD INSTANCE 001 / ACCESS LOCKED

+

世界档案将在
倒计时结束后开启

+

+
+
00DAYS / 天
+
00HOURS / 时
+
00MINUTES / 分
+
00SECONDS / 秒
+
+

SERVER PREPARATION IN PROGRESS

+ +
+ ` + + document.head.appendChild(style) + document.body.appendChild(overlay) + + let controls = document.getElementById('mclive-floating-controls') + if (!controls) { + controls = document.createElement('div') + controls.id = 'mclive-floating-controls' + document.body.appendChild(controls) + } + + const restoreButton = document.createElement('button') + restoreButton.className = 'mclive-floating-control mclive-countdown-toggle mclive-countdown-restore' + restoreButton.type = 'button' + restoreButton.setAttribute('aria-label', '展开倒计时覆盖层') + restoreButton.innerHTML = ` + + ARCHIVE + --D --H + ` + controls.appendChild(restoreButton) + + const values = { + days: overlay.querySelector('[data-countdown="days"]'), + hours: overlay.querySelector('[data-countdown="hours"]'), + minutes: overlay.querySelector('[data-countdown="minutes"]'), + seconds: overlay.querySelector('[data-countdown="seconds"]'), + } + const targetLabel = overlay.querySelector('.mclive-countdown-target') + const collapseButton = overlay.querySelector('.mclive-countdown-collapse') + const compactCountdown = restoreButton.querySelector('.mclive-countdown-compact') + + collapseButton.addEventListener('click', () => { + overlay.classList.add('is-collapsed') + overlay.setAttribute('aria-hidden', 'true') + restoreButton.classList.add('is-visible') + restoreButton.focus() + }) + + restoreButton.addEventListener('click', () => { + overlay.classList.remove('is-collapsed') + overlay.removeAttribute('aria-hidden') + restoreButton.classList.remove('is-visible') + collapseButton.focus() + }) + + const targetDate = new Date(targetTimestamp) + targetLabel.textContent = `TARGET / ${targetDate.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + })}` + + function updateCountdown() { + const remaining = Math.max(0, targetTimestamp - Date.now()) + const totalSeconds = Math.floor(remaining / 1000) + const days = Math.floor(totalSeconds / 86400) + const hours = Math.floor((totalSeconds % 86400) / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const seconds = totalSeconds % 60 + + values.days.textContent = String(days).padStart(2, '0') + values.hours.textContent = String(hours).padStart(2, '0') + values.minutes.textContent = String(minutes).padStart(2, '0') + values.seconds.textContent = String(seconds).padStart(2, '0') + compactCountdown.textContent = days > 0 + ? `${days}D ${String(hours).padStart(2, '0')}H` + : `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` + } + + updateCountdown() + window.setInterval(updateCountdown, 250) +})()