公司动态

原生HTML+JS实现动态星空:从DOM操作到粒子动画的实战指南

📅 2026/8/11 11:22:57
原生HTML+JS实现动态星空:从DOM操作到粒子动画的实战指南
1. 从零到一为什么选择原生HTMLJS来画星空很多刚接触前端的朋友一听到要做一个视觉效果第一反应可能就是去找一个现成的库比如Three.js、Pixi.js或者直接用Canvas API。这当然没问题但对于“动态星空”这样一个经典又浪漫的练手项目我强烈建议你从最原生的HTML、CSS和JavaScript开始。原因很简单它能让你真正理解浏览器绘制动态图形的底层逻辑而不是被封装好的API“惯坏”。当你亲手用document.createElement创造一颗星星用Math.random()赋予它生命再用requestAnimationFrame让它舞动起来时那种对代码的掌控感和创造乐趣是使用高级框架初期很难体会到的。我们这个项目要实现的不仅仅是一个背景。它是一个完全由代码生成的、拥有呼吸感的动态星空。星星有大小、亮度和颜色的随机变化会有缓慢的位移模拟出宇宙深邃、静谧又充满生机的感觉。它不依赖任何外部图片资源所有效果均通过代码实时计算和渲染这意味着它极其轻量在任何现代浏览器中都能流畅运行。无论你是想为自己的个人博客、作品集网站增加一个炫酷而不过分张扬的背景还是单纯想深入理解前端动画与DOM操作的结合这个项目都是一个绝佳的起点。接下来我会带你一步步拆解这个效果从最基础的HTML结构搭建到用JavaScript创造星星、管理它们的生命周期再到实现平滑的动画和交互。过程中我会分享很多我实际编码时踩过的坑和优化技巧比如如何避免内存泄漏、如何让动画更流畅、如何优雅地处理窗口尺寸变化等。我们最终完成的将是一个完整、健壮、可直接复用的动态星空组件。2. 搭建舞台HTML结构与CSS样式的核心设计任何前端视觉效果第一步都是搭建一个稳固且合适的“舞台”。对于星空效果这个舞台就是一个全屏的、作为背景的容器。2.1 基础HTML极简主义的容器我们的HTML结构可以简单到极致因为所有星星都将由JavaScript动态生成并插入。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title美丽的动态星空/title link relstylesheet hrefstyle.css /head body !-- 这里是你的网站主内容 -- div classcontent h1欢迎来到我的宇宙/h1 p这片星空由纯粹的代码绘制而成。/p /div !-- 星空画布容器 -- div idstarfield/div script srcscript.js/script /body /html这里的关键是div idstarfield/div。它就是我们星空的画布。为什么用div而不用canvas对于这种数量较多几百到几千、但单个元素样式简单主要是圆点、阴影的粒子系统使用DOM元素div配合CSS3动画在开发复杂度和性能上对于入门到中级项目是一个很好的平衡。canvas更适合需要像素级操作、复杂物理模拟或粒子数量极多上万的场景但代码复杂度会显著上升。2.2 核心CSS营造深邃的宇宙感CSS部分的目标有两个一是让星空容器覆盖整个视口并置于底层二是定义星星的基本样式和动画。/* style.css */ * { margin: 0; padding: 0; box-sizing: border-box; } body { margin: 0; overflow-x: hidden; /* 防止水平滚动条 */ color: #fff; /* 假设主内容为浅色 */ font-family: sans-serif; /* 给body一个深空背景色作为星空的底色 */ background-color: #0a0a20; } .content { position: relative; /* 确保内容层在星空之上 */ z-index: 10; /* 内容层需要高于星空 */ padding: 2rem; max-width: 800px; margin: 0 auto; } /* 星空画布容器 */ #starfield { position: fixed; /* 使用fixed定位使其相对于视口固定 */ top: 0; left: 0; width: 100vw; height: 100vh; z-index: 1; /* 置于内容层之下 */ pointer-events: none; /* 非常重要让鼠标事件穿透星空层不影响内容交互 */ }现在我们来定义星星的样式。星星本质上是一个个绝对定位的圆形div。我们将通过JavaScript来设置它们的具体位置、大小和颜色但基础样式可以在CSS中预定义。/* 星星的基础样式 */ .star { position: absolute; border-radius: 50%; /* 使其成为圆形 */ background-color: #ffffff; /* 默认白色 */ box-shadow: 0 0 5px 1px rgba(255, 255, 255, 0.8); /* 添加辉光效果 */ will-change: transform, opacity; /* 性能优化提示告诉浏览器该元素将发生变换和透明度变化 */ }关键点解析position: fixedvsabsolute: 这里我选择了fixed。因为absolute是相对于最近的非static定位祖先元素而fixed直接相对于浏览器窗口。对于全屏背景fixed更简单直接能确保无论页面如何滚动星空都完整覆盖窗口。pointer-events: none: 这是实现“背景”效果的关键。没有这个属性星空层会拦截所有的鼠标点击、悬停等事件导致其下方的按钮、链接无法点击。加上它鼠标事件就会“穿透”星空层直达真正的内容。will-change: 这是一个给浏览器的性能优化提示。它提示浏览器“这个元素的transform和opacity属性将会被频繁动画”浏览器可以据此提前做一些优化如将其提升到GPU层。但切忌滥用只对确实会频繁变化的少数元素使用。在我们的场景中每个星星都会动所以使用是合适的。box-shadow: 用来模拟星星的辉光。通过调整模糊半径和颜色可以创造出不同氛围的星光。3. 注入灵魂JavaScript动态生成与动画逻辑这是项目的核心部分。我们将编写script.js负责创建星星、设置它们的随机属性并让它们动起来。3.1 初始化与星星创建函数首先我们需要获取星空容器并定义一些控制参数。// script.js (function() { use strict; // 使用严格模式避免常见错误 // 获取星空容器 const starfield document.getElementById(starfield); if (!starfield) { console.error(未找到ID为“starfield”的元素); return; } // 配置参数 const config { starCount: 400, // 星星总数根据性能调整 starColorRange: { // 星星颜色范围RGB min: { r: 200, g: 220, b: 255 }, // 偏冷的淡蓝色 max: { r: 255, g: 250, b: 220 } // 偏暖的淡黄色 }, starSizeRange: { min: 0.5, max: 3 }, // 星星大小范围像素 twinkleSpeedRange: { min: 1, max: 3 }, // 闪烁速度范围秒一个周期 moveSpeed: 0.05 // 星星平移速度像素/帧正值向右下负值可向左上 }; // 存储所有星星元素的数组用于动画循环更新 let stars []; // 创建一颗星星 function createStar() { const star document.createElement(div); star.className star; // 1. 随机大小 const size Math.random() * (config.starSizeRange.max - config.starSizeRange.min) config.starSizeRange.min; star.style.width ${size}px; star.style.height ${size}px; // 2. 随机位置在视口范围内 star.style.left ${Math.random() * 100}vw; star.style.top ${Math.random() * 100}vh; // 3. 随机颜色 const r Math.floor(Math.random() * (config.starColorRange.max.r - config.starColorRange.min.r 1)) config.starColorRange.min.r; const g Math.floor(Math.random() * (config.starColorRange.max.g - config.starColorRange.min.g 1)) config.colorRange.min.g; const b Math.floor(Math.random() * (config.starColorRange.max.b - config.starColorRange.min.b 1)) config.colorRange.min.b; star.style.backgroundColor rgb(${r}, ${g}, ${b}); // 同步更新辉光颜色使其与星星本体颜色相近但更淡 star.style.boxShadow 0 0 ${size * 2}px ${size * 0.5}px rgba(${r}, ${g}, ${b}, 0.7); // 4. 为每颗星星添加自定义属性用于控制其独立行为 star._starData { // 闪烁相关 twinkleSpeed: Math.random() * (config.twinkleSpeedRange.max - config.twinkleSpeedRange.min) config.twinkleSpeedRange.min, twinklePhase: Math.random() * Math.PI * 2, // 随机初始相位让星星闪烁不同步 baseOpacity: 0.3 Math.random() * 0.7, // 随机基础亮度 // 移动相关 moveVector: { x: (Math.random() - 0.5) * config.moveSpeed, // 随机X方向速度分量 y: (Math.random() - 0.5) * config.moveSpeed // 随机Y方向速度分量 } }; // 将星星添加到容器中 starfield.appendChild(star); return star; } })();关键点与踩坑经验使用vw/vh单位定位星星的初始位置使用Math.random() * 100配合vw视口宽度百分比和vh视口高度百分比。这比使用px更灵活能确保在窗口大小改变时星星的初始分布仍然是全屏的。重绘窗口大小时我们只需要更新已有星星的位置算法而不是重新初始化。将动态数据挂在DOM元素上注意star._starData。我们为每个星星DOM元素附加了一个自定义对象用来存储它独有的动画参数闪烁速度、相位、移动向量等。这是一种常见的模式避免了为这些数据单独维护一个与DOM元素平行的数组简化了数据与视图的绑定。使用_前缀是一种约定表明这是自定义的内部属性。颜色随机算法颜色不是完全随机取0-255而是在一个范围内偏蓝到偏黄这样生成的星空色调更和谐更接近真实的星空视觉效果。辉光与大小关联box-shadow的模糊半径和扩散半径与星星大小size关联。这样大星星的辉光也更明显小星星的辉光更含蓄层次感更强。3.2 初始化星空与动画循环接下来我们创建初始化函数来批量生成星星并启动动画循环。// 接上面的 script.js 代码 // 初始化创建所有星星 function initStars() { // 清空现有星星如果重新初始化 stars.forEach(star star.remove()); stars []; for (let i 0; i config.starCount; i) { stars.push(createStar()); } console.log(星空初始化完成共 ${stars.length} 颗星星。); } // 更新单颗星星的状态闪烁和移动 function updateStar(star, deltaTime) { const data star._starData; if (!data) return; // 1. 更新闪烁透明度变化 // 使用正弦函数模拟平滑的周期性闪烁 data.twinklePhase (Math.PI * 2) / (data.twinkleSpeed * 60) * (deltaTime / 16.67); // 60 FPS为基准 const twinkleFactor 0.5 0.5 * Math.sin(data.twinklePhase); // 值在0到1之间波动 star.style.opacity data.baseOpacity * twinkleFactor; // 2. 更新位置缓慢移动 let currentLeft parseFloat(star.style.left); let currentTop parseFloat(star.style.top); // 使用parseFloat转换并处理vw/vh单位这里我们初始用的是vw/vh但移动用px更直观 // 为了逻辑简单我们假设初始设置后left/top是px值。更健壮的做法是始终用px计算。 // 这里我们做一个转换先将百分比转换为像素值进行计算再存回百分比以适应响应式。 const vwToPx window.innerWidth / 100; const vhToPx window.innerHeight / 100; currentLeft parseFloat(star.style.left) * vwToPx; currentTop parseFloat(star.style.top) * vhToPx; currentLeft data.moveVector.x * deltaTime; currentTop data.moveVector.y * deltaTime; // 边界处理如果星星移出视口将其重置到另一侧 if (currentLeft window.innerWidth) currentLeft 0; else if (currentLeft 0) currentLeft window.innerWidth; if (currentTop window.innerHeight) currentTop 0; else if (currentTop 0) currentTop window.innerHeight; // 转回vw/vh单位存储 star.style.left ${(currentLeft / window.innerWidth) * 100}vw; star.style.top ${(currentTop / window.innerHeight) * 100}vh; } // 动画循环 let lastTime 0; function animate(currentTime) { // 计算上一帧到这一帧的时间差毫秒 const deltaTime lastTime ? currentTime - lastTime : 16.67; // 第一帧假设60FPS lastTime currentTime; // 更新每一颗星星 for (const star of stars) { updateStar(star, deltaTime); } // 请求下一帧动画 requestAnimationFrame(animate); } // 窗口大小改变时的重绘处理 function handleResize() { // 当窗口大小变化时星星的vw/vh位置会自动更新其实际像素位置。 // 但我们的移动逻辑是基于像素的为了保持移动连贯性这里可以选择 // 1. 什么都不做移动会因基准变化而跳跃一下。 // 2. 重新初始化星星体验更统一但会重置所有状态。 // 我们选择方案1因为跳跃通常不明显且保持了星星的“生命”连续性。 // 如果需要更精确的处理可以记录星星的初始像素位置但复杂度会提高。 console.log(窗口尺寸已变化星空自适应调整中。); } // 启动一切 window.addEventListener(load, () { initStars(); requestAnimationFrame(animate); }); window.addEventListener(resize, handleResize); })(); // 立即执行函数结束关键点与踩坑经验基于时间的动画Time-based Animation这是最重要的优化点之一。注意updateStar函数接收了一个deltaTime参数时间差单位毫秒。所有位置和动画进度的计算都基于这个时间差而不是假设每一帧都是固定的16.67ms60FPS。这样无论用户的设备刷新率是60Hz、120Hz还是因为性能波动导致帧率下降星星的移动速度和闪烁频率在真实时间上都是恒定的动画不会“掉速”或“加速”。使用requestAnimationFrame这是浏览器为动画提供的专用API它会在下一次浏览器重绘之前调用你指定的回调函数通常频率是60Hz。它比setInterval或setTimeout更高效、更节能并且当页面不可见如标签页切换时浏览器会自动暂停动画节省资源。边界处理与循环空间当星星移动出视口后我们将其重置到对侧。这创造了一个“无限循环”的空间感而不是星星消失后再在原点生成体验更连贯。这种技巧在游戏和粒子效果中非常常见。单位转换的坑我们在初始化时用vw/vh设置位置但在移动计算时用px。这中间存在单位转换。上面的代码在updateStar里进行了实时转换这是一种方法。另一种更清晰的做法是始终以像素为单位进行计算和存储只在设置样式时如果需要响应式再转换为vw/vh。这里为了演示混合单位的处理我展示了转换过程。在实际项目中我推荐统一使用一种单位通常是像素然后用window.innerWidth/innerHeight来计算相对位置这样逻辑更清晰。handleResize的取舍窗口缩放时星星的vw/vh位置会自动更新到新的像素位置但其“像素移动向量”没变。这可能导致缩放瞬间星星位置有一个基于新基准的跳跃。对于星空背景这种跳跃通常可以接受。如果要求极高可以在resize事件中根据窗口比例的变化按比例缩放所有星星的像素位置和移动向量但这会显著增加复杂度。4. 性能优化与高级效果拓展一个基础的动态星空已经完成了。但要让它在各种设备上丝滑流畅并且效果更出众我们还需要做一些优化和增强。4.1 性能优化关键点减少DOM数量与使用CSStransform问题我们目前每个星星都是一个div元素。当星星数量starCount达到1000甚至更多时大量的DOM节点会带来沉重的布局、样式计算和绘制压力。优化方案A减少数量通过艺术手段用更少的星星营造丰富的层次感。例如让少数大星星移动快一些多数小星星移动慢或不移动。400-600颗星星通常能在性能和效果间取得良好平衡。优化方案B硬件加速我们已经用了will-change: transform。更进一步在updateStar中修改位置时使用transform: translate(x, y)替代修改left/top。因为transform的变化通常由GPU处理不触发重排Reflow只触发重绘Repaint效率更高。// 在updateStar中替换修改left/top的代码 star.style.transform translate(${currentDeltaX}px, ${currentDeltaY}px); // 注意此时星星的初始left/top应为0定位靠transform控制。使用requestAnimationFrame的节流在极少数复杂页面中如果动画回调函数本身执行太慢会导致卡顿。可以添加一个简单的帧率限制逻辑比如确保两次更新之间至少间隔16ms约60FPS但这通常不是主要矛盾。避免在动画循环中触发强制同步布局不要在animate或updateStar函数中频繁读取会引发浏览器重新计算布局的样式属性如offsetWidth,offsetHeight。我们的代码中window.innerWidth/Height的读取是不可避免的但它在每一帧只读一次影响微乎其微。4.2 实现流星效果流星是星空的点睛之笔。我们可以通过创建一个特殊的“流星”元素并控制它沿着一条路径快速移动并消失来实现。// 在配置中添加流星参数 const config { // ... 其他配置不变 ... shootingStarInterval: { min: 3000, max: 8000 }, // 流星出现间隔毫秒 shootingStarLife: 1000 // 流星持续时间毫秒 }; // 创建一颗流星 function createShootingStar() { const shootingStar document.createElement(div); shootingStar.className shooting-star; // 流星样式更亮、有尾巴通过线性渐变或多个元素模拟 shootingStar.style.width 2px; shootingStar.style.height 2px; shootingStar.style.borderRadius 50%; shootingStar.style.backgroundColor #ffffff; shootingStar.style.boxShadow 0 0 10px 2px rgba(255, 255, 200, 0.9); shootingStar.style.position absolute; shootingStar.style.zIndex 2; // 在普通星星之上 // 随机起始位置从屏幕上方或左侧开始 const startSide Math.random() 0.5 ? top : left; if (startSide top) { shootingStar.style.left ${Math.random() * 100}vw; shootingStar.style.top -10px; } else { shootingStar.style.left -10px; shootingStar.style.top ${Math.random() * 100}vh; } // 随机结束位置斜对角方向 const endX Math.random() * 100; const endY Math.random() * 100; starfield.appendChild(shootingStar); // 使用CSS动画控制流星的移动和淡出 const animationDuration config.shootingStarLife ms; shootingStar.style.transition all ${animationDuration} linear; // 强制重排以触发动画 shootingStar.offsetWidth; // 设置终点状态 shootingStar.style.left ${endX}vw; shootingStar.style.top ${endY}vh; shootingStar.style.opacity 0; // 动画结束后移除元素 setTimeout(() { if (shootingStar.parentNode starfield) { starfield.removeChild(shootingStar); } }, config.shootingStarLife); } // 定时生成流星 function startShootingStars() { function scheduleNextStar() { const interval Math.random() * (config.shootingStarInterval.max - config.shootingStarInterval.min) config.shootingStarInterval.min; setTimeout(() { createShootingStar(); scheduleNextStar(); // 递归调用形成循环 }, interval); } scheduleNextStar(); } // 在 window.addEventListener(load, ...) 中启动 window.addEventListener(load, () { initStars(); requestAnimationFrame(animate); startShootingStars(); // 启动流星 });关键点这里用CSStransition来实现流星动画比用requestAnimationFrame逐帧控制更简单高效。通过设置transition属性然后改变left、top和opacity浏览器会自动补间生成平滑动画。setTimeout用于在动画结束后清理DOM元素防止内存泄漏。4.3 添加交互鼠标引力或悬停效果让星空与用户产生互动能极大提升体验。一个经典效果是让星星远离或趋向鼠标指针。// 在配置中添加交互参数 const config { // ... 其他配置不变 ... mouseRepelRadius: 150, // 鼠标斥力影响半径 mouseRepelStrength: 0.5 // 斥力强度 }; let mouseX 0; let mouseY 0; // 监听鼠标移动 document.addEventListener(mousemove, (event) { mouseX event.clientX; mouseY event.clientY; }); // 在 updateStar 函数中增加鼠标交互逻辑 function updateStar(star, deltaTime) { // ... 原有的闪烁和移动逻辑 ... // 3. 鼠标斥力效果 const starX currentLeft; // 使用之前计算出的像素位置 const starY currentTop; const dx mouseX - starX; const dy mouseY - starY; const distance Math.sqrt(dx * dx dy * dy); if (distance config.mouseRepelRadius distance 0) { // 距离越近斥力越强使用反比平方衰减更自然 const force (1 - distance / config.mouseRepelRadius) * config.mouseRepelStrength; const repelX (dx / distance) * force * -10; // 方向远离鼠标 const repelY (dy / distance) * force * -10; currentLeft repelX; currentTop repelY; } // ... 更新后的边界处理和样式设置 ... }这个效果计算了每颗星星与鼠标指针的距离。如果距离小于设定半径则给星星施加一个远离鼠标方向的力距离越近力越大。这需要每一帧都为所有星星计算一次距离当星星数量很多时1000会对性能有影响。在实际使用时可以根据性能情况调整mouseRepelRadius或只为靠近视口中心的星星计算。5. 常见问题排查与调试技巧即使按照步骤操作你也可能会遇到一些问题。这里列出一些常见坑点及其解决方案。5.1 星星不显示或位置不对检查CSS选择器和ID确保JavaScript中getElementById(starfield)的ID与HTML中div的ID完全一致包括大小写。检查容器尺寸在浏览器开发者工具中检查#starfield元素确认其width和height不为0并且position是fixed或absolute。有时父元素的样式会影响到它。检查z-index确保#starfield的z-index低于内容层.content否则会遮盖住你的文字。检查星星样式确认.star类被正确应用并且background-color或box-shadow不是透明的。可以临时给星星加一个红色的边框border: 1px solid red;来确认元素是否被创建和定位。5.2 动画卡顿或不流畅减少星星数量这是最直接有效的方法。将config.starCount从400逐步调低观察性能变化。使用性能面板Performance Tab打开Chrome DevTools的Performance面板录制几秒动画查看主要的耗时任务是在“Scripting”脚本执行、“Rendering”渲染还是“Painting”绘制。如果“Scripting”耗时高优化JavaScript循环比如减少在动画循环中的复杂计算如果“Rendering/Painting”高考虑减少DOM数量或使用transform优化。检查will-change确保只对动画元素添加will-change并且值正确。滥用will-change会消耗更多内存。关闭其他浏览器标签页特别是那些有复杂动画或视频的页面它们会争夺GPU和CPU资源。5.3 内存泄漏问题内存泄漏在长期运行的页面如单页应用中需要警惕。我们的代码在以下地方需要注意流星元素的清理createShootingStar函数中我们使用setTimeout在动画结束后移除DOM元素。这是正确的。确保清理逻辑一定能执行到。事件监听器我们只在全局添加了resize和mousemove监听器并且页面生命周期内不会移除这通常是可接受的因为页面卸载时浏览器会自动清理。但如果这个星空组件会被动态创建和销毁比如在SPA的路由中那么就需要在组件销毁时手动移除这些事件监听器。星星数组如果实现“重新初始化”功能比如切换主题时在initStars开头一定要遍历旧的stars数组调用star.remove()将其从DOM中移除并清空数组否则旧的星星DOM节点会一直留在内存中。5.4 响应式布局下的问题窗口缩放后星星位置跳跃这是我们之前讨论过的单位混合问题。如果追求完美可以放弃vw/vh全程使用像素坐标并在resize事件中根据窗口新旧比例重新计算所有星星的像素位置保持其相对视口的比例不变。但这计算量较大。一个折中方案是只在resize时重置星星位置调用initStars体验上的中断感可能比跳跃感更容易接受。移动端触摸事件如果需要支持移动端可以将鼠标交互改为触摸交互监听touchmove事件并从event.touches[0]获取触摸点坐标。这个项目到这里就完成了。从一片黑暗的div开始到繁星点点、缓缓流动、偶有流星划过的生动星空我们只用了不到200行核心代码。最重要的是你理解了每一行代码背后的意图知道了如何让元素动起来如何处理时间如何与用户交互。你可以随意调整config对象里的参数创造出属于你自己的、独一无二的星空——是密集的银河还是疏朗的深空是冷峻的蓝白色调还是温暖的橙黄色调尽在你的掌握之中。