blog/content/theme.js

322 lines
11 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const sidebar = document.getElementById('sidebar');
const closeBtn = document.getElementById('close-sidebar');
const trigger = document.getElementById('read-more-trigger');
const body = document.body;
let manualClose = false;
// --- Shy Logo Logic ---
const logo = document.querySelector('.header-logo img');
const header = document.querySelector('.main-header');
if (logo && header) {
let offsetX = 0, offsetY = 0;
let velX = 0, velY = 0;
let mouseX = 0, mouseY = 0;
let chaseStarted = false;
let approaching = false;
let stillTimer = null;
let animating = false;
const FLEE_RADIUS = 130;
const FLEE_FORCE = 18;
const FRICTION = 0.82;
const APPROACH_SPEED = 0.035;
const STILL_DELAY = 1000;
function logoCenter() {
const r = logo.getBoundingClientRect();
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
}
function headerBounds() {
const hr = header.getBoundingClientRect();
const lr = logo.getBoundingClientRect();
return {
minX: -(lr.left - hr.left) + 8,
maxX: (hr.right - lr.right) - 8,
minY: -(lr.top - hr.top) + 8,
maxY: (hr.bottom - lr.bottom) - 8
};
}
function clamp(x, y) {
const b = headerBounds();
return {
x: Math.max(b.minX, Math.min(b.maxX, x)),
y: Math.max(b.minY, Math.min(b.maxY, y))
};
}
function applyTransform() {
logo.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
}
function tick() {
if (approaching) {
// Gently approach where the mouse is
const c = logoCenter();
const targetX = mouseX - (c.x - offsetX);
const targetY = mouseY - (c.y - offsetY);
velX += (targetX - offsetX) * APPROACH_SPEED;
velY += (targetY - offsetY) * APPROACH_SPEED;
velX *= 0.85;
velY *= 0.85;
}
velX *= FRICTION;
velY *= FRICTION;
offsetX += velX;
offsetY += velY;
const c = clamp(offsetX, offsetY);
// Bounce off header edges
if (c.x !== offsetX) velX *= -0.3;
if (c.y !== offsetY) velY *= -0.3;
offsetX = c.x;
offsetY = c.y;
applyTransform();
const moving = Math.abs(velX) > 0.1 || Math.abs(velY) > 0.1;
const farFromHome = Math.abs(offsetX) > 0.5 || Math.abs(offsetY) > 0.5;
if (moving || approaching || farFromHome) {
requestAnimationFrame(tick);
} else {
animating = false;
}
}
function startAnim() {
if (!animating) {
animating = true;
requestAnimationFrame(tick);
}
}
header.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
const c = logoCenter();
const dx = c.x - mouseX;
const dy = c.y - mouseY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < FLEE_RADIUS) {
chaseStarted = true;
approaching = false;
// Flee away from cursor
const angle = Math.atan2(dy, dx);
const force = ((FLEE_RADIUS - dist) / FLEE_RADIUS) * FLEE_FORCE;
velX += Math.cos(angle) * force;
velY += Math.sin(angle) * force;
startAnim();
}
// Reset the "stopped chasing" timer
clearTimeout(stillTimer);
stillTimer = setTimeout(() => {
if (chaseStarted) {
approaching = true;
startAnim();
}
}, STILL_DELAY);
});
header.addEventListener('mouseleave', () => {
clearTimeout(stillTimer);
approaching = false;
chaseStarted = false;
// Drift home
velX += -offsetX * 0.08;
velY += -offsetY * 0.08;
startAnim();
});
}
// --- Sidebar Logic ---
function checkSidebarVisibility() {
if (manualClose) return;
const scrollTop = window.scrollY;
const windowHeight = window.innerHeight;
const docHeight = document.documentElement.scrollHeight;
// "Sidebar fix": Check if page is short (content fits in window or close to it)
// If content is short, sidebar should be visible immediately (if desktop)
if (docHeight <= windowHeight * 1.2 && window.innerWidth > 900) {
sidebar.classList.add('visible');
sidebar.style.opacity = 1;
sidebar.style.pointerEvents = 'auto';
return;
}
// Scroll Logic for longer pages
if (scrollTop + windowHeight > docHeight * 0.9) {
sidebar.classList.add('visible');
trigger.classList.add('visible');
// Dynamic Opacity near bottom
const remaining = docHeight - (scrollTop + windowHeight);
const threshold = docHeight * 0.1;
if (remaining < threshold) {
const opacity = 1 - (remaining / threshold);
sidebar.style.opacity = Math.min(Math.max(opacity, 0), 1);
if (opacity > 0) {
sidebar.style.pointerEvents = 'auto';
} else {
sidebar.style.pointerEvents = 'none';
}
} else {
// Should be redundant with visibility check but safe
}
} else {
// Hide if scrolled up?
// "It just is gone then... manual close... refresh if want it back"
// But scroll behavior implies transient visibility.
// We'll hide it if we scroll back up, unless it's short content.
sidebar.style.opacity = 0;
sidebar.style.pointerEvents = 'none';
}
}
window.addEventListener('scroll', checkSidebarVisibility);
window.addEventListener('resize', checkSidebarVisibility);
// Initial check
setTimeout(checkSidebarVisibility, 100); // Slight delay for rendering
// Mobile Trigger
trigger.addEventListener('click', () => {
sidebar.classList.add('force-visible');
sidebar.style.opacity = 1;
sidebar.style.pointerEvents = 'auto';
});
// Close Button
closeBtn.addEventListener('click', () => {
sidebar.classList.remove('visible');
sidebar.classList.remove('force-visible');
sidebar.style.opacity = 0;
sidebar.style.pointerEvents = 'none';
manualClose = true;
trigger.style.display = 'none';
});
// --- Font Tribute: Carter & Connare ---
const TRIBUTE_FONTS = ['Georgia', 'Tahoma', 'Comic Sans MS', 'Trebuchet MS'];
function tributeTextNodes() {
const content = document.querySelector('.main-content');
if (!content) return [];
const nodes = [];
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, {
acceptNode: (n) => {
const el = n.parentElement;
if (n.textContent.trim().length < 2) return NodeFilter.FILTER_REJECT;
if (['SCRIPT','STYLE','CODE','PRE'].includes(el.tagName)) return NodeFilter.FILTER_REJECT;
if (el.classList.contains('font-tribute')) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
while (walker.nextNode()) nodes.push(walker.currentNode);
return nodes;
}
function sprinkleFont() {
const nodes = tributeTextNodes();
if (!nodes.length) return;
const node = nodes[Math.floor(Math.random() * nodes.length)];
const text = node.textContent;
// Find a non-whitespace character
let pos, tries = 0;
do {
pos = Math.floor(Math.random() * text.length);
} while (text[pos].trim() === '' && ++tries < 20);
if (text[pos].trim() === '') return;
const font = TRIBUTE_FONTS[Math.floor(Math.random() * TRIBUTE_FONTS.length)];
const span = document.createElement('span');
span.textContent = text[pos];
span.style.fontFamily = '"' + font + '"';
span.className = 'font-tribute';
const before = document.createTextNode(text.slice(0, pos));
const after = document.createTextNode(text.slice(pos + 1));
node.parentNode.insertBefore(before, node);
node.parentNode.insertBefore(span, node);
node.parentNode.insertBefore(after, node);
node.parentNode.removeChild(node);
}
// Initial sprinkle: 3 letters
for (let i = 0; i < 3; i++) sprinkleFont();
// Occasionally, mid-read, one more letter quietly changes
(function scheduleNext() {
setTimeout(() => {
sprinkleFont();
scheduleNext();
}, 12000 + Math.random() * 30000); // every 12-42 seconds
})();
// --- Dark Mode Logic ---
// Create Toggle Button
const sidebarContent = document.querySelector('.sidebar-content');
const themeBtn = document.createElement('button');
themeBtn.id = 'theme-toggle';
themeBtn.textContent = 'Toggle Theme';
sidebarContent.appendChild(themeBtn);
function setDarkMode(enable) {
if (enable) {
body.classList.add('dark-mode');
themeBtn.textContent = 'Switch to Light Mode';
} else {
body.classList.remove('dark-mode');
themeBtn.textContent = 'Switch to Dark Mode';
}
}
function getCookie(name) {
const v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
return v ? v[2] : null;
}
function setCookie(name, value, hours) {
const d = new Date();
d.setTime(d.getTime() + (hours * 60 * 60 * 1000));
document.cookie = name + "=" + value + ";path=/;expires=" + d.toUTCString();
}
// Init Dark Mode
const cookieTheme = getCookie('theme');
if (cookieTheme === 'dark') {
setDarkMode(true);
} else if (cookieTheme === 'light') {
setDarkMode(false);
} else {
// Time based auto-detect
const hour = new Date().getHours();
if (hour < 6 || hour >= 18) {
setDarkMode(true);
} else {
setDarkMode(false);
}
}
themeBtn.addEventListener('click', () => {
const isDark = body.classList.contains('dark-mode');
setDarkMode(!isDark);
setCookie('theme', !isDark ? 'dark' : 'light', 24);
});
});