MediaWiki:Common.js: Difference between revisions
Appearance
Sonja says (talk | contribs) No edit summary Tag: Manual revert |
floating particles putting back in |
||
| (15 intermediate revisions by one other user not shown) | |||
| Line 1: | Line 1: | ||
/* Any JavaScript here will be loaded for all users on every page load. */ | /* Any JavaScript here will be loaded for all users on every page load. */ | ||
(function () { | |||
'use strict'; | |||
/* ========================================================= | |||
CONFIG | |||
========================================================= */ | |||
const DISCORD_URL = 'https://discord.gg/t2Rp2dRvze'; | |||
// | // Countdown target: Sat, 18 Apr 2026 00:00 UTC | ||
const COUNTDOWN_TARGET_UTC = Date.parse('2026-04-18T00:00:00Z'); | |||
// Clock timezone (same for everyone) | |||
const CLOCK_TIMEZONE = 'Etc/UTC'; | |||
// Game time anchor (real UTC -> game time) | |||
// At 2026-02-19 14:00:00 UTC, game time was Jan 21 YR2 | |||
const GAME_ANCHOR_REAL_UTC = Date.parse('2026-02-19T14:00:00Z'); | |||
const GAME_ANCHOR_MONTH_INDEX = 0; // Jan | |||
const GAME_ANCHOR_DAY = 21; // 1..24 | |||
const GAME_ANCHOR_YEAR = 2; // YR2 | |||
const GAME_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; | |||
// Tick warning threshold | |||
const TICK_SOON_MINUTES = 5; | |||
const pad2 = (n) => String(n).padStart(2, '0'); | |||
/ | /* ========================================================= | ||
Tabs (your existing code) | |||
========================================================= */ | |||
$(function () { | |||
$('.wiki-tabs-container').each(function () { | |||
const $container = $(this); | |||
function | const $firstButton = $container.find('.wiki-tab-button').first(); | ||
const | const firstTabId = $firstButton.data('tab'); | ||
if (!firstTabId) return; | |||
$firstButton.addClass('active'); | |||
$container.find('#' + firstTabId).addClass('active').show(); | |||
}); | |||
$('.wiki-tab-button').on('click', function () { | |||
const $button = $(this); | |||
const tabId = $button.data('tab'); | |||
if (!tabId) return; | |||
const $container = $button.closest('.wiki-tabs-container'); | |||
$container.find('.wiki-tab-button').removeClass('active'); | |||
$container.find('.wiki-tab-pane').removeClass('active').hide(); | |||
$button.addClass('active'); | |||
$container.find('#' + tabId).addClass('active').show(); | |||
}); | |||
}); | |||
if (document.getElementById( | /* ========================================================= | ||
Helpers: find/create containers in BOTH headers | |||
========================================================= */ | |||
function ensureDiscordLink(container, id) { | |||
if (!container) return null; | |||
const existing = document.getElementById(id); | |||
if (existing) return existing; | |||
const link = document.createElement('a'); | const link = document.createElement('a'); | ||
link.id = | link.id = id; | ||
link.href = | link.href = DISCORD_URL; | ||
link.target = '_blank'; | link.target = '_blank'; | ||
link.className = 'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet'; | link.className = | ||
'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet sticky-discord-link'; | |||
link.textContent = 'Join Us on Discord!'; | link.textContent = 'Join Us on Discord!'; | ||
link.style.fontWeight = 'bold'; | link.style.fontWeight = 'bold'; | ||
container.prepend(link); | |||
return link; | |||
} | } | ||
// Sticky header: insert our widget bar inside sticky icons row | |||
// Sticky | function getStickyBar() { | ||
const icons = document.querySelector('.vector-sticky-header-icons'); | const icons = document.querySelector('.vector-sticky-header-icons'); | ||
if (!icons) return; | if (!icons) return null; | ||
let bar = document.getElementById('sticky-time-widgets'); | |||
if (bar) return bar; | |||
const | // Keep Discord where it was (far-left in sticky icons) | ||
const discord = ensureDiscordLink(icons, 'custom-sticky-link'); | |||
bar = document.createElement('span'); | |||
bar.id = 'sticky-time-widgets'; | |||
bar.className = 'time-widgets'; | |||
// Put widgets immediately AFTER Discord (so Discord stays at the left) | |||
if (discord && discord.parentNode === icons) { | |||
discord.insertAdjacentElement('afterend', bar); | |||
} else { | |||
icons.prepend(bar); | |||
} | |||
return bar; | |||
} | |||
// Main header: put our widgets in header end area | |||
function getMainBar() { | |||
const header = document.querySelector('.vector-header'); | |||
if (!header) return null; | |||
const headerEnd = header.querySelector('.vector-header-end'); | |||
if (!headerEnd) return null; | |||
let bar = document.getElementById('main-time-widgets'); | |||
if (bar) return bar; | |||
bar = document.createElement('span'); | |||
bar.id = 'main-time-widgets'; | |||
bar.className = 'time-widgets'; | |||
headerEnd.prepend(bar); | |||
ensureDiscordLink(bar, 'main-discord-link'); | |||
return bar; | |||
} | |||
function ensureWidget(bar, role, labelText) { | |||
if (!bar) return; | |||
if (bar.querySelector(`[data-role="${role}"]`)) return; | |||
const wrap = document.createElement('span'); | const wrap = document.createElement('span'); | ||
wrap.className = 'header-widget'; | |||
wrap.className = ' | |||
const label = document.createElement('span'); | if (labelText) { | ||
const label = document.createElement('span'); | |||
label.className = 'header-widget__label'; | |||
label.textContent = labelText; | |||
wrap.appendChild(label); | |||
} | |||
const value = document.createElement('span'); | const value = document.createElement('span'); | ||
value.className = ' | value.className = 'header-widget__value'; | ||
value.dataset.role = role; | |||
wrap.appendChild(value); | wrap.appendChild(value); | ||
bar.appendChild(wrap); | |||
} | |||
/* ========================================================= | |||
Calculations / formatting | |||
========================================================= */ | |||
function 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 ${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`; | |||
return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`; | |||
} | |||
const | function computeGameString(nowMs) { | ||
const hoursPassed = Math.floor((nowMs - GAME_ANCHOR_REAL_UTC) / 3600000); | |||
// Day increments every hour (1..24) | |||
const dayIndex = (GAME_ANCHOR_DAY - 1) + hoursPassed; | |||
const dayNumber = ((dayIndex % 24) + 24) % 24 + 1; | |||
// Month increments every 24 hours (Jan..Jul cycle) | |||
const daysPassed = Math.floor(dayIndex / 24); | |||
const totalMonthIndex = GAME_ANCHOR_MONTH_INDEX + daysPassed; | |||
const monthIndex = ((totalMonthIndex % 7) + 7) % 7; | |||
// Year increments every 7 real days | |||
const yearsPassed = Math.floor(totalMonthIndex / 7); | |||
const year = GAME_ANCHOR_YEAR + yearsPassed; | |||
return `Current Game Date: ${GAME_MONTHS[monthIndex]} ${dayNumber} YR${year}`; | |||
} | |||
const clockFmt = new Intl.DateTimeFormat('en-GB', { | |||
timeZone: CLOCK_TIMEZONE, | |||
hour: '2-digit', | |||
minute: '2-digit', | |||
second: '2-digit', | |||
hour12: false | |||
}); | |||
function updateAll() { | |||
const now = Date.now(); | |||
// --- CLOCK --- | |||
document.querySelectorAll('[data-role="clock"]').forEach((el) => { | |||
el.textContent = clockFmt.format(new Date(now)); | |||
}); | }); | ||
// --- GAME DATE (Tick warning 5 minutes before the hour) --- | |||
const msUntilNextHour = 3600000 - (now % 3600000); | |||
const thresholdMs = TICK_SOON_MINUTES * 60 * 1000; | |||
const shouldWarn = msUntilNextHour > 0 && msUntilNextHour <= thresholdMs; | |||
const minutesLeft = Math.ceil(msUntilNextHour / 60000); | |||
document.querySelectorAll('[data-role="game"]').forEach((el) => { | |||
if (shouldWarn) { | |||
el.textContent = `TICK SOON (${minutesLeft}m)`; | |||
el.classList.add('pulse-red'); | |||
} else { | |||
el.textContent = computeGameString(now); | |||
el.classList.remove('pulse-red'); | |||
} | |||
}); | |||
// --- COUNTDOWN --- | |||
document.querySelectorAll('[data-role="countdown"]').forEach((el) => { | |||
el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now); | |||
}); | |||
} | |||
/* ========================================================= | |||
Build + Boot | |||
========================================================= */ | |||
function buildBarsIfPossible() { | |||
const stickyBar = getStickyBar(); | |||
const mainBar = getMainBar(); | |||
if (stickyBar) { | |||
ensureWidget(stickyBar, 'clock', 'UTC:'); | |||
ensureWidget(stickyBar, 'game', null); | |||
ensureWidget(stickyBar, 'countdown', 'Age ends in:'); | |||
} | |||
if (mainBar) { | |||
ensureWidget(mainBar, 'clock', 'UTC:'); | |||
ensureWidget(mainBar, 'game', null); | |||
ensureWidget(mainBar, 'countdown', 'Age ends in:'); | |||
} | } | ||
} | |||
function startTickerOnce() { | |||
setInterval( | if (window.__timeWidgetsIntervalId) return; | ||
}); | updateAll(); | ||
window.__timeWidgetsIntervalId = setInterval(updateAll, 1000); | |||
} | |||
function initAll() { | |||
buildBarsIfPossible(); | |||
updateAll(); | |||
startTickerOnce(); | |||
} | |||
$(initAll); | |||
mw.hook('wikipage.content').add(initAll); | |||
mw.hook('skin.ready').add(initAll); | |||
})(); | |||
// Floating particles | // Floating particles | ||
| Line 132: | Line 269: | ||
'rgba(0, 29, 61, 0.8)', // #001d3d | 'rgba(0, 29, 61, 0.8)', // #001d3d | ||
'rgba(0, 8, 20, 0.7)', // #000814 | 'rgba(0, 8, 20, 0.7)', // #000814 | ||
'rgba(255, 255, 255, 0.15)' // subtle white glint | 'rgba(255, 255, 255, 0.15)', // subtle white glint | ||
]; | ]; | ||
| Line 154: | Line 291: | ||
opacity: 0, | opacity: 0, | ||
fadeIn: randomBetween(0.003, 0.008), | fadeIn: randomBetween(0.003, 0.008), | ||
life: randomBetween(0.4, 1) | life: randomBetween(0.4, 1), // 0–1, fades out after 0.8 | ||
}; | }; | ||
} | } | ||
| Line 160: | Line 297: | ||
for (let i = 0; i < COUNT; i++) { | for (let i = 0; i < COUNT; i++) { | ||
const p = createParticle(); | const p = createParticle(); | ||
p.y = randomBetween(0, canvas.height); | p.y = randomBetween(0, canvas.height); // spread on init | ||
p.opacity = randomBetween(0, 1); | p.opacity = randomBetween(0, 1); | ||
particles.push(p); | particles.push(p); | ||
| Line 169: | Line 306: | ||
particles.forEach((p, i) => { | particles.forEach((p, i) => { | ||
// Fade in / out | |||
if (p.life < 0.8) { | if (p.life < 0.8) { | ||
p.opacity = Math.min(1, p.opacity + p.fadeIn); | p.opacity = Math.min(1, p.opacity + p.fadeIn); | ||
| Line 179: | Line 317: | ||
p.x += p.speedX; | p.x += p.speedX; | ||
// Reset when off screen or fully faded | |||
if (p.y < -10 || p.life > 1.2) { | if (p.y < -10 || p.life > 1.2) { | ||
particles[i] = createParticle(); | particles[i] = createParticle(); | ||
| Line 190: | Line 329: | ||
ctx.fill(); | ctx.fill(); | ||
// Soft glow | |||
ctx.shadowBlur = 8; | ctx.shadowBlur = 8; | ||
ctx.shadowColor = p.color; | ctx.shadowColor = p.color; | ||
| Line 204: | Line 344: | ||
window.addEventListener('resize', resize); | window.addEventListener('resize', resize); | ||
})(); | })(); | ||
Latest revision as of 02:55, 21 February 2026
/* Any JavaScript here will be loaded for all users on every page load. */
(function () {
'use strict';
/* =========================================================
CONFIG
========================================================= */
const DISCORD_URL = 'https://discord.gg/t2Rp2dRvze';
// Countdown target: Sat, 18 Apr 2026 00:00 UTC
const COUNTDOWN_TARGET_UTC = Date.parse('2026-04-18T00:00:00Z');
// Clock timezone (same for everyone)
const CLOCK_TIMEZONE = 'Etc/UTC';
// Game time anchor (real UTC -> game time)
// At 2026-02-19 14:00:00 UTC, game time was Jan 21 YR2
const GAME_ANCHOR_REAL_UTC = Date.parse('2026-02-19T14:00:00Z');
const GAME_ANCHOR_MONTH_INDEX = 0; // Jan
const GAME_ANCHOR_DAY = 21; // 1..24
const GAME_ANCHOR_YEAR = 2; // YR2
const GAME_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
// Tick warning threshold
const TICK_SOON_MINUTES = 5;
const pad2 = (n) => String(n).padStart(2, '0');
/* =========================================================
Tabs (your existing code)
========================================================= */
$(function () {
$('.wiki-tabs-container').each(function () {
const $container = $(this);
const $firstButton = $container.find('.wiki-tab-button').first();
const firstTabId = $firstButton.data('tab');
if (!firstTabId) return;
$firstButton.addClass('active');
$container.find('#' + firstTabId).addClass('active').show();
});
$('.wiki-tab-button').on('click', function () {
const $button = $(this);
const tabId = $button.data('tab');
if (!tabId) return;
const $container = $button.closest('.wiki-tabs-container');
$container.find('.wiki-tab-button').removeClass('active');
$container.find('.wiki-tab-pane').removeClass('active').hide();
$button.addClass('active');
$container.find('#' + tabId).addClass('active').show();
});
});
/* =========================================================
Helpers: find/create containers in BOTH headers
========================================================= */
function ensureDiscordLink(container, id) {
if (!container) return null;
const existing = document.getElementById(id);
if (existing) return existing;
const link = document.createElement('a');
link.id = id;
link.href = DISCORD_URL;
link.target = '_blank';
link.className =
'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet sticky-discord-link';
link.textContent = 'Join Us on Discord!';
link.style.fontWeight = 'bold';
container.prepend(link);
return link;
}
// Sticky header: insert our widget bar inside sticky icons row
function getStickyBar() {
const icons = document.querySelector('.vector-sticky-header-icons');
if (!icons) return null;
let bar = document.getElementById('sticky-time-widgets');
if (bar) return bar;
// Keep Discord where it was (far-left in sticky icons)
const discord = ensureDiscordLink(icons, 'custom-sticky-link');
bar = document.createElement('span');
bar.id = 'sticky-time-widgets';
bar.className = 'time-widgets';
// Put widgets immediately AFTER Discord (so Discord stays at the left)
if (discord && discord.parentNode === icons) {
discord.insertAdjacentElement('afterend', bar);
} else {
icons.prepend(bar);
}
return bar;
}
// Main header: put our widgets in header end area
function getMainBar() {
const header = document.querySelector('.vector-header');
if (!header) return null;
const headerEnd = header.querySelector('.vector-header-end');
if (!headerEnd) return null;
let bar = document.getElementById('main-time-widgets');
if (bar) return bar;
bar = document.createElement('span');
bar.id = 'main-time-widgets';
bar.className = 'time-widgets';
headerEnd.prepend(bar);
ensureDiscordLink(bar, 'main-discord-link');
return bar;
}
function ensureWidget(bar, role, labelText) {
if (!bar) return;
if (bar.querySelector(`[data-role="${role}"]`)) return;
const wrap = document.createElement('span');
wrap.className = 'header-widget';
if (labelText) {
const label = document.createElement('span');
label.className = 'header-widget__label';
label.textContent = labelText;
wrap.appendChild(label);
}
const value = document.createElement('span');
value.className = 'header-widget__value';
value.dataset.role = role;
wrap.appendChild(value);
bar.appendChild(wrap);
}
/* =========================================================
Calculations / formatting
========================================================= */
function 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 ${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
}
function computeGameString(nowMs) {
const hoursPassed = Math.floor((nowMs - GAME_ANCHOR_REAL_UTC) / 3600000);
// Day increments every hour (1..24)
const dayIndex = (GAME_ANCHOR_DAY - 1) + hoursPassed;
const dayNumber = ((dayIndex % 24) + 24) % 24 + 1;
// Month increments every 24 hours (Jan..Jul cycle)
const daysPassed = Math.floor(dayIndex / 24);
const totalMonthIndex = GAME_ANCHOR_MONTH_INDEX + daysPassed;
const monthIndex = ((totalMonthIndex % 7) + 7) % 7;
// Year increments every 7 real days
const yearsPassed = Math.floor(totalMonthIndex / 7);
const year = GAME_ANCHOR_YEAR + yearsPassed;
return `Current Game Date: ${GAME_MONTHS[monthIndex]} ${dayNumber} YR${year}`;
}
const clockFmt = new Intl.DateTimeFormat('en-GB', {
timeZone: CLOCK_TIMEZONE,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
function updateAll() {
const now = Date.now();
// --- CLOCK ---
document.querySelectorAll('[data-role="clock"]').forEach((el) => {
el.textContent = clockFmt.format(new Date(now));
});
// --- GAME DATE (Tick warning 5 minutes before the hour) ---
const msUntilNextHour = 3600000 - (now % 3600000);
const thresholdMs = TICK_SOON_MINUTES * 60 * 1000;
const shouldWarn = msUntilNextHour > 0 && msUntilNextHour <= thresholdMs;
const minutesLeft = Math.ceil(msUntilNextHour / 60000);
document.querySelectorAll('[data-role="game"]').forEach((el) => {
if (shouldWarn) {
el.textContent = `TICK SOON (${minutesLeft}m)`;
el.classList.add('pulse-red');
} else {
el.textContent = computeGameString(now);
el.classList.remove('pulse-red');
}
});
// --- COUNTDOWN ---
document.querySelectorAll('[data-role="countdown"]').forEach((el) => {
el.textContent = formatRemaining(COUNTDOWN_TARGET_UTC - now);
});
}
/* =========================================================
Build + Boot
========================================================= */
function buildBarsIfPossible() {
const stickyBar = getStickyBar();
const mainBar = getMainBar();
if (stickyBar) {
ensureWidget(stickyBar, 'clock', 'UTC:');
ensureWidget(stickyBar, 'game', null);
ensureWidget(stickyBar, 'countdown', 'Age ends in:');
}
if (mainBar) {
ensureWidget(mainBar, 'clock', 'UTC:');
ensureWidget(mainBar, 'game', null);
ensureWidget(mainBar, 'countdown', 'Age ends in:');
}
}
function startTickerOnce() {
if (window.__timeWidgetsIntervalId) return;
updateAll();
window.__timeWidgetsIntervalId = setInterval(updateAll, 1000);
}
function initAll() {
buildBarsIfPossible();
updateAll();
startTickerOnce();
}
$(initAll);
mw.hook('wikipage.content').add(initAll);
mw.hook('skin.ready').add(initAll);
})();
// Floating particles
(function () {
const canvas = document.createElement('canvas');
canvas.id = 'particle-canvas';
document.body.prepend(canvas);
const ctx = canvas.getContext('2d');
const particles = [];
const COUNT = 80;
const COLORS = [
'rgba(0, 53, 102, 0.9)', // #003566
'rgba(0, 29, 61, 0.8)', // #001d3d
'rgba(0, 8, 20, 0.7)', // #000814
'rgba(255, 255, 255, 0.15)', // subtle white glint
];
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function randomBetween(a, b) {
return a + Math.random() * (b - a);
}
function createParticle() {
return {
x: randomBetween(0, canvas.width),
y: randomBetween(canvas.height * 0.2, canvas.height),
radius: randomBetween(1.5, 5),
color: COLORS[Math.floor(Math.random() * COLORS.length)],
speedY: randomBetween(0.2, 0.7),
speedX: randomBetween(-0.2, 0.2),
opacity: 0,
fadeIn: randomBetween(0.003, 0.008),
life: randomBetween(0.4, 1), // 0–1, fades out after 0.8
};
}
for (let i = 0; i < COUNT; i++) {
const p = createParticle();
p.y = randomBetween(0, canvas.height); // spread on init
p.opacity = randomBetween(0, 1);
particles.push(p);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((p, i) => {
// Fade in / out
if (p.life < 0.8) {
p.opacity = Math.min(1, p.opacity + p.fadeIn);
} else {
p.opacity = Math.max(0, p.opacity - p.fadeIn * 0.5);
}
p.life += 0.001;
p.y -= p.speedY;
p.x += p.speedX;
// Reset when off screen or fully faded
if (p.y < -10 || p.life > 1.2) {
particles[i] = createParticle();
return;
}
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.globalAlpha = p.opacity;
ctx.fillStyle = p.color;
ctx.fill();
// Soft glow
ctx.shadowBlur = 8;
ctx.shadowColor = p.color;
ctx.fill();
ctx.shadowBlur = 0;
});
ctx.globalAlpha = 1;
requestAnimationFrame(draw);
}
resize();
draw();
window.addEventListener('resize', resize);
})();