ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

满屏爱心弹窗表白网页制作教程:基于Canvas粒子动画与JavaScript交互

2026/9/4 20:49:16 拓冰建站 浏览量
满屏爱心弹窗表白网页制作教程:基于Canvas粒子动画与JavaScript交互 每次到了情人节、纪念日或者对方生日程序员总想用自己的方式表达一点特别的心意。比起直接买礼物亲手写一个满屏爱心弹窗的小网页反而更有仪式感。本文从一个可运行的完整 HTML 项目出发拆解爱心弹窗的实现原理、粒子动画逻辑、告白文案设计以及常见浏览器拦截问题帮助你把这份“代码级浪漫”稳稳落地。1. 爱心弹窗项目是什么项目标题里提到的“满屏爱心弹窗代码”本质上是一个跑在浏览器里的前端小页面它会完成三件事页面打开后屏幕中浮现飘动的爱心粒子。按顺序弹出多个告白弹窗每次标题和内容不同。最终跳出“答应我”或者“做我男/女朋友吧”这类互动按钮营造表白的仪式感。这类项目在 GitHub 和 CSDN 上长期都很热门常见叫法是“表白代码”“爱心弹窗”“告白网页”。它不依赖后端不依赖数据库只需要一个 HTML 文件就能跑起来非常适合零基础开发者直接复制学习。从技术角度看这个项目包含三个核心知识点技术点作用HTML 结构搭建页面骨架承载弹窗和画布CSS 样式控制爱心大小、颜色、动画、按钮样式JavaScript负责生成弹窗、绘制爱心粒子、处理交互逻辑读者在学完这个项目后不仅能给对象一个惊喜也能把 Canvas 粒子动画、DOM 创建、定时器管理这些前端基础知识点系统过一遍。2. 环境准备与版本说明这个项目对环境要求非常低几乎任何电脑都能运行。2.1 硬件与操作系统Windows 7/10/11、macOS、Linux 均可。屏幕分辨率建议 1366×768 以上高分辨率屏效果更好。不要求独立显卡粒子数量适中时集成显卡也能流畅运行。2.2 软件与工具你只需要准备一个文本编辑器和一款浏览器工具推荐选项用途代码编辑器VS Code、Notepad、Sublime Text、WebStorm编写并保存 HTML 文件浏览器Chrome、Edge、Firefox、Safari运行页面查看效果调试工具浏览器 F12 开发者工具排查 JS 报错本文示例使用 HTML5 CSS3 原生 JavaScript 编写不需要安装 Node.js不需要 npm也不需要任何第三方库。文件编码统一推荐使用 UTF-8防止中文文案出现乱码。2.3 准备一个项目目录为了方便管理建议在桌面或任意目录下新建一个文件夹mkdir love-popup-demo cd love-popup-demo然后在目录中新建index.html所有代码都写在这个文件里。如果后续要做动画资源分离可以继续拆分style.css和script.js但本文为了降低复制成本采用单文件模式。3. 核心原理拆解爱心弹窗项目虽然看起来视觉效果很丰富但核心原理并不复杂。下面按照模块来拆解。3.1 满屏爱心粒子的绘制原理目前主流的“满屏爱心”实现方式有两种第一种是使用 Canvas 绘制粒子爱心曲线。通过数学公式计算出爱心的形状坐标然后让粒子沿着这些坐标运动、漂浮、渐隐。这种方案动画流畅可控性强但是代码量稍多。第二种是使用 DOM 元素加 CSS 动画。把爱心图片或 CSS 绘制的小爱心随机插入到页面上再使用keyframes控制上升、旋转和透明度变化。这种方案代码直观适合初学者。下面是一个 Canvas 粒子爱心的最小示例核心是使用参数方程生成爱心轮廓点// 爱心参数方程 for (let t 0; t 2 * Math.PI; t 0.01) { let x 16 * Math.pow(Math.sin(t), 3); let y 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t); points.push({ x: x, y: -y }); }这段代码里的x和y计算出来的是一个标准爱心坐标宽度约 32 个单位高度约 30 个单位。实际使用时需要把坐标等比例放大并平移到画布中央。示例解释Math.pow(Math.sin(t), 3)负责生成爱心的左右两个尖角。13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)是经典的爱心曲线公式。-y是因为 Canvas 坐标系中y 轴正方向朝下翻转之后爱心才能正立。3.2 弹窗的实现逻辑弹窗的实现有两种方式alert()原生弹窗。自定义模态框。标题里说的“弹窗代码”很多初学者第一反应就是alert()。然而如果直接在页面加载时写一个for循环去调alert()会有一个严重问题浏览器会拦截连续弹窗并且页面会卡死。因此更推荐使用“自定义模态框 定时器队列”的方式。每间隔 1.5 秒或 2 秒弹出一个模态框模拟连续弹窗的效果同时不会触发浏览器拦截。自定义模态框的组件逻辑如下function createModal(title, message, delay) { setTimeout(() { // 创建遮罩层 let mask document.createElement(div); mask.className mask; // 创建弹窗主体 let box document.createElement(div); box.className modal-box; box.innerHTML h2 title /h2p message /pbutton继续/button; mask.appendChild(box); document.body.appendChild(mask); // 点击按钮关闭弹窗并移除遮罩 box.querySelector(button).onclick function () { document.body.removeChild(mask); }; }, delay); }这样做的好处是弹窗之间不互相阻塞。用户可以主动点击“继续”进入下一条。页面动画不会被alert()中断。3.3 背景音乐与交互彩蛋完整的表白页面通常还会加入背景音乐和按钮交互。背景音乐可以使用audio标签引入本地 MP3 文件比如《Beautiful In White》《爱的供养》等但需要注意版权风险。如果不想引入外部音频也可以直接用 Web Audio API 生成简单的提示音。按钮交互逻辑则更简单常见做法是一个“我愿意”按钮一个“再想想”按钮。点击“再想想”时“我愿意”按钮变大同时“再想想”按钮的文字变成“不忍心拒绝”。点击“我愿意”后全屏撒花或弹出最终提示。4. 完整实战案例偷偷做一个满屏爱心弹窗表白页接下来我们开始完整实现一个可直接运行的表白页面。所有代码编写在index.html中。4.1 创建项目结构love-popup-demo/ └── index.html4.2 完整代码实现下面给出完整代码。代码分三部分HTML 结构、CSS 样式、JavaScript 逻辑。直接复制到index.html即可运行。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title给你的一份特别代码/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { overflow: hidden; background: linear-gradient(135deg, #1d1c2b, #2b1f3d, #14121f); min-height: 100vh; position: relative; font-family: Microsoft YaHei, PingFang SC, sans-serif; } /* 全屏 Canvas 画布 */ #heartCanvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; } /* 页面中央的提示文字 */ .main-text { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; text-align: center; color: rgba(255, 180, 200, 0.9); pointer-events: none; user-select: none; } .main-text h1 { font-size: 28px; letter-spacing: 6px; text-shadow: 0 0 20px rgba(255, 80, 120, 0.7); } .main-text p { margin-top: 16px; font-size: 14px; color: rgba(255, 255, 255, 0.5); } /* 遮罩层 */ .mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.35); display: flex; align-items: center; justify-content: center; z-index: 999; } /* 弹窗主体 */ .modal-box { background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 30px 36px; max-width: 420px; width: 90%; box-shadow: 0 20px 60px rgba(255, 105, 135, 0.3); text-align: center; animation: popIn 0.3s ease-out; } .modal-box h2 { color: #e75480; font-size: 24px; margin-bottom: 12px; } .modal-box p { color: #555; font-size: 15px; line-height: 1.8; margin-bottom: 20px; } .modal-box button { background: linear-gradient(135deg, #ff7eb3, #ff5e8a); border: none; color: #fff; padding: 10px 32px; border-radius: 30px; font-size: 15px; cursor: pointer; transition: transform 0.2s; outline: none; } .modal-box button:hover { transform: scale(1.06); } /* 最终选择按钮组 */ .final-buttons { display: flex; gap: 20px; justify-content: center; margin-top: 10px; flex-wrap: wrap; } .final-buttons button { background: linear-gradient(135deg, #ff7eb3, #ff5e8a); border: none; color: #fff; padding: 10px 32px; border-radius: 30px; font-size: 15px; cursor: pointer; transition: all 0.3s; outline: none; } .final-buttons button:last-child { background: #aaa; } keyframes popIn { 0% { transform: scale(0.7); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } /* 浮动粒子样式用于辅助装饰 */ .float-heart { position: fixed; bottom: -60px; z-index: 3; pointer-events: none; animation: rise linear infinite; } keyframes rise { 0% { transform: translateY(0) rotate(0deg); opacity: 0; } 10% { opacity: 0.8; } 100% { transform: translateY(-110vh) rotate(360deg); opacity: 0; } } /style /head body canvas idheartCanvas/canvas div classmain-text h1❤ 你好呀我有话想对你说/h1 p请耐心看完每一个弹窗后面有惊喜/p /div script // 1. Canvas 爱心粒子动画 const canvas document.getElementById(heartCanvas); const ctx canvas.getContext(2d); let width, height; let points []; let particles []; function resize() { width canvas.width window.innerWidth; height canvas.height window.innerHeight; } window.addEventListener(resize, resize); resize(); // 生成爱心坐标点集 function createHeartPoints() { points []; const scale Math.min(width, height) / 20; const centerX width / 2; const centerY height / 2; for (let t 0; t 2 * Math.PI; t 0.03) { let x 16 * Math.pow(Math.sin(t), 3); let y 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t); // 坐标映射到屏幕 let screenX centerX x * scale; let screenY centerY - y * scale; points.push({ x: screenX, y: screenY }); } } // 粒子类 class Particle { constructor() { this.reset(); } reset() { this.baseIndex Math.floor(Math.random() * points.length); this.baseX points[this.baseIndex].x; this.baseY points[this.baseIndex].y; this.speed 0.5 Math.random() * 1.2; this.offsetX 0; this.offsetY 0; this.size 1 Math.random() * 2.5; this.hue 330 Math.random() * 30; // 粉色系 this.alpha 0.6 Math.random() * 0.4; this.angle Math.random() * Math.PI * 2; } update() { // 每一帧围绕目标点做轻微浮动 this.angle 0.02; this.offsetX Math.sin(this.angle) * 6; this.offsetY Math.cos(this.angle * 0.8) * 4; // 随时间缓慢沿爱心路径移动 this.baseIndex this.speed * 0.02; if (this.baseIndex points.length) { this.baseIndex 0; } this.baseX points[Math.floor(this.baseIndex)].x; this.baseY points[Math.floor(this.baseIndex)].y; } draw() { ctx.beginPath(); ctx.arc(this.baseX this.offsetX, this.baseY this.offsetY, this.size, 0, Math.PI * 2); ctx.fillStyle hsla( this.hue , 80%, 70%, this.alpha ); ctx.fill(); } } // 初始化粒子 function initParticles() { createHeartPoints(); particles []; const count Math.min(80, Math.floor(width / 15)); for (let i 0; i count; i) { particles.push(new Particle()); } } // 动画循环 function animate() { ctx.clearRect(0, 0, width, height); for (let p of particles) { p.update(); p.draw(); } requestAnimationFrame(animate); } initParticles(); animate(); // 2. 弹窗队列 function createModal(title, message, delay) { setTimeout(() { let mask document.createElement(div); mask.className mask; let box document.createElement(div); box.className modal-box; let h2 document.createElement(h2); h2.textContent title; let p document.createElement(p); p.textContent message; let btn document.createElement(button); btn.textContent 继续 →; box.appendChild(h2); box.appendChild(p); box.appendChild(btn); mask.appendChild(box); document.body.appendChild(mask); btn.onclick function () { document.body.removeChild(mask); }; }, delay); } // 依次弹出告白文案 createModal(第一句话, 从认识你的那天起我的代码世界就多了一个变量。, 800); createModal(第二句话, 这个变量没有默认值所有赋值都来自你。, 2600); createModal(第三句话, 以前我总在找 bug现在我总在想你。, 4500); createModal(第四句话, 所以我想申请成为你的默认对象。, 6400); // 3. 最终选择弹窗 setTimeout(() { let mask document.createElement(div); mask.className mask; let box document.createElement(div); box.className modal-box; let h2 document.createElement(h2); h2.textContent 最后的问题; let p document.createElement(p); p.textContent 你愿意做我的男/女朋友吗; let btnWrap document.createElement(div); btnWrap.className final-buttons; let yesBtn document.createElement(button); yesBtn.textContent 我愿意 ❤; let noBtn document.createElement(button); noBtn.textContent 再想想; btnWrap.appendChild(yesBtn); btnWrap.appendChild(noBtn); box.appendChild(h2); box.appendChild(p); box.appendChild(btnWrap); mask.appendChild(box); document.body.appendChild(mask); // 点击“我愿意” yesBtn.onclick function () { document.body.removeChild(mask); alert(那就说好啦 ❤ 余生请多指教); }; // 点击“再想想”时“我愿意”按钮变大 let clickCount 0; noBtn.onclick function () { clickCount; let fontSize 15 clickCount * 4; yesBtn.style.fontSize fontSize px; yesBtn.style.padding (10 clickCount * 2) px (32 clickCount * 4) px; noBtn.textContent 你真的忍心吗; if (clickCount 4) { noBtn.disabled true; noBtn.textContent 好吧你只能选左边了; noBtn.style.opacity 0.5; } }; }, 8400); // 4. 浮动爱心装饰 const symbols [❤, , ✨, ]; function addFloatHeart() { let span document.createElement(span); span.className float-heart; span.textContent symbols[Math.floor(Math.random() * symbols.length)]; span.style.left Math.random() * 100 vw; span.style.fontSize (14 Math.random() * 24) px; span.style.animationDuration (6 Math.random() * 6) s; document.body.appendChild(span); // 动画结束后移除节点 setTimeout(() { span.remove(); }, 12000); } // 每 800ms 生成一个浮动爱心 setInterval(addFloatHeart, 800); /script /body /html4.3 运行与验证在确认index.html文件保存为 UTF-8 编码后有两种方式运行方式一直接双击index.html使用系统默认浏览器打开。方式二在项目目录启动本地静态服务器。cd love-popup-demo python3 -m http.server 8080然后浏览器访问http://localhost:8080。运行后预期效果时间节点页面表现0 秒页面全屏渐变背景Canvas 中心出现由粒子组成的爱心0.8 秒弹出第一个弹窗“第一句话”2.6 秒弹出第二个弹窗“第二句话”4.5 秒弹出第三个弹窗“第三句话”6.4 秒弹出第四个弹窗“第四句话”8.4 秒弹出最终选择弹窗显示“我愿意”和“再想想”按钮持续运行页面底部不断有爱心和星星图案上浮4.4 关键代码说明上面代码虽然行数不多但有几个关键点需要单独说明。第一个要点是requestAnimationFrame。很多新手会使用setInterval来驱动动画但这样有两个问题一是定时器在页面切换到后台时会继续运行浪费资源二是定时器不随屏幕刷新率自适应。requestAnimationFrame会在浏览器准备重绘下一帧时自动执行回调是 Canvas 动画的推荐方案。第二个要点是粒子生成的数量。代码中限制了粒子数量const count Math.min(80, Math.floor(width / 15));这是为了避免在高分屏上生成过多粒子导致性能下降。如果你的电脑性能较好可以把 80 改大到 120让爱心看起来更密集。第三个要点是弹窗的时间线。这里把弹窗出现的时间写死为多个setTimeout优点是直观缺点是调整顺序时要反复修改时间数字。进阶做法是维护一个弹窗数据数组使用一个递归函数依次弹出。下面给出优化思路const popupQueue [ { title: 第一句话, message: 从认识你的那天起我的代码世界就多了一个变量。 }, { title: 第二句话, message: 这个变量没有默认值所有赋值都来自你。 }, { title: 第三句话, message: 以前我总在找 bug现在我总在想你。 }, { title: 第四句话, message: 所以我想申请成为你的默认对象。 } ]; function showQueue(index) { if (index popupQueue.length) { // 队列结束执行最终逻辑 showFinalQuestion(); return; } let item popupQueue[index]; let modal createModal(item.title, item.message, function () { showQueue(index 1); }); document.body.appendChild(modal); }这样做的好处是以后要加弹窗只需要往数组里加对象不需要重新算时间线。5. 进阶变形把爱心弹窗改成各种场景爱心弹窗代码的精髓在于“弹窗 粒子动画 交互”的组合这套模板换一下文案和装饰就能用到不同场景。5.1 生日祝福版把爱心粒子颜色改成金色和橙色粒子数量不变文案替换成生日祝福弹窗标题改为“第一条祝福”“第二条祝福”。推荐配色修改body { background: linear-gradient(135deg, #2b1d0f, #3d2b1f, #14121f); }粒子色相改为this.hue 30 Math.random() * 30; // 金色系5.2 周年纪念版在弹窗中加入纪念日倒计时或者加入“在一起第 XXX 天”的数字。这里可以在 HTML 中预留一个span在页面加载时用 JavaScript 自动计算天数。const startDate new Date(2022-02-14); const now new Date(); const diffDays Math.floor((now - startDate) / (1000 * 60 * 60 * 24)); document.getElementById(loveDay).textContent diffDays;5.3 好友整蛊版如果你想做一款“99 个弹窗关不掉”的整蛊页面请谨慎使用。这种做法不友好也容易被浏览器拦截。如果要保留趣味建议把弹窗次数限制在 5 次以内并且提供一键关闭按钮。6. 常见问题与排查思路下面整理这个项目最常见的 5 类问题按排查优先级排列。问题现象常见原因解决方案打开页面一片空白HTML 文件编码错误或 JS 报错按 F12 打开控制台查看红色报错信息爱心粒子不显示Canvas 高度未正确设置确认.heartCanvas样式和resize()函数执行了弹窗只弹出一个setTimeout被浏览器后台节流保持页面在前台或改用 requestAnimationFrame 调度中文乱码文件保存为 ANSI 编码另存为 UTF-8 编码页面卡顿粒子数量太多或浮动节点未清理降低粒子数量确保浮动爱心超出屏幕后移除节点6.1 如何快速定位 JS 报错打开页面后按F12切到Console控制台面板。如果代码有错误控制台会显示红色的错误信息并给出具体行号。常见错误示例Uncaught TypeError: Cannot read properties of null (reading getContext)这个错误表示document.getElementById(heartCanvas)没有找到元素。可能原因是script写在元素创建之前或者 canvas 的 id 不一致。6.2 为什么弹窗没有连续弹出如果你使用的是原生的alert()弹窗并且代码里写了一个for循环那么浏览器会认为这是恶意弹窗自动拦截。解决办法就是使用第 4 节的自定义模态框方案每次弹窗由setTimeout触发用户点击“继续”按钮关闭当前弹窗后下一个弹窗的计时器再自动弹出。6.3 如何避免浏览器误拦截现代浏览器对“页面加载后自动弹出的弹窗”非常敏感。如果弹窗出现得太密集即使自定义模态框也可能会被浏览器安全策略限制。规避建议弹窗数量控制在 5 ~ 8 个以内。弹窗间隔时间至少 1.5 秒。第一次弹窗延迟在 500ms 以上。建议让用户点击页面任意位置后再启动弹窗队列而不是加载后立刻弹窗。例如document.body.addEventListener(click, function startQueue() { document.body.removeEventListener(click, startQueue); showQueue(0); }, { once: true });这样用户点击页面后才会触发弹窗用户体验更自然也符合浏览器交互预期。7. 最佳实践与工程建议如果你想把这个爱心弹窗代码从“跑起来”变成“稳定、好看、易维护”下面的建议值得关注。7.1 代码组织与命名规范单文件模式适合分享和演示但如果要长期维护建议拆分成三个文件love-popup-demo/ ├── index.html ├── css/style.css └── js/script.js变量命名建议采用语义化命名。粒子相关变量用particle、points、ctx弹窗相关变量用modal、mask、popupQueue。不要使用a、b、data这种泛化命名。7.2 前端性能优化Canvas 粒子动画的性能主要受粒子数量和绘制次数影响。建议指标项建议值粒子数量60 ~ 120浮动装饰节点同时存在数量不超过 15弹窗停留时间2 ~ 4 秒代码中已经对浮动爱心做了移除处理防止 DOM 节点无限增长。每次添加新节点时可以顺手清理已经完成动画的节点。7.3 安全与隐私提醒给对象发链接时建议用一个临时部署地址而不是直接把本地文件发过去。注意以下几个方面不要把网页部署到不安全的第三方平台。网页内不要收集任何个人信息。不要将这段代码用于恶意整蛊、诈骗或骚扰。如果对方的浏览器有安全拦截提示建议提前把页面部署到 HTTPS 域名。7.4 增强可维护性使用配置对象代替散落的常量便于后期修改文案和样式。const CONFIG { particleCount: 80, particleHue: [330, 360], popupInterval: 1800, floatHeartSymbols: [❤, , ✨, ], popupQueue: [ { title: 第一句话, message: ... }, { title: 第二句话, message: ... } ] };这样修改文案只需要改配置文件不需要在逻辑代码里到处找字符串。7.5 移动端适配当前代码已经使用了window.innerWidth和window.innerHeight在手机上打开会自动适应屏幕。但需要注意两点手机上字体需要适当调整弹窗宽度建议设置为 90% 以内。手机端播放音乐需要用户主动点击页面后才能触发因此如果要加背景音乐可以在用户点击页面时调用audio.play()。8. 从表白代码到前端基础学习很多人把这份爱心弹窗代码当成一个“一次性表白工具”用完之后就丢在硬盘里。其实从学习角度来说这个项目是非常好的前端入门练手素材。通过这个项目可以延伸学习的内容包括Canvas 基础绘图arc、fill、beginPath。浏览器动画循环requestAnimationFrame的使用与节流。DOM 创建与移除createElement、appendChild、removeChild。事件处理onclick、addEventListener。CSS3 动画keyframes、transition、transform。响应式布局vw、vh、window.innerWidth。如果你想进一步深入可以试着给项目增加以下功能使用requestAnimationFrame实现弹窗队列调度不再依赖setTimeout。引入 TypeScript把粒子对象和弹窗对象类型化。使用 Vite 搭建项目把 CSS 和 JS 抽取为独立模块。增加 Web Audio API 生成简单的音符代替背景音乐文件。下次再遇到类似“给对象写代码”的需求直接用这套模板改文案就能快速交付。同时你也能在对方惊讶的眼神中淡淡地说一句“这只是 Canvas 粒子和定时器的基本用法而已。”