<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">简易屏幕保护</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: black;
font-family: Arial, Helvetica, sans-serif;
}
#canvas-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: none; /* 隐藏鼠标指针,更像屏保 */
}
#info {
position: fixed;
bottom: 20px;
left: 0;
width: 100%;
text-align: center;
color: rgba(255, 255, 255, 0.2);
font-size: 14px;
pointer-events: none;
user-select: none;
z-index: 10;
}
@media (max-width: 600px) {
#info {
font-size: 10px;
bottom: 10px;
}
}
</style>
</head>
<body>
<div id="canvas-container">
<canvas id="screenSaverCanvas"></canvas>
</div>
<div id="info">点击或移动鼠标退出 · 简易屏保</div>
<script>
(function() {
// ----- 获取 Canvas 和上下文 -----
const canvas = document.getElementById('screenSaverCanvas');
const ctx = canvas.getContext('2d');
// ----- 尺寸变量 -----
let width = window.innerWidth;
let height = window.innerHeight;
// ----- 粒子系统参数 -----
const PARTICLE_COUNT = 80; // 粒子数量
const CONNECTION_DISTANCE = 150; // 连线最大距离 (像素)
const PARTICLE_RADIUS = 2.5; // 粒子半径
const SPEED = 1.2; // 移动速度因子
// ----- 颜色配置 (柔和的蓝色/紫色调) -----
const COLORS = [
{ r: 100, g: 180, b: 255 }, // 淡蓝
{ r: 150, g: 120, b: 255 }, // 淡紫
{ r: 80, g: 220, b: 200 }, // 青绿
{ r: 255, g: 150, b: 200 } // 淡粉
];
// ----- 粒子数组 -----
let particles = [];
// ----- 鼠标 / 触摸交互 (用于退出屏保) -----
let interactionTimeout = null;
const EXIT_DELAY = 1000; // 无操作后重新启动 (实际上通过点击/移动立即退出,这里用于重启逻辑)
// ----- 控制变量 -----
let isRunning = true; // 控制动画循环
let animationFrameId = null;
// ----- 退出屏保:通过重置状态或跳转? 这里我们设计为点击或移动鼠标时,停止动画并显示一个简单的退出效果,但作为屏保脚本,我们更倾向于“退出屏保模式”回到正常页面。
// 但因为我们没有实际的后台,我们实现为:当检测到交互,清空画布并显示一条消息,然后可以重新开始。
// 为了让演示更友好,我们设计为:第一次交互停止动画并显示“已退出”,再次点击重新激活。
// 但更符合屏保习惯:任何交互都停止屏保,显示空白或提示,直到用户再次点击“激活”。
// 我们实现一个切换功能:点击/移动鼠标 切换 运行/停止。
// 但是为了模仿真实屏保,一般移动鼠标或点击会立即退出到桌面,这里我们简化为停止动画并显示文字。
// 为了不干扰展示,如果停止后3秒无操作,自动重新启动。
// 我们采用更直接的方式:默认运行,一旦检测到鼠标移动或点击,立即停止动画并显示“屏保已退出”,
// 然后过2秒后自动重新启动,如果再次交互,再次停止,如此循环。
// 这样做可以完整展示屏保效果,同时演示退出机制。
// ----- 初始化粒子 -----
function initParticles() {
particles = [];
for (let i = 0; i < PARTICLE_COUNT; i++) {
particles.push({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * SPEED * 2,
vy: (Math.random() - 0.5) * SPEED * 2,
color: COLORS[Math.floor(Math.random() * COLORS.length)],
radius: PARTICLE_RADIUS * (0.8 + Math.random() * 0.6) // 随机大小
});
}
}
// ----- 更新粒子位置 (边界反弹) -----
function updateParticles() {
for (let p of particles) {
p.x += p.vx;
p.y += p.vy;
// 边界反弹 (并稍微随机化速度防止周期性)
if (p.x < 0) {
p.x = 0;
p.vx = -p.vx * (0.9 + Math.random() * 0.2);
} else if (p.x > width) {
p.x = width;
p.vx = -p.vx * (0.9 + Math.random() * 0.2);
}
if (p.y < 0) {
p.y = 0;
p.vy = -p.vy * (0.9 + Math.random() * 0.2);
} else if (p.y > height) {
p.y = height;
p.vy = -p.vy * (0.9 + Math.random() * 0.2);
}
// 限制速度范围,避免过快
const maxSpeed = SPEED * 2.5;
const speedVal = Math.hypot(p.vx, p.vy);
if (speedVal > maxSpeed) {
p.vx = (p.vx / speedVal) * maxSpeed;
p.vy = (p.vy / speedVal) * maxSpeed;
}
}
}
// ----- 绘制粒子及连线 -----
function draw() {
ctx.clearRect(0, 0, width, height);
// 1. 绘制连线 (先绘制连线,让粒子在上一层)
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const p1 = particles[i];
const p2 = particles[j];
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < CONNECTION_DISTANCE) {
// 根据距离调整透明度
const opacity = 1 - (dist / CONNECTION_DISTANCE);
// 混合颜色 (取两个粒子的颜色平均)
const avgR = (p1.color.r + p2.color.r) / 2;
const avgG = (p1.color.g + p2.color.g) / 2;
const avgB = (p1.color.b + p2.color.b) / 2;
ctx.strokeStyle = `rgba(${avgR | 0}, ${avgG | 0}, ${avgB | 0}, ${opacity * 0.6})`;
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
}
}
// 2. 绘制粒子
for (let p of particles) {
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
// 粒子颜色 (稍微带点发光效果)
ctx.fillStyle = `rgba(${p.color.r}, ${p.color.g}, ${p.color.b}, 0.9)`;
ctx.fill();
// 增加一点光晕
ctx.shadowColor = `rgba(${p.color.r}, ${p.color.g}, ${p.color.b}, 0.5)`;
ctx.shadowBlur = 8;
ctx.fill();
ctx.shadowBlur = 0; // 重置避免影响其他绘制
}
}
// ----- 动画循环 -----
function animate() {
if (!isRunning) {
// 如果停止,则不更新画面,但可以绘制一个退出提示
// 但为了干净,我们会在停止时清空画布并显示文字,所以这里直接返回
return;
}
updateParticles();
draw();
animationFrameId = requestAnimationFrame(animate);
}
// ----- 启动动画 -----
function startAnimation() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
isRunning = true;
// 重新初始化粒子以适应新尺寸 (如果尺寸变了)
initParticles();
animate();
// 隐藏任何覆盖提示 (如果有)
const infoEl = document.getElementById('info');
infoEl.style.color = 'rgba(255, 255, 255, 0.2)';
infoEl.textContent = '点击或移动鼠标退出 · 简易屏保';
}
// ----- 停止动画 (退出屏保) -----
function stopAnimation() {
if (!isRunning) return;
isRunning = false;
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
// 清空画布并显示退出文字
ctx.clearRect(0, 0, width, height);
// 绘制一些淡出的粒子痕迹 (可选) 或者直接显示文字
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
ctx.fillRect(0, 0, width, height);
// 显示退出文字
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(200, 200, 255, 0.3)';
ctx.font = '24px Arial, sans-serif';
ctx.fillText('屏幕保护已退出', width/2, height/2 - 20);
ctx.fillStyle = 'rgba(200, 200, 255, 0.15)';
ctx.font = '16px Arial, sans-serif';
ctx.fillText('点击或移动鼠标重新启动', width/2, height/2 + 30);
// 更新底部信息
const infoEl = document.getElementById('info');
infoEl.style.color = 'rgba(255, 255, 255, 0.4)';
infoEl.textContent = '点击或移动鼠标重新启动屏保';
}
// ----- 切换状态 (交互触发) -----
function toggleScreenSaver() {
if (isRunning) {
stopAnimation();
// 设置一个定时器,如果一段时间无操作自动重启 (但为了演示,我们让用户通过交互重启)
// 清除之前的定时器
if (interactionTimeout) {
clearTimeout(interactionTimeout);
interactionTimeout = null;
}
// 设置5秒后自动重启 (如果仍处于停止状态)
interactionTimeout = setTimeout(() => {
if (!isRunning) {
startAnimation();
// 更新底部信息
const infoEl = document.getElementById('info');
infoEl.style.color = 'rgba(255, 255, 255, 0.2)';
infoEl.textContent = '点击或移动鼠标退出 · 简易屏保';
}
}, 5000);
} else {
// 如果当前停止,则重新启动
if (interactionTimeout) {
clearTimeout(interactionTimeout);
interactionTimeout = null;
}
startAnimation();
}
}
// ----- 事件监听:鼠标移动、点击、触摸 -----
function handleInteraction(event) {
// 阻止默认行为,避免页面滚动等
event.preventDefault();
toggleScreenSaver();
}
// 鼠标移动 (使用节流,避免过于频繁)
let moveThrottle = null;
function handleMouseMove(event) {
if (moveThrottle) return;
moveThrottle = setTimeout(() => {
moveThrottle = null;
}, 300); // 300ms 内只触发一次
handleInteraction(event);
}
// 鼠标点击
function handleMouseClick(event) {
handleInteraction(event);
}
// 触摸事件 (移动端)
function handleTouch(event) {
handleInteraction(event);
}
// ----- 窗口大小自适应 -----
function handleResize() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
// 如果正在运行,重新初始化粒子以适应新尺寸
if (isRunning) {
initParticles();
} else {
// 如果停止,重绘停止画面
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
ctx.fillRect(0, 0, width, height);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(200, 200, 255, 0.3)';
ctx.font = '24px Arial, sans-serif';
ctx.fillText('屏幕保护已退出', width/2, height/2 - 20);
ctx.fillStyle = 'rgba(200, 200, 255, 0.15)';
ctx.font = '16px Arial, sans-serif';
ctx.fillText('点击或移动鼠标重新启动', width/2, height/2 + 30);
}
}
// ----- 初始化 Canvas 尺寸和粒子 -----
function setup() {
canvas.width = width;
canvas.height = height;
initParticles();
// 默认启动
startAnimation();
// 注册事件
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('click', handleMouseClick);
document.addEventListener('touchstart', handleTouch, { passive: false });
window.addEventListener('resize', handleResize);
}
// ----- 启动一切 -----
setup();
// ----- 清理 (理论上脚本执行期间不会卸载,但提供良好习惯) -----
// 如果需要清理,可以导出清理函数,但这里不涉及。
})();
</script>
</body>
</html>
屏保机制与交互
您可以通过鼠标或触屏与这个动态粒子屏保进行互动,它的核心逻辑围绕粒子运动与视觉连接展开。

- 动态粒子网络:屏幕上有数十个带有柔和色彩的粒子随机运动,当粒子之间的距离小于设定值时,它们之间会绘制出半透明的连线,形成不断变化的网状结构。
- 交互与状态切换:移动鼠标、点击或触摸屏幕会触发屏保的“退出”与“重启”,首次交互会停止动画并显示退出提示;再次交互或等待约5秒,屏保会自动恢复运行。
- 自适应与视觉:页面会自适应窗口大小,粒子在碰到屏幕边界时会反弹,并随机微调速度,使其运动更自然,整体采用深色背景与低饱和度的彩色粒子,视觉上较为舒适。
优化建议: 您可以根据喜好调整 PARTICLE_COUNT(粒子数量)、SPEED(运动速度)和 CONNECTION_DISTANCE(连线距离)等参数,以改变屏保的视觉效果和密集度。