Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>OwO.FM // Radio Active</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Luckiest+Guy&family=Outfit:wght@400;600;700;800&family=Syne:wght@800&display=swap');
:root {
--orange-rich: #ff4500;
--orange-deep: #cc2900;
--purple-neon: #7b1fa2;
--purple-deep: #30004a;
--vfd-cyan: #00cca3;
--card-bg: rgba(18, 12, 22, 0.72);
--bg-dark: #050308;
--glass-border: rgba(255, 255, 255, 0.09);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
body {
background-color: var(--bg-dark);
font-family: 'Outfit', sans-serif;
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
color: #ffffff;
}
/* --- СЛОЙ 0: АВРОРА И ШУМ --- */
.aurora-container {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
z-index: 0;
overflow: hidden;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.03'/%3E%3C/svg%3E");
}
.aurora-blob {
position: absolute;
border-radius: 50%;
filter: blur(90px);
opacity: 0.65;
animation: float-aurora 12s infinite alternate cubic-bezier(0.4, 0, 0.2, 1);
}
.aurora-1 { width: 75vw; height: 75vw; background: var(--purple-neon); top: -15%; left: -20%; }
.aurora-2 { width: 85vw; height: 85vw; background: var(--orange-rich); bottom: -20%; right: -20%; animation-delay: -5s; }
.aurora-3 { width: 55vw; height: 55vw; background: var(--purple-deep); top: 15%; right: -10%; animation-delay: -8s; opacity: 0.8; }
/* --- PAGE 1: СТАРТОВЫЙ ЭКРАН --- */
#phase1 {
position: absolute;
inset: 0;
z-index: 10;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
padding-bottom: 24px;
transition: opacity 0.8s cubic-bezier(0.25, 1, 0.3, 1), transform 0.8s ease;
}
#phase1.hidden {
opacity: 0;
pointer-events: none;
transform: scale(0.95);
}
/* Летящий текст */
#fx-container {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
z-index: 1;
pointer-events: none;
overflow: hidden;
}
.fox-word {
position: absolute;
font-family: 'Luckiest Guy', cursive;
color: #ffffff;
font-size: 2.3rem;
letter-spacing: 2px;
white-space: nowrap;
text-shadow:
0 0 12px var(--orange-rich),
0 0 25px var(--purple-neon),
0 0 45px rgba(255, 69, 0, 0.6);
opacity: 0;
will-change: transform, opacity;
animation: continuousFly 3.4s linear forwards;
}
/* Маскот Лис */
.mascot-layer {
position: absolute;
top: 38%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
height: 72vh;
max-height: 680px;
max-width: 100vw;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
animation: breathe 7s infinite alternate ease-in-out;
}
.mascot-layer img {
height: 100%;
width: auto;
object-fit: contain;
mix-blend-mode: screen;
filter:
drop-shadow(0 0 30px rgba(255, 69, 0, 0.5))
drop-shadow(0 0 65px rgba(123, 31, 162, 0.4));
}
/* Стартовая карточка */
.radio-card {
position: relative;
z-index: 10;
width: calc(100% - 32px);
max-width: 480px; /* Увеличено для лучшего вида на ПК */
background: var(--card-bg);
backdrop-filter: blur(28px) saturate(150%);
-webkit-backdrop-filter: blur(28px) saturate(150%);
border-radius: 32px;
padding: 26px 24px;
border: 1px solid var(--glass-border);
box-shadow:
0 30px 60px rgba(0, 0, 0, 0.9),
inset 0 1px 1px rgba(255, 255, 255, 0.15);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.fm-badge {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 5px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 700;
color: #e2d9eb;
letter-spacing: 0.5px;
}
.status-indicator {
font-size: 11px;
font-weight: 800;
letter-spacing: 2px;
color: #9d91a6;
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 6px;
height: 6px;
background-color: #8c7f96;
border-radius: 50%;
}
.card-body {
text-align: left;
margin-bottom: 22px;
}
.meta-label {
font-size: 11px;
font-weight: 800;
letter-spacing: 2px;
color: #9d91a6;
margin-bottom: 4px;
text-transform: uppercase;
}
.main-title {
font-family: 'Syne', sans-serif;
font-size: 44px;
font-weight: 800;
color: #ffffff;
line-height: 0.95;
letter-spacing: -1px;
margin-bottom: 8px;
}
.accent-title {
font-size: 15px;
font-weight: 800;
letter-spacing: 3px;
color: var(--orange-rich);
margin-bottom: 10px;
text-transform: uppercase;
}
.tagline {
font-size: 13.5px;
color: #b3a5bd;
font-weight: 400;
line-height: 1.4;
}
.btn-tune {
width: 100%;
padding: 16px 0;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: linear-gradient(180deg, #362f3b 0%, #1c1721 100%);
color: #ffffff;
font-size: 15px;
font-weight: 800;
letter-spacing: 2px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow:
0 10px 25px rgba(0,0,0,0.6),
inset 0 1px 1px rgba(255,255,255,0.2);
transition: all 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
font-family: inherit;
}
.btn-tune:hover {
background: linear-gradient(180deg, #483e4f 0%, #28212e 100%);
border-color: rgba(255, 69, 0, 0.5);
box-shadow:
0 12px 30px rgba(255, 69, 0, 0.25),
inset 0 1px 1px rgba(255,255,255,0.3);
transform: translateY(-2px);
}
.loader {
display: none;
width: 22px;
height: 22px;
border: 3px solid rgba(255, 255, 255, 0.15);
border-radius: 50%;
border-top-color: var(--orange-rich);
border-right-color: var(--purple-neon);
animation: spin 0.8s linear infinite;
}
/* --- PAGE 2: ПЛЕЕР В ЕДИНОМ СТИЛЕ --- */
#phase2 {
position: absolute;
inset: 0;
z-index: 20;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
opacity: 0;
pointer-events: none;
transition: opacity 0.8s cubic-bezier(0.25, 1, 0.3, 1), transform 0.8s ease;
transform: scale(1.05);
}
#phase2.active {
opacity: 1;
pointer-events: auto;
transform: scale(1);
}
.player-card {
width: 100%;
max-width: 480px; /* Увеличено для лучшего вида на ПК */
background: var(--card-bg);
backdrop-filter: blur(32px) saturate(160%);
-webkit-backdrop-filter: blur(32px) saturate(160%);
border-radius: 32px;
padding: 24px;
border: 1px solid var(--glass-border);
box-shadow:
0 35px 70px rgba(0, 0, 0, 0.95),
inset 0 1px 1px rgba(255, 255, 255, 0.15);
display: flex;
flex-direction: column;
gap: 18px;
}
/* Дисплей Плеера (VFD / Cover Art) */
.vfd-screen {
position: relative;
width: 100%;
aspect-ratio: 1 / 1; /* Жестко заданный квадратный формат */
background: rgba(3, 2, 6, 0.85);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: inset 0 0 25px rgba(0,0,0,0.9);
overflow: hidden;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
#visualizerCanvas {
width: 100%;
height: 100%;
display: block;
}
.cover-container {
position: absolute;
inset: 0;
display: none;
justify-content: center;
align-items: center;
background: rgba(3, 2, 6, 1);
}
.cover-art {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.95;
transition: opacity 0.3s ease;
}
.cover-overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(0,0,0,0.2) 50%, rgba(0,0,0,0.6) 100%);
box-shadow: inset 0 0 40px rgba(0,0,0,0.8);
pointer-events: none;
}
.view-badge {
position: absolute;
top: 12px;
right: 12px;
font-size: 9px;
font-weight: 800;
letter-spacing: 1.5px;
color: var(--vfd-cyan);
background: rgba(12, 8, 18, 0.85);
padding: 4px 10px;
border-radius: 20px;
border: 1px solid rgba(0, 204, 163, 0.3);
backdrop-filter: blur(4px);
z-index: 5;
}
/* Бегущая строка VFD Ticker */
.vfd-ticker-box {
width: 100%;
height: 44px;
background: rgba(3, 2, 6, 0.85);
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: inset 0 0 12px rgba(0,0,0,0.9);
overflow: hidden;
display: flex;
align-items: center;
}
#tickerCanvas {
width: 100%;
height: 100%;
display: block;
}
/* Кнопки управления плеером (Стеклянный Vista стиль) */
.controls-row {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
width: 100%;
}
.ctrl-btn {
height: 54px;
border-radius: 18px;
/* Стеклянный градиент с резким переходом */
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0.03) 49%,
rgba(0, 0, 0, 0.25) 50%,
rgba(0, 0, 0, 0.4) 100%);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow:
0 8px 20px rgba(0,0,0,0.5),
inset 0 1px 1px rgba(255,255,255,0.25);
backdrop-filter: blur(10px);
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
font-family: inherit;
}
.ctrl-btn:hover {
border-color: rgba(255, 255, 255, 0.25);
box-shadow:
0 0 25px rgba(255, 69, 0, 0.25),
inset 0 1px 2px rgba(255,255,255,0.4);
transform: translateY(-2px);
}
.ctrl-btn:active {
transform: translateY(1px) scale(0.97);
background: linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.5) 100%);
}
.play-main {
width: 140px;
font-size: 22px;
color: var(--orange-rich);
}
.next-btn {
width: 80px;
font-size: 18px;
color: var(--purple-neon);
}
/* Выбор радиостанций */
.stations-grid {
display: flex;
gap: 12px;
width: 100%;
}
.station-card-btn {
flex: 1;
padding: 14px 10px;
background: linear-gradient(180deg, rgba(35, 27, 43, 0.8) 0%, rgba(18, 13, 23, 0.8) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
font-size: 12px;
font-weight: 800;
letter-spacing: 1px;
color: #b3a5bd;
cursor: pointer;
transition: all 0.25s ease;
text-align: center;
}
.station-card-btn:hover {
color: #ffffff;
border-color: rgba(255, 255, 255, 0.2);
}
.station-card-btn.active {
color: #ffffff;
border-color: var(--orange-rich);
background: linear-gradient(180deg, rgba(255, 69, 0, 0.25) 0%, rgba(48, 0, 74, 0.4) 100%);
box-shadow: 0 0 20px rgba(255, 69, 0, 0.25);
text-shadow: 0 0 10px rgba(255, 69, 0, 0.6);
}
/* --- АНИМАЦИИ --- */
@keyframes float-aurora {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(5%, 5%) scale(1.1); }
100% { transform: translate(-5%, -5%) scale(0.9); }
}
@keyframes breathe {
0% { transform: translate(-50%, -50%) scale(0.97) rotate(-1deg); }
100% { transform: translate(-50%, -50%) scale(1.02) rotate(1deg); }
}
@keyframes continuousFly {
0% { opacity: 0; transform: translate(var(--dx-start), 0) scale(0.8) rotate(var(--rot-start)); }
15% { opacity: 0.9; }
80% { opacity: 0.9; }
100% { opacity: 0; transform: translate(var(--dx-end), -85vh) scale(1.1) rotate(var(--rot-end)); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 480px) {
.mascot-layer { height: 60vh; top: 35%; }
.radio-card, .player-card { padding: 22px 20px; }
.main-title { font-size: 38px; }
.fox-word { font-size: 1.8rem; }
/* Убрано ограничение высоты .vfd-screen для сохранения квадратных пропорций на мобильных */
}
</style>
</head>
<body>
<!-- Слой 0: Неоновая Аврора -->
<div class="aurora-container">
<div class="aurora-blob aurora-1"></div>
<div class="aurora-blob aurora-2"></div>
<div class="aurora-blob aurora-3"></div>
</div>
<!-- PHASE 1: СТАРТОВЫЙ ЭКРАН -->
<div id="phase1">
<div id="fx-container"></div>
<div class="mascot-layer">
<img id="mascot-img" src="https://i.postimg.cc/yNBJTg1H/e6d91e43-7287-49ed-8327-ef521fd39899.webp" alt="OwO Fox Mascot">
</div>
<div class="radio-card" id="main-panel">
<div class="card-header">
<span class="fm-badge">88.7 FM</span>
<div class="status-indicator">
<span class="status-dot"></span>
STANDBY
</div>
</div>
<div class="card-body">
<div class="meta-label">ONLINE RADIO</div>
<h1 class="main-title">OwO.FM</h1>
<div class="accent-title">RADIO ACTIVE</div>
<p class="tagline">Cinematic nights. A loud fox. One live signal.</p>
</div>
<button class="btn-tune" id="enter-btn">
<span class="btn-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.93 19.07A10 10 0 0 1 4.93 4.93M7.76 16.24a7 7 0 0 1 0-9.48M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0M16.24 7.76a7 7 0 0 1 0 9.48M19.07 4.93a10 10 0 0 1 0 14.14"/>
</svg>
</span>
<span id="btn-text">TUNE IN</span>
<div class="loader" id="loader"></div>
</button>
</div>
</div>
<!-- PHASE 2: ПЛЕЕР С VFD ЭКРАНОМ -->
<div id="phase2">
<div class="player-card">
<div class="card-header" style="margin-bottom: 0;">
<span class="fm-badge" id="playerFmBadge">88.7 FM</span>
<div class="status-indicator">
<span class="status-dot" style="background-color: var(--orange-rich); box-shadow: 0 0 8px var(--orange-rich);"></span>
LIVE STREAM
</div>
</div>
<!-- VFD Экран / Обложка -->
<div class="vfd-screen" id="vfdDisplay">
<div class="view-badge" id="viewBadge">VFD SPECTRUM</div>
<canvas id="visualizerCanvas"></canvas>
<div class="cover-container" id="coverContainer">
<img class="cover-art" id="coverImg" src="" alt="Station Artwork">
<div class="cover-overlay"></div>
</div>
</div>
<!-- VFD Dot-Matrix Бегущая строка -->
<div class="vfd-ticker-box">
<canvas id="tickerCanvas"></canvas>
</div>
<!-- Управление -->
<div class="controls-row">
<button class="ctrl-btn play-main" id="playBtn" aria-label="Play/Pause">▶</button>
<button class="ctrl-btn next-btn" id="nextBtn" aria-label="Next Station">⏭</button>
</div>
<!-- Переключатель станций -->
<div class="stations-grid" id="stationsList"></div>
</div>
</div>
<script>
/* === НАСТРОЙКИ РАДИОСТАНЦИЙ И ПОТОКОВ === */
const stations = [
{
id: 'owo',
name: 'OwO by NFX',
freq: '88.7 FM',
url: 'https://stream.nightride.fm/nightride.mp3', // Замените на ваш Icecast поток
cover: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=600&auto=format&fit=crop'
},
{
id: 'citypop',
name: 'City Pop',
freq: '101.3 FM',
url: 'https://stream.nightride.fm/chillsynth.mp3', // Замените на ваш Icecast поток
cover: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?q=80&w=600&auto=format&fit=crop'
}
];
/* === ЭФФЕКТЫ СТАРТОВОГО ЭКРАНА === */
const foxSounds = ["RING DING DING!", "WA PA PA PA!", "HATEE HATEE!", "JACHA CHACHA!", "FRAKA KAKA!", "A-OOO-OO-OOO!", "YIP YIP!", "PAW PAW!"];
const fxContainer = document.getElementById('fx-container');
let spawnInterval;
function spawnWord() {
const wordEl = document.createElement('div');
wordEl.className = 'fox-word';
wordEl.innerText = foxSounds[Math.floor(Math.random() * foxSounds.length)];
const startX = Math.floor(Math.random() * 60) + 20;
wordEl.style.left = `${startX}%`;
wordEl.style.top = `75%`;
const driftX = (Math.random() - 0.5) * 140;
const rotStart = (Math.random() - 0.5) * 16;
const rotEnd = rotStart + (Math.random() - 0.5) * 20;
wordEl.style.setProperty('--dx-start', '0px');
wordEl.style.setProperty('--dx-end', `${driftX}px`);
wordEl.style.setProperty('--rot-start', `${rotStart}deg`);
wordEl.style.setProperty('--rot-end', `${rotEnd}deg`);
fxContainer.appendChild(wordEl);
setTimeout(() => wordEl.remove(), 3400);
}
spawnInterval = setInterval(spawnWord, 650);
/* === ПЕРЕХОД МЕЖДУ СТРАНИЦАМИ === */
const phase1 = document.getElementById('phase1');
const phase2 = document.getElementById('phase2');
const enterBtn = document.getElementById('enter-btn');
const btnText = document.getElementById('btn-text');
const btnIcon = enterBtn.querySelector('.btn-icon');
const loader = document.getElementById('loader');
enterBtn.addEventListener('click', () => {
enterBtn.style.pointerEvents = 'none';
btnIcon.style.display = 'none';
btnText.style.display = 'none';
loader.style.display = 'block';
clearInterval(spawnInterval);
spawnInterval = setInterval(spawnWord, 180);
setTimeout(() => {
phase1.classList.add('hidden');
setTimeout(() => {
phase2.classList.add('active');
initPlayer();
loadStation(0, true);
}, 400);
}, 600);
});
/* === ПЛЕЕР И VFD СИСТЕМА === */
let activeStationIdx = 0;
let isPlaying = false;
let viewMode = 'VFD'; // 'VFD' или 'COVER'
let tempNotice = null;
let tempNoticeTimer = null;
const audio = new Audio();
audio.crossOrigin = "anonymous";
let audioCtx, analyser, freqData;
const vfdDisplay = document.getElementById('vfdDisplay');
const viewBadge = document.getElementById('viewBadge');
const coverContainer = document.getElementById('coverContainer');
const coverImg = document.getElementById('coverImg');
const playerFmBadge = document.getElementById('playerFmBadge');
const playBtn = document.getElementById('playBtn');
const nextBtn = document.getElementById('nextBtn');
const stationsList = document.getElementById('stationsList');
const vCanvas = document.getElementById('visualizerCanvas');
const vCtx = vCanvas.getContext('2d');
const tCanvas = document.getElementById('tickerCanvas');
const tCtx = tCanvas.getContext('2d');
const NUM_COLS = 32;
let peaks = new Array(NUM_COLS).fill(0);
let tickerScrollX = 0;
// VFD Шрифт 5x7
const FONT_5X7 = {
'A': [0x0e,0x11,0x11,0x1f,0x11,0x11,0x11], 'B': [0x1e,0x11,0x11,0x1e,0x11,0x11,0x1e],
'C': [0x0e,0x11,0x10,0x10,0x10,0x11,0x0e], 'D': [0x1c,0x12,0x11,0x11,0x11,0x12,0x1c],
'E': [0x1f,0x10,0x10,0x1e,0x10,0x10,0x1f], 'F': [0x1f,0x10,0x10,0x1e,0x10,0x10,0x10],
'G': [0x0e,0x11,0x10,0x17,0x11,0x11,0x0e], 'H': [0x11,0x11,0x11,0x1f,0x11,0x11,0x11],
'I': [0x0e,0x04,0x04,0x04,0x04,0x04,0x0e], 'J': [0x07,0x02,0x02,0x02,0x02,0x12,0x0c],
'K': [0x11,0x12,0x14,0x18,0x14,0x12,0x11], 'L': [0x10,0x10,0x10,0x10,0x10,0x10,0x1f],
'M': [0x11,0x1b,0x15,0x15,0x11,0x11,0x11], 'N': [0x11,0x19,0x15,0x13,0x11,0x11,0x11],
'O': [0x0e,0x11,0x11,0x11,0x11,0x11,0x0e], 'P': [0x1e,0x11,0x11,0x1e,0x10,0x10,0x10],
'Q': [0x0e,0x11,0x11,0x11,0x15,0x12,0x0d], 'R': [0x1e,0x11,0x11,0x1e,0x14,0x12,0x11],
'S': [0x0e,0x11,0x10,0x0e,0x01,0x11,0x0e], 'T': [0x1f,0x04,0x04,0x04,0x04,0x04,0x04],
'U': [0x11,0x11,0x11,0x11,0x11,0x11,0x0e], 'V': [0x11,0x11,0x11,0x11,0x11,0x0a,0x04],
'W': [0x11,0x11,0x11,0x15,0x15,0x1b,0x11], 'X': [0x11,0x11,0x0a,0x04,0x0a,0x11,0x11],
'Y': [0x11,0x11,0x11,0x0a,0x04,0x04,0x04], 'Z': [0x1f,0x01,0x02,0x04,0x08,0x10,0x1f],
'0': [0x0e,0x13,0x15,0x19,0x15,0x13,0x0e], '1': [0x04,0x0c,0x04,0x04,0x04,0x04,0x0e],
'2': [0x0e,0x11,0x01,0x02,0x04,0x08,0x1f], '3': [0x1f,0x01,0x02,0x0e,0x01,0x11,0x0e],
'4': [0x02,0x06,0x0a,0x12,0x1f,0x02,0x02], '5': [0x1f,0x10,0x1e,0x01,0x01,0x11,0x0e],
'6': [0x0e,0x10,0x10,0x1e,0x11,0x11,0x0e], '7': [0x1f,0x01,0x02,0x04,0x08,0x08,0x08],
'8': [0x0e,0x11,0x11,0x0e,0x11,0x11,0x0e], '9': [0x0e,0x11,0x11,0x0f,0x01,0x01,0x0e],
'-': [0x00,0x00,0x00,0x1f,0x00,0x00,0x00], '.': [0x00,0x00,0x00,0x00,0x00,0x06,0x06],
':': [0x00,0x06,0x06,0x00,0x06,0x06,0x00], '/': [0x01,0x02,0x04,0x08,0x10,0x00,0x00],
' ': [0x00,0x00,0x00,0x00,0x00,0x00,0x00]
};
function initPlayer() {
renderStationButtons();
setupCanvases();
window.addEventListener('resize', setupCanvases);
vfdDisplay.addEventListener('click', toggleViewMode);
playBtn.addEventListener('click', () => togglePlay());
// Кнопка NEXT - переключает на следующую станцию
nextBtn.addEventListener('click', () => {
const nextIdx = (activeStationIdx + 1) % stations.length;
loadStation(nextIdx, isPlaying);
triggerNotice('>> NEXT STATION');
});
requestAnimationFrame(animationLoop);
}
function renderStationButtons() {
stationsList.innerHTML = '';
stations.forEach((s, i) => {
const btn = document.createElement('button');
btn.className = `station-card-btn ${i === activeStationIdx ? 'active' : ''}`;
btn.innerText = s.name;
btn.onclick = () => loadStation(i, true);
stationsList.appendChild(btn);
});
}
function loadStation(idx, autoPlay = false) {
activeStationIdx = idx;
const st = stations[idx];
audio.src = st.url;
coverImg.src = st.cover;
playerFmBadge.innerText = st.freq;
Array.from(stationsList.children).forEach((b, i) => {
b.classList.toggle('active', i === idx);
});
if (autoPlay) togglePlay(true);
}
function toggleViewMode() {
if (viewMode === 'VFD') {
viewMode = 'COVER';
vCanvas.style.display = 'none';
coverContainer.style.display = 'flex';
viewBadge.innerText = 'COVER ART';
} else {
viewMode = 'VFD';
vCanvas.style.display = 'block';
coverContainer.style.display = 'none';
viewBadge.innerText = 'VFD SPECTRUM';
}
}
function triggerNotice(msg) {
tempNotice = msg;
if (tempNoticeTimer) clearTimeout(tempNoticeTimer);
tempNoticeTimer = setTimeout(() => tempNotice = null, 1800);
}
function initAudioContext() {
if (audioCtx) return;
const AudioCtx = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioCtx();
analyser = audioCtx.createAnalyser();
analyser.fftSize = 128;
freqData = new Uint8Array(analyser.frequencyBinCount);
const source = audioCtx.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioCtx.destination);
}
function togglePlay(force) {
isPlaying = force !== undefined ? force : !isPlaying;
if (isPlaying) {
initAudioContext();
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
audio.play().catch(() => { isPlaying = false; updatePlayBtn(); });
} else {
audio.pause();
}
updatePlayBtn();
}
function updatePlayBtn() {
playBtn.innerText = isPlaying ? '❚❚' : '▶';
}
function setupCanvases() {
const dpr = window.devicePixelRatio || 1;
[vCanvas, tCanvas].forEach(c => {
const rect = c.parentElement.getBoundingClientRect();
c.width = rect.width * dpr;
c.height = rect.height * dpr;
});
}
function animationLoop() {
requestAnimationFrame(animationLoop);
if (viewMode === 'VFD') drawVfdSpectrum();
drawTicker();
}
function drawVfdSpectrum() {
const dpr = window.devicePixelRatio || 1;
const w = vCanvas.width / dpr;
const h = vCanvas.height / dpr;
vCtx.save();
vCtx.scale(dpr, dpr);
vCtx.clearRect(0, 0, w, h);
if (isPlaying && analyser) analyser.getByteFrequencyData(freqData);
const colWidth = w / NUM_COLS;
const barWidth = colWidth * 0.65;
const radius = barWidth / 2;
// Стеклянный градиент в тонах интерфейса
const gradient = vCtx.createLinearGradient(0, h, 0, 0);
gradient.addColorStop(0, '#7b1fa2'); /* purple-neon */
gradient.addColorStop(0.6, '#ff4500'); /* orange-rich */
gradient.addColorStop(1, '#ffc107'); /* яркий акцент на пиках */
for (let i = 0; i < NUM_COLS; i++) {
const val = isPlaying && freqData ? freqData[i] : 0;
peaks[i] = Math.max(val, peaks[i] - 1.2); // плавное падение пиков
const barHeight = (val / 255) * (h * 0.85); // небольшой отступ сверху для пиков
const peakHeight = (peaks[i] / 255) * (h * 0.85);
const x = i * colWidth + (colWidth - barWidth) / 2;
// Отрисовка основного столбца
if (barHeight > 0) {
vCtx.fillStyle = gradient;
vCtx.globalAlpha = 0.85;
vCtx.beginPath();
if (vCtx.roundRect) {
vCtx.roundRect(x, h - barHeight, barWidth, barHeight, radius);
} else {
vCtx.fillRect(x, h - barHeight, barWidth, barHeight);
}
vCtx.fill();
}
// Отрисовка светящейся точки пика
if (peakHeight > 0) {
vCtx.fillStyle = '#ffffff';
vCtx.globalAlpha = 1;
vCtx.beginPath();
vCtx.arc(x + barWidth / 2, h - peakHeight - (barWidth), barWidth / 2, 0, Math.PI * 2);
vCtx.fill();
// Эффект свечения
vCtx.shadowBlur = 12;
vCtx.shadowColor = '#ff4500';
vCtx.fill();
vCtx.shadowBlur = 0;
}
}
vCtx.restore();
}
function drawTicker() {
const dpr = window.devicePixelRatio || 1;
const w = tCanvas.width / dpr;
const h = tCanvas.height / dpr;
tCtx.save();
tCtx.scale(dpr, dpr);
tCtx.clearRect(0, 0, w, h);
const st = stations[activeStationIdx];
const text = tempNotice ? tempNotice : (isPlaying ? `NOW PLAYING: ${st.name} // LIVE STEREO SIGNAL` : `STANDBY - ${st.name} - PRESS PLAY`);
const dotSize = 2.2;
const dotGap = 1;
const charWidth = 6 * (dotSize + dotGap);
tickerScrollX -= 0.9;
if (tickerScrollX < -(text.length * charWidth)) tickerScrollX = w;
let startX = tickerScrollX;
const startY = (h - (7 * (dotSize + dotGap))) / 2;
for (let c = 0; c < text.length; c++) {
const char = text[c].toUpperCase();
const matrix = FONT_5X7[char] || FONT_5X7[' '];
if (startX > -charWidth && startX < w) {
for (let row = 0; row < 7; row++) {
const bits = matrix[row] || 0;
for (let col = 0; col < 5; col++) {
const bit = (bits >> (4 - col)) & 1;
const dx = startX + col * (dotSize + dotGap);
const dy = startY + row * (dotSize + dotGap);
tCtx.fillStyle = '#ff4500';
tCtx.globalAlpha = bit ? 0.9 : 0.05;
tCtx.fillRect(dx, dy, dotSize, dotSize);
}
}
}
startX += charWidth;
}
tCtx.restore();
}
</script>
</body>
</html>