guanghulab/frontend/ftchat/theme-toggle.js
2026-05-10 13:12:44 +08:00

121 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ═══════════════════════════════════════════════════════════
* 主题切换 + 星空动态背景
* 守护: 铸渊 · 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);
});
})();