121 lines
3.7 KiB
JavaScript
121 lines
3.7 KiB
JavaScript
|
|
/* ═══════════════════════════════════════════════════════════
|
|||
|
|
* 主题切换 + 星空动态背景
|
|||
|
|
* 守护: 铸渊 · ICE-GL-ZY001
|
|||
|
|
* ═══════════════════════════════════════════════════════════ */
|
|||
|
|
|
|||
|
|
(function () {
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
var THEME_KEY = 'ftchat_theme';
|
|||
|
|
var body = document.body;
|
|||
|
|
var toggleBtn = document.getElementById('themeToggle');
|
|||
|
|
var canvas = document.getElementById('starfield');
|
|||
|
|
var ctx = canvas.getContext('2d');
|
|||
|
|
|
|||
|
|
// ── 初始化主题 ──
|
|||
|
|
function applyTheme(theme) {
|
|||
|
|
body.setAttribute('data-theme', theme);
|
|||
|
|
try { localStorage.setItem(THEME_KEY, theme); } catch (_e) { /* ignore */ }
|
|||
|
|
var meta = document.querySelector('meta[name="theme-color"]');
|
|||
|
|
if (meta) meta.setAttribute('content', theme === 'dark' ? '#0a0e27' : '#f5f7fc');
|
|||
|
|
}
|
|||
|
|
var savedTheme = null;
|
|||
|
|
try { savedTheme = localStorage.getItem(THEME_KEY); } catch (_e) { /* ignore */ }
|
|||
|
|
if (!savedTheme) {
|
|||
|
|
savedTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark';
|
|||
|
|
}
|
|||
|
|
applyTheme(savedTheme);
|
|||
|
|
|
|||
|
|
toggleBtn.addEventListener('click', function () {
|
|||
|
|
var cur = body.getAttribute('data-theme');
|
|||
|
|
applyTheme(cur === 'dark' ? 'light' : 'dark');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── 星空粒子动画 ───
|
|||
|
|
var stars = [];
|
|||
|
|
var dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|||
|
|
var width = 0, height = 0;
|
|||
|
|
var isMobile = window.innerWidth <= 768;
|
|||
|
|
var STAR_COUNT = isMobile ? 40 : 90;
|
|||
|
|
|
|||
|
|
function resize() {
|
|||
|
|
width = window.innerWidth;
|
|||
|
|
height = window.innerHeight;
|
|||
|
|
canvas.style.width = width + 'px';
|
|||
|
|
canvas.style.height = height + 'px';
|
|||
|
|
canvas.width = Math.floor(width * dpr);
|
|||
|
|
canvas.height = Math.floor(height * dpr);
|
|||
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function spawn() {
|
|||
|
|
return {
|
|||
|
|
x: Math.random() * width,
|
|||
|
|
y: Math.random() * height,
|
|||
|
|
r: Math.random() * 1.4 + 0.3,
|
|||
|
|
vx: (Math.random() - 0.5) * 0.08,
|
|||
|
|
vy: (Math.random() - 0.5) * 0.08,
|
|||
|
|
tw: Math.random() * Math.PI * 2 // twinkle phase
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function init() {
|
|||
|
|
resize();
|
|||
|
|
stars = [];
|
|||
|
|
for (var i = 0; i < STAR_COUNT; i++) stars.push(spawn());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var lastFrame = 0;
|
|||
|
|
var FRAME_BUDGET = 1000 / 30; // 30fps,省电
|
|||
|
|
|
|||
|
|
function tick(ts) {
|
|||
|
|
if (ts - lastFrame < FRAME_BUDGET) {
|
|||
|
|
requestAnimationFrame(tick);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
lastFrame = ts;
|
|||
|
|
|
|||
|
|
ctx.clearRect(0, 0, width, height);
|
|||
|
|
|
|||
|
|
var theme = body.getAttribute('data-theme');
|
|||
|
|
var baseColor = theme === 'dark' ? '255, 255, 255' : '126, 140, 224';
|
|||
|
|
|
|||
|
|
for (var i = 0; i < stars.length; i++) {
|
|||
|
|
var s = stars[i];
|
|||
|
|
s.x += s.vx; s.y += s.vy; s.tw += 0.02;
|
|||
|
|
// 边界回绕
|
|||
|
|
if (s.x < 0) s.x = width;
|
|||
|
|
if (s.x > width) s.x = 0;
|
|||
|
|
if (s.y < 0) s.y = height;
|
|||
|
|
if (s.y > height) s.y = 0;
|
|||
|
|
|
|||
|
|
var alpha = (Math.sin(s.tw) * 0.4 + 0.6) * (theme === 'dark' ? 0.8 : 0.35);
|
|||
|
|
ctx.fillStyle = 'rgba(' + baseColor + ',' + alpha.toFixed(3) + ')';
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
|
|||
|
|
ctx.fill();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
requestAnimationFrame(tick);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
init();
|
|||
|
|
requestAnimationFrame(tick);
|
|||
|
|
|
|||
|
|
// 窗口尺寸变化
|
|||
|
|
var resizeTimer = null;
|
|||
|
|
window.addEventListener('resize', function () {
|
|||
|
|
if (resizeTimer) clearTimeout(resizeTimer);
|
|||
|
|
resizeTimer = setTimeout(function () {
|
|||
|
|
isMobile = window.innerWidth <= 768;
|
|||
|
|
var newCount = isMobile ? 40 : 90;
|
|||
|
|
if (newCount !== STAR_COUNT) {
|
|||
|
|
STAR_COUNT = newCount;
|
|||
|
|
init();
|
|||
|
|
} else {
|
|||
|
|
resize();
|
|||
|
|
}
|
|||
|
|
}, 200);
|
|||
|
|
});
|
|||
|
|
})();
|