MediaWiki:Common.js
From Skateboarding on the Pacific Ocean
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* =============================================================
* PREACHER'S DAUGHTER — MediaWiki Wiki Theme JS
* Based on the 2022 album by Ethel Cain
* Southern Gothic · Dark Americana · Sacred & Profane
* =============================================================
* Install: Paste into MediaWiki:Common.js
* Requires: PreachersDaughter.css (MediaWiki:Common.css)
* =============================================================
*/
( function () {
'use strict';
/* ── CONFIG ─────────────────────────────────────────────── */
var CFG = {
dust: {
enabled: true,
count: 28, /* live particles on screen */
minSize: 1, /* px */
maxSize: 3.5, /* px */
minSpeed: 0.18, /* px / frame */
maxSpeed: 0.55,
minDrift: -0.25, /* horizontal drift px/frame */
maxDrift: 0.35,
colors: ['rgba(224,200,160,{a})', 'rgba(160,80,80,{a})', 'rgba(255,240,210,{a})']
},
flicker: {
enabled: true,
heading: true, /* animate page heading */
interval: [4000, 14000] /* random ms between flickers */
},
reveal: {
enabled: true,
rootMargin: '0px 0px -60px 0px'
},
watermark: {
enabled: true,
char: '✝'
},
candle: {
enabled: true /* ambient glow pulse on toc/sidebar */
},
typewriter: {
enabled: false /* set true to typewrite the main heading */
}
};
/* ── UTILITY ─────────────────────────────────────────────── */
function rand( min, max ) { return Math.random() * ( max - min ) + min; }
function randInt( min, max ) { return Math.floor( rand( min, max + 1 ) ); }
function qs( sel, ctx ) { return ( ctx || document ).querySelector( sel ); }
function qsa( sel, ctx ) { return Array.from( ( ctx || document ).querySelectorAll( sel ) ); }
/* ── 1. WATERMARK CROSS ──────────────────────────────────── */
function initWatermark() {
if ( !CFG.watermark.enabled ) return;
var el = document.createElement( 'div' );
el.id = 'pd-watermark';
el.setAttribute( 'aria-hidden', 'true' );
el.textContent = CFG.watermark.char;
document.body.appendChild( el );
}
/* ── 2. DUST / ASH PARTICLE SYSTEM ──────────────────────── */
function initDust() {
if ( !CFG.dust.enabled ) return;
var canvas = document.createElement( 'canvas' );
canvas.id = 'pd-dust-canvas';
canvas.setAttribute( 'aria-hidden', 'true' );
document.body.appendChild( canvas );
var ctx = canvas.getContext( '2d' );
var W, H;
var particles = [];
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
window.addEventListener( 'resize', resize, { passive: true } );
resize();
function makeParticle( initial ) {
var colorTpl = CFG.dust.colors[ randInt( 0, CFG.dust.colors.length - 1 ) ];
var alpha = rand( 0.25, 0.75 );
var color = colorTpl.replace( '{a}', alpha.toFixed( 2 ) );
return {
x: rand( 0, W ),
y: initial ? rand( 0, H ) : rand( -20, -2 ),
r: rand( CFG.dust.minSize, CFG.dust.maxSize ),
vy: rand( CFG.dust.minSpeed, CFG.dust.maxSpeed ),
vx: rand( CFG.dust.minDrift, CFG.dust.maxDrift ),
alpha: alpha,
color: color,
rot: rand( 0, Math.PI * 2 ),
rspd: rand( -0.01, 0.01 )
};
}
for ( var i = 0; i < CFG.dust.count; i++ ) {
particles.push( makeParticle( true ) );
}
function draw() {
ctx.clearRect( 0, 0, W, H );
particles.forEach( function ( p, idx ) {
p.y += p.vy;
p.x += p.vx + Math.sin( p.y * 0.008 + idx ) * 0.18;
p.rot += p.rspd;
if ( p.y > H + 10 ) {
particles[ idx ] = makeParticle( false );
return;
}
ctx.save();
ctx.translate( p.x, p.y );
ctx.rotate( p.rot );
ctx.fillStyle = p.color;
ctx.beginPath();
/* small elongated flake */
ctx.ellipse( 0, 0, p.r * 0.55, p.r, 0, 0, Math.PI * 2 );
ctx.fill();
ctx.restore();
} );
requestAnimationFrame( draw );
}
requestAnimationFrame( draw );
}
/* ── 3. CANDLE FLICKER (heading glow) ────────────────────── */
function initFlicker() {
if ( !CFG.flicker.enabled || !CFG.flicker.heading ) return;
var heading = qs( '#firstHeading, .mw-first-heading, .page-header h1' );
if ( !heading ) return;
heading.classList.add( 'pd-flicker' );
/* Random additional intensity bursts */
function scheduleBurst() {
var delay = rand( CFG.flicker.interval[0], CFG.flicker.interval[1] );
setTimeout( function () {
heading.style.transition = 'text-shadow 0.05s ease';
heading.style.textShadow =
'0 0 50px rgba(200,50,50,0.75), 0 0 10px rgba(255,160,100,0.3), 0 2px 4px rgba(0,0,0,.9)';
setTimeout( function () {
heading.style.textShadow = '';
heading.style.transition = '';
scheduleBurst();
}, rand( 60, 180 ) );
}, delay );
}
scheduleBurst();
}
/* ── 4. SCROLL REVEAL ────────────────────────────────────── */
function initReveal() {
if ( !CFG.reveal.enabled ) return;
if ( typeof IntersectionObserver === 'undefined' ) return;
var targets = qsa(
'.mw-body-content h2, .mw-body-content h3, ' +
'.mw-body-content p, .mw-body-content .thumb, ' +
'.mw-body-content table, .mw-body-content blockquote, ' +
'.mw-body-content ul, .mw-body-content ol'
);
var seen = new Set();
var obs = new IntersectionObserver( function ( entries ) {
entries.forEach( function ( entry ) {
if ( entry.isIntersecting && !seen.has( entry.target ) ) {
seen.add( entry.target );
entry.target.classList.add( 'pd-reveal' );
obs.unobserve( entry.target );
}
} );
}, { rootMargin: CFG.reveal.rootMargin } );
/* Pre-set opacity so reveal is visible */
targets.forEach( function ( el ) {
if ( el.getBoundingClientRect().top > window.innerHeight * 0.3 ) {
el.style.opacity = '0';
obs.observe( el );
}
} );
}
/* ── 5. CANDLE AMBIENT PULSE ─────────────────────────────── */
function initCandlePulse() {
if ( !CFG.candle.enabled ) return;
var targets = qsa( '#toc, .toc, .infobox, table.infobox, #content, .mw-body' );
targets.forEach( function ( el ) {
el.classList.add( 'pd-pulse-border' );
} );
}
/* ── 6. ORNATE SECTION DIVIDERS ──────────────────────────── */
function initDividers() {
/* Insert a decorative vine divider after every h2 */
var ornament = '<span class="pd-ornament" aria-hidden="true" ' +
'style="display:block;text-align:center;font-size:0.75rem;' +
'color:rgba(122,28,36,0.45);letter-spacing:0.6em;' +
'margin:-0.2rem 0 0.8rem;font-family:serif;">' +
'— ✦ —' +
'</span>';
qsa( '.mw-body-content h2' ).forEach( function ( h ) {
h.insertAdjacentHTML( 'afterend', ornament );
} );
}
/* ── 7. TYPEWRITER HEADING (opt-in) ──────────────────────── */
function initTypewriter() {
if ( !CFG.typewriter.enabled ) return;
var heading = qs( '#firstHeading, .mw-first-heading' );
if ( !heading ) return;
var originalText = heading.textContent.trim();
heading.textContent = '';
heading.style.opacity = '1';
var i = 0;
var cursor = document.createElement( 'span' );
cursor.textContent = '|';
cursor.style.cssText =
'color:rgba(160,43,55,0.8);animation:pd-flickerB 1.1s ease-in-out infinite;';
heading.appendChild( cursor );
var interval = setInterval( function () {
if ( i < originalText.length ) {
cursor.insertAdjacentText( 'beforebegin', originalText.charAt( i ) );
i++;
} else {
clearInterval( interval );
setTimeout( function () { cursor.remove(); }, 600 );
}
}, 55 );
}
/* ── 8. LINK HOVER SOUND (optional — disabled by default) ── */
/*
function initHoverSound() {
// Uncomment if you want subtle audio on link hover
// Requires AudioContext support
qsa( 'a' ).forEach( function ( a ) {
a.addEventListener( 'mouseenter', function () {
try {
var ctx = new AudioContext();
var osc = ctx.createOscillator();
var gain = ctx.createGain();
osc.connect( gain );
gain.connect( ctx.destination );
osc.frequency.value = 220;
gain.gain.setValueAtTime( 0.04, ctx.currentTime );
gain.gain.exponentialRampToValueAtTime( 0.0001, ctx.currentTime + 0.25 );
osc.start();
osc.stop( ctx.currentTime + 0.25 );
} catch ( e ) {}
} );
} );
}
*/
/* ── 9. REDLINK STYLING ──────────────────────────────────── */
function styleRedlinks() {
qsa( 'a.new' ).forEach( function ( a ) {
a.title = ( a.title || '' ) + ' [page not yet written]';
} );
}
/* ── 10. SIDEBAR SECTION COLLAPSE ANIMATION ──────────────── */
function initSidebarAnimations() {
qsa( '#mw-panel .portal, .vector-menu' ).forEach( function ( portal, idx ) {
portal.style.opacity = '0';
portal.style.transform = 'translateX(-8px)';
portal.style.transition =
'opacity 0.5s ease ' + ( idx * 0.08 ) + 's, ' +
'transform 0.5s ease ' + ( idx * 0.08 ) + 's';
/* Trigger reflow then animate in */
setTimeout( function () {
portal.style.opacity = '1';
portal.style.transform = 'translateX(0)';
}, 80 );
} );
}
/* ── 11. TABLE ROW STAGGER ───────────────────────────────── */
function initTableStagger() {
qsa( '.wikitable tr' ).forEach( function ( tr, idx ) {
tr.style.opacity = '0';
tr.style.transition = 'opacity 0.4s ease ' + ( idx * 0.04 ) + 's';
setTimeout( function () {
tr.style.opacity = '1';
}, 200 + idx * 40 );
} );
}
/* ── 12. CROSS CURSOR TRAIL ──────────────────────────────── */
function initCursorTrail() {
/* Subtle: drops tiny crimson sparks on fast mouse move */
var lastX = 0, lastY = 0, moving = false, timer;
document.addEventListener( 'mousemove', function ( e ) {
var dx = e.clientX - lastX;
var dy = e.clientY - lastY;
var dist = Math.sqrt( dx * dx + dy * dy );
if ( dist < 12 ) return; /* only on fast moves */
lastX = e.clientX;
lastY = e.clientY;
var spark = document.createElement( 'div' );
spark.setAttribute( 'aria-hidden', 'true' );
spark.style.cssText =
'position:fixed;' +
'left:' + e.clientX + 'px;' +
'top:' + e.clientY + 'px;' +
'width:4px;height:4px;' +
'border-radius:50%;' +
'background:rgba(160,43,55,0.7);' +
'pointer-events:none;' +
'z-index:99999;' +
'transform:translate(-50%,-50%);' +
'transition:opacity 0.6s ease, transform 0.6s ease;';
document.body.appendChild( spark );
/* Animate out */
requestAnimationFrame( function () {
spark.style.opacity = '0';
spark.style.transform =
'translate(-50%,-50%) translateY(' + rand( -12, -30 ) + 'px) scale(0.3)';
} );
setTimeout( function () {
if ( spark.parentNode ) spark.parentNode.removeChild( spark );
}, 650 );
}, { passive: true } );
}
/* ── 13. PAGE TRANSITION OVERLAY ─────────────────────────── */
function initPageTransition() {
/* Fade-out on navigation */
var overlay = document.createElement( 'div' );
overlay.setAttribute( 'aria-hidden', 'true' );
overlay.style.cssText =
'position:fixed;inset:0;' +
'background:#0e0a09;' +
'opacity:0;pointer-events:none;' +
'z-index:99997;' +
'transition:opacity 0.3s ease;';
document.body.appendChild( overlay );
/* Fade in (we arrive on a new page) */
document.body.style.opacity = '0';
document.body.style.transition = 'opacity 0.6s ease';
requestAnimationFrame( function () {
document.body.style.opacity = '1';
} );
/* Fade out before leaving */
document.addEventListener( 'click', function ( e ) {
var a = e.target.closest( 'a' );
if ( !a || !a.href || a.target === '_blank' ||
a.href.startsWith( '#' ) || a.href.startsWith( 'javascript' ) ) return;
/* Skip edit/action links */
if ( /[?&]action=/.test( a.href ) && !/action=view/.test( a.href ) ) return;
overlay.style.pointerEvents = 'all';
overlay.style.opacity = '1';
} );
}
/* ── 14. QUOTE PULL — RANDOM TRACK EPIGRAPH ──────────────── */
var EPIGRAPHS = [
'"I was baptized but I still die." — Family Tree',
'"God only listens to the men around here." — Gibson Girl',
'"Nothing happened here except everything." — Thoroughfare',
'"Hallelujah, hallelujah, come back to me." — Strangers',
'"Sometimes I swear I can still hear the choir." — Hard Road',
'"Do not mistake her softness for surrender." — American Teenager'
];
function initEpigraph() {
var sidebar = qs( '#mw-panel, .vector-sidebar-toc-container, #vector-toc-collapsed-button' );
if ( !sidebar ) return;
var quote = EPIGRAPHS[ randInt( 0, EPIGRAPHS.length - 1 ) ];
var el = document.createElement( 'div' );
el.setAttribute( 'aria-label', 'Album epigraph' );
el.style.cssText =
'margin: 1.5rem 0.8rem;' +
'padding: 0.8rem 1rem;' +
'border-left: 2px solid rgba(122,28,36,0.5);' +
'font-family: "IM Fell English", Georgia, serif;' +
'font-style: italic;' +
'font-size: 0.82rem;' +
'color: rgba(158,139,114,0.8);' +
'line-height: 1.6;';
el.textContent = quote;
sidebar.appendChild( el );
}
/* ── INIT — run after DOM ready ──────────────────────────── */
function main() {
initWatermark();
initDust();
initFlicker();
initReveal();
initCandlePulse();
initDividers();
initTypewriter();
styleRedlinks();
initSidebarAnimations();
initTableStagger();
initCursorTrail();
initPageTransition();
initEpigraph();
/* Mark body so CSS can scope theme selectors */
document.documentElement.classList.add( 'pd-theme' );
/* Console signature */
console.log(
'%c✝ Preacher\'s Daughter Wiki Theme%c\n' +
' Ethel Cain · 2022 · Southern Gothic\n' +
' "God is a place you will wait for the rest of your life."',
'color:#c03040;font-size:14px;font-weight:bold;',
'color:#9e8b72;font-size:11px;'
);
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', main );
} else {
main();
}
} )();