love_heart 3D爱心颗粒 旋转

JavaScript性能优化实战 10w+人浏览 429人参与

该文章已生成可运行项目,

💗 3D爱心动画页面
 

项目简介

这是一个充满浪漫色彩的3D爱心动画网页,通过WebGL和Canvas技术创造出一个梦幻般的爱心世界。页面包含多种粒子效果、音效和交互功能,为用户带来沉浸式的视觉和听觉体验。

✨ 主要特性

🎨 视觉效果

  • 3D爱心模型:使用数学函数构建的真实3D爱心形状
  • 粒子系统:3000个动态粒子在爱心内部流动
  • 星河背景:梦幻的星空背景效果
  • 流星雨:从天空飘落的爱心流星
  • 点击特效:点击屏幕产生星星和爱心爆炸效果
  • 自动旋转:爱心自动缓慢旋转展示立体效果

🎵 音效系统

  • 心跳音效:三种不同风格的心跳声音
  • 经典心跳声(lub-dub)
  • 梦幻风格心跳
  • 钢琴和弦式心跳
  • 智能触发:根据爱心跳动自动播放音效
  • 用户控制:点击按钮启用心跳音效

🎮 交互功能

  • 键盘控制:左右箭头键控制爱心旋转
  • 鼠标交互:点击屏幕产生特效
  • 音频初始化:友好的音效启用提示

🛠️ 技术实现

核心技术

  • HTML5 Canvas:2D图形渲染
  • Web Audio API:音频合成和播放
  • JavaScript ES6+:现代JavaScript特性
  • CSS3:样式和动画效果

数学算法

  • 3D爱心方程:(x² + 9y²/4 + z² - 1)³ - x²z³ - 9y²z³/80 ≤ 0
  • 粒子物理:位置、速度、重力和碰撞检测
  • 3D投影:3D坐标到2D屏幕的投影变换

性能优化

  • 粒子数量控制:平衡视觉效果和性能
  • 动画帧率:使用requestAnimationFrame优化
  • 内存管理:及时清理过期粒子对象

🎯 使用说明

  1. 启动页面:打开HTML文件即可看到3D爱心动画
  1. 启用心跳音效:点击页面顶部的粉色按钮
  1. 控制旋转:使用键盘左右箭头键手动控制
  1. 产生特效:点击屏幕任意位置产生星星和爱心特效
  1. 享受体验:静静欣赏自动播放的爱心跳动和流星雨

💝 设计理念

这个项目融合了艺术、技术和情感,通过代码创造出充满爱意的数字艺术品。每一个粒子、每一帧动画都承载着创作者的心意,为用户带来温暖和美好的感受。


"用代码书写浪漫,用技术传递爱意" 💕

 

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>•͈ ₃ •͈꧞</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background: black;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            overflow: hidden;
            font-family: Arial, sans-serif;
            cursor: crosshair;
        }
        canvas {
            border: none;
            background: black;
        }
        .title {
            position: absolute;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            color: #ff69b4;
            font-size: 24px;
            font-weight: bold;
            text-shadow: 0 0 10px #ff69b4;
            z-index: 100;
        }
        .controls {
            position: absolute;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            color: white;
            font-size: 14px;
            text-align: center;
            z-index: 100;
        }
    </style>
