我一直对浩瀚的宇宙和星空怀有一种特殊的情感。那些闪烁的星星、划过的流星,总能让我内心沉静下来。一次偶然的念头促使我萌生了开发一款星空屏保的想法——能在电脑空闲时自动启动,带我遨游在静谧的星空下。后来,这个小项目逐渐丰满起来,过程中的种种设计抉择和技术难题也让我收获颇丰。
初衷与设计思考
项目的最初想法其实非常简单,我希望:
- 画面上随机分布许多星星,大小不一且自然闪烁;
- 偶尔有流星划过,带有真实的尾迹效果;
- 能够优雅地响应用户操作,空闲时自动进入屏保,操作时退出。
但随着设计细节逐渐展开,我意识到实现这些功能比想象中要复杂得多。比如如何高效渲染上百颗星星?如何用有限资源模拟星星闪烁?怎样让流星划过时看上去更加自然?还有,如何用现代框架保证代码结构清晰可维护?
经过一番调研和思考,我决定基于 Vue 3 的组合式 API 搭配 HTML5 Canvas 来完成这个屏保。Vue 提供了响应式和组件化架构,Canvas 则能高效渲染复杂动画,TypeScript 让代码更安全易维护。
技术选型和项目架构
技术栈方面,我选用了:
- Vue 3(组合式 API):模块化逻辑,方便管理动画和状态。
- Vite:极速构建和热更新,极大提升开发效率。
- HTML5 Canvas:负责所有星空与流星的渲染,避免 DOM 操作带来的性能瓶颈。
- TypeScript:提升代码质量,减少低级错误。
整体项目结构如下:
src/
├── assets/ # 静态资源,如字体或图片
├── components/ # Vue 组件
│ ├── StarCanvas.vue # 星空与流星渲染核心组件
│ ├── ChargingText.vue # “正在充电中...”提示组件
├── composables/ # 组合式函数
│ ├── useStarfield.ts # 星空动画逻辑封装
│ ├── useScreensaverTrigger.ts # 屏保触发逻辑
├── views/ # 页面视图
│ ├── HomeView.vue # 首页视图
│ └── ScreensaverView.vue # 屏保视图
├── router/ # 路由配置
├── App.vue # 根组件
└── main.ts # 入口核心功能实现详解
星空渲染的逻辑
StarCanvas.vue 是项目的灵魂,它利用 Canvas 绘制星星和流星。
我定义了每颗星星的属性:
- 位置(x、y,均为 0~1 的比例值,方便响应不同分辨率)
- z 坐标,模拟深度,影响大小和亮度
- 大小,0.5 至 3 像素不等
- 闪烁周期,用于实现周期性透明度变化,模拟闪烁
星星的位置在 3D 空间中随机分布,但最终映射到二维画布上。
我用 requestAnimationFrame 实现循环动画:
const animate = () => {
ctx.clearRect(0, 0, width.value, height.value);
drawStars();
drawMeteor();
animationId = requestAnimationFrame(animate);
};流星的设计
流星是随机出现的,带有尾迹,划过画布。它有:
- 起始位置 (x, y)
- 速度
- 角度(用来确定划过方向)
- 尾迹长度
- 透明度逐渐变化
通过一个类来封装流星的属性与行为:
class Meteor {
constructor() { this.reset(); }
reset() {
this.x = Math.random() * width.value;
this.y = -20;
this.speed = 2 + Math.random() * 5;
this.length = 5 + Math.random() * 15;
this.angle = Math.PI / 6 + Math.random() * Math.PI / 3;
this.alpha = 0;
}
update() {
this.x += Math.sin(this.angle) * this.speed;
this.y += Math.cos(this.angle) * this.speed;
this.alpha = Math.min(this.alpha + 0.01, 0.7);
if (this.y > height.value + 20 || this.x < -20 || this.x > width.value + 20) this.reset();
}
draw(ctx) {
ctx.strokeStyle = `rgba(255,255,255,${this.alpha})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x - Math.sin(this.angle) * this.length, this.y - Math.cos(this.angle) * this.length);
ctx.stroke();
}
}屏保自动触发机制
我用组合式函数封装了用户无操作 30 秒后自动进入屏保的逻辑:
export function useScreensaverTrigger(timeout = 30000) {
const isActive = ref(false);
let timer;
const resetTimer = () => {
clearTimeout(timer);
isActive.value = false;
timer = setTimeout(() => isActive.value = true, timeout);
};
onMounted(() => {
resetTimer();
window.addEventListener('mousemove', resetTimer);
window.addEventListener('keydown', resetTimer);
window.addEventListener('touchstart', resetTimer);
});
onUnmounted(() => {
clearTimeout(timer);
window.removeEventListener('mousemove', resetTimer);
window.removeEventListener('keydown', resetTimer);
window.removeEventListener('touchstart', resetTimer);
});
return { isActive };
}这确保了屏保的自动启停与用户活动联动。
详细流程与逻辑图
为了理清动画和交互的流程,我绘制了一张整体流程图:

这张流程图描述了星空屏保的运行机制,每一帧都会清理画布,然后重新绘制所有元素,同时根据用户操作管理屏保的启停。
关键代码块与知识点讲解
1. 三维视觉深度
每颗星星的 z 值控制其大小和亮度,让画面有层次感:
ctx.arc(x, y, star.size * star.z, 0, 2 * Math.PI);
ctx.fillStyle = `rgba(255, 255, 255, ${alpha * star.z})`;这小小的乘法让远处星星更小更暗,近处星星更亮更大,极大丰富了视觉效果。
2. 闪烁的实现
通过正弦函数周期性调整透明度,模仿星星闪烁:
const alpha = 0.5 + Math.sin((Date.now() / 1000) * (2 * Math.PI) / star.blinkPeriod) * 0.5;这个思路简单但有效,且易于参数调整。
3. 流星的尾迹绘制
流星尾迹由一条线段构成,透明度逐渐递减,显得自然:
ctx.strokeStyle = `rgba(255, 255, 255, ${this.alpha})`;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x - Math.sin(this.angle) * this.length, this.y - Math.cos(this.angle) * this.length);
ctx.stroke();4. 响应式 Canvas 大小调整
监听窗口变化,自动调整 Canvas 大小避免模糊或黑边:
window.addEventListener('resize', () => {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
});UI设计细节
项目的 UI 极简且符合现代设计规范。全黑背景,星星纯白渐变,流星光效柔和,整体沉稳安静。
首页简单清晰,提示用户:
- 屏保会在 30 秒无操作后自动启动;
- 任何鼠标或键盘操作会立即退出屏保。
屏保页面以全屏 Canvas 呈现,画布尺寸动态适配,保持高性能且画质细腻。
文字提示组件“正在充电中...”采用淡入淡出动画,字体简洁,位置居中底部,避免遮挡主画面。
如果你也对星空屏保或者Canvas动画感兴趣,希望我的分享能带给你一些灵感和帮助。也欢迎随时交流,我们一起探索数字艺术的无限可能。
















