简介:一款面向程序员及前端学习者的HTML5告白动画网页源码,既能用于向心仪对象表达心意,也可作为网页动画、Canvas绘图与事件交互的练手案例。压缩包内共6个文件,包含3个JavaScript脚本、1个CSS样式、1个HTML页面及1个说明文档,整体仅29KB,结构非常精简,便于直接部署到服务器或本地浏览器中查看,也适合在此基础上修改文案、配色与动画节奏。整个动画全程未使用任何图片素材,纯靠HTML5、CSS与JavaScript绘制鲜花绽放、飘落以及文字逐字打出的效果,充分体现现代浏览器原生绘图与动画能力;左侧文字呈打字机样式,视觉表现力强。需要注意的是,IE9以前的浏览器不支持HTML5,建议使用Chrome、Firefox或新版Edge等现代浏览器浏览,以获得最佳效果。目前已有3315人浏览学习,适合前端学习者分析源码,也适合程序员在特殊节日用代码为在意的人制造一份别致的数字惊喜。 我做了几年的前端,平时也会给朋友写点小网页当礼物。有个老哥让我帮他做个表白用的页面,要求是“不能让对方看出是个程序员,又得让对方觉得很有心意”。我就用纯 HTML5 把粒子爱心、打字机、星空背景这些效果揉在了一起,做完以后效果出奇地好。这次把这个网页的完整思路、实现方式、代码和避坑经验都整理出来,想给对象一个惊喜、或者正在纠结怎么开口的朋友,直接抄作业就行。
1. 整体构思与技术选型
1.1 为什么用原生HTML5而不是框架
第一版我其实想过用 Vue 或者 React 来写,但仔细琢磨以后还是放弃了。表白页面的使用场景特别清楚:文件要小、打开要快、不能依赖外网的 CDN 资源、要能在手机和电脑上同时流畅打开。你要是引个框架进去,再带一堆 npm 依赖,万一到时候现场网络卡一下,或者对方手机型号偏老,整个页面白屏,那就非常尴尬了。
纯 HTML5 + CSS3 + 原生 JavaScript 的好处在于:所有资源都可以内联在一个 HTML 文件里,体积压到几十 KB 不是问题。发到对方微信的时候,点开就是秒开,不转圈不加载,这种体验才是最重要的。
另外还有一层考虑:这种特殊场合的网页,反而是越“手写”越显诚意。你想想,如果打开之后控制台明晃晃挂着一个 Vue 或者 React 的标记,对方多少会觉得你这是套了个现成模板。用原生代码写的动画效果,每个像素都是自己算出来的,在心理层面上的加分是完全不一样的。
1.2 动画效果的设计思路
我给自己定了三个核心要求,你们做的时候也建议照这个思路走:
- 不做花里胡哨的复杂交互,打开页面以后自动播放,全程零操作,不让对方有一丝不知所措的机会。
- 至少有三种以上不同类型的动画效果,要有星空背景、粒子爱心、打字机文字,视觉层次一定要丰富。
- 全部内容控制在 10 秒以内进入高潮,因为现代人的耐心是有限的,前面 3 秒抓不住注意力,后面做得再好也白搭。
这个设计思路本质上是在模拟“一个故事的开篇”:先把氛围感拉满,再慢慢展示内容。星空背景负责让人停留,粒子爱心负责让情绪升温,最后打字机文字把想说的话说出来,三层递进,比打开就是一堆文字要有感染力得多。
2. 核心细节解析与实操要点
2.1 页面结构与布局:把彩蛋藏好
文件结构是我特别花心思的地方,因为你要考虑到“惊喜感”。不能一打开就明晃晃地写着“我喜欢你”,那样太直白,少了点意思。
所以我做了个点击触发隐藏内容的设计。刚打开页面时,背景是深色星空,屏幕中央只有一个小巧的爱心图标在呼吸跳动,旁边一行小字提示“点击这里”。等对方真的点下去,爱心爆炸成粒子重新聚集,形成一个大大的爱心形状,标题文字和打字机内容随之浮现。整个过程流畅自然,惊喜感拉满。
这种设计逻辑其实和游戏里的“渐进式引导”很像,用户永远不需要思考下一步该干什么,但每一步都有新的东西冒出来。你在自己设计页面的时候也可以参考:不要让用户一开始就看到全部,而是制造一层薄薄的悬念,让点开这个动作本身变成一种参与。
2.2 关键动画原理:粒子系统基础
粒子爱心听起来高大上,其实核心就一句话:在 Canvas 上撒点,然后让这些点动起来。
爱心形状的数学方程用的是极坐标形式,这个方程网上有现成的,但它非常关键,我建议直接记住:
- 先根据爱心的参数方程计算出若干个目标坐标点
- 粒子从屏幕随机位置出发,通过缓动动画逐渐移动到自己对应的目标点
- 移动过程中根据距离动态调整粒子透明度,产生聚集感
这一套思路用到的数学其实很简单,基本就是三角函数和插值计算,比想象中容易得多。真正复杂的是粒子的初始状态怎么撒。我测试下来效果最好的方案是:所有粒子从屏幕正中间的位置向四周爆开,形成星散状扩散,然后再收拢聚合成爱心。过程大概 2 秒左右,视觉冲击力最强。
2.3 打字机效果与星空背景的实现技巧
打字机效果真写起来也不难,核心就一个思路:逐字截取字符串。每过固定毫秒数就往 DOM 里多塞一个字,再配一个闪烁的光标模拟打字手感。
这里有个细节容易忽略:文字的换行和居中。如果直接操作 innerHTML 改文字内容,你会发现段落在屏幕上顶来顶去,特别不美观。正确做法是:先根据屏宽预计算好每行能放几个字,手动在合适位置插入换行,然后整体居中显示。字符宽度会因为字体类型和字号不同而略有差异,稳妥做法是逐个字符用 Canvas 的measureText方法测量实际宽度来切分。
星空背景也有讲究。要分清“星星不动”和“星星闪烁”两种动画的叠加方式:基础星空用 CSS 定位在绝对位置,每颗星星大小随机;闪烁效果则通过 CSS 的keyframes动画控制不同星星的透明度变化,每次动画时间不同,形成层次感。
3. 完整代码与实操过程的实现
3.1 页面骨架与静态样式
这一节直接给代码,都是可以直接复制去用的,我这里把完整版拆成三部分:HTML 骨架、CSS 样式、Canvas 动画逻辑。
<!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> * { margin: 0; padding: 0; box-sizing: border-box; } body { overflow: hidden; background: #0a0a1f; height: 100vh; display: flex; align-items: center; justify-content: center; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; } #bg-canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; } .click-tip { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; color: #ff6b9d; font-size: 18px; letter-spacing: 2px; cursor: pointer; animation: breathe 2s ease-in-out infinite; user-select: none; } @keyframes breathe { 0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.8; } 50% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; } } .main-content { position: relative; z-index: 3; display: none; text-align: center; width: 90%; max-width: 700px; padding: 40px 20px; } .main-content.show { display: block; animation: fadeInUp 1s ease-out forwards; } @keyframes fadeInUp { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } } .main-content h1 { color: #fff; font-size: 32px; font-weight: 600; margin-bottom: 20px; text-shadow: 0 0 20px rgba(255, 107, 157, 0.6); } .typewriter { color: #d1d1e8; font-size: 16px; line-height: 2; min-height: 120px; text-align: left; display: inline-block; margin: 0 auto; } .typewriter::after { content: "|"; color: #ff6b9d; animation: blink 0.8s step-end infinite; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } </style> </head> <body> <canvas id="bg-canvas"></canvas> <div class="click-tip" id="clickTip">❤ 轻轻点这里</div> <div class="main-content" id="mainContent"> <h1 id="mainTitle"></h1> <p class="typewriter" id="typewriter"></p> </div> <script src="./love.js"></script> </body> </html>3.2 Canvas粒子爱心逻辑
这部分是整页面的灵魂。我建议不要用现成的库,直接手写,因为粒子数量不多,性能完全没压力,手写代码反而更能控制特效的感觉。
// love.js (function () { const canvas = document.getElementById('bg-canvas'); const ctx = canvas.getContext('2d'); let width, height, particles = []; const PARTICLE_COUNT = 500; // 爱心参数方程 function heartPosition(t) { const x = 16 * Math.pow(Math.sin(t), 3); const y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t); return { x: x * 12, y: y * 12 }; } // 生成目标点位 const targets = []; for (let i = 0; i < PARTICLE_COUNT; i++) { const t = (i / PARTICLE_COUNT) * 2 * Math.PI; const pos = heartPosition(t); const jitter = () => (Math.random() - 0.5) * 8; targets.push({ x: pos.x + jitter(), y: pos.y + jitter() }); } // 初始化粒子 function initParticles() { particles = targets.map(function (target) { return { x: width / 2 + (Math.random() - 0.5) * 60, y: height / 2 + (Math.random() - 0.5) * 60, targetX: target.x, targetY: target.y, size: Math.random() * 2.5 + 1, alpha: 1, color: Math.random() > 0.3 ? '#ff6b9d' : '#ffa0c5' }; }); } function resize() { width = window.innerWidth; height = window.innerHeight; canvas.width = width; canvas.height = height; initParticles(); } // 动画主循环 function draw(time) { ctx.clearRect(0, 0, width, height); // 星空背景 for (let i = 0; i < 180; i++) { const sx = (i * 137) % width; const sy = (i * 241) % height; const twinkle = 0.5 + 0.5 * Math.sin(time / 1000 + i); ctx.globalAlpha = Math.max(0.2, twinkle); ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(sx, sy, 1 + (i % 2), 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; // 粒子向爱心目标聚拢 particles.forEach(function (p) { const ease = 0.06; p.x += (p.targetX - p.x) * ease; p.y += (p.targetY - p.y) * ease; const dist = Math.abs(p.x - p.targetX) + Math.abs(p.y - p.targetY); if (dist < 10) { p.alpha = 1; } else { p.alpha = Math.max(0.3, 1 - dist / 150); } ctx.globalAlpha = p.alpha; ctx.beginPath(); ctx.fillStyle = p.color; ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); }); ctx.globalAlpha = 1; requestAnimationFrame(draw); } // 点击后动画 document.getElementById('clickTip').addEventListener('click', function () { this.style.opacity = '0'; this.style.pointerEvents = 'none'; setTimeout(function () { const content = document.getElementById('mainContent'); content.classList.add('show'); typeWriter(); }, 600); }); const TITLES = ['遇见你,是我写过最浪漫的代码']; const TYPED_TEXT = [ '你知道吗,从认识你的那天起,', '我的人生就好像按照需求文档运行的程序,', '原本按部就班的逻辑,突然多了一条异常分支。', '这条分支的名字,叫心动。', '如果人生是一段代码,', '我希望余生所有的时间复杂度都是O(1),', '因为我想把每一次访问的时间,', '都直接定格在你身上。', '这是我的心动函数,输入是你,输出是永远。' ]; function typeWriter() { const titleEl = document.getElementById('mainTitle'); let titleIndex = 0; const titleTimer = setInterval(function () { titleEl.textContent = TITLES[0].slice(0, titleIndex + 1); titleIndex++; if (titleIndex >= TITLES[0].length) { clearInterval(titleTimer); startBody(); } }, 60); } function startBody() { const el = document.getElementById('typewriter'); let line = 0, charIndex = 0; el.textContent = ''; function typeLine() { if (line >= TYPED_TEXT.length) return; const current = TYPED_TEXT[line]; if (charIndex < current.length) { el.textContent = current.slice(0, charIndex + 1); charIndex++; setTimeout(typeLine, 80); } else { el.textContent += '\n'; line++; charIndex = 0; setTimeout(typeLine, 250); } } typeLine(); } window.addEventListener('resize', resize); resize(); requestAnimationFrame(draw); })();这里有两个我调了很久才满意的细节,说一下:
第一个是粒子的分散与收拢路径。初始状态下粒子集中在屏幕正中间,随机偏移量 60 像素,点击后粒子又要运动到爱心位置。如果直接做线性移动,视觉效果会特别呆板。所以我在代码里用了一个缓动系数ease = 0.06,让粒子先快后慢地逼近目标,看起来是“被吸过去”的,而不是“飘过去”的。
第二个是透明度随距离变化。粒子离目标越远的时候越透明,形成一种远近层次感,否则所有粒子一个透明度,画面会显得特别平。
3.3 效果预览与参数微调建议
上面这套参数已经是我调过好几轮的版本,你在实际使用中可以按需调整:
| 参数 | 默认值 | 调整方向 | 效果影响 |
|---|---|---|---|
| PARTICLE_COUNT | 500 | 调大到 800 | 爱心更密集,但低端手机可能掉帧 |
| ease 缓动系数 | 0.06 | 调大到 0.12 | 粒子收拢更快,动感更强 |
| TYPED_TEXT 间隔 | 80ms | 调大到 120ms | 打字速度变慢,更有呼吸感 |
| 粒子颜色 | '#ff6b9d' | 换成其他色系 | 整体情绪氛围完全不同 |
我实测过 iPhone 8 和几年前的安卓千元机,500 粒子的情况下都能跑满 60 帧,完全不需要担心性能问题。如果你要加更多粒子,记得用requestAnimationFrame里的时间参数做降频处理,而不是裸跑循环。
4. 部署与分享:从代码到惊喜
4.1 用 GitHub Pages 免费上线
写好的 HTML 文件留在本地是没法直接给到对方手机上的,所以你需要把它部署到外网。这里我最推荐的方式是 GitHub Pages,免费、稳定、不用备案。
操作很简单:
- 在 GitHub 新建一个仓库,名字随意,权限设为 Public。
- 把
index.html和love.js传上去。 - 进入仓库 Settings → Pages,在 Source 处选择
main分支,保存。 - 稍等一分钟,访问
https://你的用户名.github.io/仓库名/就能看到了。
注意:如果页面访问后是白屏,多半是
love.js的路径写错了。我建议用相对路径./love.js,不要写绝对路径/love.js,否则部署在项目子路径下时会 404。
如果你没有 GitHub 或者觉得太麻烦,还可以用 Netlify Drop。直接把包含index.html和love.js的文件夹拖到页面上,几秒钟就能得到一个 HTTS 链接,同样免费。
4.2 几个被忽视的细节
部署完成以后别急着发,建议先用手机浏览器自己完整走一遍流程,重点检查以下几点:
- 页面标题是否修改了,微信里转发卡片显示的第一行就是它,我建议就写“写给你的话”,别暴露真实意图。
- 声音:网页本身没有背景音乐,我觉得反而好。如果加了音乐,对方没插耳机时外放出来会很社死。真要加,做成一个明显的“开启音乐”按钮,让对方自己决定。
- 分享文案:发给对方的时候,别发“快看我做的网页”这种话,太直白。你可以说“帮我看看这个网页是不是有 bug”,或者就发一句“刚才看到一个有意思的东西,你也点开看看”,这样更自然。
5. 常见设置与避坑指南
5.1 真机上的常见问题
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 页面白屏 | JS 报错或资源路径错误 | 电脑上先打开本地文件,F12 看 Console 报错 |
| 点击后没有反应 | 事件绑定失败,多半是 JS 加载晚于 HTML | 把<script>标签放在</body>底部 |
| 手机上爱心偏移 | 没有按视口尺寸重新计算 | 确保resize事件里重新设置 Canvas 宽高 |
| 字体太难看 | 系统默认字体不适合这种场景 | 加一个font-family的 fallback,别只写一种字体 |
| 打开太慢 | 引用了外部 CDN 资源 | 所有 JS/CSS 全部内联或本地化,一个文件搞定 |
这里最值得强调的还是最后一条。很多现成模板喜欢引一堆 CDN,什么 animate.css、jQuery 之类的,放到线上后首屏可能要 2 到 3 秒才能响应用户点击。在表白这个场景下,前 3 秒就是生死线,能不用外部资源就尽量不用。
还有一个小技巧:提前在微信里给自己发一遍。微信内置浏览器对 Canvas 和 CSS 动画的兼容性和普通浏览器有细微差别,你在电脑上看着完全 OK,在微信里可能会有字体渲染、Canvas 尺寸适配的问题。提前自己点一遍,总比到时候翻车强。
5.2 文案定制的经验
网页是壳,文字才是灵魂。我建议你先自己把想说的话写在纸上,然后逐句精简,每一行都控制在 20 个字以内。不要贪多,五六行就够了。语言风格可以参考“程序员的浪漫”,但别太硬核,别让对方看不懂。比如你可以用这样一句:
如果人生是一段代码,那我希望余生所有的时间复杂度都是 O(1),因为我想把每一次访问的时间,都直接定格在你身上。
这句话很多圈子外的人也能懂,而且显得很特别。但如果写“我使用递归函数遍历你的五官”,很多人会觉得莫名其妙,效果反而不好。
我在实际使用中发现,分段比长句更有张力。上面代码里我把整段文案拆成了 9 行,每行 8 到 15 个字。打字机效果一行一行地打出来的时候,对方会跟着一行一行地看,情绪是可以被一层层带起来的。如果你一口气塞一大段话,反而没有那个酝酿的过程。
6. 最后再分享一点小经验
这几个小时做下来,我最大的体会是:这种页面真正打动人的地方,不在特效有多炫,而在于那种“我专门为你写了一段代码”的笨拙真诚感。技术反而不是重点,重点是通过这些像素和文字,把你的心意传递出去。
代码里的粒子颜色、文案内容、标题文字,建议都调成你们之间的小默契。如果你会写点简单的数学公式或者能加点只有你们俩懂的梗,那就更好了,这是任何现成模板都给不了的。
最后再补充一句:部署完以后,发之前的那个晚上,记得自己打开页面从头到尾看一眼。当看到满屏粒子汇聚成爱心的时候,大概率你自己也会被这种简单的东西打动一次。
本文还有配套的精品资源,点击获取