MediaWiki:Common.js: Difference between revisions
Appearance
Tag: Undo |
No edit summary Tag: Reverted |
||
| Line 257: | Line 257: | ||
setInterval(updateClock, 1000); | setInterval(updateClock, 1000); | ||
}); | }); | ||
/** | |||
* UTOPIA WIKI HEADER - GAMING ELITE ANIMATIONS | |||
* Sleek, professional animations for hardcore gamers | |||
*/ | |||
(function() { | |||
'use strict'; | |||
// Configuration | |||
const CONFIG = { | |||
particleCount: 30, | |||
parallaxStrength: 15, | |||
colors: { | |||
primary: '#ffc300', | |||
secondary: '#ffd60a', | |||
dark: '#000814' | |||
} | |||
}; | |||
// Initialize when DOM is ready | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', initUtopiaHeader); | |||
} else { | |||
initUtopiaHeader(); | |||
} | |||
function initUtopiaHeader() { | |||
const header = findHeaderSection(); | |||
if (!header) { | |||
console.warn('Utopia header section not found'); | |||
return; | |||
} | |||
// Check for reduced motion preference | |||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |||
if (!prefersReducedMotion) { | |||
initParticleSystem(header); | |||
initParallaxEffect(header); | |||
initMouseTrailEffect(header); | |||
initCornerAccents(header); | |||
} | |||
initHoverEffects(header); | |||
initScrollReveal(header); | |||
} | |||
function findHeaderSection() { | |||
// Look for the header section with class | |||
let header = document.querySelector('.utopia-header-section'); | |||
// Fallback: find by image | |||
if (!header) { | |||
const logo = document.querySelector('img[alt*="Utopiawiki"], img[src*="Utopiawiki"]'); | |||
if (logo) { | |||
header = logo.closest('div[style*="padding"]')?.parentElement; | |||
} | |||
} | |||
return header; | |||
} | |||
/** | |||
* Particle System - Floating tech particles | |||
*/ | |||
function initParticleSystem(header) { | |||
const canvas = document.createElement('canvas'); | |||
canvas.style.cssText = ` | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
width: 100%; | |||
height: 100%; | |||
pointer-events: none; | |||
z-index: 1; | |||
`; | |||
header.style.position = 'relative'; | |||
header.insertBefore(canvas, header.firstChild); | |||
const ctx = canvas.getContext('2d'); | |||
let particles = []; | |||
let animationId; | |||
function resize() { | |||
canvas.width = header.offsetWidth; | |||
canvas.height = header.offsetHeight; | |||
initParticles(); | |||
} | |||
function initParticles() { | |||
particles = []; | |||
for (let i = 0; i < CONFIG.particleCount; i++) { | |||
particles.push(createParticle()); | |||
} | |||
} | |||
function createParticle() { | |||
return { | |||
x: Math.random() * canvas.width, | |||
y: canvas.height + Math.random() * 100, | |||
size: Math.random() * 2 + 1, | |||
speedY: Math.random() * 0.5 + 0.3, | |||
speedX: (Math.random() - 0.5) * 0.3, | |||
opacity: Math.random() * 0.5 + 0.3, | |||
hue: Math.random() > 0.7 ? 45 : 42 // Gold hues | |||
}; | |||
} | |||
function updateParticles() { | |||
particles.forEach((particle, index) => { | |||
particle.y -= particle.speedY; | |||
particle.x += particle.speedX; | |||
// Reset particle when it goes off screen | |||
if (particle.y < -10 || particle.x < -10 || particle.x > canvas.width + 10) { | |||
particles[index] = createParticle(); | |||
} | |||
}); | |||
} | |||
function drawParticles() { | |||
ctx.clearRect(0, 0, canvas.width, canvas.height); | |||
particles.forEach(particle => { | |||
ctx.beginPath(); | |||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); | |||
ctx.fillStyle = `hsla(${particle.hue}, 100%, 50%, ${particle.opacity})`; | |||
ctx.shadowBlur = 8; | |||
ctx.shadowColor = `hsla(${particle.hue}, 100%, 50%, 0.8)`; | |||
ctx.fill(); | |||
ctx.shadowBlur = 0; | |||
}); | |||
} | |||
function animate() { | |||
updateParticles(); | |||
drawParticles(); | |||
animationId = requestAnimationFrame(animate); | |||
} | |||
resize(); | |||
animate(); | |||
window.addEventListener('resize', resize); | |||
// Cleanup on navigation | |||
const observer = new MutationObserver(() => { | |||
if (!document.body.contains(canvas)) { | |||
cancelAnimationFrame(animationId); | |||
observer.disconnect(); | |||
} | |||
}); | |||
observer.observe(document.body, { childList: true, subtree: true }); | |||
} | |||
/** | |||
* Parallax Effect - Logo follows mouse | |||
*/ | |||
function initParallaxEffect(header) { | |||
const logo = header.querySelector('img[alt*="Utopiawiki"], img[src*="Utopiawiki"]'); | |||
if (!logo) return; | |||
header.setAttribute('data-parallax', 'true'); | |||
let mouseX = 0; | |||
let mouseY = 0; | |||
let currentX = 0; | |||
let currentY = 0; | |||
header.addEventListener('mousemove', (e) => { | |||
const rect = header.getBoundingClientRect(); | |||
mouseX = (e.clientX - rect.left - rect.width / 2) / rect.width; | |||
mouseY = (e.clientY - rect.top - rect.height / 2) / rect.height; | |||
}); | |||
header.addEventListener('mouseleave', () => { | |||
mouseX = 0; | |||
mouseY = 0; | |||
}); | |||
function animate() { | |||
currentX += (mouseX - currentX) * 0.1; | |||
currentY += (mouseY - currentY) * 0.1; | |||
const offsetX = currentX * CONFIG.parallaxStrength; | |||
const offsetY = currentY * CONFIG.parallaxStrength; | |||
logo.style.transform = `translate(${offsetX}px, ${offsetY}px)`; | |||
requestAnimationFrame(animate); | |||
} | |||
animate(); | |||
} | |||
/** | |||
* Mouse Trail Effect - Subtle glow follows cursor | |||
*/ | |||
function initMouseTrailEffect(header) { | |||
const trail = document.createElement('div'); | |||
trail.style.cssText = ` | |||
position: absolute; | |||
width: 200px; | |||
height: 200px; | |||
border-radius: 50%; | |||
background: radial-gradient(circle, rgba(255, 195, 0, 0.15) 0%, transparent 70%); | |||
pointer-events: none; | |||
z-index: 0; | |||
transition: opacity 0.3s ease; | |||
opacity: 0; | |||
`; | |||
header.appendChild(trail); | |||
let mouseX = 0; | |||
let mouseY = 0; | |||
let currentX = 0; | |||
let currentY = 0; | |||
header.addEventListener('mouseenter', () => { | |||
trail.style.opacity = '1'; | |||
}); | |||
header.addEventListener('mouseleave', () => { | |||
trail.style.opacity = '0'; | |||
}); | |||
header.addEventListener('mousemove', (e) => { | |||
const rect = header.getBoundingClientRect(); | |||
mouseX = e.clientX - rect.left; | |||
mouseY = e.clientY - rect.top; | |||
}); | |||
function animate() { | |||
currentX += (mouseX - currentX) * 0.15; | |||
currentY += (mouseY - currentY) * 0.15; | |||
trail.style.left = (currentX - 100) + 'px'; | |||
trail.style.top = (currentY - 100) + 'px'; | |||
requestAnimationFrame(animate); | |||
} | |||
animate(); | |||
} | |||
/** | |||
* Corner Accents - Animated corner decorations | |||
*/ | |||
function initCornerAccents(header) { | |||
const corners = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; | |||
corners.forEach((corner, index) => { | |||
const accent = document.createElement('div'); | |||
const [vertical, horizontal] = corner.split('-'); | |||
accent.style.cssText = ` | |||
position: absolute; | |||
${vertical}: 0; | |||
${horizontal}: 0; | |||
width: 40px; | |||
height: 40px; | |||
border-${vertical}: 2px solid rgba(255, 195, 0, 0.3); | |||
border-${horizontal}: 2px solid rgba(255, 195, 0, 0.3); | |||
z-index: 3; | |||
animation: cornerPulse ${2 + index * 0.3}s ease-in-out infinite; | |||
animation-delay: ${index * 0.2}s; | |||
`; | |||
header.appendChild(accent); | |||
}); | |||
// Add corner pulse animation | |||
if (!document.getElementById('utopia-corner-styles')) { | |||
const style = document.createElement('style'); | |||
style.id = 'utopia-corner-styles'; | |||
style.textContent = ` | |||
@keyframes cornerPulse { | |||
0%, 100% { | |||
opacity: 0.3; | |||
transform: scale(1); | |||
} | |||
50% { | |||
opacity: 0.6; | |||
transform: scale(1.1); | |||
} | |||
} | |||
`; | |||
document.head.appendChild(style); | |||
} | |||
} | |||
/** | |||
* Hover Effects - Enhanced age link interactions | |||
*/ | |||
function initHoverEffects(header) { | |||
const links = header.querySelectorAll('a'); | |||
links.forEach(link => { | |||
// Add ripple effect on click | |||
link.addEventListener('click', function(e) { | |||
const ripple = document.createElement('div'); | |||
const rect = this.getBoundingClientRect(); | |||
const size = Math.max(rect.width, rect.height); | |||
const x = e.clientX - rect.left - size / 2; | |||
const y = e.clientY - rect.top - size / 2; | |||
ripple.style.cssText = ` | |||
position: absolute; | |||
left: ${x}px; | |||
top: ${y}px; | |||
width: ${size}px; | |||
height: ${size}px; | |||
border-radius: 50%; | |||
background: rgba(255, 213, 10, 0.4); | |||
pointer-events: none; | |||
transform: scale(0); | |||
animation: rippleEffect 0.6s ease-out; | |||
`; | |||
this.style.position = 'relative'; | |||
this.style.overflow = 'hidden'; | |||
this.appendChild(ripple); | |||
setTimeout(() => ripple.remove(), 600); | |||
}); | |||
}); | |||
// Add ripple animation if not exists | |||
if (!document.getElementById('utopia-ripple-styles')) { | |||
const style = document.createElement('style'); | |||
style.id = 'utopia-ripple-styles'; | |||
style.textContent = ` | |||
@keyframes rippleEffect { | |||
to { | |||
transform: scale(2); | |||
opacity: 0; | |||
} | |||
} | |||
`; | |||
document.head.appendChild(style); | |||
} | |||
} | |||
/** | |||
* Scroll Reveal - Fade in elements on scroll | |||
*/ | |||
function initScrollReveal(header) { | |||
const observer = new IntersectionObserver((entries) => { | |||
entries.forEach(entry => { | |||
if (entry.isIntersecting) { | |||
entry.target.style.opacity = '1'; | |||
entry.target.style.transform = 'translateY(0)'; | |||
} | |||
}); | |||
}, { | |||
threshold: 0.1, | |||
rootMargin: '0px 0px -50px 0px' | |||
}); | |||
observer.observe(header); | |||
} | |||
})(); | |||
Revision as of 21:06, 16 February 2026
/* Any JavaScript here will be loaded for all users on every page load. */
// Tab functionality
$(document).ready(function() {
// Initialize tabs - show first tab by default
$('.wiki-tabs-container').each(function() {
var $container = $(this);
var $firstButton = $container.find('.wiki-tab-button').first();
var firstTabId = $firstButton.data('tab');
$firstButton.addClass('active');
$container.find('#' + firstTabId).addClass('active').show();
});
// Tab click handler
$('.wiki-tab-button').on('click', function() {
var $button = $(this);
var tabId = $button.data('tab');
var $container = $button.closest('.wiki-tabs-container');
// Remove active class from all buttons and panes in this container
$container.find('.wiki-tab-button').removeClass('active');
$container.find('.wiki-tab-pane').removeClass('active').hide();
// Add active class to clicked button and corresponding pane
$button.addClass('active');
$container.find('#' + tabId).addClass('active').show();
});
});
// ===============================
// Add Custom Sticky Header Link
// ===============================
mw.hook('wikipage.content').add(function () {
const icons = document.querySelector('.vector-sticky-header-icons');
if (!icons) return;
// Prevent duplicate button on navigation
if (document.getElementById('custom-sticky-link')) return;
const link = document.createElement('a');
link.id = 'custom-sticky-link';
link.href = 'https://discord.gg/t2Rp2dRvze'; // CHANGE THIS
link.target = '_blank';
link.className = 'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet';
link.textContent = 'Join Us on Discord!'; // CHANGE TEXT
link.style.marginLeft = '10px';
link.style.fontWeight = 'bold';
icons.prepend(link); // use appendChild() if you want it at the end
});
// ===============================
// Sticky Header Countdown Timer (Vector 2022) - to Apr 18, 2026 00:00 UTC
// ===============================
mw.hook('wikipage.content').add(function () {
const icons = document.querySelector('.vector-sticky-header-icons');
if (!icons) return;
// Prevent duplicates
if (document.getElementById('sticky-countdown')) return;
// Target: Sat, 18 Apr 2026 at 00:00 GMT+00:00 (UTC)
const target = new Date('2026-04-18T00:00:00Z');
const wrap = document.createElement('span');
wrap.id = 'sticky-countdown';
wrap.className = 'sticky-countdown';
const label = document.createElement('span');
label.className = 'sticky-countdown__label';
label.textContent = 'Ends in:';
const value = document.createElement('span');
value.className = 'sticky-countdown__value';
value.textContent = '--:--:--';
wrap.appendChild(label);
wrap.appendChild(value);
// Put it before the default icons (Talk/History/Edit)
icons.prepend(wrap);
const pad = (n) => String(n).padStart(2, '0');
const formatRemaining = (ms) => {
if (ms <= 0) return '00:00:00';
const totalSeconds = Math.floor(ms / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (days > 0) return `${days}d ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
};
const update = () => {
const now = new Date();
const diff = target.getTime() - now.getTime();
value.textContent = formatRemaining(diff);
wrap.classList.toggle('is-expired', diff <= 0);
};
update();
const timerId = window.setInterval(update, 1000);
// Cleanup if element removed (rare, but safe)
const observer = new MutationObserver(() => {
if (!document.getElementById('sticky-countdown')) {
clearInterval(timerId);
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
// ===============================
// ===============================
// Sticky Header Game Time
// Display: "Mar 1 YR1" (anchored at install time)
// Rules:
// - Each real hour = +1 in-game day number
// - Each 24 hours = +1 in-game month (Jan..Jul via 7-day cycle)
// - Each 7 months/days = +1 in-game year
// ===============================
mw.hook('wikipage.content').add(function () {
const icons = document.querySelector('.vector-sticky-header-icons');
if (!icons) return;
if (document.getElementById('sticky-game-time')) return;
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul'];
// --------- ANCHOR: "right now" is Mar 1 YR1 ----------
const ANCHOR_MONTH_INDEX = 2; // Mar (Jan=0)
const ANCHOR_DAY = 1; // Day 1
const ANCHOR_YEAR = 1; // YR1
// Optional timezone shift (0 = UTC). If you want server/local behavior, tell me your target TZ.
const GAME_TZ_OFFSET_HOURS = 0;
const getShiftedNow = () => new Date(Date.now() + GAME_TZ_OFFSET_HOURS * 3600000);
// Anchor to the current real-hour boundary so changes happen cleanly on the hour
const now = getShiftedNow();
const anchorRealHour = new Date(Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate(),
now.getUTCHours(), 0, 0, 0
));
// Build UI pill
const wrap = document.createElement('span');
wrap.id = 'sticky-game-time';
wrap.className = 'sticky-game-time';
const value = document.createElement('span');
value.className = 'sticky-game-time__value';
wrap.appendChild(value);
icons.prepend(wrap);
function computeGameString() {
const t = getShiftedNow();
// Total whole hours passed since anchor
const hoursPassed = Math.floor((t.getTime() - anchorRealHour.getTime()) / 3600000);
// Day number advances each hour starting from ANCHOR_DAY
const dayIndexFromAnchor = (ANCHOR_DAY - 1) + hoursPassed; // 0-based
const dayNumber = (dayIndexFromAnchor % 24) + 1;
// Every 24 hours, advance the "month/day-of-week" cycle
const daysPassed = Math.floor(dayIndexFromAnchor / 24); // 0..∞
// Total "day-of-week" index (0..6) across years
const totalDayOfWeekIndex =
(ANCHOR_YEAR - 1) * 7 + ANCHOR_MONTH_INDEX + daysPassed;
const year = Math.floor(totalDayOfWeekIndex / 7) + 1;
const month = months[((totalDayOfWeekIndex % 7) + 7) % 7];
return `${month} ${dayNumber} YR${year}`;
}
function update() {
value.textContent = computeGameString();
}
// Update now
update();
// Update exactly on the next hour boundary, then every hour
const now2 = getShiftedNow();
const msUntilNextHour =
3600000 - (now2.getUTCMinutes() * 60000 + now2.getUTCSeconds() * 1000 + now2.getUTCMilliseconds());
setTimeout(function () {
update();
setInterval(update, 3600000);
}, msUntilNextHour);
});
// ===============================
// Sticky Header Clock (timezone-stable)
// Shows the same time for everyone by forcing a timezone (UTC by default)
// ===============================
mw.hook('wikipage.content').add(function () {
const icons = document.querySelector('.vector-sticky-header-icons');
if (!icons) return;
if (document.getElementById('sticky-clock')) return;
// Choose the clock timezone:
// - "Etc/UTC" for UTC (same for all users)
// - Or "America/New_York" (same for all users, displayed in NY time)
const CLOCK_TIMEZONE = 'Etc/UTC'; // <-- change if you want a specific zone
const wrap = document.createElement('span');
wrap.id = 'sticky-clock';
wrap.className = 'sticky-clock';
const label = document.createElement('span');
label.className = 'sticky-clock__label';
label.textContent = (CLOCK_TIMEZONE === 'Etc/UTC') ? 'UTC:' : 'Time:';
const value = document.createElement('span');
value.className = 'sticky-clock__value';
wrap.appendChild(label);
wrap.appendChild(value);
// Put it near the front (use appendChild to put it at the end)
icons.prepend(wrap);
const fmt = new Intl.DateTimeFormat('en-GB', {
timeZone: CLOCK_TIMEZONE,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
function updateClock() {
value.textContent = fmt.format(new Date());
}
updateClock();
setInterval(updateClock, 1000);
});
/**
* UTOPIA WIKI HEADER - GAMING ELITE ANIMATIONS
* Sleek, professional animations for hardcore gamers
*/
(function() {
'use strict';
// Configuration
const CONFIG = {
particleCount: 30,
parallaxStrength: 15,
colors: {
primary: '#ffc300',
secondary: '#ffd60a',
dark: '#000814'
}
};
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUtopiaHeader);
} else {
initUtopiaHeader();
}
function initUtopiaHeader() {
const header = findHeaderSection();
if (!header) {
console.warn('Utopia header section not found');
return;
}
// Check for reduced motion preference
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReducedMotion) {
initParticleSystem(header);
initParallaxEffect(header);
initMouseTrailEffect(header);
initCornerAccents(header);
}
initHoverEffects(header);
initScrollReveal(header);
}
function findHeaderSection() {
// Look for the header section with class
let header = document.querySelector('.utopia-header-section');
// Fallback: find by image
if (!header) {
const logo = document.querySelector('img[alt*="Utopiawiki"], img[src*="Utopiawiki"]');
if (logo) {
header = logo.closest('div[style*="padding"]')?.parentElement;
}
}
return header;
}
/**
* Particle System - Floating tech particles
*/
function initParticleSystem(header) {
const canvas = document.createElement('canvas');
canvas.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
`;
header.style.position = 'relative';
header.insertBefore(canvas, header.firstChild);
const ctx = canvas.getContext('2d');
let particles = [];
let animationId;
function resize() {
canvas.width = header.offsetWidth;
canvas.height = header.offsetHeight;
initParticles();
}
function initParticles() {
particles = [];
for (let i = 0; i < CONFIG.particleCount; i++) {
particles.push(createParticle());
}
}
function createParticle() {
return {
x: Math.random() * canvas.width,
y: canvas.height + Math.random() * 100,
size: Math.random() * 2 + 1,
speedY: Math.random() * 0.5 + 0.3,
speedX: (Math.random() - 0.5) * 0.3,
opacity: Math.random() * 0.5 + 0.3,
hue: Math.random() > 0.7 ? 45 : 42 // Gold hues
};
}
function updateParticles() {
particles.forEach((particle, index) => {
particle.y -= particle.speedY;
particle.x += particle.speedX;
// Reset particle when it goes off screen
if (particle.y < -10 || particle.x < -10 || particle.x > canvas.width + 10) {
particles[index] = createParticle();
}
});
}
function drawParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(particle => {
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${particle.hue}, 100%, 50%, ${particle.opacity})`;
ctx.shadowBlur = 8;
ctx.shadowColor = `hsla(${particle.hue}, 100%, 50%, 0.8)`;
ctx.fill();
ctx.shadowBlur = 0;
});
}
function animate() {
updateParticles();
drawParticles();
animationId = requestAnimationFrame(animate);
}
resize();
animate();
window.addEventListener('resize', resize);
// Cleanup on navigation
const observer = new MutationObserver(() => {
if (!document.body.contains(canvas)) {
cancelAnimationFrame(animationId);
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
/**
* Parallax Effect - Logo follows mouse
*/
function initParallaxEffect(header) {
const logo = header.querySelector('img[alt*="Utopiawiki"], img[src*="Utopiawiki"]');
if (!logo) return;
header.setAttribute('data-parallax', 'true');
let mouseX = 0;
let mouseY = 0;
let currentX = 0;
let currentY = 0;
header.addEventListener('mousemove', (e) => {
const rect = header.getBoundingClientRect();
mouseX = (e.clientX - rect.left - rect.width / 2) / rect.width;
mouseY = (e.clientY - rect.top - rect.height / 2) / rect.height;
});
header.addEventListener('mouseleave', () => {
mouseX = 0;
mouseY = 0;
});
function animate() {
currentX += (mouseX - currentX) * 0.1;
currentY += (mouseY - currentY) * 0.1;
const offsetX = currentX * CONFIG.parallaxStrength;
const offsetY = currentY * CONFIG.parallaxStrength;
logo.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
requestAnimationFrame(animate);
}
animate();
}
/**
* Mouse Trail Effect - Subtle glow follows cursor
*/
function initMouseTrailEffect(header) {
const trail = document.createElement('div');
trail.style.cssText = `
position: absolute;
width: 200px;
height: 200px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255, 195, 0, 0.15) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
transition: opacity 0.3s ease;
opacity: 0;
`;
header.appendChild(trail);
let mouseX = 0;
let mouseY = 0;
let currentX = 0;
let currentY = 0;
header.addEventListener('mouseenter', () => {
trail.style.opacity = '1';
});
header.addEventListener('mouseleave', () => {
trail.style.opacity = '0';
});
header.addEventListener('mousemove', (e) => {
const rect = header.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
});
function animate() {
currentX += (mouseX - currentX) * 0.15;
currentY += (mouseY - currentY) * 0.15;
trail.style.left = (currentX - 100) + 'px';
trail.style.top = (currentY - 100) + 'px';
requestAnimationFrame(animate);
}
animate();
}
/**
* Corner Accents - Animated corner decorations
*/
function initCornerAccents(header) {
const corners = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
corners.forEach((corner, index) => {
const accent = document.createElement('div');
const [vertical, horizontal] = corner.split('-');
accent.style.cssText = `
position: absolute;
${vertical}: 0;
${horizontal}: 0;
width: 40px;
height: 40px;
border-${vertical}: 2px solid rgba(255, 195, 0, 0.3);
border-${horizontal}: 2px solid rgba(255, 195, 0, 0.3);
z-index: 3;
animation: cornerPulse ${2 + index * 0.3}s ease-in-out infinite;
animation-delay: ${index * 0.2}s;
`;
header.appendChild(accent);
});
// Add corner pulse animation
if (!document.getElementById('utopia-corner-styles')) {
const style = document.createElement('style');
style.id = 'utopia-corner-styles';
style.textContent = `
@keyframes cornerPulse {
0%, 100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.1);
}
}
`;
document.head.appendChild(style);
}
}
/**
* Hover Effects - Enhanced age link interactions
*/
function initHoverEffects(header) {
const links = header.querySelectorAll('a');
links.forEach(link => {
// Add ripple effect on click
link.addEventListener('click', function(e) {
const ripple = document.createElement('div');
const rect = this.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
left: ${x}px;
top: ${y}px;
width: ${size}px;
height: ${size}px;
border-radius: 50%;
background: rgba(255, 213, 10, 0.4);
pointer-events: none;
transform: scale(0);
animation: rippleEffect 0.6s ease-out;
`;
this.style.position = 'relative';
this.style.overflow = 'hidden';
this.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
});
});
// Add ripple animation if not exists
if (!document.getElementById('utopia-ripple-styles')) {
const style = document.createElement('style');
style.id = 'utopia-ripple-styles';
style.textContent = `
@keyframes rippleEffect {
to {
transform: scale(2);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
}
/**
* Scroll Reveal - Fade in elements on scroll
*/
function initScrollReveal(header) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
observer.observe(header);
}
})();