MediaWiki:Common.js: Difference between revisions
Appearance
Sonja says (talk | contribs) No edit summary Tag: Reverted |
floating particles |
||
| (18 intermediate revisions by 2 users not shown) | |||
| Line 81: | Line 81: | ||
// Put it before the default icons (Talk/History/Edit) | // Put it before the default icons (Talk/History/Edit) | ||
icons.prepend(wrap); | |||
const pad = (n) => String(n).padStart(2, '0'); | const pad = (n) => String(n).padStart(2, '0'); | ||
| Line 124: | Line 124: | ||
// =============================== | // =============================== | ||
// Sticky Header Game Time | // Sticky Header Game Time | ||
// Display: "Mar 1 YR1" | // Display: "Mar 1 YR1" | ||
// Rules: | // Rules: | ||
// - Each real hour = +1 in-game day | // - Each real hour = +1 in-game day (1..24) | ||
// - Each 24 hours = +1 in-game month (Jan..Jul | // - Each 24 hours = +1 in-game month (Jan..Jul cycle) | ||
// - Each 7 | // - Each 7 days = +1 in-game year | ||
// =============================== | // =============================== | ||
mw.hook('wikipage.content').add(function () { | mw.hook('wikipage.content').add(function () { | ||
| Line 138: | Line 138: | ||
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul']; | const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul']; | ||
// --------- ANCHOR: | // --------- ANCHOR: right now is Mar 1 YR1 ---------- | ||
const ANCHOR_MONTH_INDEX = | const ANCHOR_MONTH_INDEX = 5; // Mar (Jan=0) | ||
const ANCHOR_DAY = | const ANCHOR_DAY = 14; // 1..24 | ||
const ANCHOR_YEAR = 1; // YR1 | const ANCHOR_YEAR = 1; // YR1 | ||
// | // If you want the game to follow a fixed timezone for everyone: | ||
const | // 0 = UTC. (Use 0 unless you KNOW you want another fixed offset.) | ||
const GAME_TZ_OFFSET_HOURS = 0; | |||
const getShiftedNow = () => new Date(Date.now() + GAME_TZ_OFFSET_HOURS * 3600000); | const getShiftedNow = () => new Date(Date.now() + GAME_TZ_OFFSET_HOURS * 3600000); | ||
// Anchor to the current | // Anchor to the current hour boundary (so it flips cleanly each hour) | ||
const now = getShiftedNow(); | const now = getShiftedNow(); | ||
const anchorRealHour = new Date(Date.UTC( | const anchorRealHour = new Date(Date.UTC( | ||
| Line 157: | Line 158: | ||
)); | )); | ||
// | // UI pill | ||
const wrap = document.createElement('span'); | const wrap = document.createElement('span'); | ||
wrap.id = 'sticky-game-time'; | wrap.id = 'sticky-game-time'; | ||
| Line 166: | Line 167: | ||
wrap.appendChild(value); | wrap.appendChild(value); | ||
// Put it before the default icons | |||
icons.prepend(wrap); | |||
function computeGameString() { | function computeGameString() { | ||
const t = getShiftedNow(); | const t = getShiftedNow(); | ||
// | // Whole hours passed since we declared "Mar 1 YR1" | ||
const hoursPassed = Math.floor((t.getTime() - anchorRealHour.getTime()) / 3600000); | const hoursPassed = Math.floor((t.getTime() - anchorRealHour.getTime()) / 3600000); | ||
// Day | // Day increments each hour | ||
const | const dayIndex = (ANCHOR_DAY - 1) + hoursPassed; // 0-based | ||
const dayNumber = ( | const dayNumber = (dayIndex % 24) + 1; // 1..24 | ||
// | // Month increments each 24 hours | ||
const daysPassed = Math.floor( | const daysPassed = Math.floor(dayIndex / 24); // 0..∞ | ||
// | // Month+Year cycle is 7 days long | ||
const | const totalMonthIndex = (ANCHOR_YEAR - 1) * 7 + ANCHOR_MONTH_INDEX + daysPassed; | ||
const year = Math.floor( | const year = Math.floor(totalMonthIndex / 7) + 1; | ||
const month = months[(( | const month = months[((totalMonthIndex % 7) + 7) % 7]; | ||
return `${month} ${dayNumber} YR${year}`; | return `${month} ${dayNumber} YR${year}`; | ||
| Line 195: | Line 197: | ||
} | } | ||
// | // Initial update | ||
update(); | update(); | ||
// Update exactly on | // Update exactly on next hour, then every hour | ||
const now2 = getShiftedNow(); | const now2 = getShiftedNow(); | ||
const msUntilNextHour = | const msUntilNextHour = | ||
| Line 240: | Line 242: | ||
// Put it near the front (use appendChild to put it at the end) | // Put it near the front (use appendChild to put it at the end) | ||
icons.prepend(wrap); | |||
const fmt = new Intl.DateTimeFormat('en-GB', { | const fmt = new Intl.DateTimeFormat('en-GB', { | ||
| Line 258: | Line 260: | ||
}); | }); | ||
// ====== | // 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); | |||
})(); | |||
Latest revision as of 04:10, 17 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"
// Rules:
// - Each real hour = +1 in-game day (1..24)
// - Each 24 hours = +1 in-game month (Jan..Jul cycle)
// - Each 7 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 = 5; // Mar (Jan=0)
const ANCHOR_DAY = 14; // 1..24
const ANCHOR_YEAR = 1; // YR1
// If you want the game to follow a fixed timezone for everyone:
// 0 = UTC. (Use 0 unless you KNOW you want another fixed offset.)
const GAME_TZ_OFFSET_HOURS = 0;
const getShiftedNow = () => new Date(Date.now() + GAME_TZ_OFFSET_HOURS * 3600000);
// Anchor to the current hour boundary (so it flips cleanly each hour)
const now = getShiftedNow();
const anchorRealHour = new Date(Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate(),
now.getUTCHours(), 0, 0, 0
));
// 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);
// Put it before the default icons
icons.prepend(wrap);
function computeGameString() {
const t = getShiftedNow();
// Whole hours passed since we declared "Mar 1 YR1"
const hoursPassed = Math.floor((t.getTime() - anchorRealHour.getTime()) / 3600000);
// Day increments each hour
const dayIndex = (ANCHOR_DAY - 1) + hoursPassed; // 0-based
const dayNumber = (dayIndex % 24) + 1; // 1..24
// Month increments each 24 hours
const daysPassed = Math.floor(dayIndex / 24); // 0..∞
// Month+Year cycle is 7 days long
const totalMonthIndex = (ANCHOR_YEAR - 1) * 7 + ANCHOR_MONTH_INDEX + daysPassed;
const year = Math.floor(totalMonthIndex / 7) + 1;
const month = months[((totalMonthIndex % 7) + 7) % 7];
return `${month} ${dayNumber} YR${year}`;
}
function update() {
value.textContent = computeGameString();
}
// Initial update
update();
// Update exactly on next hour, 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);
});
// 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);
})();