149 lines
4.0 KiB
JavaScript
149 lines
4.0 KiB
JavaScript
const themeToggles = document.querySelectorAll('.theme-toggle');
|
|
const html = document.documentElement;
|
|
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
const supportsViewTransition = typeof document.startViewTransition === 'function';
|
|
const viewTransitionStyleId = 'itstudio-view-transition-style';
|
|
let isThemeTransitioning = false;
|
|
|
|
function ensureViewTransitionStyles() {
|
|
if (!supportsViewTransition || document.getElementById(viewTransitionStyleId)) {
|
|
return;
|
|
}
|
|
|
|
const style = document.createElement('style');
|
|
style.id = viewTransitionStyleId;
|
|
style.textContent = `
|
|
::view-transition-old(root),
|
|
::view-transition-new(root) {
|
|
animation: none;
|
|
mix-blend-mode: normal;
|
|
}
|
|
|
|
::view-transition-group(root) {
|
|
animation: none;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
function updateLogoColor(theme) {
|
|
const logoText = document.querySelector('#logo-text-cn');
|
|
if (logoText) {
|
|
logoText.setAttribute('fill', theme === 'dark' ? '#ffffff' : '#000000');
|
|
}
|
|
}
|
|
|
|
function getPreferredTheme() {
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme) {
|
|
return savedTheme;
|
|
}
|
|
|
|
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
return systemPreference;
|
|
}
|
|
|
|
function setTheme(theme) {
|
|
html.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme);
|
|
themeToggles.forEach((toggle) => {
|
|
toggle.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false');
|
|
});
|
|
updateLogoColor(theme);
|
|
}
|
|
|
|
const initialTheme = getPreferredTheme();
|
|
setTheme(initialTheme);
|
|
ensureViewTransitionStyles();
|
|
|
|
function getToggleCenter(toggle) {
|
|
if (!toggle) {
|
|
return {
|
|
x: window.innerWidth / 2,
|
|
y: window.innerHeight / 2,
|
|
};
|
|
}
|
|
|
|
const rect = toggle.getBoundingClientRect();
|
|
return {
|
|
x: rect.left + rect.width / 2,
|
|
y: rect.top + rect.height / 2,
|
|
};
|
|
}
|
|
|
|
function getTransitionOrigin(event, toggle) {
|
|
const hasPointerPoint = Number.isFinite(event.clientX) && Number.isFinite(event.clientY) && (event.clientX !== 0 || event.clientY !== 0);
|
|
if (hasPointerPoint) {
|
|
return {
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
};
|
|
}
|
|
|
|
return getToggleCenter(toggle);
|
|
}
|
|
|
|
function getRevealRadius(x, y) {
|
|
const maxX = Math.max(x, window.innerWidth - x);
|
|
const maxY = Math.max(y, window.innerHeight - y);
|
|
return Math.hypot(maxX, maxY);
|
|
}
|
|
|
|
function switchThemeWithScatter(theme, origin) {
|
|
if (isThemeTransitioning || prefersReducedMotion.matches || !supportsViewTransition) {
|
|
setTheme(theme);
|
|
return;
|
|
}
|
|
|
|
isThemeTransitioning = true;
|
|
const x = origin.x;
|
|
const y = origin.y;
|
|
const endRadius = getRevealRadius(x, y);
|
|
const clipFrom = `circle(0px at ${x}px ${y}px)`;
|
|
const clipTo = `circle(${endRadius}px at ${x}px ${y}px)`;
|
|
|
|
const transition = document.startViewTransition(() => {
|
|
setTheme(theme);
|
|
});
|
|
|
|
transition.ready
|
|
.then(() => {
|
|
document.documentElement.animate(
|
|
{
|
|
clipPath: [clipFrom, clipTo],
|
|
},
|
|
{
|
|
duration: 650,
|
|
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
pseudoElement: '::view-transition-new(root)',
|
|
}
|
|
);
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
transition.finished.finally(() => {
|
|
isThemeTransitioning = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
themeToggles.forEach((themeToggle) => {
|
|
themeToggle.addEventListener('click', (event) => {
|
|
const currentTheme = html.getAttribute('data-theme');
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
const origin = getTransitionOrigin(event, themeToggle);
|
|
switchThemeWithScatter(newTheme, origin);
|
|
});
|
|
});
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
if (!localStorage.getItem('theme')) {
|
|
const newTheme = e.matches ? 'dark' : 'light';
|
|
setTheme(newTheme);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('load', () => {
|
|
updateLogoColor(html.getAttribute('data-theme') || getPreferredTheme());
|
|
});
|