38428-vm/index.php
2026-02-15 06:08:18 +00:00

1040 lines
36 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . "/includes/tracker.php";
track_visitor();
$projectDescription = $_SERVER["PROJECT_DESCRIPTION"] ?? "Lili Records Radio - La mejor música en vivo.";
$projectImageUrl = $_SERVER["PROJECT_IMAGE_URL"] ?? "assets/images/featured.jpg";
// WhatsApp info
$whatsapp_link = "https://chat.whatsapp.com/DkG96pTzAFO3hvLqmzwmTY";
// Program Schedule
$schedule = [
[
"time" => "14:00",
"name" => "Amanecer Techno",
"desc" => "Comienza el día con los ritmos más puros y energéticos del techno underground.",
"image" => "assets/images/programs/techno_sunrise.jpg"
],
[
"time" => "16:30",
"name" => "Sesiones de House Vocal",
"desc" => "Una selección exquisita de house melódico con las voces más cautivadoras.",
"image" => "assets/images/programs/vocal_house.jpg"
],
[
"time" => "19:00",
"name" => "Invitado Especial Lili",
"desc" => "Cada semana, un invitado especial nos trae su visión única de la pista de baile.",
"image" => "assets/images/programs/lili_guest.jpg"
],
[
"time" => "21:00",
"name" => "Vibras de la Noche",
"desc" => "Sonidos profundos y envolventes para acompañar la calma de la noche.",
"image" => "assets/images/programs/deep_night.jpg"
],
];
$defaultStudioImage = "assets/pasted-20260215-020116-2dc16355.jpg";
function get_live_index($schedule) {
$current = date("H:i");
$count = count($schedule);
for ($i = 0; $i < $count; $i++) {
$start = $schedule[$i]["time"];
$next = ($i < $count - 1) ? $schedule[$i+1]["time"] : "23:59";
if ($current >= $start && $current < $next) return $i;
}
// Handling night transition
if ($current >= $schedule[$count-1]["time"] || $current < $schedule[0]["time"]) return $count - 1;
return -1;
}
$liveIndex = get_live_index($schedule);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lili Records Radio</title>
<!-- Meta tags SEO -->
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="og:title" content="Lili Records Radio" />
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<meta property="twitter:card" content="summary_large_image" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--accent-color: #00e676;
--primary-color: #38bdf8;
--secondary-color: #f472b6;
--bg-dark: #0f172a;
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
--flash-color: #ffffff;
}
.flash-effect {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
background-color: var(--flash-color);
opacity: 0;
mix-blend-mode: overlay;
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: "Inter", sans-serif;
color: #ffffff;
background-color: #0f172a;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
@keyframes shake {
0% { transform: translate(0, 0); }
25% { transform: translate(-8px, 8px); }
50% { transform: translate(8px, -8px); }
75% { transform: translate(-8px, -8px); }
100% { transform: translate(0, 0); }
}
.shake {
animation: shake 0.15s cubic-bezier(.36,.07,.19,.97) both;
}
.background-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -2;
background-image: url("assets/pasted-20260215-011439-25779668.jpg?v=<?php echo time(); ?>");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
animation: ken-burns 60s ease-in-out infinite alternate;
transition: background-image 2s ease-in-out, transform 0.1s ease-out;
transform: scale(var(--pulse-intensity, 1));
}
@keyframes ken-burns {
0% { transform: scale(var(--pulse-intensity, 1)); }
100% { transform: scale(calc(var(--pulse-intensity, 1) * 1.1)); }
}
.background-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background: radial-gradient(circle at top right, rgba(56, 189, 248, 0.1), transparent 40%),
radial-gradient(circle at bottom left, rgba(244, 114, 182, 0.1), transparent 40%),
linear-gradient(135deg, rgba(15, 23, 42, 0.4) 0%, rgba(15, 23, 42, 0.7) 100%);
transition: opacity 0.1s ease-out;
opacity: calc(0.7 + (var(--pulse-intensity, 1) - 1) * 2);
}
/* Floating Microphones Effect */
.floating-mic {
position: absolute;
color: var(--mic-color, rgba(255, 255, 255, 0.1));
font-size: 3.5rem;
z-index: 0;
pointer-events: none;
animation: float-and-rotate 25s cubic-bezier(0.4, 0, 0.2, 1) infinite;
filter: drop-shadow(0 0 15px var(--mic-color));
}
@keyframes float-and-rotate {
0% {
transform: translate(0, 0) rotate(0deg) scale(0.8);
opacity: 0;
}
15% { opacity: 0.4; }
85% { opacity: 0.4; }
100% {
transform: translate(var(--move-x), var(--move-y)) rotate(360deg) scale(1.4);
opacity: 0;
}
}
.app-container {
display: flex;
width: 100%;
max-width: 1800px;
height: auto;
align-items: center;
justify-content: center;
padding: 2rem;
box-sizing: border-box;
position: relative;
z-index: 10;
}
.side-image-section {
width: 100%;
max-width: 320px;
display: flex;
flex-direction: column;
justify-content: center;
}
.side-image-card {
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 40px;
padding: 1rem;
box-shadow: 0 40px 80px -20px rgba(0, 0, 0, 0.4);
transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.side-image-card:hover {
transform: translateY(-5px) rotate(1deg);
border-color: var(--secondary-color);
}
.side-img {
width: 100%;
height: auto;
border-radius: 32px;
display: block;
}
.app-content {
display: flex;
gap: 3rem;
align-items: center;
justify-content: center;
flex-wrap: wrap;
width: 100%;
}
.studio-section {
width: 100%;
max-width: 480px;
display: flex;
flex-direction: column;
justify-content: center;
}
.studio-card {
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 40px;
padding: 1rem;
box-shadow: 0 40px 80px -20px rgba(0, 0, 0, 0.4);
transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
overflow: hidden;
}
.studio-tag {
position: absolute;
top: 2.2rem;
left: 2.2rem;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 0.6rem 1.2rem;
border-radius: 100px;
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 0.6rem;
z-index: 5;
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
transition: all 0.4s ease;
}
.studio-card:hover {
transform: translateY(-5px) rotate(-1deg);
border-color: rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.04);
}
.studio-card:hover .studio-tag {
transform: scale(1.05);
background: rgba(15, 23, 42, 0.7);
border-color: var(--primary-color);
}
.studio-photo {
width: 100%;
height: auto;
border-radius: 32px;
display: block;
filter: saturate(1.1) brightness(1.05);
}
.player-section {
width: 100%;
max-width: 420px;
display: flex;
flex-direction: column;
justify-content: center;
perspective: 1000px;
}
/* Upcoming Programs Styles */
.upcoming-section {
width: 100%;
max-width: 340px;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.small-glass {
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 36px;
padding: 2rem;
box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.3);
transition: all 0.5s ease;
}
.upcoming-section h2 {
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1.5rem;
color: var(--primary-color);
letter-spacing: 0.05em;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 0.8rem;
}
.program-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.program-item {
display: flex;
flex-direction: column;
gap: 0.2rem;
padding-bottom: 1.2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
transition: all 0.3s ease;
cursor: pointer;
}
.program-header {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
}
.program-description {
max-height: 0;
overflow: hidden;
opacity: 0;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.45);
line-height: 1.5;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
padding-left: 4.1rem;
}
.program-item.is-expanded .program-description {
max-height: 100px;
opacity: 1;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.program-item.is-live {
background: rgba(0, 230, 118, 0.08);
border-bottom: 1px solid rgba(0, 230, 118, 0.2);
padding: 1rem;
margin: 0 -1rem;
border-radius: 16px;
position: relative;
}
.program-item.is-live::after {
content: "VIVO";
position: absolute;
right: 1rem;
top: 1.2rem;
font-size: 0.5rem;
font-weight: 800;
color: var(--accent-color);
border: 1px solid var(--accent-color);
padding: 2px 6px;
border-radius: 4px;
letter-spacing: 0.1em;
animation: pulse-glow 2s infinite;
}
.program-time {
font-size: 0.75rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.4);
min-width: 50px;
}
.program-name {
font-size: 0.9rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.85);
letter-spacing: 0.01em;
}
.glass-card {
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 60px;
padding: 4rem 3rem;
box-shadow: 0 60px 120px -30px rgba(0, 0, 0, 0.6);
text-align: center;
transition: all 0.8s ease;
}
.brand h1 {
font-size: 2.8rem;
font-weight: 700;
margin: 0 0 0.5rem;
color: #fff;
letter-spacing: -0.04em;
text-transform: lowercase;
}
.brand-logo {
width: 110px;
height: auto;
margin: 0 auto 2rem;
display: block;
filter: drop-shadow(0 10px 20px rgba(0,0,0,0.3));
}
.brand p {
font-size: 0.95rem;
font-weight: 300;
opacity: 0.6;
margin-bottom: 3rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.now-playing-container {
background: rgba(255, 255, 255, 0.02);
border-radius: 32px;
padding: 3rem 2rem;
border: 1px solid rgba(255, 255, 255, 0.05);
margin-bottom: 2.5rem;
position: relative;
overflow: hidden;
transition: all 0.5s ease;
}
.track-title {
font-weight: 300;
font-size: 1.2rem;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0.02em;
position: relative;
z-index: 2;
margin-top: 1rem;
}
.track-cover {
width: 120px;
height: 120px;
border-radius: 20px;
margin: 0 auto 1.5rem;
display: block;
object-fit: cover;
box-shadow: 0 15px 30px rgba(0,0,0,0.3);
position: relative;
z-index: 2;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.5s ease;
}
.track-cover:hover {
transform: scale(1.05);
border-color: var(--primary-color);
}
.play-btn {
width: 90px;
height: 90px;
border-radius: 50%;
background: #fff;
border: none;
color: var(--bg-dark);
font-size: 3rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 30px 60px -15px rgba(0, 0, 0, 0.5);
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
margin: 0 auto;
}
.play-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 40px rgba(255, 255, 255, 0.2);
}
.play-btn.is-playing {
background: #ff3d00;
color: #fff;
box-shadow: 0 0 50px rgba(255, 61, 0, 0.4);
animation: pulse-red 2s infinite;
}
@keyframes pulse-red {
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 61, 0, 0.7); }
70% { transform: scale(1.05); box-shadow: 0 0 0 20px rgba(255, 61, 0, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 61, 0, 0); }
}
.volume-container {
width: 100%;
max-width: 180px;
display: flex;
align-items: center;
gap: 1.2rem;
margin: 2.5rem auto 0;
opacity: 0.5;
transition: opacity 0.3s ease;
}
.volume-container:hover {
opacity: 1;
}
.volume-slider {
flex: 1;
height: 3px;
-webkit-appearance: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
cursor: pointer;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #fff;
border-radius: 50%;
}
#visualizer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 80px;
opacity: 0.8;
pointer-events: none;
z-index: 1;
}
.track-status {
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.3em;
color: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
background: rgba(56, 189, 248, 0.08);
padding: 0.5rem 1.2rem;
border-radius: 100px;
width: fit-content;
margin: 0 auto 1.2rem;
border: 1px solid rgba(56, 189, 248, 0.15);
position: relative;
z-index: 2;
}
.live-dot {
width: 8px;
height: 8px;
background-color: var(--primary-color);
border-radius: 50%;
box-shadow: 0 0 10px var(--primary-color);
animation: pulse-glow 2s infinite;
}
@keyframes pulse-glow {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.4); opacity: 0.8; }
100% { transform: scale(1); opacity: 1; }
}
.payment-section {
margin-top: 2rem;
opacity: 0.5;
}
.qr-container img {
width: 90px;
border-radius: 12px;
}
.whatsapp-float {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 60px;
height: 60px;
background: var(--accent-color);
border-radius: 50%;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
z-index: 100;
}
@media (max-width: 992px) {
body, html { overflow-y: auto; height: auto; }
.app-container { padding: 1rem; }
}
</style>
</head>
<body>
<div class="flash-effect"></div>
<div class="background-container"></div>
<div class="background-overlay"></div>
<div id="mics-background"></div>
<div class="app-container">
<div class="app-content">
<div class="side-image-section">
<div class="side-image-card">
<img src="./assets/pasted-20260215-024434-5730389a.jpg" alt="Side Image" class="side-img">
</div>
</div>
<section class="studio-section">
<div class="studio-card">
<div class="studio-tag">
<span class="live-dot"></span> <span id="studio-program-name"><?= ($liveIndex !== -1) ? htmlspecialchars($schedule[$liveIndex]["name"]) : "Directo desde el estudio" ?></span>
</div>
<img id="studio-photo" src="<?= ($liveIndex !== -1 && !empty($schedule[$liveIndex]["image"])) ? $schedule[$liveIndex]["image"] : $defaultStudioImage ?>?v=<?php echo time(); ?>" alt="Studio Live" class="studio-photo">
</div>
</section>
<section class="player-section">
<div class="glass-card">
<header class="brand">
<img src="assets/pasted-20260214-203540-699a2e6a.png" alt="Logo" class="brand-logo">
<h1>Lili Records</h1>
<p>La sintonía que eleva tus sentidos.</p>
</header>
<div class="now-playing-container">
<canvas id="visualizer"></canvas>
<div class="track-status"><span class="live-dot"></span> AL AIRE</div>
<img id="track-cover" class="track-cover" src="assets/pasted-20260214-203540-699a2e6a.png" alt="Cover">
<div id="track-title" class="track-title">Conectando...</div>
</div>
<button class="play-btn" onclick="togglePlay()"><i id="play-icon" class="bi bi-play-fill"></i></button>
<div class="volume-container">
<i class="bi bi-volume-down"></i>
<input type="range" class="volume-slider" min="0" max="1" step="0.01" value="1" oninput="changeVolume(this.value)">
<i class="bi bi-volume-up"></i>
</div>
<div class="payment-section">
<p style="font-size:0.7rem; margin-bottom:1rem;">APOYA NUESTRA VIBRA</p>
<div class="qr-container"><img src="assets/pasted-20260214-203505-f7d808c3.jpg" alt="QR"></div>
</div>
</div>
</section>
<section class="upcoming-section">
<div class="small-glass">
<h2>Próximos Programas</h2>
<ul class="program-list">
<?php foreach ($schedule as $index => $item): ?>
<li class="program-item <?= ($index === $liveIndex) ? "is-live" : "" ?>" onclick="toggleDescription(this)">
<div class="program-header">
<span class="program-time"><?= $item["time"] ?></span>
<span class="program-name"><?= $item["name"] ?></span>
</div>
<div class="program-description"><?= htmlspecialchars($item["desc"]) ?></div>
</li>
<?php endforeach; ?>
</ul>
</div>
</section>
</div>
</div>
<a href="<?= $whatsapp_link ?>" target="_blank" class="whatsapp-float"><i class="bi bi-whatsapp"></i></a>
<audio id="radio-audio" src="https://play.radioking.io/lili-records-radio-2" preload="none" crossorigin="anonymous"></audio>
<script>
// Mics background with vibrant colors
const micBg = document.getElementById("mics-background");
const micColors = ["#00e676", "#38bdf8", "#f472b6", "#fbbf24", "#fb7185", "#2dd4bf"];
for (let i = 0; i < 18; i++) {
const mic = document.createElement("i");
mic.className = "bi bi-mic-fill floating-mic";
mic.style.left = Math.random() * 100 + "vw";
mic.style.top = Math.random() * 100 + "vh";
mic.style.setProperty("--mic-color", micColors[Math.floor(Math.random() * micColors.length)]);
mic.style.setProperty("--move-x", (Math.random() - 0.5) * 600 + "px");
mic.style.setProperty("--move-y", (Math.random() - 0.5) * 600 + "px");
mic.style.animationDelay = Math.random() * -25 + "s";
micBg.appendChild(mic);
floatingMics.push(mic);
}
const audio = document.getElementById("radio-audio");
const playIcon = document.getElementById("play-icon");
const trackTitle = document.getElementById("track-title");
const trackCover = document.getElementById("track-cover");
const bgContainer = document.querySelector(".background-container");
const appContainer = document.querySelector(".app-container");
const canvas = document.getElementById("visualizer");
const canvasCtx = canvas.getContext("2d");
let currentTrackTitle = "";
let audioCtx;
let analyser;
let source;
let dataArray;
let freqDataArray;
let animationId;
let floatingMics = [];
let particles = [];
let waveColor = "rgba(56, 189, 248, 0.8)"; // Default primary
let waveGlow = "rgba(56, 189, 248, 0.4)";
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function getComplementaryColor(rgbaStr) {
// Extract r, g, b from rgba(r, g, b, a)
const match = rgbaStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (!match) return "#ffffff";
const r = 255 - parseInt(match[1]);
const g = 255 - parseInt(match[2]);
const b = 255 - parseInt(match[3]);
return `rgb(${r}, ${g}, ${b})`;
}
const programColors = {
"Amanecer Techno": { main: "rgba(0, 230, 118, 0.8)", glow: "rgba(0, 230, 118, 0.4)" },
"Sesiones de House Vocal": { main: "rgba(244, 114, 182, 0.8)", glow: "rgba(244, 114, 182, 0.4)" },
"Invitado Especial Lili": { main: "rgba(251, 191, 36, 0.8)", glow: "rgba(251, 191, 36, 0.4)" },
"Vibras de la Noche": { main: "rgba(129, 140, 248, 0.8)", glow: "rgba(129, 140, 248, 0.4)" }
};
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser();
source = audioCtx.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioCtx.destination);
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
freqDataArray = new Uint8Array(bufferLength);
}
}
function drawWave() {
animationId = requestAnimationFrame(drawWave);
analyser.getByteTimeDomainData(dataArray);
analyser.getByteFrequencyData(freqDataArray);
// Calculate average volume for pulse effect
let sum = 0;
for (let i = 0; i < 30; i++) { // Focus on bass frequencies
sum += freqDataArray[i];
}
const average = sum / 30;
const boost = (average / 128.0); // Factor between 0 and 2
// Flash Effect Detection (Drop detection)
const flashEl = document.querySelector(".flash-effect");
if (boost > 1.6 && Math.random() > 0.96) {
const useWhite = Math.random() > 0.5;
const flashColor = useWhite ? "#ffffff" : getComplementaryColor(waveColor);
document.documentElement.style.setProperty("--flash-color", flashColor);
flashEl.style.opacity = "0.3";
// Shake UI effect
appContainer.classList.remove("shake");
void appContainer.offsetWidth; // Trigger reflow
appContainer.classList.add("shake");
// Randomize wave color on drop
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
waveColor = `rgba(${r}, ${g}, ${b}, 0.8)`;
waveGlow = `rgba(${r}, ${g}, ${b}, 0.4)`;
setTimeout(() => {
flashEl.style.transition = "opacity 0.4s ease-out";
flashEl.style.opacity = "0";
}, 40);
setTimeout(() => { flashEl.style.transition = ""; }, 500);
}
// Update CSS variables for background pulse
document.body.style.setProperty("--pulse-intensity", (1 + boost * 0.15).toString());
floatingMics.forEach(mic => {
mic.style.transform = `translate(var(--move-x), var(--move-y)) rotate(360deg) scale(${1 + boost * 0.5})`;
});
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
// --- Particle System (Fire/Sparks with Smoke Trail) ---
if (boost > 1.2) {
const particleCount = Math.floor(boost * 2);
for (let i = 0; i < particleCount; i++) {
const x = Math.random() * canvas.width;
const dataIdx = Math.floor((x / canvas.width) * dataArray.length);
const v = dataArray[dataIdx] / 128.0;
const amplitude = (v - 1) * (1.5 + boost * 2) + 1;
const y = amplitude * canvas.height / 2;
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 4,
vy: -Math.random() * 6 - (boost * 4),
alpha: 1,
size: Math.random() * 3 + 1,
color: waveColor.replace("0.8", "1"),
trail: []
});
}
}
// Update and draw particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
// Add to trail
p.trail.push({ x: p.x, y: p.y, alpha: p.alpha });
if (p.trail.length > 10) p.trail.shift();
p.x += p.vx;
p.y += p.vy;
p.vy += 0.22; // Gravity
p.alpha -= 0.015; // Slower fade for smoke effect
if (p.alpha <= 0) {
particles.splice(i, 1);
continue;
}
// Draw Trail (Smoke)
p.trail.forEach((point, idx) => {
canvasCtx.beginPath();
canvasCtx.arc(point.x, point.y, p.size * (idx / p.trail.length), 0, Math.PI * 2);
canvasCtx.fillStyle = p.color.replace("1)", `${point.alpha * 0.2})`);
canvasCtx.fill();
});
canvasCtx.save();
canvasCtx.globalAlpha = p.alpha;
canvasCtx.fillStyle = p.color;
canvasCtx.shadowBlur = 5;
canvasCtx.shadowColor = p.color;
canvasCtx.beginPath();
canvasCtx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
canvasCtx.fill();
canvasCtx.restore();
}
// --- End Particle System ---
// Dynamic glow based on volume
canvasCtx.shadowBlur = 10 + (boost * 20);
canvasCtx.shadowColor = waveGlow;
canvasCtx.lineWidth = 2 + (boost * 3);
canvasCtx.strokeStyle = waveColor;
canvasCtx.beginPath();
const sliceWidth = canvas.width * 1.0 / dataArray.length;
let x = 0;
for (let i = 0; i < dataArray.length; i++) {
const v = dataArray[i] / 128.0;
// Scale amplitude by boost factor for more intensity
const amplitude = (v - 1) * (1.5 + boost * 2) + 1;
const y = amplitude * canvas.height / 2;
if (i === 0) canvasCtx.moveTo(x, y);
else canvasCtx.lineTo(x, y);
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height / 2);
canvasCtx.stroke();
canvasCtx.shadowBlur = 0;
}
function togglePlay() {
const btn = document.querySelector(".play-btn");
initAudio();
if (audio.paused) {
if (audioCtx.state === "suspended") audioCtx.resume();
audio.play();
playIcon.className = "bi bi-pause-fill";
btn.classList.add("is-playing");
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
drawWave();
}
else {
audio.pause();
playIcon.className = "bi bi-play-fill";
btn.classList.remove("is-playing");
cancelAnimationFrame(animationId);
}
}
function changeVolume(val) { audio.volume = val; }
async function updateBackgroundImage(query = "abstract music") {
try {
const res = await fetch(`api/pexels.php?query=${encodeURIComponent(query)}`);
const data = await res.json();
if (data.success && data.url) {
const img = new Image();
img.src = data.url;
img.onload = () => { bgContainer.style.backgroundImage = `url("${data.url}")`; };
}
} catch (e) { console.error(e); }
}
async function updateMetadata() {
try {
const res = await fetch("https://api.radioking.io/widget/radio/lili-records-radio-2/track/current");
const data = await res.json();
if (data && data.title) {
let full = data.artist ? `${data.artist} - ${data.title}` : data.title;
if (data.cover) {
trackCover.src = data.cover;
} else {
trackCover.src = "assets/pasted-20260214-203540-699a2e6a.png";
}
if (currentTrackTitle !== full) {
currentTrackTitle = full;
trackTitle.textContent = full;
document.title = `▶ ${full} | Lili Records Radio`;
updateBackgroundImage(data.artist ? `${data.artist} music` : "abstract music background");
}
}
} catch (e) { console.error(e); }
}
function toggleDescription(el) {
const was = el.classList.contains("is-expanded");
document.querySelectorAll(".program-item").forEach(i => i.classList.remove("is-expanded"));
if (!was) el.classList.add("is-expanded");
}
setInterval(updateMetadata, 30000);
updateMetadata();
function updateHighlight() {
const now = new Date();
const cur = now.getHours().toString().padStart(2, "0") + ":" + now.getMinutes().toString().padStart(2, "0");
const schedule = <?= json_encode($schedule) ?>;
let idx = -1;
for (let i = 0; i < schedule.length; i++) {
const start = schedule[i].time;
const next = schedule[i+1] ? schedule[i+1].time : "23:59";
if (cur >= start && cur < next) { idx = i; break; }
}
if (idx === -1) idx = schedule.length - 1;
document.querySelectorAll(".program-item").forEach((item, i) => {
item.classList.toggle("is-live", i === idx);
});
if (idx !== -1) {
const programName = schedule[idx].name;
if (programColors[programName]) {
waveColor = programColors[programName].main;
waveGlow = programColors[programName].glow;
}
}
const stName = document.getElementById("studio-program-name");
const stPhoto = document.getElementById("studio-photo");
if (stName && idx !== -1) {
stName.textContent = schedule[idx].name;
if (stPhoto && !stPhoto.src.includes(schedule[idx].image)) stPhoto.src = schedule[idx].image;
}
}
setInterval(updateHighlight, 60000);
updateHighlight();
</script>
</body>
</html>