396 lines
14 KiB
JavaScript
396 lines
14 KiB
JavaScript
/* Progressive Blur */
|
|
function initProgressiveHeaderBlur() {
|
|
const header = document.querySelector('header');
|
|
if (!header || header.querySelector('.header-blur')) return;
|
|
|
|
const container = document.createElement('div');
|
|
container.className = 'header-blur';
|
|
container.setAttribute('aria-hidden', 'true');
|
|
for (let i = 0; i < 25; i++) container.appendChild(document.createElement('div'));
|
|
header.prepend(container);
|
|
}
|
|
initProgressiveHeaderBlur();
|
|
|
|
/* View Transition */
|
|
(() => {
|
|
if (!document.startViewTransition) return;
|
|
|
|
function preloadAssets(newDoc) {
|
|
const curStyleHrefs = new Set(
|
|
[...document.head.querySelectorAll('link[rel="stylesheet"]')].map(l => l.href)
|
|
);
|
|
const curScriptSrcs = new Set(
|
|
[...document.head.querySelectorAll('script[src]')].map(s => s.src)
|
|
);
|
|
|
|
const tasks = [];
|
|
|
|
[...newDoc.head.querySelectorAll('link[rel="stylesheet"]')]
|
|
.filter(l => !curStyleHrefs.has(new URL(l.href, location.href).href))
|
|
.forEach(link => tasks.push(new Promise(resolve => {
|
|
const l = link.cloneNode(true);
|
|
l.addEventListener('load', resolve, { once: true });
|
|
l.addEventListener('error', resolve, { once: true });
|
|
document.head.appendChild(l);
|
|
})));
|
|
|
|
[...newDoc.head.querySelectorAll('script[src]')]
|
|
.filter(s => !curScriptSrcs.has(new URL(s.src, location.href).href))
|
|
.forEach(script => tasks.push(new Promise(resolve => {
|
|
const s = document.createElement('script');
|
|
[...script.attributes].forEach(a => s.setAttribute(a.name, a.value));
|
|
s.addEventListener('load', resolve, { once: true });
|
|
s.addEventListener('error', resolve, { once: true });
|
|
document.head.appendChild(s);
|
|
})));
|
|
|
|
return Promise.all(tasks);
|
|
}
|
|
|
|
function updateHead(newDoc) {
|
|
const head = document.head;
|
|
const newHead = newDoc.head;
|
|
|
|
const t = newHead.querySelector('title');
|
|
if (t) document.title = t.textContent;
|
|
|
|
const META_KEEP = new Set(['charset', 'viewport', 'color-scheme', 'theme-color']);
|
|
head.querySelectorAll('meta').forEach(m => {
|
|
const key = m.getAttribute('name') || m.getAttribute('property');
|
|
if (!key || META_KEEP.has(key)) return;
|
|
m.remove();
|
|
});
|
|
const insertRef = head.querySelector(
|
|
'link[rel="preconnect"], link[rel="stylesheet"], link[rel="manifest"], link[rel="icon"], script'
|
|
);
|
|
newHead.querySelectorAll('meta[name], meta[property]').forEach(m => {
|
|
const key = m.getAttribute('name') || m.getAttribute('property');
|
|
if (!META_KEEP.has(key)) head.insertBefore(m.cloneNode(true), insertRef);
|
|
});
|
|
|
|
const nc = newHead.querySelector('link[rel="canonical"]');
|
|
const cc = head.querySelector('link[rel="canonical"]');
|
|
if (nc && cc) cc.href = nc.href;
|
|
|
|
const newStyleHrefs = new Set(
|
|
[...newHead.querySelectorAll('link[rel="stylesheet"]')]
|
|
.map(l => new URL(l.href, location.href).href)
|
|
);
|
|
head.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
|
|
if (!newStyleHrefs.has(l.href)) l.remove();
|
|
});
|
|
|
|
const newScriptSrcs = new Set(
|
|
[...newHead.querySelectorAll('script[src]')]
|
|
.map(s => new URL(s.src, location.href).href)
|
|
);
|
|
head.querySelectorAll('script[src]').forEach(s => {
|
|
if (!newScriptSrcs.has(s.src)) s.remove();
|
|
});
|
|
|
|
head.querySelectorAll('style').forEach(s => s.remove());
|
|
newHead.querySelectorAll('style').forEach(s => head.appendChild(s.cloneNode(true)));
|
|
}
|
|
|
|
let abortController = null;
|
|
|
|
async function navigate(url) {
|
|
if (abortController) abortController.abort();
|
|
const ac = new AbortController();
|
|
abortController = ac;
|
|
|
|
document.startViewTransition(async () => {
|
|
let response;
|
|
try {
|
|
response = await fetch(url.href, {
|
|
headers: { 'X-Requested-With': 'view-transition' },
|
|
signal: ac.signal
|
|
});
|
|
} catch (err) {
|
|
if (err.name === 'AbortError') return;
|
|
location.href = url.href;
|
|
return;
|
|
}
|
|
|
|
if (ac.signal.aborted) return;
|
|
|
|
const html = await response.text();
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
|
|
if (typeof window.__cursorCleanup === 'function') {
|
|
window.__cursorCleanup();
|
|
}
|
|
|
|
await preloadAssets(doc);
|
|
updateHead(doc);
|
|
|
|
for (const tag of ['header', 'main', 'footer']) {
|
|
const newEl = doc.querySelector(tag);
|
|
const curEl = document.querySelector(tag);
|
|
if (!newEl || !curEl) { location.href = url.href; return; }
|
|
[...curEl.attributes].forEach(a => curEl.removeAttribute(a.name));
|
|
[...newEl.attributes].forEach(a => curEl.setAttribute(a.name, a.value));
|
|
curEl.innerHTML = newEl.innerHTML;
|
|
curEl.querySelectorAll('script').forEach(old => {
|
|
const s = document.createElement('script');
|
|
[...old.attributes].forEach(a => s.setAttribute(a.name, a.value));
|
|
s.textContent = old.textContent;
|
|
old.replaceWith(s);
|
|
});
|
|
}
|
|
|
|
initProgressiveHeaderBlur();
|
|
history.pushState(null, '', response.url);
|
|
|
|
if (typeof window.__cursorReinit === 'function') {
|
|
window.__cursorReinit();
|
|
}
|
|
});
|
|
}
|
|
|
|
window.__navigate = function (href) {
|
|
let url;
|
|
try { url = new URL(href, location.href); } catch (_) { location.href = href; return; }
|
|
if (url.origin !== location.origin) { location.href = href; return; }
|
|
navigate(url);
|
|
};
|
|
|
|
document.addEventListener('click', (event) => {
|
|
const link = event.target.closest('a');
|
|
if (!link || link.hasAttribute('download')) return;
|
|
|
|
const url = new URL(link.href, location.href);
|
|
if (url.origin !== location.origin) return;
|
|
if (link.target || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
|
|
if (url.hash && url.pathname === location.pathname) {
|
|
event.preventDefault();
|
|
const target = document.querySelector(url.hash);
|
|
if (target) {
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
history.pushState(null, '', url.hash);
|
|
}
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
navigate(url);
|
|
});
|
|
|
|
window.addEventListener('popstate', () => location.reload());
|
|
})();
|
|
|
|
/* Cursor */
|
|
(() => {
|
|
const textSelectors = 'p, h1, h2, h3, h4, h5, h6, span, li, label, td, th, pre, .code';
|
|
const linkSelectors = 'a, button, [role="button"], input[type="submit"], input[type="button"]';
|
|
const padding = 6;
|
|
|
|
let ac = null;
|
|
let sig = null;
|
|
|
|
let mouseX = 0, mouseY = 0;
|
|
let currentLinkEl = null;
|
|
let rafId = null;
|
|
let cursor = null;
|
|
let cursorVisible = false;
|
|
let lastTouchTime = 0;
|
|
let isMouseDown = false;
|
|
const TOUCH_MOUSE_GUARD_MS = 800;
|
|
|
|
window.__cursorCleanup = () => {
|
|
if (ac) ac.abort();
|
|
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
|
|
document.documentElement.style.cursor = '';
|
|
if (cursor) cursor.classList.remove('visible', 'on-link', 'on-text');
|
|
cursorVisible = false;
|
|
currentLinkEl = null;
|
|
};
|
|
|
|
function showCursor() {
|
|
if (!cursorVisible && cursor) {
|
|
cursorVisible = true;
|
|
cursor.classList.add('visible');
|
|
}
|
|
}
|
|
|
|
function hideCursor() {
|
|
if (cursor) {
|
|
cursorVisible = false;
|
|
cursor.classList.remove('visible');
|
|
currentLinkEl = null;
|
|
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
|
|
cursor.classList.remove('on-link', 'on-text');
|
|
}
|
|
}
|
|
|
|
function isSyntheticMouse() {
|
|
return Date.now() - lastTouchTime < TOUCH_MOUSE_GUARD_MS;
|
|
}
|
|
|
|
function updateCursorForLink(el) {
|
|
const rect = el.getBoundingClientRect();
|
|
cursor.classList.remove('on-text');
|
|
cursor.classList.add('on-link');
|
|
cursor.style.transform = 'none';
|
|
cursor.style.left = (rect.left - padding) + 'px';
|
|
cursor.style.top = (rect.top - padding) + 'px';
|
|
cursor.style.width = (rect.width + padding * 2) + 'px';
|
|
cursor.style.height = (rect.height + padding * 2) + 'px';
|
|
}
|
|
|
|
function trackLink() {
|
|
if (currentLinkEl) {
|
|
updateCursorForLink(currentLinkEl);
|
|
rafId = requestAnimationFrame(trackLink);
|
|
}
|
|
}
|
|
|
|
document.documentElement.style.cursor = 'none';
|
|
|
|
function init() {
|
|
ac = new AbortController();
|
|
sig = ac.signal;
|
|
|
|
cursor = document.getElementById('cursor');
|
|
if (!cursor) return;
|
|
|
|
document.addEventListener('touchstart', () => { lastTouchTime = Date.now(); hideCursor(); }, { passive: true, signal: sig });
|
|
document.addEventListener('touchmove', () => { lastTouchTime = Date.now(); hideCursor(); }, { passive: true, signal: sig });
|
|
document.addEventListener('touchend', () => { lastTouchTime = Date.now(); }, { passive: true, signal: sig });
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (isSyntheticMouse()) return;
|
|
if (e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents) return;
|
|
|
|
mouseX = e.clientX;
|
|
mouseY = e.clientY;
|
|
|
|
showCursor();
|
|
|
|
const el = document.elementFromPoint(mouseX, mouseY);
|
|
const linkEl = el ? el.closest(linkSelectors) : null;
|
|
|
|
if (linkEl) {
|
|
if (currentLinkEl !== linkEl) {
|
|
currentLinkEl = linkEl;
|
|
if (rafId) cancelAnimationFrame(rafId);
|
|
rafId = requestAnimationFrame(trackLink);
|
|
}
|
|
} else {
|
|
if (currentLinkEl) {
|
|
currentLinkEl = null;
|
|
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
|
|
}
|
|
cursor.classList.remove('on-link');
|
|
cursor.style.transform = isMouseDown ? 'translate(-50%, -50%) scale(0.9)' : 'translate(-50%, -50%)';
|
|
cursor.style.left = mouseX + 'px';
|
|
cursor.style.top = mouseY + 'px';
|
|
cursor.style.width = '';
|
|
cursor.style.height = '';
|
|
|
|
if (el && el.closest(textSelectors)) {
|
|
cursor.classList.add('on-text');
|
|
} else {
|
|
cursor.classList.remove('on-text');
|
|
}
|
|
}
|
|
}, { signal: sig });
|
|
|
|
document.addEventListener('mousedown', () => {
|
|
isMouseDown = true;
|
|
cursor.style.transform = currentLinkEl ? 'none' : 'translate(-50%, -50%) scale(0.9)';
|
|
}, { signal: sig });
|
|
document.addEventListener('mouseup', () => {
|
|
isMouseDown = false;
|
|
cursor.style.transform = currentLinkEl ? 'none' : 'translate(-50%, -50%) scale(1)';
|
|
}, { signal: sig });
|
|
|
|
window.addEventListener('scroll', () => {
|
|
if (currentLinkEl) updateCursorForLink(currentLinkEl);
|
|
}, { passive: true, signal: sig });
|
|
}
|
|
|
|
function reinit() {
|
|
cursor = document.getElementById('cursor');
|
|
if (!cursor) return;
|
|
|
|
currentLinkEl = null;
|
|
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
|
|
cursor.classList.remove('on-link', 'on-text');
|
|
cursor.style.transform = 'translate(-50%, -50%)';
|
|
cursor.style.left = mouseX + 'px';
|
|
cursor.style.top = mouseY + 'px';
|
|
cursor.style.width = '';
|
|
cursor.style.height = '';
|
|
|
|
const el = document.elementFromPoint(mouseX, mouseY);
|
|
const newLinkEl = el ? el.closest(linkSelectors) : null;
|
|
if (newLinkEl) {
|
|
currentLinkEl = newLinkEl;
|
|
rafId = requestAnimationFrame(trackLink);
|
|
} else if (el && el.closest(textSelectors)) {
|
|
cursor.classList.add('on-text');
|
|
}
|
|
|
|
init();
|
|
}
|
|
|
|
window.__cursorReinit = reinit;
|
|
window.__cursorGetState = () => ({ mouseX, mouseY, currentLinkEl, rafId, trackLink, linkSelectors, textSelectors });
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init, { once: true });
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|
|
|
|
/* Loading Animation */
|
|
(() => {
|
|
const overlay = document.getElementById('loading-overlay');
|
|
if (!overlay) return;
|
|
|
|
const svg = overlay.querySelector('svg');
|
|
const line = overlay.querySelector('polyline');
|
|
if (!svg || !line) { overlay.remove(); return; }
|
|
|
|
const length = line.getTotalLength();
|
|
line.style.strokeDasharray = length;
|
|
line.style.strokeDashoffset = length;
|
|
|
|
const ease = 'cubic-bezier(0.22, 1, 0.36, 1)';
|
|
const PHASE_IN = 3000;
|
|
const PHASE_WAIT = 1000;
|
|
const PHASE_OUT = 1000;
|
|
const opts = (d) => ({ duration: d, easing: ease, fill: 'forwards' });
|
|
|
|
svg.animate([
|
|
{ opacity: 0, transform: 'scale(1)', filter: 'blur(20px)' },
|
|
{ opacity: 1, transform: 'scale(0.5)', filter: 'blur(0px)' }
|
|
], opts(PHASE_IN));
|
|
line.animate([
|
|
{ strokeDashoffset: length },
|
|
{ strokeDashoffset: 0 }
|
|
], opts(PHASE_IN));
|
|
|
|
setTimeout(() => {
|
|
svg.animate([
|
|
{ opacity: 1, transform: 'scale(0.5)', filter: 'blur(0px)' },
|
|
{ opacity: 0, transform: 'scale(0.75)', filter: 'blur(20px)' }
|
|
], opts(PHASE_OUT));
|
|
|
|
line.animate([
|
|
{ strokeDashoffset: 0 },
|
|
{ strokeDashoffset: length }
|
|
], opts(PHASE_OUT));
|
|
|
|
overlay.animate([
|
|
{ opacity: 1, backdropFilter: 'blur(40px)', WebkitBackdropFilter: 'blur(40px)' },
|
|
{ opacity: 0, backdropFilter: 'blur(0px)', WebkitBackdropFilter: 'blur(0px)' }
|
|
], opts(PHASE_OUT));
|
|
|
|
setTimeout(() => overlay.remove(), PHASE_OUT);
|
|
}, PHASE_IN + PHASE_WAIT);
|
|
})();
|