1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
import Prism from 'prismjs';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-markup';
import { initOneko } from './oneko';
type Theme = 'light' | 'dark';
const THEME_STORAGE_KEY = 'theme';
const DARK_THEME_ATTRIBUTE = 'data-theme';
function detectAssetBase(): string {
const currentScript = document.currentScript as HTMLScriptElement | null;
const scriptSrc =
currentScript?.src ??
document.querySelector<HTMLScriptElement>('script[src*="bundle"]')?.src ??
'';
if (!scriptSrc) return '';
try {
return new URL(scriptSrc, window.location.href).origin;
} catch {
return '';
}
}
function setAssetBase(): void {
window.ASSET_BASE = detectAssetBase();
}
function getSystemTheme(): Theme {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function applyTheme(theme: Theme): void {
if (theme === 'dark') {
document.documentElement.setAttribute(DARK_THEME_ATTRIBUTE, 'dark');
} else {
document.documentElement.removeAttribute(DARK_THEME_ATTRIBUTE);
}
}
function getStoredTheme(): Theme | null {
const theme = sessionStorage.getItem(THEME_STORAGE_KEY);
return theme === 'dark' || theme === 'light' ? theme : null;
}
function initThemeToggle(): void {
const toggleButton = document.getElementById('theme-toggle');
if (!(toggleButton instanceof HTMLInputElement)) return;
const initialTheme = getStoredTheme() ?? getSystemTheme();
applyTheme(initialTheme);
toggleButton.checked = initialTheme === 'dark';
toggleButton.addEventListener('change', () => {
const nextTheme: Theme = toggleButton.checked ? 'dark' : 'light';
applyTheme(nextTheme);
sessionStorage.setItem(THEME_STORAGE_KEY, nextTheme);
});
}
function sample<T>(items: readonly T[]): T {
return items[Math.floor(Math.random() * items.length)];
}
function initFairyDust(): void {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const colors = ['#ff69b4', '#b19cd9', '#8b6f47', '#ff85c0', '#c4b5fd', '#d4a574'] as const;
const shapes = ['❀', '✿', '✽', '✾', '✻', '❊', '❋', '✼'] as const;
const particleLifetimeMs = 800;
function createParticle(x: number, y: number): void {
const particle = document.createElement('div');
particle.className = 'fairy-dust';
const offsetX = (Math.random() - 0.5) * 20;
const offsetY = (Math.random() - 0.5) * 20;
const rotationDeg = Math.random() * 360;
const sizePx = Math.random() * 8 + 6;
particle.textContent = sample(shapes);
particle.style.left = `${x + offsetX}px`;
particle.style.top = `${y + offsetY}px`;
particle.style.fontSize = `${sizePx}px`;
particle.style.color = sample(colors);
particle.style.transform = `rotate(${rotationDeg}deg)`;
document.body.appendChild(particle);
window.setTimeout(() => particle.remove(), particleLifetimeMs);
}
// rAF throttle to avoid creating a particle per mouse event.
let latestX = 0;
let latestY = 0;
let scheduled = false;
document.addEventListener('mousemove', (event: MouseEvent) => {
latestX = event.clientX;
latestY = event.clientY;
if (scheduled) return;
scheduled = true;
window.requestAnimationFrame(() => {
scheduled = false;
createParticle(latestX, latestY);
});
});
}
function init(): void {
setAssetBase();
initThemeToggle();
initFairyDust();
Prism.highlightAll();
initOneko();
}
document.addEventListener('DOMContentLoaded', init);
|