MediaWiki:Common.js: Difference between revisions
From Skateboarding on the Pacific Ocean
No edit summary |
|||
| Line 1: | Line 1: | ||
/** | /** | ||
* | * Willoughby Tucker, I'll Always Love You — MediaWiki Skin JS | ||
* Ethel Cain, 2025 | |||
* | * | ||
* | * Load via MediaWiki:Common.js or skin template | ||
* | * Requires: vanilla JS, no dependencies | ||
* | * | ||
* | * Effects: | ||
* | * 1. Amber loading bar | ||
* 2. Dust particle system | |||
* 3. Magnetic cursor (dot + ring) | |||
* 4. Scroll-reveal observer | |||
* 5. Sidebar link ripples | |||
* 6. Title ambient pulse | |||
* 7. Search placeholder cycling | |||
* 8. VHS glitch on title | |||
* 9. Table row glow | |||
* 10. Fog parallax on sidebar | |||
*/ | */ | ||
/* ═══════════════════════════════════════════════════════ | |||
JS EFFECTS | |||
═══════════════════════════════════════════════════════ */ | |||
/* 1. Amber loading bar */ | |||
(function() { | |||
const bar = document.getElementById('loading-bar'); | |||
if (!bar) return; | |||
let w = 0; | |||
const iv = setInterval(() => { | |||
w += Math.random() * 18 + 4; | |||
if (w >= 95) { clearInterval(iv); w = 95; } | |||
bar.style.width = w + '%'; | |||
}, 80); | |||
window.addEventListener('load', () => { | |||
clearInterval(iv); | |||
bar.style.width = '100%'; | |||
setTimeout(() => { bar.style.opacity = '0'; bar.style.transition = 'opacity 0.5s'; }, 300); | |||
}); | |||
})(); | |||
/* 2. Dust particle system */ | |||
(function() { | |||
const canvas = document.getElementById('dust-canvas'); | |||
if (!canvas) return; | |||
const ctx = canvas.getContext('2d'); | |||
let W, H, particles = []; | |||
function resize() { | |||
function | W = canvas.width = window.innerWidth; | ||
H = canvas.height = window.innerHeight; | |||
} | } | ||
function createParticle() { | |||
return { | |||
x: Math.random() * W, | |||
y: H + Math.random() * 20, | |||
vx: (Math.random() - 0.5) * 0.4, | |||
vy: -(Math.random() * 0.6 + 0.1), | |||
size: Math.random() * 2 + 0.3, | |||
opacity: Math.random() * 0.5 + 0.1, | |||
life: 0, | |||
maxLife: Math.random() * 600 + 200, | |||
hue: Math.random() * 20 + 25 | |||
}; | |||
} | |||
for (let i = 0; i < 80; i++) { | |||
const p = createParticle(); | |||
p.y = Math.random() * H; | |||
p.life = Math.random() * p.maxLife; | |||
particles.push(p); | |||
} | |||
function draw() { | |||
ctx.clearRect(0, 0, W, H); | |||
for (let i = particles.length - 1; i >= 0; i--) { | |||
const p = particles[i]; | |||
p.x += p.vx + Math.sin(p.life * 0.01) * 0.15; | |||
p.y += p.vy; | |||
p.life++; | |||
const fade = p.life < 60 ? p.life / 60 : p.life > p.maxLife - 60 ? (p.maxLife - p.life) / 60 : 1; | |||
ctx.beginPath(); | |||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); | |||
ctx.fillStyle = `hsla(${p.hue}, 55%, 60%, ${p.opacity * fade})`; | |||
ctx.fill(); | |||
if (p.life >= p.maxLife || p.y < -10) { | |||
particles[i] = createParticle(); | |||
} | |||
} | |||
} | } | ||
// Occasional larger dust mote | |||
if (Math.random() < 0.01 && particles.length < 120) { | |||
const mote = createParticle(); | |||
mote.size = Math.random() * 3 + 1.5; | |||
mote.opacity = 0.08; | |||
mote.vy = -(Math.random() * 0.2 + 0.05); | |||
particles.push(mote); | |||
} | } | ||
requestAnimationFrame( draw | requestAnimationFrame(draw); | ||
} | } | ||
resize(); | |||
window.addEventListener('resize', resize); | |||
draw(); | |||
})(); | |||
/* 3. Magnetic cursor */ | |||
(function() { | |||
const dot = document.getElementById('cursor-dot'); | |||
const ring = document.getElementById('cursor-ring'); | |||
if (!dot || !ring) return; | |||
let mx = 0, my = 0, rx = 0, ry = 0; | |||
let entered = false; | |||
document.addEventListener('mouseenter', () => { | |||
entered = true; | |||
dot.style.opacity = '1'; | |||
ring.style.opacity = '1'; | |||
}); | |||
document.addEventListener('mouseleave', () => { | |||
entered = false; | |||
dot.style.opacity = '0'; | |||
ring.style.opacity = '0'; | |||
}); | |||
document.addEventListener('mousemove', e => { | |||
mx = e.clientX; my = e.clientY; | |||
dot.style.left = mx + 'px'; | |||
dot.style.top = my + 'px'; | |||
if (!entered) { dot.style.opacity = '1'; ring.style.opacity = '0.7'; entered = true; } | |||
}); | |||
/ | // Links make cursor expand | ||
document.querySelectorAll('a, button').forEach(el => { | |||
el.addEventListener('mouseenter', () => { | |||
ring.style.width = '44px'; | |||
ring.style.height = '44px'; | |||
' | ring.style.borderColor = 'rgba(200,132,58,0.8)'; | ||
' | dot.style.transform = 'translate(-50%,-50%) scale(1.8)'; | ||
' | }); | ||
' | el.addEventListener('mouseleave', () => { | ||
ring.style.width = '28px'; | |||
ring.style.height = '28px'; | |||
ring.style.borderColor = 'rgba(200,132,58,0.4)'; | |||
dot.style.transform = 'translate(-50%,-50%) scale(1)'; | |||
}); | |||
}); | |||
// Smooth ring follow | |||
function lerp(a, b, t) { return a + (b - a) * t; } | |||
function animate() { | |||
rx = lerp(rx, mx, 0.12); | |||
ry = lerp(ry, my, 0.12); | |||
ring.style.left = rx + 'px'; | |||
ring.style.top = ry + 'px'; | |||
requestAnimationFrame(animate); | |||
} | } | ||
animate(); | |||
})(); | |||
/* 4. Scroll-reveal observer */ | |||
(function() { | |||
const els = document.querySelectorAll('.scroll-reveal'); | |||
const obs = new IntersectionObserver(entries => { | |||
entries.forEach(e => { | |||
if (e.isIntersecting) { | |||
e.target.classList.add('revealed'); | |||
obs.unobserve(e.target); | |||
if ( | |||
} | } | ||
}, | }); | ||
} | }, { threshold: 0.12 }); | ||
els.forEach(el => obs.observe(el)); | |||
})(); | |||
/* 5. Sidebar link ripple */ | |||
(function() { | |||
document.querySelectorAll('.portal a').forEach(link => { | |||
link.addEventListener('click', function(e) { | |||
const ripple = document.createElement('span'); | |||
ripple.style.cssText = ` | |||
position: absolute; | |||
border-radius: 50%; | |||
background: rgba(200,132,58,0.25); | |||
width: 6px; height: 6px; | |||
top: 50%; left: ${e.offsetX}px; | |||
transform: translate(-50%,-50%) scale(0); | |||
animation: ripple-out 0.5s ease forwards; | |||
pointer-events: none; | |||
`; | |||
link.appendChild(ripple); | |||
setTimeout(() => ripple.remove(), 500); | |||
}); | |||
}); | |||
} ); | |||
} | |||
const style = document.createElement('style'); | |||
style.textContent = ` | |||
@keyframes ripple-out { | |||
to { transform: translate(-50%,-50%) scale(20); opacity: 0; } | |||
} | |||
`; | |||
document.head.appendChild(style); | |||
})(); | |||
} | |||
/* 6. Ambient audio visualizer hint (subtle amber pulse on #firstHeading) */ | |||
(function() { | |||
const title = document.getElementById('firstHeading'); | |||
if (!title) return; | |||
let frame = 0; | |||
function pulse() { | |||
frame++; | |||
const s = 1 + Math.sin(frame * 0.025) * 0.003; | |||
const g = 0.3 + Math.sin(frame * 0.018) * 0.15; | |||
title.style.textShadow = `0 2px ${30 + Math.sin(frame*0.02)*20}px rgba(200,132,58,${g})`; | |||
requestAnimationFrame(pulse); | |||
} | } | ||
pulse(); | |||
})(); | |||
/* 7. Search bar typewriter placeholder cycling */ | |||
(function() { | |||
' | const input = document.getElementById('searchInput'); | ||
' | if (!input) return; | ||
' | const phrases = [ | ||
' | 'search the dust…', | ||
' | 'find Willoughby…', | ||
' | 'janie, please stay…', | ||
'nettles and ghosts…', | |||
'waco, texas, 1986…', | |||
'i\'ll always love you…' | |||
]; | ]; | ||
let i = 0; | |||
setInterval(() => { | |||
if (document.activeElement !== input) { | |||
i = (i + 1) % phrases.length; | |||
input.placeholder = phrases[i]; | |||
} | |||
}, 3200); | |||
})(); | |||
/* 8. VHS glitch flicker on title — rare, occasional */ | |||
(function() { | |||
const title = document.getElementById('firstHeading'); | |||
if (!title) return; | |||
function maybeGlitch() { | |||
if (Math.random() < 0.3) { | |||
title.style.transition = 'none'; | |||
title.style.transform = `translateX(${(Math.random()-0.5)*3}px)`; | |||
title.style.filter = 'brightness(1.3) saturate(0.5)'; | |||
setTimeout(() => { | |||
title.style.transform = 'translateX(0)'; | |||
title.style.filter = ''; | |||
}, 60 + Math.random() * 80); | |||
} | |||
setTimeout(maybeGlitch, 4000 + Math.random() * 8000); | |||
} | } | ||
setTimeout(maybeGlitch, 5000); | |||
})(); | |||
/* 9. Table row glow on hover */ | |||
(function() { | |||
document.querySelectorAll('.wikitable tbody tr').forEach(row => { | |||
row.addEventListener('mouseenter', () => { | |||
row.style.boxShadow = 'inset 0 0 40px rgba(200,132,58,0.07)'; | |||
}); | |||
row.addEventListener('mouseleave', () => { | |||
row.style.boxShadow = ''; | |||
}); | |||
}); | |||
})(); | |||
/* 10. Fog drifting parallax on sidebar */ | |||
(function() { | |||
let ticking = false; | |||
window.addEventListener('scroll', () => { | |||
if (!ticking) { | |||
requestAnimationFrame(() => { | |||
const offset = window.scrollY * 0.15; | |||
const panel = document.getElementById('mw-panel'); | |||
if (panel) panel.style.backgroundPositionY = offset + 'px'; | |||
ticking = false; | |||
}); | |||
ticking = true; | |||
} | |||
}); | |||
})(); | |||
} | |||
} )(); | |||
Latest revision as of 11:00, 16 April 2026
/**
* Willoughby Tucker, I'll Always Love You — MediaWiki Skin JS
* Ethel Cain, 2025
*
* Load via MediaWiki:Common.js or skin template
* Requires: vanilla JS, no dependencies
*
* Effects:
* 1. Amber loading bar
* 2. Dust particle system
* 3. Magnetic cursor (dot + ring)
* 4. Scroll-reveal observer
* 5. Sidebar link ripples
* 6. Title ambient pulse
* 7. Search placeholder cycling
* 8. VHS glitch on title
* 9. Table row glow
* 10. Fog parallax on sidebar
*/
/* ═══════════════════════════════════════════════════════
JS EFFECTS
═══════════════════════════════════════════════════════ */
/* 1. Amber loading bar */
(function() {
const bar = document.getElementById('loading-bar');
if (!bar) return;
let w = 0;
const iv = setInterval(() => {
w += Math.random() * 18 + 4;
if (w >= 95) { clearInterval(iv); w = 95; }
bar.style.width = w + '%';
}, 80);
window.addEventListener('load', () => {
clearInterval(iv);
bar.style.width = '100%';
setTimeout(() => { bar.style.opacity = '0'; bar.style.transition = 'opacity 0.5s'; }, 300);
});
})();
/* 2. Dust particle system */
(function() {
const canvas = document.getElementById('dust-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let W, H, particles = [];
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
function createParticle() {
return {
x: Math.random() * W,
y: H + Math.random() * 20,
vx: (Math.random() - 0.5) * 0.4,
vy: -(Math.random() * 0.6 + 0.1),
size: Math.random() * 2 + 0.3,
opacity: Math.random() * 0.5 + 0.1,
life: 0,
maxLife: Math.random() * 600 + 200,
hue: Math.random() * 20 + 25
};
}
for (let i = 0; i < 80; i++) {
const p = createParticle();
p.y = Math.random() * H;
p.life = Math.random() * p.maxLife;
particles.push(p);
}
function draw() {
ctx.clearRect(0, 0, W, H);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx + Math.sin(p.life * 0.01) * 0.15;
p.y += p.vy;
p.life++;
const fade = p.life < 60 ? p.life / 60 : p.life > p.maxLife - 60 ? (p.maxLife - p.life) / 60 : 1;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue}, 55%, 60%, ${p.opacity * fade})`;
ctx.fill();
if (p.life >= p.maxLife || p.y < -10) {
particles[i] = createParticle();
}
}
// Occasional larger dust mote
if (Math.random() < 0.01 && particles.length < 120) {
const mote = createParticle();
mote.size = Math.random() * 3 + 1.5;
mote.opacity = 0.08;
mote.vy = -(Math.random() * 0.2 + 0.05);
particles.push(mote);
}
requestAnimationFrame(draw);
}
resize();
window.addEventListener('resize', resize);
draw();
})();
/* 3. Magnetic cursor */
(function() {
const dot = document.getElementById('cursor-dot');
const ring = document.getElementById('cursor-ring');
if (!dot || !ring) return;
let mx = 0, my = 0, rx = 0, ry = 0;
let entered = false;
document.addEventListener('mouseenter', () => {
entered = true;
dot.style.opacity = '1';
ring.style.opacity = '1';
});
document.addEventListener('mouseleave', () => {
entered = false;
dot.style.opacity = '0';
ring.style.opacity = '0';
});
document.addEventListener('mousemove', e => {
mx = e.clientX; my = e.clientY;
dot.style.left = mx + 'px';
dot.style.top = my + 'px';
if (!entered) { dot.style.opacity = '1'; ring.style.opacity = '0.7'; entered = true; }
});
// Links make cursor expand
document.querySelectorAll('a, button').forEach(el => {
el.addEventListener('mouseenter', () => {
ring.style.width = '44px';
ring.style.height = '44px';
ring.style.borderColor = 'rgba(200,132,58,0.8)';
dot.style.transform = 'translate(-50%,-50%) scale(1.8)';
});
el.addEventListener('mouseleave', () => {
ring.style.width = '28px';
ring.style.height = '28px';
ring.style.borderColor = 'rgba(200,132,58,0.4)';
dot.style.transform = 'translate(-50%,-50%) scale(1)';
});
});
// Smooth ring follow
function lerp(a, b, t) { return a + (b - a) * t; }
function animate() {
rx = lerp(rx, mx, 0.12);
ry = lerp(ry, my, 0.12);
ring.style.left = rx + 'px';
ring.style.top = ry + 'px';
requestAnimationFrame(animate);
}
animate();
})();
/* 4. Scroll-reveal observer */
(function() {
const els = document.querySelectorAll('.scroll-reveal');
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('revealed');
obs.unobserve(e.target);
}
});
}, { threshold: 0.12 });
els.forEach(el => obs.observe(el));
})();
/* 5. Sidebar link ripple */
(function() {
document.querySelectorAll('.portal a').forEach(link => {
link.addEventListener('click', function(e) {
const ripple = document.createElement('span');
ripple.style.cssText = `
position: absolute;
border-radius: 50%;
background: rgba(200,132,58,0.25);
width: 6px; height: 6px;
top: 50%; left: ${e.offsetX}px;
transform: translate(-50%,-50%) scale(0);
animation: ripple-out 0.5s ease forwards;
pointer-events: none;
`;
link.appendChild(ripple);
setTimeout(() => ripple.remove(), 500);
});
});
const style = document.createElement('style');
style.textContent = `
@keyframes ripple-out {
to { transform: translate(-50%,-50%) scale(20); opacity: 0; }
}
`;
document.head.appendChild(style);
})();
/* 6. Ambient audio visualizer hint (subtle amber pulse on #firstHeading) */
(function() {
const title = document.getElementById('firstHeading');
if (!title) return;
let frame = 0;
function pulse() {
frame++;
const s = 1 + Math.sin(frame * 0.025) * 0.003;
const g = 0.3 + Math.sin(frame * 0.018) * 0.15;
title.style.textShadow = `0 2px ${30 + Math.sin(frame*0.02)*20}px rgba(200,132,58,${g})`;
requestAnimationFrame(pulse);
}
pulse();
})();
/* 7. Search bar typewriter placeholder cycling */
(function() {
const input = document.getElementById('searchInput');
if (!input) return;
const phrases = [
'search the dust…',
'find Willoughby…',
'janie, please stay…',
'nettles and ghosts…',
'waco, texas, 1986…',
'i\'ll always love you…'
];
let i = 0;
setInterval(() => {
if (document.activeElement !== input) {
i = (i + 1) % phrases.length;
input.placeholder = phrases[i];
}
}, 3200);
})();
/* 8. VHS glitch flicker on title — rare, occasional */
(function() {
const title = document.getElementById('firstHeading');
if (!title) return;
function maybeGlitch() {
if (Math.random() < 0.3) {
title.style.transition = 'none';
title.style.transform = `translateX(${(Math.random()-0.5)*3}px)`;
title.style.filter = 'brightness(1.3) saturate(0.5)';
setTimeout(() => {
title.style.transform = 'translateX(0)';
title.style.filter = '';
}, 60 + Math.random() * 80);
}
setTimeout(maybeGlitch, 4000 + Math.random() * 8000);
}
setTimeout(maybeGlitch, 5000);
})();
/* 9. Table row glow on hover */
(function() {
document.querySelectorAll('.wikitable tbody tr').forEach(row => {
row.addEventListener('mouseenter', () => {
row.style.boxShadow = 'inset 0 0 40px rgba(200,132,58,0.07)';
});
row.addEventListener('mouseleave', () => {
row.style.boxShadow = '';
});
});
})();
/* 10. Fog drifting parallax on sidebar */
(function() {
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
const offset = window.scrollY * 0.15;
const panel = document.getElementById('mw-panel');
if (panel) panel.style.backgroundPositionY = offset + 'px';
ticking = false;
});
ticking = true;
}
});
})();