​HTML+CSS+JS 动态浪漫爱心表白网页设计(附完整源码)​

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

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

运行效果(是动态的,这里是截图效果):

一、网页效果预览

  • 视觉风格:深紫色渐变背景搭配粉色系爱心元素,浪漫氛围拉满,视觉层次丰富。
  • 动态特效
    1. 中心粒子爱心:循环缩放,带有高光质感,粒子扩散效果自然;
    2. 漂浮爱心图标:从屏幕顶部下落,伴随旋转,色彩柔和渐变;
    3. 玫瑰花瓣飘落:随机大小、随机轨迹,模拟真实花瓣飘落;
    4. 点击互动:点击屏幕任意位置,弹出随机颜色小爱心并向上飞散。
  • 适配性:响应式设计,支持电脑、手机等不同尺寸设备,文字大小自动适配。

二、技术栈与核心知识点

  • 技术栈:HTML5(结构搭建)+ CSS3(样式美化 + 动画)+ JavaScript(动态交互)
  • 核心知识点
    1. Canvas 绘图:实现粒子爱心和漂浮爱心效果;
    2. CSS 高级特性:渐变背景、文字阴影、自定义鼠标指针、关键帧动画;
    3. JavaScript 面向对象:封装粒子、爱心等对象,批量控制动态元素;
    4. 事件监听:窗口大小变化适配、鼠标点击互动;
    5. 动态 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 核心逻辑说明:
  1. 爱心路径生成:通过数学公式 pointOnHeart(t) 计算爱心轮廓上的点,t 从 -π 到 π 循环,生成标准爱心形状。
  2. 粒子池优化:创建固定数量的粒子对象并复用,避免频繁创建 / 删除 DOM 元素,提升页面性能(尤其粒子数量多时)。
  3. 粒子生命周期管理:粒子从爱心路径上发射,随时间推移逐渐缩小、变暗,生命周期结束后被复用。
  4. 缓动效果:粒子大小和透明度变化采用缓动函数,避免生硬的线性变化,让动画更自然。

模块 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>

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值