音乐播放器前端源码
先放出前端源码,后端还在完善
<!DOCTYPE html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>沉浸式悬浮球播放器</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #8b5cf6;
--secondary: #06b6d4;
--accent: #ec4899;
--glass-bg: rgba(15, 15, 25, 0.65);
--glass-border: rgba(255, 255, 255, 0.08);
}
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: #09090b;
color: white;
overflow: hidden;
height: 100vh;
}
/* --- 1. 全屏 Canvas 背景 --- */
#visualizer-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
/* --- 2. 悬浮球与环形歌词 --- */
#float-container {
position: fixed;
bottom: 40px;
right: 40px;
width: 64px;
height: 64px;
z-index: 1000;
}
#float-ball {
width: 64px;
height: 64px;
border-radius: 50%;
/* 3D 玻璃球质感 */
background: radial-gradient(circle at 30% 30%, #a78bfac2, #7c3aed87 50%, #4c1d9587);
box-shadow:
0 0 20px rgba(139, 92, 246, 0.4),
inset 0 -4px 8px rgba(0,0,0,0.4),
inset 0 4px 8px rgba(255,255,255,0.2);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
animation: breathe 3s infinite ease-in-out;
position: relative;
z-index: 2;
}
#float-ball:hover { transform: scale(1.08); }
#float-ball i { color: white; font-size: 22px; text-shadow: 0 2px 4px rgba(0,0,0,0.3); }
@keyframes breathe {
0%, 100% { box-shadow: 0 0 20px rgba(139, 92, 246, 0.4), inset 0 -4px 8px rgba(0,0,0,0.4), inset 0 4px 8px rgba(255,255,255,0.2); }
50% { box-shadow: 0 0 35px rgba(139, 92, 246, 0.8), inset 0 -4px 8px rgba(0,0,0,0.4), inset 0 4px 8px rgba(255,255,255,0.2); }
}
#orbit-lyrics {
position: absolute;
top: 50%;
left: 50%;
width: 180px;
height: 180px;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 1;
opacity: 0;
transform: translate(-50%, -50%) scale(0.6);
transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
animation: orbit-spin 25s linear infinite;
animation-play-state: paused;
}
#orbit-lyrics.visible {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
animation-play-state: running;
}
@keyframes orbit-spin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.orbit-char {
position: absolute;
top: 50%;
left: 50%;
font-size: 13px;
font-weight: 800;
/* 主题色渐变文字 */
background: linear-gradient(135deg, var(--secondary), var(--primary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 0 5px rgba(139, 92, 246, 0.6));
transform: translate(-50%, -50%);
}
/* --- 3. 播放器面板 --- */
#player-panel {
position: fixed;
bottom: 40px;
right: 40px;
width: 360px;
height: 580px;
/* 高级毛玻璃 */
background: var(--glass-bg);
backdrop-filter: blur(24px) saturate(150%);
-webkit-backdrop-filter: blur(24px) saturate(150%);
border: 1px solid var(--glass-border);
border-radius: 28px;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.8),
inset 0 1px 1px rgba(255, 255, 255, 0.05);
z-index: 999;
opacity: 0;
transform: translateY(20px) scale(0.9);
transform-origin: bottom right;
pointer-events: none;
transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1);
display: flex;
flex-direction: column;
overflow: hidden;
}
#player-panel.active {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.panel-content {
padding: 24px;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
position: relative;
z-index: 2;
}
.close-btn {
position: absolute;
top: 20px;
right: 24px;
color: rgba(255, 255, 255, 0.4);
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
z-index: 10;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.close-btn:hover { color: white; background: rgba(255,255,255,0.1); }
.cover-area {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 24px;
}
.cover-wrapper {
width: 140px;
height: 140px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.5);
border: 4px solid rgba(255, 255, 255, 0.05);
margin-bottom: 20px;
position: relative;
}
.cover-wrapper.playing { animation: spin 12s linear infinite; }
/* 纯 CSS 封面 */
.cover-img {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
}
.cover-img::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.3), transparent 60%);
}
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.track-info { text-align: center; }
.track-title {
font-size: 20px;
font-weight: 700;
margin: 0 0 6px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 280px;
letter-spacing: 0.5px;
}
.track-artist {
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
margin: 0;
letter-spacing: 1px;
text-transform: uppercase;
}
/* --- 4. 歌词区域 --- */
.lyrics-container {
flex: 1;
position: relative;
overflow: hidden;
margin: 10px 0;
mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
-webkit-mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
}
.lyrics-scroll {
transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
text-align: center;
padding-top: 80px;
}
.lyric-line {
font-size: 15px;
margin: 12px 0;
opacity: 0.3;
transition: all 0.4s ease;
line-height: 1.5;
color: #fff;
}
.lyric-line.active {
opacity: 1;
font-size: 18px;
font-weight: 600;
transform: scale(1.05);
/* 高级极光流光 */
background: linear-gradient(90deg, #fff, var(--secondary), var(--primary), #fff);
background-size: 200% auto;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: aurora-flow 4s linear infinite;
filter: drop-shadow(0 0 12px rgba(139, 92, 246, 0.4));
}
@keyframes aurora-flow {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
/* --- 5. 控制区域 --- */
.controls-area { margin-top: auto; }
.progress-bar-bg {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.08);
border-radius: 3px;
cursor: pointer;
margin-bottom: 10px;
position: relative;
}
.progress-bar-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--secondary), var(--primary));
border-radius: 3px;
position: relative;
box-shadow: 0 0 10px rgba(139, 92, 246, 0.5);
}
.progress-thumb {
position: absolute;
right: -6px;
top: 50%;
transform: translateY(-50%) scale(0);
width: 14px;
height: 14px;
border-radius: 50%;
background: white;
box-shadow: 0 0 10px var(--primary), 0 0 20px rgba(139, 92, 246, 0.4);
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.progress-bar-bg:hover .progress-thumb {
transform: translateY(-50%) scale(1);
}
.time-display {
display: flex;
justify-content: space-between;
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
margin-bottom: 20px;
font-variant-numeric: tabular-nums;
}
.buttons {
display: flex;
justify-content: center;
align-items: center;
gap: 30px;
}
.btn {
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}
.btn:hover {
color: white;
transform: scale(1.15);
}
.btn-play {
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--accent));
color: white;
font-size: 20px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 8px 20px rgba(139, 92, 246, 0.4);
transition: all 0.2s;
}
.btn-play:hover {
transform: scale(1.08);
box-shadow: 0 12px 25px rgba(139, 92, 246, 0.6);
}
audio { display: none; }
</style><canvas id="visualizer-canvas"></canvas>
<div id="float-container">
<div id="orbit-lyrics"></div>
<div id="float-ball">
<i class="fas fa-music"></i>
</div>
</div>
<div id="player-panel">
<div class="close-btn" id="close-panel"><i class="fas fa-chevron-down"></i></div>
<div class="panel-content">
<div class="cover-area">
<div class="cover-wrapper" id="cover-wrapper">
<div class="cover-img"></div>
</div>
<div class="track-info">
<h2 class="track-title" id="track-title">Loading...</h2>
<p class="track-artist" id="track-artist">Artist</p>
</div>
</div>
<div class="lyrics-container">
<div class="lyrics-scroll" id="lyrics-scroll"></div>
</div>
<div class="controls-area">
<div class="progress-bar-bg" id="progress-bg">
<div class="progress-bar-fill" id="progress-fill">
<div class="progress-thumb"></div>
</div>
</div>
<div class="time-display">
<span id="current-time">0:00</span>
<span id="total-time">0:00</span>
</div>
<div class="buttons">
<button class="btn" id="prev-btn"><i class="fas fa-backward-step"></i></button>
<button class="btn btn-play" id="play-btn"><i class="fas fa-play"></i></button>
<button class="btn" id="next-btn"><i class="fas fa-forward-step"></i></button>
</div>
</div>
</div>
</div>
<audio id="audio-player"></audio>
<script>
// --- 数据 ---
const playlist = [
{
title: "Neon Dreams",
artist: "Cyber Synth",
src: "https://img2.tukuppt.com/newpreview_music/09/01/96/5c8a23d4239e332055.mp3",
coverGrad: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
lyrics: [
{ time: 0, text: "System initializing..." },
{ time: 2, text: "Walking through the neon rain" },
{ time: 6, text: "Digital ghosts in my brain" },
{ time: 10, text: "Chrome hearts beating fast" },
{ time: 14, text: "Running from the past" },
{ time: 18, text: "Lights flash, colors blend" },
{ time: 22, text: "Where does the simulation end?" },
{ time: 28, text: "(Synth Solo)" },
{ time: 35, text: "Just keep driving..." }
]
},
{
title: "Deep Space",
artist: "Star Walker",
src: "https://img2.tukuppt.com/newpreview_music/09/01/96/5c8a23d4239e332055.mp3",
coverGrad: "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)",
lyrics: [
{ time: 0, text: "Floating in the void" },
{ time: 4, text: "Silence is loud" },
{ time: 8, text: "Stars align" },
{ time: 12, text: "Crossing the timeline" }
]
}
];
// --- DOM ---
const floatBall = document.getElementById('float-ball');
const playerPanel = document.getElementById('player-panel');
const closePanelBtn = document.getElementById('close-panel');
const orbitLyrics = document.getElementById('orbit-lyrics');
const audio = document.getElementById('audio-player');
const playBtn = document.getElementById('play-btn');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const coverWrapper = document.getElementById('cover-wrapper');
const coverImg = document.querySelector('.cover-img');
const trackTitle = document.getElementById('track-title');
const trackArtist = document.getElementById('track-artist');
const progressBg = document.getElementById('progress-bg');
const progressFill = document.getElementById('progress-fill');
const currentTimeEl = document.getElementById('current-time');
const totalTimeEl = document.getElementById('total-time');
const lyricsScroll = document.getElementById('lyrics-scroll');
const canvas = document.getElementById('visualizer-canvas');
const ctx = canvas.getContext('2d');
// --- State ---
let currentTrackIndex = 0;
let isPlaying = false;
let isPanelOpen = false;
let isDragging = false;
let particles = [];
let simulatedBeat = 0;
let currentActiveLyricText = "";
// --- Init ---
function init() {
loadTrack(currentTrackIndex);
setupEventListeners();
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
animateCanvas();
}
function setupEventListeners() {
floatBall.addEventListener('click', togglePanel);
closePanelBtn.addEventListener('click', togglePanel);
playBtn.addEventListener('click', togglePlay);
prevBtn.addEventListener('click', playPrev);
nextBtn.addEventListener('click', playNext);
audio.addEventListener('timeupdate', updateProgress);
audio.addEventListener('loadedmetadata', () => {
totalTimeEl.textContent = formatTime(audio.duration);
});
audio.addEventListener('ended', playNext);
// 进度条交互
progressBg.addEventListener('mousedown', (e) => {
isDragging = true;
setProgress(e);
});
document.addEventListener('mousemove', (e) => {
if (isDragging) setProgress(e, true);
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
// --- Panel Logic ---
function togglePanel() {
isPanelOpen = !isPanelOpen;
if (isPanelOpen) {
playerPanel.classList.add('active');
orbitLyrics.classList.remove('visible');
} else {
playerPanel.classList.remove('active');
if (currentActiveLyricText) {
renderOrbitLyrics(currentActiveLyricText);
orbitLyrics.classList.add('visible');
}
}
}
// --- Player Logic ---
function loadTrack(index) {
const track = playlist[index];
audio.src = track.src;
trackTitle.textContent = track.title;
trackArtist.textContent = track.artist;
coverImg.style.background = track.coverGrad;
renderLyrics(track.lyrics);
progressFill.style.width = '0%';
currentTimeEl.textContent = '0:00';
currentActiveLyricText = "";
orbitLyrics.innerHTML = '';
orbitLyrics.classList.remove('visible');
if (isPlaying) audio.play().catch(e => console.log("Autoplay blocked"));
else resetPlayUI();
}
function togglePlay() {
if (isPlaying) {
audio.pause();
resetPlayUI();
} else {
audio.play().catch(e => console.log("Playback failed"));
setPlayUI();
}
}
function setPlayUI() {
isPlaying = true;
playBtn.innerHTML = '<i class="fas fa-pause"></i>';
coverWrapper.classList.add('playing');
}
function resetPlayUI() {
isPlaying = false;
playBtn.innerHTML = '<i class="fas fa-play"></i>';
coverWrapper.classList.remove('playing');
}
function playPrev() {
currentTrackIndex = (currentTrackIndex - 1 + playlist.length) % playlist.length;
loadTrack(currentTrackIndex);
if (isPlaying) audio.play();
}
function playNext() {
currentTrackIndex = (currentTrackIndex + 1) % playlist.length;
loadTrack(currentTrackIndex);
if (isPlaying) audio.play();
}
function updateProgress() {
if (isDragging) return;
const { duration, currentTime } = audio;
if (isNaN(duration)) return;
const percent = (currentTime / duration) * 100;
progressFill.style.width = `${percent}%`;
currentTimeEl.textContent = formatTime(currentTime);
highlightLyrics(currentTime);
simulatedBeat = Math.abs(Math.sin(currentTime * 4)) * 0.5 + Math.random() * 0.2;
}
function setProgress(e, isMove = false) {
const rect = progressBg.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const width = rect.width;
const percent = Math.max(0, Math.min(1, clickX / width));
if (!isNaN(audio.duration)) {
audio.currentTime = percent * audio.duration;
progressFill.style.width = `${percent * 100}%`;
if (!isMove) currentTimeEl.textContent = formatTime(audio.currentTime);
}
}
function formatTime(seconds) {
if (isNaN(seconds)) return "0:00";
const min = Math.floor(seconds / 60);
const sec = Math.floor(seconds % 60);
return `${min}:${sec < 10 ? '0' + sec : sec}`;
}
// --- Lyrics Logic ---
function renderLyrics(lyricsData) {
lyricsScroll.innerHTML = '';
lyricsScroll.style.transform = `translateY(0px)`;
if (!lyricsData) return;
lyricsData.forEach((line, index) => {
const div = document.createElement('div');
div.className = 'lyric-line';
div.textContent = line.text;
div.dataset.time = line.time;
div.id = `lyric-${index}`;
lyricsScroll.appendChild(div);
});
}
function highlightLyrics(currentTime) {
let activeIndex = -1;
const lyricsData = playlist[currentTrackIndex].lyrics;
for (let i = 0; i < lyricsData.length; i++) {
if (currentTime >= lyricsData[i].time) activeIndex = i;
else break;
}
if (activeIndex !== -1) {
const oldActive = document.querySelector('.lyric-line.active');
if (oldActive) oldActive.classList.remove('active');
const newActive = document.getElementById(`lyric-${activeIndex}`);
if (newActive) {
newActive.classList.add('active');
const lineHeight = 45;
lyricsScroll.style.transform = `translateY(${-(activeIndex * lineHeight) + 80}px)`;
const newText = lyricsData[activeIndex].text;
if (newText !== currentActiveLyricText) {
currentActiveLyricText = newText;
if (!isPanelOpen) {
renderOrbitLyrics(currentActiveLyricText);
orbitLyrics.classList.add('visible');
}
}
}
}
}
// --- Orbit Lyrics ---
function renderOrbitLyrics(text) {
orbitLyrics.innerHTML = '';
if (!text) return;
let chars = text.replace(/\s/g, '').split('');
const maxChars = 16;
if (chars.length > maxChars) {
chars = chars.slice(0, maxChars - 1);
chars.push('…');
}
if (chars.length === 0) return;
const radius = 72;
const totalChars = chars.length;
const angleStep = 360 / totalChars;
chars.forEach((char, index) => {
const angle = angleStep * index - 90;
const radian = (angle * Math.PI) / 180;
const span = document.createElement('span');
span.className = 'orbit-char';
span.textContent = char;
const x = Math.cos(radian) * radius;
const y = Math.sin(radian) * radius;
span.style.transform = `translate(-50%, -50%) translate(${x}px, ${y}px) rotate(${angle + 90}deg)`;
orbitLyrics.appendChild(span);
});
}
// --- Canvas Visualizer ---
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
class Particle {
constructor() { this.reset(); }
reset() {
this.x = Math.random() * canvas.width;
this.y = canvas.height + Math.random() * 50;
this.size = Math.random() * 2 + 0.5;
this.speedY = Math.random() * 1.5 + 0.5;
const hue = Math.random() > 0.5 ? 260 : 190;
this.color = `hsla(${hue}, 80%, 70%, ${Math.random() * 0.4 + 0.1})`;
this.wobble = Math.random() * Math.PI * 2;
}
update(beat) {
this.y -= this.speedY * (1 + beat * 0.5);
this.x += Math.sin(this.wobble) * 0.5;
this.wobble += 0.02;
if (this.y < -10) this.reset();
}
draw() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
for (let i = 0; i < 80; i++) particles.push(new Particle());
function animateCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 背景渐变
const gradient = ctx.createRadialGradient(canvas.width/2, canvas.height/2, 0, canvas.width/2, canvas.height/2, canvas.width/1.5);
gradient.addColorStop(0, '#1e1b4b');
gradient.addColorStop(1, '#09090b');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 粒子
particles.forEach(p => {
p.update(simulatedBeat);
p.draw();
});
// 底部柔和波浪
ctx.beginPath();
ctx.moveTo(0, canvas.height);
const sliceWidth = canvas.width / 60;
let x = 0;
for (let i = 0; i <= 60; i++) {
const amplitude = 30 + simulatedBeat * 60;
const y = canvas.height - 40 - Math.sin(i * 0.4 + Date.now() * 0.002) * amplitude * 0.5;
ctx.lineTo(x, y);
x += sliceWidth;
}
ctx.lineTo(canvas.width, canvas.height);
ctx.closePath();
const waveGrad = ctx.createLinearGradient(0, canvas.height - 100, 0, canvas.height);
waveGrad.addColorStop(0, 'rgba(139, 92, 246, 0.1)');
waveGrad.addColorStop(1, 'rgba(139, 92, 246, 0.02)');
ctx.fillStyle = waveGrad;
ctx.fill();
requestAnimationFrame(animateCanvas);
}
init();
</script>
评论 (0)
暂无评论,快来抢沙发吧!