1. 项目背景与核心思路
那天晚上和女友视频时,她半开玩笑地说:"你们程序员整天对着电脑敲代码,一点都不浪漫。"这句话像一记重拳打在我这个前端工程师的自尊心上。挂断电话后,我盯着桌面上闪烁的代码编辑器,突然萌生了一个想法——为什么不能用代码创造浪漫?
星空告白墙的核心设计思路很简单:用JavaScript在网页上模拟星空效果,然后让用户可以在"星空"中留下告白文字。但要让这个创意真正打动人心,需要解决三个技术难点:
- 如何高效渲染大量星星粒子并实现流畅动画
- 如何实现用户交互式的文字输入与展示
- 如何让整体效果既有科技感又不失温馨浪漫
2. 技术选型与实现方案
2.1 星空背景的实现
最初考虑过三种方案:
- 纯CSS实现:用box-shadow生成星星,但性能差且动态效果有限
- Canvas 2D渲染:性能较好但缺乏三维感
- WebGL/Three.js:效果最好但学习成本高
最终选择了Canvas 2D方案,在效果和实现难度间取得了平衡。核心代码如下:
class Star { constructor(canvas) { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.size = Math.random() * 3; this.speed = Math.random() * 0.2; } update() { this.y += this.speed; if (this.y > canvas.height) { this.y = 0; this.x = Math.random() * canvas.width; } } draw(ctx) { ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); } }提示:使用requestAnimationFrame而不是setInterval来实现动画循环,能获得更流畅的性能表现。
2.2 告白文字处理
为了让文字输入有仪式感,我设计了两种展示模式:
- 流星模式:文字像流星一样划过星空
- 星座模式:文字以星座连线方式慢慢显现
关键实现技巧:
function drawTextAsMeteor(ctx, text, x, y) { ctx.save(); ctx.font = '24px Arial'; const gradient = ctx.createLinearGradient(x, y, x+200, y); gradient.addColorStop(0, 'rgba(255,255,255,0)'); gradient.addColorStop(0.5, '#ff5e7d'); gradient.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = gradient; ctx.fillText(text, x, y); ctx.restore(); }2.3 交互设计细节
为了让体验更自然,加入了以下交互细节:
- 星星在鼠标靠近时会微微避让
- 点击空白处会出现爱心粒子特效
- 背景音乐随文字出现淡入淡出
canvas.addEventListener('mousemove', (e) => { stars.forEach(star => { const dx = star.x - e.clientX; const dy = star.y - e.clientY; const distance = Math.sqrt(dx*dx + dy*dy); if (distance < 100) { star.x += dx/distance * 5; star.y += dy/distance * 5; } }); });3. 性能优化实战
当星星数量达到500+时,在低端设备上出现了明显卡顿。通过以下优化手段解决了问题:
3.1 对象池技术
复用星星对象而不是频繁创建销毁:
class StarPool { constructor(size) { this.pool = Array(size).fill().map(() => new Star()); this.index = 0; } get() { const star = this.pool[this.index]; this.index = (this.index + 1) % this.pool.length; star.reset(); return star; } }3.2 分层渲染
将静态背景和动态元素分开渲染:
// 背景层(不透明部分) const bgCanvas = document.createElement('canvas'); // 动态层(带alpha通道) const dynamicCanvas = document.createElement('canvas'); function render() { // 先清空动态层 dynamicCtx.clearRect(0, 0, width, height); // 绘制动态元素 stars.forEach(star => star.draw(dynamicCtx)); // 合成到主canvas mainCtx.drawImage(bgCanvas, 0, 0); mainCtx.drawImage(dynamicCanvas, 0, 0); }3.3 节流处理
对高频率事件进行节流控制:
let lastTime = 0; function throttle(callback, limit = 100) { return function() { const now = Date.now(); if (now - lastTime >= limit) { callback.apply(this, arguments); lastTime = now; } }; }4. 完整实现步骤
4.1 基础结构搭建
- 创建HTML骨架:
<!DOCTYPE html> <html> <head> <title>星空告白墙</title> <style> body { margin: 0; overflow: hidden; background: #000; } canvas { display: block; } #text-input { position: fixed; bottom: 20px; width: 80%; left: 10%; padding: 10px; background: rgba(0,0,0,0.5); color: white; border: 1px solid #555; } </style> </head> <body> <canvas id="star-canvas"></canvas> <input id="text-input" type="text" placeholder="输入你想说的话..."> <script src="app.js"></script> </body> </html>4.2 JavaScript主逻辑
// app.js const canvas = document.getElementById('star-canvas'); const ctx = canvas.getContext('2d'); const input = document.getElementById('text-input'); // 适配屏幕尺寸 function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } window.addEventListener('resize', resize); resize(); // 初始化星星 const stars = Array(200).fill().map(() => new Star(canvas)); // 动画循环 function animate() { ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; ctx.fillRect(0, 0, canvas.width, canvas.height); stars.forEach(star => { star.update(); star.draw(ctx); }); requestAnimationFrame(animate); } animate(); // 文字输入处理 input.addEventListener('keypress', (e) => { if (e.key === 'Enter' && input.value) { createMessage(input.value); input.value = ''; } });4.3 进阶功能扩展
- 添加本地存储,保存历史告白:
const messages = JSON.parse(localStorage.getItem('star-messages')) || []; function saveMessage(text) { messages.push({ text, date: new Date().toISOString(), x: Math.random() * canvas.width, y: Math.random() * canvas.height }); localStorage.setItem('star-messages', JSON.stringify(messages)); }- 响应式设计优化:
function adjustForMobile() { if (window.innerWidth < 768) { input.style.fontSize = '16px'; starCount = 100; // 减少星星数量 } }5. 常见问题与解决方案
5.1 文字渲染模糊
问题:Canvas上的文字在Retina屏幕上显示模糊 解决:使用设备像素比进行缩放
const dpr = window.devicePixelRatio || 1; canvas.style.width = `${canvas.width}px`; canvas.style.height = `${canvas.height}px`; canvas.width = canvas.width * dpr; canvas.height = canvas.height * dpr; ctx.scale(dpr, dpr);5.2 动画卡顿
问题:在背景标签页运行时动画变卡 解决:使用Page Visibility API
document.addEventListener('visibilitychange', () => { if (document.hidden) { cancelAnimationFrame(animationId); } else { animate(); } });5.3 移动端触摸支持
问题:手机上没有hover效果 解决:添加触摸事件支持
canvas.addEventListener('touchmove', (e) => { const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousemove', { clientX: touch.clientX, clientY: touch.clientY }); canvas.dispatchEvent(mouseEvent); });6. 项目部署与分享
为了让女友能方便地看到这个惊喜,我选择了以下部署方案:
- 使用GitHub Pages免费托管
- 购买一个浪漫的域名重定向过去
- 生成二维码方便手机访问
部署步骤:
# 初始化Git仓库 git init git add . git commit -m "星空告白墙初版" # 创建GitHub仓库并推送 gh repo create star-wall --public --source=. git push -u origin main # 启用GitHub Pages # 在仓库Settings > Pages中启用,选择main分支/docs目录最终效果让女友惊喜不已,她没想到代码也能如此浪漫。这个项目后来还被同事要去做求婚用,证明技术人的浪漫同样能打动人心。