纯HTML/CSS/JS实现动态图驱动的表白页

发布时间:2026/9/14 5:12:46
纯HTML/CSS/JS实现动态图驱动的表白页 简介这是一份面向前端初学者与情感类网页开发者的HTML表白源码包主打怀旧氛围与动态交互体验适用于情人节、纪念日等场景的个性化网页制作。资源以HTML为核心融合CSS3动画与JavaScript事件控制实现图片轮播、文字渐显、背景切换等浪漫效果并支持音频嵌入与响应式适配便于在PC及移动端直接部署运行。压缩包为RAR格式大小6.87MB包含HTML主页面、配套CSS样式表、JS交互脚本及素材资源如动态图、背景音乐等文件结构简洁清晰利于快速理解DOM操作、过渡动画与用户点击响应等核心知识点。目前已有2760人学习下载开发者可直接部署预览演示地址http://www.lmtaolu.cn/biaobai/huiyi/亦可基于源码二次定制主题、替换图文内容或拓展交互逻辑是练手HTML/CSS/JS综合能力的优质轻量级项目范例。1. 用纯 HTML/CSS/JS 实现可复用的“回忆版表白页”不是模板搬运而是动态图驱动的情感表达你见过那种打开就自动播放老照片轮播、文字逐字浮现、背景音乐渐入、点击还能触发粒子特效的表白页吗它不依赖任何后端服务不调用外部 CDN连 jQuery 都没引入所有逻辑封装在单个 HTML 文件里——这就是“回忆版-表白源码html动态图”的真实形态。它不是静态截图拼接而是用 CSS 动画 JS 时间轴控制 SVG/Canvas 动态图生成技术把“回忆”这个抽象概念转化成可交互的视觉流比如用canvas绘制手绘风格的心跳曲线用keyframes实现胶片划痕过渡用requestAnimationFrame同步照片淡入与文字打字机效果。适合前端初学者照着改名字和照片路径也适合有经验的开发者替换动画引擎或接入本地相册 API。核心不在“多炫”而在“每帧都承载叙事逻辑”——第 3 张图出现时背景色才从灰蓝转为暖粉最后一句告白显示后粒子才从屏幕中心炸开。全文无框架、无构建工具、无网络请求双击即运行微信内可直接分享。2. 构建动态图底层用 Canvas 绘制可编程的心跳曲线与粒子系统2.1 为什么选 Canvas 而非 GIF 或 CSS 动画GIF 无法响应用户交互如暂停/重放且帧率固定难以匹配文字节奏纯 CSS 动画在复杂路径如心形轨迹上控制精度低且无法实时计算数据点。而 Canvas 提供像素级控制配合requestAnimationFrame可实现毫秒级同步——这正是“回忆版”中“心跳曲线随文字进度起伏”“粒子爆炸位置锚定在鼠标点击点”的技术基础。我们不预渲染动画序列而是每帧动态计算心跳幅度由当前展示的回忆条目数决定粒子数量由用户点击时长决定。2.1.1 初始化 Canvas 与心跳曲线绘制逻辑canvas idheartCanvas width400 height200/canvas// 获取上下文并设置初始状态 const canvas document.getElementById(heartCanvas); const ctx canvas.getContext(2d); let time 0; const heartPoints []; // 存储心跳路径点 // 心跳曲线生成函数基于正弦波变形 function generateHeartPath() { const points []; const scale 80; // 基础振幅 const offsetX canvas.width / 2; const offsetY canvas.height / 2; for (let t 0; t Math.PI * 2; t 0.05) { // 心形参数方程x16sin³t, y13cos t-5cos(2t)-2cos(3t)-cos(4t) const x offsetX scale * (16 * Math.pow(Math.sin(t), 3)); const y offsetY scale * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)); points.push({ x, y }); } return points; } // 每帧绘制平滑连接点并模拟“搏动”效果 function drawHeart() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); ctx.strokeStyle #ff6b6b; ctx.lineWidth 3; ctx.lineCap round; // 动态缩放time 控制搏动频率 const pulseScale 1 0.1 * Math.sin(time * 0.01); heartPoints.forEach((point, i) { const scaledX canvas.width / 2 (point.x - canvas.width / 2) * pulseScale; const scaledY canvas.height / 2 (point.y - canvas.height / 2) * pulseScale; if (i 0) { ctx.moveTo(scaledX, scaledY); } else { ctx.lineTo(scaledX, scaledY); } }); ctx.stroke(); time 1; }提示generateHeartPath()生成的是静态心形坐标点drawHeart()在每帧中动态缩放这些点形成“搏动”效果。time变量作为全局动画时钟确保所有动态元素心跳、粒子、文字节奏一致。若需降低 CPU 占用可在requestAnimationFrame回调中添加if (time % 2 0)跳过部分帧。2.2 粒子系统点击即生物理模拟衰减粒子不是随机飞散而是遵循简易物理模型初始速度向量由点击位置与画布中心决定加速度模拟空气阻力透明度随生命周期线性衰减。2.2.1 粒子类定义与批量管理class Particle { constructor(x, y) { this.x x; this.y y; this.size Math.random() * 4 2; // 2-6px this.speedX (Math.random() - 0.5) * 8; // -4~4 this.speedY (Math.random() - 0.5) * 8; this.life 1; // 0~1 this.decay 0.015 Math.random() * 0.005; // 衰减速率 } update() { this.x this.speedX; this.y this.speedY; this.speedX * 0.97; // 空气阻力 this.speedY * 0.97; this.life - this.decay; } draw(ctx) { ctx.globalAlpha this.life; ctx.fillStyle hsl(${Math.random() * 60 340}, 100%, 60%); // 粉紫渐变色 ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); } } // 管理粒子池避免频繁 new/delete const particles []; const maxParticles 150; function createParticles(x, y) { const centerX canvas.width / 2; const centerY canvas.height / 2; const angle Math.atan2(y - centerY, x - centerX); for (let i 0; i 30; i) { const p new Particle(x, y); // 初始速度指向中心反方向增强“炸开”感 p.speedX Math.cos(angle (Math.random() - 0.5) * 0.5) * 6; p.speedY Math.sin(angle (Math.random() - 0.5) * 0.5) * 6; particles.push(p); } } // 动画主循环 function animate() { drawHeart(); // 先绘制心跳 // 更新并绘制粒子 for (let i particles.length - 1; i 0; i--) { particles[i].update(); if (particles[i].life 0) { particles.splice(i, 1); } } ctx.globalAlpha 1; particles.forEach(p p.draw(ctx)); requestAnimationFrame(animate); }注意粒子数量限制在maxParticles是关键优化点。未做此限制时连续点击会导致内存泄漏和帧率骤降。splice(i, 1)从末尾向前遍历删除避免索引错位。颜色使用hsl()而非rgb()便于通过色相值340° 为品红快速调整主题色。3. 驱动回忆叙事用 CSS 动画时间轴控制图文出场节奏3.1 构建可配置的回忆事件表JSON 驱动所有回忆条目照片、文字、触发时间不再硬编码在 HTML 中而是集中管理于一个 JavaScript 对象。这样修改内容无需触碰 HTML 结构且便于后续扩展为本地 JSON 文件读取。const memories [ { id: m1, img: photos/first_meet.jpg, text: 第一次见面你笑着递来一杯奶茶, delay: 0, // 秒级延迟相对于页面加载 duration: 4000 // 显示时长ms }, { id: m2, img: photos/rainy_day.jpg, text: 下雨天共撑一把伞伞倾向我这边, delay: 5000, duration: 4000 }, { id: m3, img: photos/night_walk.jpg, text: 深夜散步路灯把影子拉得很长很长, delay: 10000, duration: 4000 } ];3.1.1 动态注入 DOM 并绑定 CSS 动画类function renderMemories() { const container document.getElementById(memories-container); memories.forEach((item, index) { const div document.createElement(div); div.className memory-item; div.id item.id; // 图片区域设置懒加载占位 const imgDiv document.createElement(div); imgDiv.className memory-img; imgDiv.innerHTML img src${item.img} alt回忆 ${index 1} loadinglazy; // 文字区域逐字打字效果 const textDiv document.createElement(div); textDiv.className memory-text; textDiv.textContent item.text; div.appendChild(imgDiv); div.appendChild(textDiv); container.appendChild(div); // 关键为每个条目添加带延迟的 CSS 动画类 setTimeout(() { div.classList.add(fade-in-up); // 4秒后自动移除避免影响下一条目 setTimeout(() div.classList.remove(fade-in-up), item.duration); }, item.delay); }); }3.2 CSS 动画时间轴精确控制入场/退场时机/* 定义基础动画 */ keyframes fadeInUp { from { opacity: 0; transform: translateY(30px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } /* 应用到回忆条目 */ .memory-item { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; pointer-events: none; /* 防止遮挡下方按钮 */ opacity: 0; } .memory-item.fade-in-up { animation: fadeInUp 0.6s ease-out forwards; pointer-events: auto; } /* 图片样式圆角阴影 */ .memory-img img { width: 80vw; max-width: 500px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); margin-bottom: 20px; } /* 打字机效果文字 */ .memory-text { font-size: 1.4rem; font-weight: 500; color: #333; text-align: center; max-width: 80%; line-height: 1.6; /* 使用伪元素实现打字效果 */ position: relative; overflow: hidden; white-space: nowrap; border-right: 3px solid #ff6b6b; animation: typing 3s steps(40, end), blink-caret 0.75s step-end infinite; } keyframes typing { from { width: 0 } to { width: 100% } } keyframes blink-caret { from, to { border-color: transparent } 50% { border-color: #ff6b6b } }提示pointer-events: none是关键设计。回忆条目层叠在页面上但默认不拦截鼠标事件确保底部的“点击生成粒子”按钮仍可响应。仅当.fade-in-up类激活时pointer-events: auto才生效此时用户才能点击该条目触发特效。steps(40, end)中的40对应文字字符数需根据实际文本长度调整否则打字效果会卡顿。4. 优化移动端体验与微信兼容性响应式布局与 touch 事件适配4.1 移动端优先的 CSS 布局重构微信内置浏览器对vh单位支持不稳定且 iOS Safari 存在 viewport 缩放问题。我们放弃100vh改用min-height: 100%flex布局并针对小屏设备调整字体与间距。/* 移动端基础重置 */ media (max-width: 768px) { body { margin: 0; padding: 0; font-size: 16px; /* 防止 iOS 自动放大 */ } .container { min-height: 100vh; display: flex; flex-direction: column; } .header { padding: 15px 20px; } .header h1 { font-size: 1.8rem; margin: 0; } .memory-img img { width: 90vw; max-width: 400px; } .memory-text { font-size: 1.2rem; padding: 0 20px; } /* 修复 iOS Safari 滚动条闪烁 */ * { -webkit-overflow-scrolling: touch; } }4.1.1 Touch 事件替代 click解决微信点击延迟// 替换所有 click 事件监听器为 touchstart document.addEventListener(touchstart, function(e) { if (e.target.closest(.trigger-particle)) { e.preventDefault(); // 阻止默认滚动行为 const rect e.target.getBoundingClientRect(); const x rect.left rect.width / 2; const y rect.top rect.height / 2; createParticles(x, y); } }, { passive: false }); // 兼容 PC 端保留 click 事件 document.addEventListener(click, function(e) { if (e.target.closest(.trigger-particle)) { const rect e.target.getBoundingClientRect(); const x rect.left rect.width / 2; const y rect.top rect.height / 2; createParticles(x, y); } });注意{ passive: false }是关键。iOS Safari 默认将touchstart设为 passive禁止在其中调用preventDefault()。此处需显式声明非 passive才能阻止点击后的 300ms 延迟。但必须配合e.preventDefault()使用否则页面可能无法滚动。4.2 微信环境检测与字体 fallback微信内置浏览器不支持某些现代字体特性且font-display: swap行为异常。我们采用保守策略优先使用系统字体栈仅在支持时加载自定义字体。!-- 在 head 中 -- link relstylesheet hrefhttps://fonts.googleapis.com/css2?familyNotoSerifSC:wght400;700displayblock/* 字体回退链 */ body { font-family: Noto Serif SC, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif; } /* 强制字体加载完成后再显示内容避免 FOIT */ body::before { content: ; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #fff; z-index: 9999; transition: opacity 0.3s; } font-face { font-family: Noto Serif SC; src: url(https://fonts.gstatic.com/s/...) format(woff2); font-display: block; /* 确保加载完成前不渲染 */ } /* 加载完成后移除遮罩 */ body.font-loaded::before { opacity: 0; pointer-events: none; } /* JS 检测字体加载 */ document.fonts.load(1em Noto Serif SC).then(() { document.body.classList.add(font-loaded); }).catch(() { // 加载失败降级处理 document.body.style.fontFamily PingFang SC, Microsoft YaHei, sans-serif; });5. 进阶技巧离线缓存照片与本地存储回忆进度5.1 使用 Cache API 预缓存回忆图片Service Worker即使用户首次访问也能保证照片秒开。我们不依赖 manifest.json而是用现代 Cache API 动态缓存。// 在主 JS 中注册 Service Worker if (serviceWorker in navigator) { window.addEventListener(load, () { navigator.serviceWorker.register(/sw.js) .then(reg console.log(SW registered:, reg)) .catch(err console.log(SW registration failed:, err)); }); }// sw.js const CACHE_NAME confession-v1; const urlsToCache [ /, /index.html, /style.css, /script.js ]; // 预缓存回忆图片从 memories 数组提取 self.addEventListener(install, event { event.waitUntil( caches.open(CACHE_NAME) .then(cache { console.log(Opened cache); return cache.addAll(urlsToCache); }) ); }); // 拦截图片请求并返回缓存 self.addEventListener(fetch, event { if (event.request.destination image) { event.respondWith( caches.match(event.request) .then(response { return response || fetch(event.request); }) ); } });提示urlsToCache数组需手动维护但回忆图片路径可从memories数组动态生成。实际部署时建议在构建阶段生成sw.js将图片 URL 注入其中避免运行时读取全局变量带来的不确定性。5.2 用 localStorage 记录用户观看进度用户关闭页面后重新打开应从上次中断处继续播放而非从头开始。我们记录当前播放的回忆 ID 和已播放时长。// 播放控制器 let currentMemoryIndex 0; let playbackStartTime Date.now(); function startPlayback() { const now Date.now(); const elapsed now - playbackStartTime; // 从 localStorage 恢复进度 const saved localStorage.getItem(confession-progress); if (saved) { const progress JSON.parse(saved); currentMemoryIndex progress.index; playbackStartTime now - progress.elapsed; } // 启动定时器检查是否该切换条目 setInterval(() { const totalElapsed Date.now() - playbackStartTime; const current memories[currentMemoryIndex]; if (totalElapsed current.delay current.duration) { // 切换到下一条目 currentMemoryIndex (currentMemoryIndex 1) % memories.length; localStorage.setItem(confession-progress, JSON.stringify({ index: currentMemoryIndex, elapsed: totalElapsed - current.delay - current.duration })); } }, 100); // 100ms 检查一次平衡精度与性能 }5.2.1 清除进度的实用按钮防误操作button idreset-progress classreset-btn重置播放进度/buttondocument.getElementById(reset-progress).addEventListener(click, () { localStorage.removeItem(confession-progress); alert(播放进度已重置将从第一条回忆开始); location.reload(); });注意setInterval的检查间隔设为100ms而非1ms这是性能权衡。人眼无法分辨 100ms 级别的延迟但能显著降低 CPU 占用。localStorage存储的是相对时间差elapsed而非绝对时间戳避免时区或系统时间变更导致的错乱。本文还有配套的精品资源点击获取