在这个充满创意的时代,用代码向喜欢的人表白,既浪漫又独特。本文将分享一款高颜值动态爱心表白网页的实现过程,包含漂浮爱心、粒子爱心、玫瑰花瓣飘落和点击互动效果,采用 HTML 搭建结构、CSS 美化样式、JS 实现动态效果,全程拆解核心代码,新手也能轻松上手!
运行效果(是动态的,这里是截图效果):

一、网页效果预览
- 视觉风格:深紫色渐变背景搭配粉色系爱心元素,浪漫氛围拉满,视觉层次丰富。
- 动态特效:
- 中心粒子爱心:循环缩放,带有高光质感,粒子扩散效果自然;
- 漂浮爱心图标:从屏幕顶部下落,伴随旋转,色彩柔和渐变;
- 玫瑰花瓣飘落:随机大小、随机轨迹,模拟真实花瓣飘落;
- 点击互动:点击屏幕任意位置,弹出随机颜色小爱心并向上飞散。
- 适配性:响应式设计,支持电脑、手机等不同尺寸设备,文字大小自动适配。
二、技术栈与核心知识点
- 技术栈:HTML5(结构搭建)+ CSS3(样式美化 + 动画)+ JavaScript(动态交互)
- 核心知识点:
- Canvas 绘图:实现粒子爱心和漂浮爱心效果;
- CSS 高级特性:渐变背景、文字阴影、自定义鼠标指针、关键帧动画;
- JavaScript 面向对象:封装粒子、爱心等对象,批量控制动态元素;
- 事件监听:窗口大小变化适配、鼠标点击互动;
- 动态 DOM 操作:创建玫瑰花瓣元素并实现自动移除。
三、代码拆解实现
第一部分:HTML 结构设计(简洁清晰)
页面结构主要分为 4 个核心部分:粒子爱心画布、漂浮爱心画布、表白文字(主标题 + 副标题),玫瑰花瓣通过 JS 动态生成,无需提前在 HTML 中编写。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>💌 给你的心意</title>
<!-- 引入 CSS 样式(下方单独拆解) -->
<style type="text/css">
/* CSS 代码见第二部分 */
</style>
</head>
<body>
<!-- 1. 中心粒子爱心画布 -->
<canvas id="pinkboard"></canvas>
<!-- 2. 漂浮爱心画布 -->
<canvas id="canvas"></canvas>
<!-- 3. 表白文字:主标题 -->
<div id="name">我喜欢你</div>
<!-- 4. 表白文字:副标题 -->
<div id="message">可以给我一个机会吗?</div>
<!-- 引入 JS 脚本(下方单独拆解) -->
<script type="text/javascript">
// JS 代码见第三部分
</script>
</body>
</html>
第二部分:CSS 样式美化(营造浪漫氛围)
CSS 负责页面基础样式、动画效果和视觉美化,核心分为 6 个模块,每部分都有明确功能定位。
/* 1. 全局基础样式 */
body {
margin: 0;
overflow: hidden; /* 隐藏滚动条,避免页面滚动 */
/* 深紫色渐变背景,中心亮四周暗,增强氛围感 */
background: radial-gradient(circle at center, #2c003e 0%, #11001c 100%);
background-color: #11001c; /* 渐变降级背景 */
/* 自定义爱心鼠标指针 */
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="%23ff6b8b"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>'), auto;
}
/* 2. Canvas 画布样式:全屏覆盖 */
canvas {
position: absolute;
width: 100%;
height: 100%;
}
/* 3. 中心粒子爱心动画:循环缩放 */
#pinkboard {
animation: pulse 2s ease-in-out infinite;
transform-origin: center; /* 以中心为缩放原点 */
}
/* 缩放动画关键帧 */
@keyframes pulse {
0%, 100% {
transform: scale(0.95);
opacity: 0.9;
}
50% {
transform: scale(1.05);
opacity: 1;
}
}
/* 4. 主标题样式:居中+渐变显示+文字发光 */
#name {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 绝对居中 */
margin-top: -20px;
/* 响应式字体:最小2rem,最大3.5rem,随屏幕宽度变化 */
font-size: clamp(2rem, 5vw, 3.5rem);
color: #ffccd5; /* 浅粉色文字 */
font-family: "Arial Rounded MT Bold", "Helvetica Neue", sans-serif;
/* 三层文字阴影,营造发光效果 */
text-shadow: 0 0 10px rgba(255, 107, 139, 0.7),
0 0 20px rgba(255, 107, 139, 0.5),
0 0 30px rgba(255, 107, 139, 0.3);
z-index: 10; /* 层级最高,确保文字在最上层 */
opacity: 0; /* 初始透明,通过动画显示 */
animation: fadeIn 3s forwards 1s; /* 1秒后开始,3秒渐显 */
}
/* 5. 副标题样式:跟随主标题显示 */
#message {
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, -50%);
font-size: clamp(1rem, 3vw, 1.5rem); /* 响应式字体 */
color: #ffb6c1; /* 浅粉色文字 */
font-family: "Georgia", serif;
text-shadow: 0 0 5px rgba(255, 182, 193, 0.5); /* 轻微发光 */
z-index: 10;
opacity: 0;
animation: fadeIn 3s forwards 2s; /* 2秒后开始渐显,与主标题错开 */
white-space: nowrap; /* 避免文字换行 */
}
/* 渐显动画关键帧 */
@keyframes fadeIn {
to {
opacity: 1;
}
}
/* 6. 玫瑰花瓣样式:SVG 绘制+半透明效果 */
.petal {
position: absolute;
/* SVG 绘制粉色玫瑰花瓣,无需外部图片 */
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path fill="%23ff6b8b" d="M25,5 C15,5 5,15 5,25 C5,35 15,45 25,45 C35,45 45,35 45,25 C45,15 35,5 25,5 Z M25,40 C18,40 10,32 10,25 C10,18 18,10 25,10 C32,10 40,18 40,25 C40,32 32,40 25,40 Z"/></svg>');
background-size: contain;
background-repeat: no-repeat;
pointer-events: none; /* 忽略鼠标事件,不影响点击互动 */
opacity: 0.7; /* 半透明,更自然 */
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.5)); /* 白色阴影,增强立体感 */
}
/* 7. 点击小爱心动画:向上飞散+旋转 */
.click-heart {
position: absolute;
pointer-events: none;
z-index: 100; /* 层级高于其他元素 */
animation: flyUp 1s forwards;
}
/* 点击爱心飞散动画 */
@keyframes flyUp {
0% {
transform: scale(0);
opacity: 1;
}
50% {
opacity: 0.8;
}
100% {
transform: scale(1.5) translateY(-100px) rotate(360deg); /* 放大+上移+旋转 */
opacity: 0;
}
}
/* 8. 玫瑰花瓣飘落+旋转动画(动态添加到 style 标签) */
/* 注:这部分在 JS 中动态创建,避免 CSS 冗余 */
第三部分:JavaScript 动态效果(实现交互与动画)
JS 是页面 “活起来” 的核心,分为 5 个核心模块,每个模块负责一个动态效果,逻辑清晰易维护。
模块 1:漂浮爱心图标效果
通过封装 Heart 类,批量创建从顶部下落的爱心图标,带有随机位置、大小、旋转和速度。
// 漂浮爱心图标效果
const colors = [
"#ff6b8b", "#ff8fa3", "#ffb3c1", "#ffccd5",
"#fbc2eb", "#f8bbd0", "#e1bee7", "#d1c4e9"
];
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var ww = window.innerWidth; // 窗口宽度
var wh = window.innerHeight; // 窗口高度
var hearts = []; // 存储所有爱心对象
// 初始化漂浮爱心
function initHearts() {
requestAnimationFrame(renderHearts); // 开启动画帧循环
canvas.width = ww;
canvas.height = wh;
// 创建 80 个爱心对象(数量可调整)
for (var i = 0; i < 80; i++) {
hearts.push(new Heart());
}
}
// 爱心类:封装爱心的属性和行为
function Heart() {
this.x = Math.random() * ww; // 随机x坐标
this.y = Math.random() * wh - wh; // 初始位置在屏幕上方(看不到的地方)
this.opacity = Math.random() * 0.6 + 0.3; // 透明度 0.3-0.9
this.vel = { // 运动速度
x: (Math.random() - 0.5) * 2, // 水平方向随机偏移(左右晃动)
y: Math.random() * 3 + 2 // 垂直下落速度(2-5)
};
this.targetScale = Math.random() * 0.1 + 0.05; // 目标大小(0.05-0.15)
this.scale = this.targetScale * Math.random(); // 初始大小(随机)
this.rotation = Math.random() * Math.PI * 2; // 初始旋转角度(0-360度)
this.rotateSpeed = (Math.random() - 0.5) * 0.02; // 旋转速度(左右随机)
}
// 更新爱心状态(位置、旋转、大小)
Heart.prototype.update = function () {
this.x += this.vel.x;
this.y += this.vel.y;
this.rotation += this.rotateSpeed; // 持续旋转
// 爱心下落超出屏幕后,重置到顶部
if (this.y > wh + 100) {
this.y = -50;
this.x = Math.random() * ww;
}
// 逐渐缩放至目标大小
this.scale += (this.targetScale - this.scale) * 0.02;
this.width = 470; // 爱心文字宽度(固定值,用于定位)
this.height = 400; // 爱心文字高度(固定值,用于定位)
};
// 绘制爱心图标
Heart.prototype.draw = function (i) {
ctx.save(); // 保存画布状态
ctx.globalAlpha = this.opacity; // 设置透明度
ctx.translate(this.x, this.y); // 移动画布原点到爱心位置
ctx.rotate(this.rotation); // 旋转画布
// 设置字体大小(随缩放比例变化)
ctx.font = `${180 * this.scale}px "微软雅黑", "Arial Rounded MT Bold"`;
ctx.fillStyle = colors[i % colors.length]; // 循环使用颜色数组
ctx.fillText(
"❤", // 爱心图标
-this.width * 0.25, // 水平居中调整
this.height * 0.1, // 垂直居中调整
this.width,
this.height
);
ctx.restore(); // 恢复画布状态
};
// 渲染所有爱心
function renderHearts() {
ctx.clearRect(0, 0, ww, wh); // 清空画布(避免残影)
for (var i = 0; i < hearts.length; i++) {
hearts[i].update();
hearts[i].draw(i);
}
requestAnimationFrame(renderHearts); // 持续循环渲染
}
模块 2:玫瑰花瓣飘落效果
动态创建玫瑰花瓣元素,设置随机大小、位置和动画,动画结束后自动移除,避免内存占用。
// 玫瑰花瓣飘落效果
function createPetals() {
// 动态添加花瓣动画样式(飘落+旋转)
const style = document.createElement('style');
style.textContent = `
@keyframes fall {
0% { transform: translateY(0) translateX(0); opacity: 0.8; }
100% { transform: translateY(100vh) translateX(var(--translateX, 0)); opacity: 0.4; }
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
// 每 300 毫秒创建一个花瓣(频率可调整)
setInterval(() => {
const petal = document.createElement('div');
petal.className = 'petal';
// 随机设置花瓣大小(10-30px)
const size = Math.random() * 20 + 10;
petal.style.width = `${size}px`;
petal.style.height = `${size}px`;
// 随机设置水平位置(0-100vw)
petal.style.left = `${Math.random() * 100}vw`;
// 初始位置在屏幕顶部上方(看不到的地方)
petal.style.top = '-50px';
// 随机动画参数
const duration = Math.random() * 10 + 8; // 飘落时长(8-18秒)
const delay = Math.random() * 5; // 延迟开始时间(0-5秒)
const translateX = (Math.random() - 0.5) * 100; // 水平偏移(-50到50px)
// 应用动画:飘落+旋转
petal.style.animation = `
fall ${duration}s linear forwards ${delay}s,
rotate ${Math.random() * 5 + 5}s linear infinite ${delay}s
`;
petal.style.setProperty('--translateX', `${translateX}px`); // 水平偏移变量
// 添加花瓣到页面
document.body.appendChild(petal);
// 动画结束后移除花瓣(避免内存泄漏)
setTimeout(() => {
petal.remove();
}, (duration + delay) * 1000);
}, 300);
}
模块 3:中心粒子爱心(核心动态效果)
中心粒子爱心是页面的视觉焦点,通过数学公式生成爱心路径,粒子从路径上发射并扩散,配合循环缩放动画,营造出灵动的质感。核心分为「点类」「粒子类」「粒子池类」和「渲染逻辑」四部分,封装性强,便于维护。
// 中心粒子爱心效果(立即执行函数,避免全局变量污染)
(function (canvas) {
// 粒子爱心配置参数(可按需调整)
var settings = {
particles: {
length: 600, // 粒子总数(越多越密集)
duration: 5, // 单个粒子生命周期(秒)
velocity: 80, // 粒子发射速度
effect: -0.8, // 粒子加速度系数(负值表示减速)
size: 35, // 粒子初始大小
},
};
// 初始化变量
var context = canvas.getContext("2d"), // Canvas 绘图上下文
particles = new ParticlePool(settings.particles.length), // 粒子池(复用粒子,优化性能)
particleRate = settings.particles.length / settings.particles.duration, // 每秒生成的粒子数
time; // 记录时间,用于计算帧间隔
// 核心:通过数学公式生成爱心路径上的点
function pointOnHeart(t) {
return new Point(
160 * Math.pow(Math.sin(t), 3), // x坐标(爱心水平方向公式)
130 * Math.cos(t) -
50 * Math.cos(2 * t) -
20 * Math.cos(3 * t) -
10 * Math.cos(4 * t) +
25 // y坐标(爱心垂直方向公式)
);
}
// 生成爱心形状的粒子图片(带高光效果)
var image = (function () {
var canvas = document.createElement("canvas"), // 临时画布,用于绘制爱心形状
context = canvas.getContext("2d");
canvas.width = settings.particles.size;
canvas.height = settings.particles.size;
// 将爱心路径上的点转换为临时画布的坐标
function to(t) {
var point = pointOnHeart(t);
point.x = settings.particles.size / 2 + (point.x * settings.particles.size) / 350;
point.y = settings.particles.size / 2 - (point.y * settings.particles.size) / 350;
return point;
}
// 绘制爱心轮廓并填充
context.beginPath();
var t = -Math.PI; // 从-π开始(爱心左侧起点)
var point = to(t);
context.moveTo(point.x, point.y); // 移动到起点
while (t < Math.PI) { // 循环到π(爱心右侧终点)
t += 0.01; // 步长越小,爱心轮廓越平滑
point = to(t);
context.lineTo(point.x, point.y); // 绘制路径
}
context.closePath();
context.fillStyle = "rgba(255, 107, 139, 0.9)"; // 粉色爱心(带透明度)
context.fill();
// 给爱心添加高光效果(增强质感)
context.beginPath();
context.arc(
settings.particles.size * 0.35, // 高光x坐标(爱心左上方)
settings.particles.size * 0.35, // 高光y坐标
settings.particles.size * 0.15, // 高光大小
0,
Math.PI * 2 // 绘制圆形高光
);
context.fillStyle = "rgba(255, 255, 255, 0.3)"; // 白色半透明高光
context.fill();
// 转换为图片对象,用于粒子绘制
var image = new Image();
image.src = canvas.toDataURL();
return image;
})();
// 渲染函数:每帧更新并绘制粒子
function render() {
requestAnimationFrame(render); // 持续请求动画帧
var newTime = new Date().getTime() / 1000, // 当前时间(秒)
deltaTime = newTime - (time || newTime); // 两帧之间的时间间隔
time = newTime;
context.clearRect(0, 0, canvas.width, canvas.height); // 清空画布
// 计算当前帧需要生成的粒子数,并添加到粒子池
var amount = particleRate * deltaTime;
for (var i = 0; i < amount; i++) {
var pos = pointOnHeart(Math.PI - 2 * Math.PI * Math.random()); // 爱心路径上随机取点
var dir = pos.clone().length(settings.particles.velocity); // 计算粒子发射方向和速度
// 将粒子添加到粒子池(参数:x坐标、y坐标、水平速度、垂直速度)
particles.add(
canvas.width / 2 + pos.x, // 粒子发射x坐标(画布中心偏移)
canvas.height / 2 - pos.y, // 粒子发射y坐标(画布中心偏移)
dir.x,
-dir.y
);
}
particles.update(deltaTime); // 更新所有粒子状态(位置、速度、生命周期)
particles.draw(context, image); // 绘制所有粒子
}
// 窗口大小变化时,调整画布尺寸(响应式适配)
function onResize() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
window.onresize = onResize;
// 粒子池类:复用粒子对象,减少DOM操作,提升性能
function ParticlePool(length) {
var particles = new Array(length); // 存储粒子的数组
for (var i = 0; i < particles.length; i++) {
particles[i] = new Particle(); // 初始化粒子
}
var firstActive = 0, // 第一个活跃粒子的索引
firstFree = 0, // 第一个空闲粒子的索引
duration = settings.particles.duration; // 粒子生命周期
// 添加粒子到粒子池
this.add = function (x, y, dx, dy) {
particles[firstFree].initialize(x, y, dx, dy); // 初始化空闲粒子
firstFree = (firstFree + 1) % particles.length; // 移动空闲粒子指针(循环复用)
if (firstActive === firstFree) {
firstActive = (firstActive + 1) % particles.length; // 活跃粒子指针跟随移动
}
};
// 更新所有活跃粒子的状态
this.update = function (deltaTime) {
var i;
// 分两种情况遍历活跃粒子(避免数组越界)
if (firstActive < firstFree) {
for (i = firstActive; i < firstFree; i++) {
particles[i].update(deltaTime);
}
}
if (firstFree < firstActive) {
for (i = firstActive; i < particles.length; i++) {
particles[i].update(deltaTime);
}
for (i = 0; i < firstFree; i++) {
particles[i].update(deltaTime);
}
}
// 移除生命周期结束的粒子(移动活跃粒子指针)
while (particles[firstActive].age >= duration && firstActive !== firstFree) {
firstActive = (firstActive + 1) % particles.length;
}
};
// 绘制所有活跃粒子
this.draw = function (context, image) {
if (firstActive < firstFree) {
for (i = firstActive; i < firstFree; i++) {
particles[i].draw(context, image);
}
}
if (firstFree < firstActive) {
for (i = firstActive; i < particles.length; i++) {
particles[i].draw(context, image);
}
for (i = 0; i < firstFree; i++) {
particles[i].draw(context, image);
}
}
};
}
// 粒子类:封装粒子的属性和行为
function Particle() {
this.position = new Point(); // 粒子位置
this.velocity = new Point(); // 粒子速度
this.acceleration = new Point(); // 粒子加速度
this.age = 0; // 粒子已存在时间(生命周期)
// 初始化粒子
this.initialize = function (x, y, dx, dy) {
this.position.x = x;
this.position.y = y;
this.velocity.x = dx;
this.velocity.y = dy;
// 加速度 = 速度 * 效果系数(实现粒子减速)
this.acceleration.x = dx * settings.particles.effect;
this.acceleration.y = dy * settings.particles.effect;
this.age = 0; // 重置生命周期
};
// 更新粒子状态(位置、速度、生命周期)
this.update = function (deltaTime) {
this.position.x += this.velocity.x * deltaTime; // 位置 = 速度 * 时间
this.position.y += this.velocity.y * deltaTime;
this.velocity.x += this.acceleration.x * deltaTime; // 速度 = 加速度 * 时间(减速)
this.velocity.y += this.acceleration.y * deltaTime;
this.age += deltaTime; // 增加生命周期
};
// 绘制粒子(爱心形状)
this.draw = function (context, image) {
// 缓动函数:让粒子大小变化更自然(先快后慢)
function ease(t) {
return --t * t * t + 1;
}
// 粒子大小随生命周期变化(从大到小)
var size = image.width * ease(this.age / settings.particles.duration);
// 粒子透明度随生命周期变化(从明到暗)
context.globalAlpha = 1 - this.age / settings.particles.duration;
// 绘制爱心粒子
context.drawImage(
image,
this.position.x - size / 2, // 粒子x坐标(居中)
this.position.y - size / 2, // 粒子y坐标(居中)
size, // 粒子宽度
size // 粒子高度
);
};
}
// 点类:封装坐标和相关计算方法(用于粒子位置、速度计算)
function Point(x, y) {
this.x = x || 0; // x坐标(默认0)
this.y = y || 0; // y坐标(默认0)
// 克隆点对象(避免引用传递问题)
this.clone = function () {
return new Point(this.x, this.y);
};
// 计算点到原点的距离,或设置点的长度(用于速度方向计算)
this.length = function (length) {
if (length === undefined) {
// 计算距离(勾股定理)
return Math.sqrt(this.x * this.x + this.y * this.y);
}
this.normalize(); // 归一化(方向不变,长度为1)
this.x *= length; // 设置x方向长度
this.y *= length; // 设置y方向长度
return this;
};
// 归一化:将点的长度变为1(仅保留方向)
this.normalize = function () {
var length = this.length();
this.x /= length;
this.y /= length;
return this;
};
}
// 延迟10毫秒初始化(确保DOM加载完成)
setTimeout(function () {
onResize(); // 初始化画布尺寸
render(); // 启动粒子爱心渲染
}, 10);
})(document.getElementById("pinkboard")); // 传入中心爱心画布元素
模块 3 核心逻辑说明:
- 爱心路径生成:通过数学公式
pointOnHeart(t)计算爱心轮廓上的点,t 从 -π 到 π 循环,生成标准爱心形状。 - 粒子池优化:创建固定数量的粒子对象并复用,避免频繁创建 / 删除 DOM 元素,提升页面性能(尤其粒子数量多时)。
- 粒子生命周期管理:粒子从爱心路径上发射,随时间推移逐渐缩小、变暗,生命周期结束后被复用。
- 缓动效果:粒子大小和透明度变化采用缓动函数,避免生硬的线性变化,让动画更自然。
模块 4:点击互动效果(增强趣味性)
点击屏幕任意位置,弹出随机颜色、随机大小的小爱心,向上飞散并旋转,提升用户参与感。
// 点击屏幕出现小爱心互动效果
document.addEventListener('click', (e) => {
// 创建小爱心元素
const heart = document.createElement('div');
heart.className = 'click-heart'; // 绑定动画样式
heart.innerHTML = '❤'; // 爱心图标
// 设置小爱心位置(鼠标点击位置)
heart.style.left = `${e.clientX}px`;
heart.style.top = `${e.clientY}px`;
// 随机设置小爱心大小(16-36px)
heart.style.fontSize = `${Math.random() * 20 + 16}px`;
// 随机设置小爱心颜色(从漂浮爱心颜色数组中选取)
heart.style.color = colors[Math.floor(Math.random() * colors.length)];
// 添加到页面
document.body.appendChild(heart);
// 1秒后移除小爱心(动画结束后,避免内存占用)
setTimeout(() => {
heart.remove();
}, 1000);
});
模块 5:初始化与窗口适配(确保效果稳定)
初始化所有动态效果,监听窗口大小变化,动态调整画布尺寸,保证在不同设备上都能正常显示。
// 监听窗口大小变化,更新画布尺寸(响应式适配)
window.addEventListener("resize", function () {
ww = window.innerWidth;
wh = window.innerHeight;
canvas.width = ww;
canvas.height = wh;
});
// 启动所有动态效果
initHearts(); // 启动漂浮爱心
createPetals(); // 启动玫瑰花瓣飘落
四、完整源码整合(直接复制可用)
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>💌 给你的心意</title>
<style type="text/css">
body {
margin: 0;
overflow: hidden;
background: radial-gradient(circle at center, #2c003e 0%, #11001c 100%);
background-color: #11001c;
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="%23ff6b8b"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>'), auto;
}
canvas {
position: absolute;
width: 100%;
height: 100%;
}
#pinkboard {
animation: pulse 2s ease-in-out infinite;
transform-origin: center;
}
@keyframes pulse {
0%,
100% {
transform: scale(0.95);
opacity: 0.9;
}
50% {
transform: scale(1.05);
opacity: 1;
}
}
#name {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: -20px;
font-size: clamp(2rem, 5vw, 3.5rem);
color: #ffccd5;
font-family: "Arial Rounded MT Bold", "Helvetica Neue", sans-serif;
text-shadow: 0 0 10px rgba(255, 107, 139, 0.7),
0 0 20px rgba(255, 107, 139, 0.5),
0 0 30px rgba(255, 107, 139, 0.3);
z-index: 10;
opacity: 0;
animation: fadeIn 3s forwards 1s;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
#message {
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, -50%);
font-size: clamp(1rem, 3vw, 1.5rem);
color: #ffb6c1;
font-family: "Georgia", serif;
text-shadow: 0 0 5px rgba(255, 182, 193, 0.5);
z-index: 10;
opacity: 0;
animation: fadeIn 3s forwards 2s;
white-space: nowrap;
}
.petal {
position: absolute;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path fill="%23ff6b8b" d="M25,5 C15,5 5,15 5,25 C5,35 15,45 25,45 C35,45 45,35 45,25 C45,15 35,5 25,5 Z M25,40 C18,40 10,32 10,25 C10,18 18,10 25,10 C32,10 40,18 40,25 C40,32 32,40 25,40 Z"/></svg>');
background-size: contain;
background-repeat: no-repeat;
pointer-events: none;
opacity: 0.7;
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.5));
}
/* 点击爱心效果 */
.click-heart {
position: absolute;
pointer-events: none;
z-index: 100;
animation: flyUp 1s forwards;
}
@keyframes flyUp {
0% {
transform: scale(0);
opacity: 1;
}
50% {
opacity: 0.8;
}
100% {
transform: scale(1.5) translateY(-100px) rotate(360deg);
opacity: 0;
}
}
</style>
</head>
<body>
<canvas id="pinkboard"></canvas>
<canvas id="canvas"></canvas>
<div id="name">我喜欢你</div>
<div id="message">我会一直在你身边!</div>
<script type="text/javascript">
// 爱心文字飘落效果
const colors = [
"#ff6b8b", "#ff8fa3", "#ffb3c1", "#ffccd5",
"#fbc2eb", "#f8bbd0", "#e1bee7", "#d1c4e9"
];
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var ww = window.innerWidth;
var wh = window.innerHeight;
var hearts = [];
function initHearts() {
requestAnimationFrame(renderHearts);
canvas.width = ww;
canvas.height = wh;
for (var i = 0; i < 80; i++) {
hearts.push(new Heart());
}
}
function Heart() {
this.x = Math.random() * ww;
this.y = Math.random() * wh - wh; // 从屏幕上方开始
this.opacity = Math.random() * 0.6 + 0.3;
this.vel = {
x: (Math.random() - 0.5) * 2,
y: Math.random() * 3 + 2 // 下落速度
};
this.targetScale = Math.random() * 0.1 + 0.05;
this.scale = this.targetScale * Math.random();
this.rotation = Math.random() * Math.PI * 2;
this.rotateSpeed = (Math.random() - 0.5) * 0.02;
}
Heart.prototype.update = function () {
this.x += this.vel.x;
this.y += this.vel.y;
this.rotation += this.rotateSpeed;
// 重新循环到顶部
if (this.y > wh + 100) {
this.y = -50;
this.x = Math.random() * ww;
}
this.scale += (this.targetScale - this.scale) * 0.02;
this.width = 470;
this.height = 400;
};
Heart.prototype.draw = function (i) {
ctx.save();
ctx.globalAlpha = this.opacity;
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.font = `${180 * this.scale}px "微软雅黑", "Arial Rounded MT Bold"`;
ctx.fillStyle = colors[i % colors.length];
ctx.fillText(
"❤",
-this.width * 0.25, // 居中调整
this.height * 0.1,
this.width,
this.height
);
ctx.restore();
};
function renderHearts() {
ctx.clearRect(0, 0, ww, wh);
for (var i = 0; i < hearts.length; i++) {
hearts[i].update();
hearts[i].draw(i);
}
requestAnimationFrame(renderHearts);
}
// 玫瑰花瓣飘落效果
function createPetals() {
setInterval(() => {
const petal = document.createElement('div');
petal.className = 'petal';
// 随机大小和位置
const size = Math.random() * 20 + 10;
petal.style.width = `${size}px`;
petal.style.height = `${size}px`;
petal.style.left = `${Math.random() * 100}vw`;
petal.style.top = '-50px';
// 随机动画
const duration = Math.random() * 10 + 8;
const delay = Math.random() * 5;
const rotate = Math.random() * 360;
const translateX = (Math.random() - 0.5) * 100;
petal.style.animation = `
fall ${duration}s linear forwards ${delay}s,
rotate ${Math.random() * 5 + 5}s linear infinite ${delay}s
`;
document.body.appendChild(petal);
// 动画结束后移除
setTimeout(() => {
petal.remove();
}, (duration + delay) * 1000);
}, 300);
}
// 花瓣动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes fall {
0% { transform: translateY(0) translateX(0); opacity: 0.8; }
100% { transform: translateY(100vh) translateX(var(--translateX, 0)); opacity: 0.4; }
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
// 中心大爱心效果
(function (canvas) {
var settings = {
particles: {
length: 600,
duration: 5,
velocity: 80,
effect: -0.8,
size: 35,
},
};
var context = canvas.getContext("2d"),
particles = new ParticlePool(settings.particles.length),
particleRate = settings.particles.length / settings.particles.duration,
time;
function pointOnHeart(t) {
return new Point(
160 * Math.pow(Math.sin(t), 3),
130 * Math.cos(t) -
50 * Math.cos(2 * t) -
20 * Math.cos(3 * t) -
10 * Math.cos(4 * t) +
25
);
}
var image = (function () {
var canvas = document.createElement("canvas"),
context = canvas.getContext("2d");
canvas.width = settings.particles.size;
canvas.height = settings.particles.size;
function to(t) {
var point = pointOnHeart(t);
point.x = settings.particles.size / 2 + (point.x * settings.particles.size) / 350;
point.y = settings.particles.size / 2 - (point.y * settings.particles.size) / 350;
return point;
}
context.beginPath();
var t = -Math.PI;
var point = to(t);
context.moveTo(point.x, point.y);
while (t < Math.PI) {
t += 0.01;
point = to(t);
context.lineTo(point.x, point.y);
}
context.closePath();
context.fillStyle = "rgba(255, 107, 139, 0.9)";
context.fill();
// 爱心添加高光效果
context.beginPath();
context.arc(settings.particles.size * 0.35, settings.particles.size * 0.35,
settings.particles.size * 0.15, 0, Math.PI * 2);
context.fillStyle = "rgba(255, 255, 255, 0.3)";
context.fill();
var image = new Image();
image.src = canvas.toDataURL();
return image;
})();
function render() {
requestAnimationFrame(render);
var newTime = new Date().getTime() / 1000,
deltaTime = newTime - (time || newTime);
time = newTime;
context.clearRect(0, 0, canvas.width, canvas.height);
var amount = particleRate * deltaTime;
for (var i = 0; i < amount; i++) {
var pos = pointOnHeart(Math.PI - 2 * Math.PI * Math.random());
var dir = pos.clone().length(settings.particles.velocity);
particles.add(
canvas.width / 2 + pos.x,
canvas.height / 2 - pos.y,
dir.x,
-dir.y
);
}
particles.update(deltaTime);
particles.draw(context, image);
}
function onResize() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
window.onresize = onResize;
// 粒子池类
function ParticlePool(length) {
var particles = new Array(length);
for (var i = 0; i < particles.length; i++)
particles[i] = new Particle();
var firstActive = 0,
firstFree = 0,
duration = settings.particles.duration;
this.add = function (x, y, dx, dy) {
particles[firstFree].initialize(x, y, dx, dy);
firstFree = (firstFree + 1) % particles.length;
if (firstActive === firstFree) firstActive = (firstActive + 1) % particles.length;
};
this.update = function (deltaTime) {
var i;
if (firstActive < firstFree) {
for (i = firstActive; i < firstFree; i++)
particles[i].update(deltaTime);
}
if (firstFree < firstActive) {
for (i = firstActive; i < particles.length; i++)
particles[i].update(deltaTime);
for (i = 0; i < firstFree; i++) particles[i].update(deltaTime);
}
while (particles[firstActive].age >= duration && firstActive !== firstFree) {
firstActive = (firstActive + 1) % particles.length;
}
};
this.draw = function (context, image) {
if (firstActive < firstFree) {
for (i = firstActive; i < firstFree; i++)
particles[i].draw(context, image);
}
if (firstFree < firstActive) {
for (i = firstActive; i < particles.length; i++)
particles[i].draw(context, image);
for (i = 0; i < firstFree; i++) particles[i].draw(context, image);
}
};
}
// 粒子类
function Particle() {
this.position = new Point();
this.velocity = new Point();
this.acceleration = new Point();
this.age = 0;
this.initialize = function (x, y, dx, dy) {
this.position.x = x;
this.position.y = y;
this.velocity.x = dx;
this.velocity.y = dy;
this.acceleration.x = dx * settings.particles.effect;
this.acceleration.y = dy * settings.particles.effect;
this.age = 0;
};
this.update = function (deltaTime) {
this.position.x += this.velocity.x * deltaTime;
this.position.y += this.velocity.y * deltaTime;
this.velocity.x += this.acceleration.x * deltaTime;
this.velocity.y += this.acceleration.y * deltaTime;
this.age += deltaTime;
};
this.draw = function (context, image) {
function ease(t) { return --t * t * t + 1; }
var size = image.width * ease(this.age / settings.particles.duration);
context.globalAlpha = 1 - this.age / settings.particles.duration;
context.drawImage(
image,
this.position.x - size / 2,
this.position.y - size / 2,
size,
size
);
};
}
// 点类
function Point(x, y) {
this.x = x || 0;
this.y = y || 0;
this.clone = function () { return new Point(this.x, this.y); };
this.length = function (length) {
if (length === undefined) return Math.sqrt(this.x * this.x + this.y * this.y);
this.normalize();
this.x *= length;
this.y *= length;
return this;
};
this.normalize = function () {
var length = this.length();
this.x /= length;
this.y /= length;
return this;
};
}
setTimeout(function () {
onResize();
render();
}, 10);
})(document.getElementById("pinkboard"));
// 点击屏幕出现小爱心
document.addEventListener('click', (e) => {
const heart = document.createElement('div');
heart.className = 'click-heart';
heart.innerHTML = '❤';
heart.style.left = `${e.clientX}px`;
heart.style.top = `${e.clientY}px`;
heart.style.fontSize = `${Math.random() * 20 + 16}px`;
heart.style.color = colors[Math.floor(Math.random() * colors.length)];
document.body.appendChild(heart);
setTimeout(() => {
heart.remove();
}, 1000);
});
// 初始化
window.addEventListener("resize", function () {
ww = window.innerWidth;
wh = window.innerHeight;
canvas.width = ww;
canvas.height = wh;
});
initHearts();
createPetals();
</script>
</body>
</html>
3839

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



