<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>一颗会动的爱心</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #000;
font-family: "Archivo Black", sans-serif;
color: white;
}
#c {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
#controls-container {
position: fixed;
top: 20px;
left: 20px;
z-index: 100;
}
#toggle-controls {
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
padding: 10px;
border-radius: 5px;
cursor: pointer;
margin-bottom: 5px;
backdrop-filter: blur(5px);
}
#controls {
background: rgba(100, 100, 100, 0.2);
padding: 15px;
border-radius: 10px;
max-width: 300px;
backdrop-filter: blur(5px);
display: block;
}
.control-group {
margin: 10px 0;
display: flex;
align-items: center;
}
.control-group label {
width: 100px;
font-size: 14px;
margin-right: 10px;
}
.control-group input[type="range"] {
flex-grow: 1;
margin-right: 10px;
}
.control-group span {
width: 30px;
text-align: right;
font-size: 14px;
}
.control-group select {
flex-grow: 1;
padding: 5px;
border-radius: 4px;
background: #333;
color: white;
border: none;
}
.heart-outline {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 360px;
height: 320px;
fill: none;
stroke: rgba(255, 255, 255, 0.3);
stroke-width: 2;
pointer-events: none;
z-index: 10;
}
#intro-controls {
margin-top: 20px;
padding: 20px;
border-radius: 10px;
background-color: rgba(0, 0, 0, 0.5);
}
#intro-controls h2 {
font-size: 1.5em;
margin-bottom: 10px;
}
#intro-controls ul {
list-style-type: none;
padding: 0;
}
#intro-controls li {
margin-bottom: 5px;
font-size: 1em;
}
#toggle-fullscreen {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
padding: 10px;
border-radius: 5px;
cursor: pointer;
z-index: 100;
backdrop-filter: blur(5px);
}
@media (max-width: 600px) {
#controls-container {
top: 10px;
left: 10px;
right: 10px;
max-width: none;
}
.control-group label {
width: 80px;
font-size: 12px;
}
}
</style>
</head>
<body>
<div id="controls-container">
<button id="toggle-controls">控制面板</button>
<button id="toggle-fullscreen">全屏</button>
<div id="controls">
<div class="control-group">
<label for="colorScheme">颜色:</label>
<select id="colorScheme">
<option value="rainbow">彩虹</option>
<option value="red">红色</option>
<option value="green">绿色</option>
<option value="blue">蓝色</option>
<option value="monochrome">单色</option>
</select>
</div>
<div class="control-group">
<label for="particleSize">尺寸:</label>
<input type="range" id="particleSize" min="1" max="20" value="10" />
<span id="particleSizeValue">10</span>
</div>
<div class="control-group">
<label for="particleCount">粒子数:</label>
<input
type="range"
id="particleCount"
min="10"
max="100"
value="32"
/>
<span id="particleCountValue">32</span>
</div>
<div class="control-group">
<label for="speed">速度:</label>
<input
type="range"
id="speed"
min="0.1"
max="2"
step="0.1"
value="1"
/>
<span id="speedValue">1</span>
</div>
<div class="control-group">
<label for="mouseInfluence">鼠标跟随:</label>
<input
type="range"
id="mouseInfluence"
min="0"
max="100"
value="50"
/>
<span id="mouseInfluenceValue">50</span>
</div>
</div>
</div>
<canvas id="c"></canvas>
<script>
const config = {
particleCount: 32,
speed: 1,
colorScheme: "rainbow",
mouseInfluence: 50,
showHeartOutline: true,
particleSize: 10,
};
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
let canvasWidth = (canvas.width = window.innerWidth);
let canvasHeight = (canvas.height = window.innerHeight);
let trails = [];
let heartPath = [];
let mouseX = canvasWidth / 2;
let mouseY = canvasHeight / 2;
let mouseActive = false;
let animationRunning = false;
function initHeartPath() {
heartPath = [];
const PI2 = 6.28318;
const steps = Math.max(32, config.particleCount);
for (let i = 0; i < steps; i++) {
const t = (i / steps) * PI2;
heartPath.push([
canvasWidth / 2 + 180 * Math.pow(Math.sin(t), 3),
canvasHeight / 2 +
10 *
-(
15 * Math.cos(t) -
5 * Math.cos(2 * t) -
2 * Math.cos(3 * t) -
Math.cos(4 * t)
),
]);
}
}
function initParticles() {
trails = [];
if (heartPath.length === 0) initHeartPath();
for (let i = 0; i < config.particleCount; i++) {
const particles = [];
const x = Math.random() * canvasWidth;
const y = Math.random() * canvasHeight;
for (let k = 0; k < config.particleCount; k++) {
let hue,
saturation = Math.random() * 40 + 60;
let brightness = Math.random() * 60 + 20;
switch (config.colorScheme) {
case "red":
hue = Math.random() * 20 + 350;
break;
case "blue":
hue = Math.random() * 20 + 200;
break;
case "green":
hue = Math.random() * 20 + 100;
break;
case "monochrome":
hue = 0;
saturation = 0;
break;
default:
hue = (i / config.particleCount) * 360;
}
particles.push({
x,
y,
velX: 0,
velY: 0,
radius:
((1 - k / config.particleCount + 1) * config.particleSize) / 2,
speed: Math.random() + 1,
targetIndex: Math.floor(Math.random() * heartPath.length),
direction: (i % 2) * 2 - 1,
friction: Math.random() * 0.2 + 0.7,
color: `hsla(${hue},${saturation}%,${brightness}%,0.1)`,
});
}
trails.push(particles);
}
}
function renderParticle(particle) {
ctx.fillStyle = particle.color;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
ctx.fill();
}
function animationLoop() {
if (!animationRunning) return;
try {
ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
trails.forEach((trail) => {
if (!trail || !trail.length) return;
const leader = trail[0];
const target = heartPath[leader.targetIndex % heartPath.length];
if (!target) return;
if (mouseActive && config.mouseInfluence > 0) {
const dx = mouseX - leader.x;
const dy = mouseY - leader.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 300) {
const force = (1 - dist / 300) * (config.mouseInfluence / 20);
leader.velX += (dx / dist) * force;
leader.velY += (dy / dist) * force;
}
}
const dx = leader.x - target[0];
const dy = leader.y - target[1];
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 10) {
if (Math.random() > 0.95) {
leader.targetIndex = Math.floor(
Math.random() * heartPath.length
);
} else {
if (Math.random() > 0.99) leader.direction *= -1;
leader.targetIndex += leader.direction;
leader.targetIndex =
(leader.targetIndex + heartPath.length) % heartPath.length;
}
}
leader.velX += (-dx / dist) * leader.speed * config.speed;
leader.velY += (-dy / dist) * leader.speed * config.speed;
leader.x += leader.velX;
leader.y += leader.velY;
leader.velX *= leader.friction;
leader.velY *= leader.friction;
renderParticle(leader);
for (let k = 1; k < trail.length; k++) {
trail[k].x -= (trail[k].x - trail[k - 1].x) * 0.7;
trail[k].y -= (trail[k].y - trail[k - 1].y) * 0.7;
renderParticle(trail[k]);
}
});
} catch (error) {
console.error("Animation error:", error);
}
requestAnimationFrame(animationLoop);
}
function setupControls() {
const controls = document.getElementById("controls");
let controlsVisible = true;
document
.getElementById("toggle-controls")
.addEventListener("click", () => {
controlsVisible = !controlsVisible;
controls.style.display = controlsVisible ? "block" : "none";
});
document
.getElementById("toggle-fullscreen")
.addEventListener("click", () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
});
const updateParticles = () => {
config.particleCount = Math.min(
100,
Math.max(
10,
parseInt(document.getElementById("particleCount").value)
)
);
document.getElementById("particleCountValue").textContent =
config.particleCount;
initHeartPath();
initParticles();
};
document
.getElementById("particleCount")
.addEventListener("input", updateParticles);
document.getElementById("speed").addEventListener("input", (e) => {
config.speed = parseFloat(e.target.value);
document.getElementById("speedValue").textContent = config.speed;
});
document
.getElementById("colorScheme")
.addEventListener("change", (e) => {
config.colorScheme = e.target.value;
initParticles();
});
document
.getElementById("mouseInfluence")
.addEventListener("input", (e) => {
config.mouseInfluence = parseInt(e.target.value);
document.getElementById("mouseInfluenceValue").textContent =
config.mouseInfluence;
});
document
.getElementById("particleSize")
.addEventListener("input", (e) => {
config.particleSize = parseInt(e.target.value);
document.getElementById("particleSizeValue").textContent =
config.particleSize;
initParticles();
});
document.addEventListener("mousemove", (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
mouseActive = true;
});
document.addEventListener("mouseleave", () => (mouseActive = false));
window.addEventListener("resize", () => {
canvasWidth = canvas.width = window.innerWidth;
canvasHeight = canvas.height = window.innerHeight;
initHeartPath();
});
}
function init() {
const controls = document.getElementById("controls");
const canvas = document.getElementById("c");
controls.style.display = "block";
canvas.style.display = "block";
animationRunning = true;
setupControls();
animationLoop();
initHeartPath();
initParticles();
}
init();
</script>
</body>
</html>