💗 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优化
- 内存管理:及时清理过期粒子对象
🎯 使用说明
- 启动页面:打开HTML文件即可看到3D爱心动画
- 启用心跳音效:点击页面顶部的粉色按钮
- 控制旋转:使用键盘左右箭头键手动控制
- 产生特效:点击屏幕任意位置产生星星和爱心特效
- 享受体验:静静欣赏自动播放的爱心跳动和流星雨
💝 设计理念
这个项目融合了艺术、技术和情感,通过代码创造出充满爱意的数字艺术品。每一个粒子、每一帧动画都承载着创作者的心意,为用户带来温暖和美好的感受。
"用代码书写浪漫,用技术传递爱意" 💕
<!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>

被折叠的 条评论
为什么被折叠?



