summaryrefslogtreecommitdiff
path: root/src/ts/script.ts
blob: 81c61f7fa02756b314dcaa7ca875e06f6a2ea7e3 (plain) (blame)
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);