</head>
<body>
    <div class="title">💗💗💗</div>
    <canvas id="heartCanvas"></canvas>
    <!-- <div class="controls">
        按住 ← → 键控制旋转 | 自动跳动模式 | 点击屏幕产生满天星星和爱心 | 爱心流星雨
    </div> -->

    <script>
        let audioContext = null;
        let lastHeartbeatTime = 0;
        let heartbeatThreshold = 0.75; // 降低心跳触发阈值,更容易触发

        // 更好的音频初始化方法
        function initAudioWithFeedback() {
            try {
                if (!audioContext) {
                    audioContext = new (window.AudioContext || window.webkitAudioContext)();
                    console.log('音频上下文已初始化');
                    
                    // 测试播放一个心跳音效
                    setTimeout(() => {
                        if (audioContext) {
                            playHeartbeat();
                            console.log('测试心跳音效已播放');
                        }
                    }, 500);
                }
                return audioContext;
            } catch (error) {
                console.error('音频初始化失败:', error);
                return null;
            }
        }

        // 简化的心跳音效(更可靠)
        function playHeartbeat() {
            if (!audioContext) return;
            
            const now = audioContext.currentTime;
            
            // 第一声 "lub" - 较深沉的音
            createSimpleHeartbeat(now, 80, 0.1, 0.2);
            
            // 第二声 "dub" - 稍高一些的音,延迟150ms
            setTimeout(() => {
                if (audioContext) {
                    createSimpleHeartbeat(audioContext.currentTime, 120, 0.08, 0.15);
                }
            }, 150);
        }

        // 简化的心跳音效创建函数
        function createSimpleHeartbeat(startTime, baseFreq, volume, duration) {
            const oscillator = audioContext.createOscillator();
            const gainNode = audioContext.createGain();
            
            // 简化音频链接
            oscillator.connect(gainNode);
            gainNode.connect(audioContext.destination);
            
            // 设置心跳频率
            oscillator.type = 'sine';
            oscillator.frequency.setValueAtTime(baseFreq, startTime);
            oscillator.frequency.exponentialRampToValueAtTime(baseFreq * 0.5, startTime + duration);
            
            // 设置音量包络
            gainNode.gain.setValueAtTime(0, startTime);
            gainNode.gain.linearRampToValueAtTime(volume, startTime + 0.02);
            gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + duration);
            
            oscillator.start(startTime);
            oscillator.stop(startTime + duration);
        }

        // 创建混响效果缓冲区
        function createReverbBuffer(duration, decay) {
            const sampleRate = audioContext.sampleRate;
            const length = sampleRate * duration;
            const impulse = audioContext.createBuffer(2, length, sampleRate);
            
            for (let channel = 0; channel < 2; channel++) {
                const channelData = impulse.getChannelData(channel);
                for (let i = 0; i < length; i++) {
                    const n = length - i;
                    channelData[i] = (Math.random() * 2 - 1) * Math.pow(n / length, decay);
                }
            }
            return impulse;
        }

        // 版本2:梦幻风格心跳(备选)
        function playDreamyHeartbeat() {
            if (!audioContext) return;
            
            const now = audioContext.currentTime;
            
            // 创建多层音效
            for (let i = 0; i < 3; i++) {
                const delay = i * 0.02;
                const freq = 60 + i * 15;
                const vol = 0.04 - i * 0.01;
                
                setTimeout(() => {
                    if (audioContext) {
                        createDreamyTone(audioContext.currentTime, freq, vol);
                    }
                }, delay * 1000);
            }
        }

        function createDreamyTone(startTime, frequency, volume) {
            const oscillator = audioContext.createOscillator();
            const gainNode = audioContext.createGain();
            const filter = audioContext.createBiquadFilter();
            const delay = audioContext.createDelay();
            const delayGain = audioContext.createGain();
            
            // 音频路由
            oscillator.connect(filter);
            filter.connect(gainNode);
            gainNode.connect(audioContext.destination);
            
            // 添加延迟效果
            gainNode.connect(delay);
            delay.connect(delayGain);
            delayGain.connect(gainNode);
            
            // 设置延迟效果
            delay.delayTime.setValueAtTime(0.1, startTime);
            delayGain.gain.setValueAtTime(0.2, startTime);
            
            // 设置滤波器
            filter.type = 'bandpass';
            filter.frequency.setValueAtTime(frequency * 2, startTime);
            filter.Q.setValueAtTime(3, startTime);
            
            // 使用三角波,更柔和
            oscillator.type = 'triangle';
            oscillator.frequency.setValueAtTime(frequency, startTime);
            oscillator.frequency.linearRampToValueAtTime(frequency * 0.8, startTime + 0.2);
            
            // 柔和的音量包络
            gainNode.gain.setValueAtTime(0, startTime);
            gainNode.gain.linearRampToValueAtTime(volume, startTime + 0.05);
            gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + 0.4);
            
            oscillator.start(startTime);
            oscillator.stop(startTime + 0.4);
        }

        // 版本3:钢琴风格心跳(优雅)
        function playPianoHeartbeat() {
            if (!audioContext) return;
            
            const now = audioContext.currentTime;
            
            // 播放和弦式心跳
            const notes = [110, 138.59, 164.81]; // A2, C#3, E3 和弦
            
            notes.forEach((freq, index) => {
                setTimeout(() => {
                    if (audioContext) {
                        createPianoNote(audioContext.currentTime, freq, 0.03 - index * 0.008);
                    }
                }, index * 30);
            });
        }

        function createPianoNote(startTime, frequency, volume) {
            // 模拟钢琴音色的复合波形
            const fundamental = audioContext.createOscillator();
            const harmonic2 = audioContext.createOscillator();
            const harmonic3 = audioContext.createOscillator();
            
            const gainNode = audioContext.createGain();
            const gain2 = audioContext.createGain();
            const gain3 = audioContext.createGain();
            const masterGain = audioContext.createGain();
            
            // 连接基音
            fundamental.connect(gainNode);
            gainNode.connect(masterGain);
            
            // 连接泛音
            harmonic2.connect(gain2);
            gain2.connect(masterGain);
            harmonic3.connect(gain3);
            gain3.connect(masterGain);
            
            masterGain.connect(audioContext.destination);
            
            // 设置频率
            fundamental.frequency.setValueAtTime(frequency, startTime);
            harmonic2.frequency.setValueAtTime(frequency * 2, startTime);
            harmonic3.frequency.setValueAtTime(frequency * 3, startTime);
            
            // 设置音色
            fundamental.type = 'triangle';
            harmonic2.type = 'sine';
            harmonic3.type = 'sine';
            
            // 设置各声部音量
            gainNode.gain.setValueAtTime(volume, startTime);
            gain2.gain.setValueAtTime(volume * 0.3, startTime);
            gain3.gain.setValueAtTime(volume * 0.1, startTime);
            
            // 钢琴式的音量包络
            masterGain.gain.setValueAtTime(0, startTime);
            masterGain.gain.linearRampToValueAtTime(1, startTime + 0.01);
            masterGain.gain.exponentialRampToValueAtTime(0.3, startTime + 0.1);
            masterGain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.6);
            
            // 启动所有振荡器
            fundamental.start(startTime);
            harmonic2.start(startTime);
            harmonic3.start(startTime);
            
            fundamental.stop(startTime + 0.6);
            harmonic2.stop(startTime + 0.6);
            harmonic3.stop(startTime + 0.6);
        }

        const canvas = document.getElementById('heartCanvas');
        const ctx = canvas.getContext('2d');
        
        // 设置画布大小
        canvas.width = 800;
        canvas.height = 600;
        
        // 常量定义
        const WIDTH = 800;
        const HEIGHT = 600;
        const BLACK = [0, 0, 0];
        const PINK = [255, 105, 180];
        const LIGHT_PINK = [255, 182, 193];
        const HOT_PINK = [255, 20, 147];
        const GALAXY_COLORS = [[180, 200, 255], [120, 160, 255], [255, 255, 255], [200, 120, 255]];
        const STAR_COLORS = [[255, 255, 255], [255, 255, 180], [180, 255, 255], [255, 180, 255], [180, 255, 180]];
        
        // 全局变量
        let angleY = 0;
        let pulse = 0;
        let lastTime = 0;
        let keys = {};
        
        // 检查是否在3D爱心内
        function isIn3DHeart(x, z, y) {
            const scale = 0.7;
            x = x / scale;
            y = y / scale;
            z = z / scale;
            const term1 = (x * x + (9 * y * y) / 4.0 + z * z - 1);
            const value = term1 * term1 * term1 - x * x * z * z * z - (9 * y * y * z * z * z) / 80.0;
            return value <= 0;
        }
        
        // 随机选择函数
        function randomChoice(arr, weights = null) {
            if (!weights) {
                return arr[Math.floor(Math.random() * arr.length)];
            }
            const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
            let random = Math.random() * totalWeight;
            for (let i = 0; i < arr.length; i++) {
                random -= weights[i];
                if (random <= 0) return arr[i];
            }
            return arr[arr.length - 1];
        }
        
        // 粒子类
        class Particle {
            constructor() {
                this.reset();
            }
            
            reset() {
                let x, y, z;
                do {
                    x = Math.random() * 3 - 1.5;
                    y = Math.random() * 3 - 1.5;
                    z = Math.random() * 3 - 1.5;
                } while (!isIn3DHeart(x, y, z));
                
                this.x = x * 12;
                this.y = y * 12;
                this.z = z * 12;
                this.dx = (Math.random() - 0.5) * 0.04;
                this.dy = (Math.random() - 0.5) * 0.04;
                this.dz = (Math.random() - 0.5) * 0.04;
                this.baseColor = randomChoice([PINK, LIGHT_PINK, HOT_PINK], [0.5, 0.3, 0.2]);
                this.baseSize = Math.random() * 1.7 + 1.5;
            }
            
            update(angleY, pulseScale) {
                this.x += this.dx;
                this.y += this.dy;
                this.z += this.dz;
                
                if (!isIn3DHeart(this.x / 12, this.y / 12, this.z / 12)) {
                    this.reset();
                }
                
                const xRot = this.x * Math.cos(angleY) + this.z * Math.sin(angleY);
                const zRot = -this.x * Math.sin(angleY) + this.z * Math.cos(angleY);
                const yRot = this.y;
                
                const scale = 25 * pulseScale;
                const distance = 20;
                const xProj = WIDTH / 2 + (xRot * scale);
                const yProj = HEIGHT / 2 - (yRot * scale);
                const depthFactor = (zRot + distance) / (2 * distance);
                
                const currSize = Math.max(2, this.baseSize * depthFactor * 1.5);
                const colorFactor = Math.min(1.0, Math.max(0.35, depthFactor * 1.6));
                
                const r = Math.min(255, Math.max(0, this.baseColor[0] * colorFactor));
                const g = Math.min(255, Math.max(0, this.baseColor[1] * colorFactor));
                const b = Math.min(255, Math.max(0, this.baseColor[2] * colorFactor));
                
                return {
                    x: xProj,
                    y: yProj,
                    size: currSize,
                    color: [r, g, b],
                    depth: depthFactor
                };
            }
            
            draw(ctx, angleY, pulseScale) {
                const result = this.update(angleY, pulseScale);
                if (result.x >= 0 && result.x < WIDTH && result.y >= 0 && result.y < HEIGHT) {
                    const alpha = 0.3 + 0.7 * result.depth;
                    ctx.save();
                    ctx.globalAlpha = alpha;
                    ctx.fillStyle = `rgb(${Math.floor(result.color[0])}, ${Math.floor(result.color[1])}, ${Math.floor(result.color[2])})`;
                    ctx.beginPath();
                    ctx.arc(result.x, result.y, result.size, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.restore();
                }
            }
        }
        
        // 星河粒子类
        class GalaxyParticle {
            constructor() {
                this.x = Math.random() * WIDTH;
                this.y = Math.random() * HEIGHT;
                this.z = Math.random() * 60 - 30;
                this.size = Math.random() * 2 + 1;
                this.color = randomChoice(GALAXY_COLORS);
                this.baseAlpha = Math.random() * 208 + 12;
                this.alpha = this.baseAlpha;
                this.speed = Math.random() * 1 + 0.5;
                this.angle = Math.random() * 0.4 - 0.2;
                this.life = Math.random() * 60 + 60;
                this.fadeSpeed = Math.random() * 0.02 + 0.01;
            }
            
            update() {
                this.y -= this.speed;
                this.x += Math.sin(this.angle) * 0.5;
                this.size = Math.max(0, this.size - this.fadeSpeed);
                this.life -= 1;
                
                if (this.life < 30) {
                    this.alpha = this.alpha * this.life / 30;
                }
                
                return this.life > 0 && this.y > 0 && this.size > 0;
            }
            
            draw(ctx) {
                ctx.save();
                ctx.globalAlpha = this.alpha / 255;
                ctx.fillStyle = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`;
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                ctx.fill();
                ctx.restore();
            }
        }
        
        // 掉落粒子类
        class ParticleDrop {
            constructor(heartParticles, angleY, pulse) {
                const sourceParticle = heartParticles[Math.floor(Math.random() * heartParticles.length)];
                const result = sourceParticle.update(angleY, pulse);
                this.x = result.x + (Math.random() - 0.5) * 160;
                this.y = result.y + 50;
                this.color = result.color;
                this.size = Math.random() * 1.5 + 1.5;
                this.speed = Math.random() * 2 + 1;
                this.alpha = Math.random() * 75 + 180;
                this.fadeSpeed = Math.random() * 1.5 + 1;
                this.horizontalSpeed = (Math.random() - 0.5);
                this.trail = [];
            }
            
            update() {
                this.y += this.speed;
                this.x += this.horizontalSpeed;
                this.alpha = Math.max(0, this.alpha - this.fadeSpeed);
                
                this.trail.push({
                    x: this.x,
                    y: this.y,
                    size: this.size,
                    alpha: this.alpha
                });
                
                if (this.trail.length > 5) {
                    this.trail.shift();
                }
                
                return this.alpha > 0 && this.y < HEIGHT + 20;
            }
            
            draw(ctx) {
                // 绘制尾迹
                this.trail.forEach((point, index) => {
                    const fade = point.alpha * (index + 1) / this.trail.length;
                    const size = Math.max(1, point.size * (index + 1) / this.trail.length);
                    
                    ctx.save();
                    ctx.globalAlpha = fade / 255;
                    ctx.fillStyle = `rgb(${Math.floor(this.color[0])}, ${Math.floor(this.color[1])}, ${Math.floor(this.color[2])})`;
                    ctx.beginPath();
                    ctx.arc(point.x, point.y, size, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.restore();
                });
                
                // 绘制主粒子
                ctx.save();
                ctx.globalAlpha = this.alpha / 255;
                ctx.fillStyle = `rgb(${Math.floor(this.color[0])}, ${Math.floor(this.color[1])}, ${Math.floor(this.color[2])})`;
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                ctx.fill();
                ctx.restore();
            }
        }
        
        // 流星雨爱心粒子类
        class FallingHeartParticle {
            constructor() {
                this.reset();
            }
            
            reset() {
                this.x = Math.random() * (WIDTH + 200) - 100; // 从屏幕稍微外面开始
                this.y = -20; // 从屏幕上方开始
                this.vx = (Math.random() - 0.7) * 2; // 稍微向左飘
                this.vy = Math.random() * 3 + 2; // 下落速度
                this.size = Math.random() * 12 + 8;
                this.color = randomChoice([PINK, LIGHT_PINK, HOT_PINK], [0.4, 0.4, 0.2]);
                this.rotation = Math.random() * Math.PI * 2;
                this.rotationSpeed = (Math.random() - 0.5) * 0.1;
                this.alpha = Math.random() * 0.8 + 0.2;
                this.trail = [];
                this.life = Math.random() * 200 + 300;
                this.maxLife = this.life;
                this.twinkle = Math.random() * Math.PI * 2;
            }
            
            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.rotation += this.rotationSpeed;
                this.twinkle += 0.15;
                this.life -= 1;
                
                // 添加尾迹
                this.trail.push({
                    x: this.x,
                    y: this.y,
                    size: this.size * 0.6,
                    alpha: this.alpha * 0.3
                });
                
                // 限制尾迹长度
                if (this.trail.length > 8) {
                    this.trail.shift();
                }
                
                // 如果超出屏幕底部,重置到顶部
                if (this.y > HEIGHT + 50 || this.x < -100 || this.x > WIDTH + 100 || this.life <= 0) {
                    this.reset();
                }
                
                return true;
            }
            
            draw(ctx) {
                // 绘制尾迹
                this.trail.forEach((point, index) => {
                    const trailAlpha = point.alpha * (index + 1) / this.trail.length;
                    const trailSize = point.size * (index + 1) / this.trail.length;
                    
                    ctx.save();
                    ctx.globalAlpha = trailAlpha;
                    ctx.translate(point.x, point.y);
                    ctx.scale(trailSize / 10, trailSize / 10);
                    
                    ctx.fillStyle = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`;
                    ctx.beginPath();
                    ctx.moveTo(0, 3);
                    ctx.bezierCurveTo(-5, -2, -10, 1, -5, 6);
                    ctx.bezierCurveTo(-3, 8, 0, 10, 0, 10);
                    ctx.bezierCurveTo(0, 10, 3, 8, 5, 6);
                    ctx.bezierCurveTo(10, 1, 5, -2, 0, 3);
                    ctx.fill();
                    
                    ctx.restore();
                });
                
                // 绘制主爱心
                const twinkleEffect = 0.8 + 0.2 * Math.sin(this.twinkle);
                
                ctx.save();
                ctx.globalAlpha = this.alpha * twinkleEffect;
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                ctx.scale(this.size / 10, this.size / 10);
                
                // 爱心形状
                ctx.fillStyle = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`;
                ctx.beginPath();
                ctx.moveTo(0, 3);
                ctx.bezierCurveTo(-5, -2, -10, 1, -5, 6);
                ctx.bezierCurveTo(-3, 8, 0, 10, 0, 10);
                ctx.bezierCurveTo(0, 10, 3, 8, 5, 6);
                ctx.bezierCurveTo(10, 1, 5, -2, 0, 3);
                ctx.fill();
                
                // 发光效果
                ctx.globalAlpha = this.alpha * twinkleEffect * 0.3;
                ctx.shadowColor = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`;
                ctx.shadowBlur = 15;
                ctx.fill();
                
                ctx.restore();
            }
        }
        
        // 点击星星粒子类
        class ClickStarParticle {
            constructor(x, y) {
                this.x = x + (Math.random() - 0.5) * 100;
                this.y = y + (Math.random() - 0.5) * 100;
                this.vx = (Math.random() - 0.5) * 8;
                this.vy = (Math.random() - 0.5) * 8 - Math.random() * 3;
                this.size = Math.random() * 4 + 2;
                this.color = randomChoice(STAR_COLORS);
                this.life = Math.random() * 60 + 120;
                this.maxLife = this.life;
                this.rotation = Math.random() * Math.PI * 2;
                this.rotationSpeed = (Math.random() - 0.5) * 0.2;
                this.twinkle = Math.random() * Math.PI * 2;
            }
            
            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.vy += 0.1; // 重力
                this.vx *= 0.99; // 阻力
                this.life -= 1;
                this.rotation += this.rotationSpeed;
                this.twinkle += 0.2;
                
                return this.life > 0;
            }
            
            draw(ctx) {
                const alpha = this.life / this.maxLife;
                const twinkleAlpha = alpha * (0.7 + 0.3 * Math.sin(this.twinkle));
                
                ctx.save();
                ctx.globalAlpha = twinkleAlpha;
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                
                // 绘制星星形状
                ctx.fillStyle = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`;
                ctx.beginPath();
                
                // 5角星
                for (let i = 0; i < 5; i++) {
                    const angle = (i * Math.PI * 2) / 5 - Math.PI / 2;
                    const x = Math.cos(angle) * this.size;
                    const y = Math.sin(angle) * this.size;
                    if (i === 0) ctx.moveTo(x, y);
                    else ctx.lineTo(x, y);
                    
                    const innerAngle = ((i + 0.5) * Math.PI * 2) / 5 - Math.PI / 2;
                    const innerX = Math.cos(innerAngle) * this.size * 0.4;
                    const innerY = Math.sin(innerAngle) * this.size * 0.4;
                    ctx.lineTo(innerX, innerY);
                }
                ctx.closePath();
                ctx.fill();
                
                // 添加发光效果
                ctx.globalAlpha = twinkleAlpha * 0.3;
                ctx.shadowColor = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`;
                ctx.shadowBlur = 10;
                ctx.fill();
                
                ctx.restore();
            }
        }
        
        // 点击爱心粒子类
        class ClickHeartParticle {
            constructor(x, y) {
                this.x = x + (Math.random() - 0.5) * 50;
                this.y = y + (Math.random() - 0.5) * 50;
                this.vx = (Math.random() - 0.5) * 6;
                this.vy = -Math.random() * 8 - 2;
                this.size = Math.random() * 8 + 6;
                this.color = randomChoice([PINK, LIGHT_PINK, HOT_PINK]);
                this.life = Math.random() * 40 + 80;
                this.maxLife = this.life;
                this.rotation = Math.random() * Math.PI * 2;
                this.rotationSpeed = (Math.random() - 0.5) * 0.1;
            }
            
            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.vy += 0.15; // 重力
                this.vx *= 0.98; // 阻力
                this.life -= 1;
                this.rotation += this.rotationSpeed;
                
                return this.life > 0;
            }
            
            draw(ctx) {
                const alpha = this.life / this.maxLife;
                
                ctx.save();
                ctx.globalAlpha = alpha;
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                ctx.scale(this.size / 10, this.size / 10);
                
                // 绘制爱心形状
                ctx.fillStyle = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`;
                ctx.beginPath();
                ctx.moveTo(0, 3);
                ctx.bezierCurveTo(-5, -2, -10, 1, -5, 6);
                ctx.bezierCurveTo(-3, 8, 0, 10, 0, 10);
                ctx.bezierCurveTo(0, 10, 3, 8, 5, 6);
                ctx.bezierCurveTo(10, 1, 5, -2, 0, 3);
                ctx.fill();
                
                // 添加发光效果
                ctx.globalAlpha = alpha * 0.3;
                ctx.shadowColor = `rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})`;
                ctx.shadowBlur = 8;
                ctx.fill();
                
                ctx.restore();
            }
        }
        
        // 初始化粒子系统
        const particles = [];
        for (let i = 0; i < 3000; i++) {
            particles.push(new Particle());
        }
        
        let galaxyParticles = [];
        let dropParticles = [];
        let clickStars = [];
        let clickHearts = [];
        let fallingHearts = []; // 新增:掉落爱心数组
        
        // 初始化掉落爱心
        for (let i = 0; i < 15; i++) { // 15个爱心,性能友好
            fallingHearts.push(new FallingHeartParticle());
        }
        
        // 键盘事件处理
        document.addEventListener('keydown', (e) => {
            keys[e.code] = true;
        });
        
        document.addEventListener('keyup', (e) => {
            keys[e.code] = false;
        });
        
        // 点击事件处理
        canvas.addEventListener('click', (e) => {
            const rect = canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            
            // 生成星星粒子
            for (let i = 0; i < 15; i++) {
                clickStars.push(new ClickStarParticle(x, y));
            }
            
            // 生成爱心粒子
            for (let i = 0; i < 8; i++) {
                clickHearts.push(new ClickHeartParticle(x, y));
            }
        });
        
        // 主动画循环
        function animate(currentTime) {
            const deltaTime = currentTime - lastTime;
            lastTime = currentTime;
            
            // 处理键盘输入
            if (keys['ArrowLeft']) {
                angleY -= 0.05;
            }
            if (keys['ArrowRight']) {
                angleY += 0.05;
            }
            
            // 自动旋转和跳动
            angleY += 0.01;
            
            // 增强跳动效果 - 使用更复杂的波形组合
            const time = currentTime * 0.001;
            const heartbeat1 = Math.sin(time * 4) * 0.15; // 主心跳
            const heartbeat2 = Math.sin(time * 8) * 0.05; // 副心跳
            const slowPulse = Math.sin(time * 1.5) * 0.1; // 慢脉冲
            const prevPulse = pulse;
            pulse = 0.85 + heartbeat1 + heartbeat2 + slowPulse;
               
            // 检测心跳峰值并播放音效 - 修改这里
            if (audioContext && pulse > heartbeatThreshold && prevPulse <= heartbeatThreshold) {
                const timeSinceLastBeat = currentTime - lastHeartbeatTime;
                if (timeSinceLastBeat > 400) { // 防止音效播放过于频繁
                    // 随机选择心跳音效类型
                    const heartbeatType = Math.floor(Math.random() * 3);
                    switch(heartbeatType) {
                        case 0:
                            playHeartbeat();
                            break;
                        case 1:
                            playDreamyHeartbeat();
                            break;
                        case 2:
                            playPianoHeartbeat();
                            break;
                    }
                    lastHeartbeatTime = currentTime;
                }
            }

            // 清除画布
            ctx.fillStyle = 'black';
            ctx.fillRect(0, 0, WIDTH, HEIGHT);
            
            // 添加渐变效果
            ctx.save();
            ctx.globalAlpha = 0.1;
            ctx.fillStyle = 'black';
            ctx.fillRect(0, 0, WIDTH, HEIGHT);
            ctx.restore();
            
            // 生成新的星河粒子
            if (Math.random() < 0.6) {
                galaxyParticles.push(new GalaxyParticle());
            }
            
            // 生成新的掉落粒子
            if (dropParticles.length < 300 && Math.random() < 0.95) {
                dropParticles.push(new ParticleDrop(particles, angleY, pulse));
            }
            
            // 更新和绘制星河粒子
            galaxyParticles = galaxyParticles.filter(p => p.update());
            galaxyParticles.forEach(p => p.draw(ctx));
            
            // 更新和绘制掉落粒子
            dropParticles = dropParticles.filter(p => p.update());
            dropParticles.forEach(p => p.draw(ctx));
            
            // 更新和绘制掉落爱心流星雨
            fallingHearts.forEach(heart => {
                heart.update();
                heart.draw(ctx);
            });
            
            // 更新和绘制点击星星
            clickStars = clickStars.filter(star => star.update());
            clickStars.forEach(star => star.draw(ctx));
            
            // 更新和绘制点击爱心
            clickHearts = clickHearts.filter(heart => heart.update());
            clickHearts.forEach(heart => heart.draw(ctx));
            
            // 绘制爱心粒子
            particles.forEach(particle => {
                particle.draw(ctx, angleY, pulse);
            });
            
            requestAnimationFrame(animate);
        }

        // 改进的音频初始化
        document.addEventListener('DOMContentLoaded', () => {
            // 添加音频初始化提示
            const audioButton = document.createElement('div');
            audioButton.style.cssText = `
                position: fixed;
                top: 60px;
                left: 50%;
                transform: translateX(-50%);
                background: rgba(255, 105, 180, 0.9);
                color: white;
                padding: 12px 25px;
                border-radius: 25px;
                cursor: pointer;
                font-size: 16px;
                z-index: 1000;
                transition: all 0.3s ease;
                border: 2px solid rgba(255, 255, 255, 0.3);
                box-shadow: 0 4px 20px rgba(255, 105, 180, 0.4);
                font-weight: bold;
            `;
            audioButton.textContent = '点击启用心跳音效 💗';
            
            // 添加悬停效果
            audioButton.onmouseover = () => {
                audioButton.style.background = 'rgba(255, 20, 147, 1)';
                audioButton.style.transform = 'translateX(-50%) scale(1.05)';
                audioButton.style.boxShadow = '0 6px 25px rgba(255, 20, 147, 0.6)';
            };
            audioButton.onmouseout = () => {
                audioButton.style.background = 'rgba(255, 105, 180, 0.9)';
                audioButton.style.transform = 'translateX(-50%) scale(1)';
                audioButton.style.boxShadow = '0 4px 20px rgba(255, 105, 180, 0.4)';
            };
            
            audioButton.onclick = () => {
                const success = initAudioWithFeedback();
                if (success || audioContext) {
                    audioButton.textContent = '心跳音效已启用 ✓';
                    audioButton.style.background = 'rgba(0, 255, 100, 0.8)';
                    audioButton.style.borderColor = 'rgba(0, 255, 100, 0.5)';
                    audioButton.style.boxShadow = '0 4px 20px rgba(0, 255, 100, 0.4)';
                    setTimeout(() => {
                        audioButton.style.opacity = '0';
                        setTimeout(() => audioButton.remove(), 300);
                    }, 2000);
                } else {
                    audioButton.textContent = '音效启用失败,请重试 ❌';
                    audioButton.style.background = 'rgba(255, 50, 50, 0.8)';
                    audioButton.style.borderColor = 'rgba(255, 50, 50, 0.5)';
                    setTimeout(() => {
                        audioButton.textContent = '点击启用心跳音效 💗';
                        audioButton.style.background = 'rgba(255, 105, 180, 0.9)';
                        audioButton.style.borderColor = 'rgba(255, 255, 255, 0.3)';
                    }, 3000);
                }
            };
            document.body.appendChild(audioButton);
            
            // 也添加点击页面任意位置初始化音频的功能
            const initOnFirstClick = () => {
                if (!audioContext) {
                    initAudioWithFeedback();
                    document.removeEventListener('click', initOnFirstClick);
                }
            };
            document.addEventListener('click', initOnFirstClick);
        });

        // 开始动画
        requestAnimationFrame(animate);
    </script>
</body>
</html>

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值