音乐播放器前端源码

先放出前端源码,后端还在完善
<!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>