MediaWiki:Gadget-AnniversaryTheme.js
MediaWiki界面页面
更多操作
注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
- Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
- Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5。
/* Gadget: OTTOWikiAnniversary - confetti cannons (left+right) + 🎉 hint + ribbons + ♿ easter egg
* Changes:
* - Canvas does NOT fade out; particles fall off-screen naturally, then canvas removed.
* - Only 🎉 hint fades out.
* - Right 🎉 is mirrored (symmetry).
*/
(function () {
if (typeof mw === 'undefined' || !mw.config) return;
/** =======================
* 配置区:按需改这里
* ======================= */
var CFG = {
mainPageOnly: true,
oncePerSession: true,
sessionKey: 'ottowiki_anni_confetti_shown',
respectReducedMotion: true,
// 活动日期范围:不想限制就填 null
activeFrom: null, // '2026-02-01'
activeTo: null, // '2026-02-07'
// 进入后延迟多久再放(ms)
startDelay: 1000,
// 超时兜底:即使还有粒子没走完,超过这个时间也会结束(避免极端情况下不结束)
// 建议给足够大(比如 12s)
maxRuntime: 12000,
// 每边礼炮“普通碎片”数量(两边*2;第二波再*2)
particlesPerSide: 150,
// 每边“彩带”数量(两边*2;第二波再*2)
ribbonsPerSide: 22,
// 第二波延迟(ms),设为 0 就只有一波
secondWaveDelay: 260,
// 颜色(周年红+金+点缀)
colors: ['#d81b2a', '#f3c46b', '#ff6b6b', '#ffd166', '#ffffff'],
zIndex: 99999,
// 🎉 提示(两侧显示)
showHintEmoji: true,
hintEmoji: '🎉',
// 🎉 轻微弹跳
hintEmojiBounce: true,
// ===== ♿ 彩蛋(混入爆炸物)=====
eggEnabled: true,
eggEmojis: ['♿'],
// 每一波每一侧的 ♿ 数量随机范围(彩蛋式但偏多一点)
eggMinPerSidePerWave: 4,
eggMaxPerSidePerWave: 10,
// 字号范围
eggFontMin: 16,
eggFontMax: 24
};
function inActiveWindow() {
if (!CFG.activeFrom || !CFG.activeTo) return true;
var start = new Date(CFG.activeFrom + 'T00:00:00');
var end = new Date(CFG.activeTo + 'T23:59:59');
var now = new Date();
return now >= start && now <= end;
}
function shouldRun() {
if (CFG.mainPageOnly && !mw.config.get('wgIsMainPage')) return false;
if (CFG.respectReducedMotion && window.matchMedia) {
try {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return false;
} catch (e) {}
}
if (!inActiveWindow()) return false;
if (CFG.oncePerSession) {
try {
if (sessionStorage.getItem(CFG.sessionKey) === '1') return false;
sessionStorage.setItem(CFG.sessionKey, '1');
} catch (e) {}
}
return true;
}
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function randInt(min, max) {
return Math.floor(rand(min, max + 1));
}
function clamp(n, a, b) {
return Math.max(a, Math.min(b, n));
}
function createHintEmojiUI(z) {
var wrap = document.createElement('div');
wrap.className = 'ottowiki-anni-hint-wrap';
wrap.style.cssText = [
'position:fixed',
'inset:0',
'pointer-events:none',
'z-index:' + (z + 1),
'opacity:1'
].join(';');
// 用子 span 做镜像,避免跟父级 transform/动画打架
function makeOne(side) {
var el = document.createElement('div');
el.className = 'ottowiki-anni-hint ottowiki-anni-hint-' + side;
var span = document.createElement('span');
span.textContent = CFG.hintEmoji;
span.setAttribute('aria-hidden', 'true');
// 右侧镜像
if (side === 'right') {
span.style.transform = 'scaleX(-1)';
span.style.display = 'inline-block';
}
el.appendChild(span);
var left = side === 'left' ? '6%' : '94%';
el.style.cssText = [
'position:absolute',
'left:' + left,
'top:78%',
'transform:translate(-50%,-50%)',
'font-size:28px',
'filter:drop-shadow(0 2px 6px rgba(0,0,0,.25))',
'opacity:0',
'transition:opacity 260ms ease, transform 260ms ease'
].join(';');
wrap.appendChild(el);
return el;
}
var leftEl = makeOne('left');
var rightEl = makeOne('right');
var styleEl = null;
if (CFG.hintEmojiBounce) {
styleEl = document.createElement('style');
styleEl.textContent =
"@keyframes ottowikiAnniHintBounce{0%,100%{transform:translate(-50%,-60%) scale(1)}50%{transform:translate(-50%,-72%) scale(1.06)}}";
document.head.appendChild(styleEl);
leftEl.style.animation = 'ottowikiAnniHintBounce 900ms ease-in-out 1';
rightEl.style.animation = 'ottowikiAnniHintBounce 900ms ease-in-out 1';
}
document.body.appendChild(wrap);
requestAnimationFrame(function () {
leftEl.style.opacity = '1';
leftEl.style.transform = 'translate(-50%,-60%)';
rightEl.style.opacity = '1';
rightEl.style.transform = 'translate(-50%,-60%)';
});
return {
fadeOutAndRemove: function () {
wrap.style.transition = 'opacity 360ms ease';
wrap.style.opacity = '0';
setTimeout(function () {
if (wrap && wrap.parentNode) wrap.parentNode.removeChild(wrap);
if (styleEl && styleEl.parentNode) styleEl.parentNode.removeChild(styleEl);
}, 380);
}
};
}
function launchConfettiCannons() {
var hintUI = null;
if (CFG.showHintEmoji) {
hintUI = createHintEmojiUI(CFG.zIndex);
}
var canvas = document.createElement('canvas');
canvas.className = 'ottowiki-anni-confetti';
canvas.style.cssText = [
'position:fixed',
'inset:0',
'width:100%',
'height:100%',
'pointer-events:none',
'z-index:' + CFG.zIndex
].join(';');
var ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
function resize() {
var dpr = Math.max(1, window.devicePixelRatio || 1);
canvas.width = Math.floor(window.innerWidth * dpr);
canvas.height = Math.floor(window.innerHeight * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
window.addEventListener('resize', resize, { passive: true });
var particles = [];
var W = function () { return window.innerWidth; };
var H = function () { return window.innerHeight; };
function pickColor() {
return CFG.colors[(Math.random() * CFG.colors.length) | 0];
}
function makeEggParticle(baseX, baseY, aim) {
var angleCenter = (-Math.PI / 2) + (aim * Math.PI * 0.18);
var angle = angleCenter + rand(-0.40, 0.40);
var speed = rand(7, 13);
var fontSize = rand(CFG.eggFontMin, CFG.eggFontMax);
var text = CFG.eggEmojis[(Math.random() * CFG.eggEmojis.length) | 0];
particles.push({
type: 'emoji',
text: text,
x: baseX + rand(-10, 10),
y: baseY + rand(-10, 10),
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
g: rand(0.10, 0.16),
drag: rand(0.985, 0.994),
rot: rand(-0.25, 0.25),
vr: rand(-0.06, 0.06),
fontSize: fontSize
// 注意:emoji 不再 fade,靠“落出屏幕”消失
});
}
// type: 'confetti' | 'smoke' | 'ribbon' | 'emoji'
function fireCannon(from) {
var baseX = from === 'left' ? W() * 0.08 : W() * 0.92;
var baseY = H() * 0.78;
var aim = from === 'left' ? 1 : -1;
// ♿ 彩蛋目标数量(每波每侧随机)
var eggTarget = 0;
if (CFG.eggEnabled) {
var minEgg = clamp(CFG.eggMinPerSidePerWave, 0, 999);
var maxEgg = clamp(CFG.eggMaxPerSidePerWave, minEgg, 999);
eggTarget = randInt(minEgg, maxEgg);
}
var eggCount = 0;
// 1) confetti(部分随机替换为 emoji)
for (var i = 0; i < CFG.particlesPerSide; i++) {
if (CFG.eggEnabled && eggCount < eggTarget) {
var remainingSlots = (CFG.particlesPerSide - i);
var remainingEgg = (eggTarget - eggCount);
var pReplace = clamp(remainingEgg / Math.max(1, remainingSlots), 0.08, 0.45);
if (Math.random() < pReplace) {
makeEggParticle(baseX, baseY, aim);
eggCount++;
continue;
}
}
var angleCenter = (-Math.PI / 2) + (aim * Math.PI * 0.18);
var angle = angleCenter + rand(-0.55, 0.55);
var speed = rand(7, 14);
var size = rand(3, 7);
particles.push({
type: 'confetti',
x: baseX + rand(-8, 8),
y: baseY + rand(-8, 8),
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
g: rand(0.10, 0.16),
drag: rand(0.985, 0.994),
rot: rand(0, Math.PI),
vr: rand(-0.28, 0.28),
w: size,
h: size * 0.7,
color: pickColor(),
shape: Math.random() < 0.75 ? 'rect' : 'circle'
// 注意:confetti 不再 fade,靠“落出屏幕”消失
});
}
// 补齐彩蛋(极小概率)
if (CFG.eggEnabled && eggCount < eggTarget) {
var need = eggTarget - eggCount;
for (var k = 0; k < need; k++) makeEggParticle(baseX, baseY, aim);
}
// 2) ribbons(不 fade,靠落出屏幕消失)
for (var r = 0; r < CFG.ribbonsPerSide; r++) {
var raCenter = (-Math.PI / 2) + (aim * Math.PI * 0.16);
var ra = raCenter + rand(-0.35, 0.35);
var rs = rand(6.5, 11.5);
particles.push({
type: 'ribbon',
x: baseX + rand(-10, 10),
y: baseY + rand(-10, 10),
vx: Math.cos(ra) * rs,
vy: Math.sin(ra) * rs,
g: rand(0.07, 0.12),
drag: rand(0.987, 0.996),
rot: rand(0, Math.PI),
vr: rand(-0.16, 0.16),
w: rand(3, 5),
h: rand(18, 30),
swayPhase: rand(0, Math.PI * 2),
swaySpeed: rand(0.06, 0.12),
swayAmp: rand(0.25, 0.55),
color: pickColor()
});
}
// 3) smoke(烟雾仍然 fade 掉比较自然)
var smokeCount = 44;
for (var s = 0; s < smokeCount; s++) {
var sa = (-Math.PI / 2) + (aim * Math.PI * 0.18) + rand(-0.7, 0.7);
var ss = rand(3, 6);
particles.push({
type: 'smoke',
x: baseX + rand(-10, 10),
y: baseY + rand(-10, 10),
vx: Math.cos(sa) * ss,
vy: Math.sin(sa) * ss,
g: rand(0.03, 0.07),
drag: rand(0.97, 0.985),
w: rand(10, 18),
alpha: rand(0.20, 0.42),
fade: rand(0.004, 0.010),
color: 'rgba(255,255,255,0.9)'
});
}
}
// 第一波
fireCannon('left');
fireCannon('right');
// 第二波
if (CFG.secondWaveDelay && CFG.secondWaveDelay > 0) {
setTimeout(function () {
fireCannon('left');
fireCannon('right');
}, CFG.secondWaveDelay);
}
var start = performance.now();
function draw(now) {
var t = now - start;
ctx.clearRect(0, 0, W(), H());
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
// 更新物理
p.vx *= p.drag;
p.vy = p.vy * p.drag + p.g;
if (p.type === 'ribbon') {
p.swayPhase += p.swaySpeed;
p.x += p.vx + Math.sin(p.swayPhase) * p.swayAmp;
p.y += p.vy;
p.rot += p.vr;
} else {
p.x += p.vx;
p.y += p.vy;
if (p.type === 'emoji') {
p.rot += p.vr;
}
}
// smoke alpha fade
if (p.type === 'smoke') {
p.alpha -= p.fade;
if (p.alpha <= 0) {
particles.splice(i, 1);
continue;
}
}
// 出界移除(不靠 fade)
if (p.y > H() + 140 || p.x < -200 || p.x > W() + 200) {
particles.splice(i, 1);
continue;
}
// 绘制
if (p.type === 'smoke') {
ctx.save();
ctx.globalAlpha = Math.max(0, p.alpha);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, (p.w * 0.55), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
continue;
}
if (p.type === 'emoji') {
ctx.save();
ctx.globalAlpha = 1;
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.font = '600 ' + Math.round(p.fontSize) + 'px system-ui, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(p.text, 0, 0);
ctx.restore();
continue;
}
ctx.save();
ctx.globalAlpha = 1;
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
if (p.type === 'ribbon') {
ctx.fillStyle = p.color;
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
// 折光边
ctx.globalAlpha = 0.22;
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.fillRect(-p.w / 2, -p.h / 2, Math.max(1, p.w * 0.35), p.h);
} else {
// confetti
ctx.fillStyle = p.color;
if (p.shape === 'circle') {
ctx.beginPath();
ctx.arc(0, 0, Math.max(1, p.w * 0.55), 0, Math.PI * 2);
ctx.fill();
} else {
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
}
}
ctx.restore();
}
// 结束条件:粒子走完(自然落出屏幕)或超时兜底
if (particles.length === 0 || t > CFG.maxRuntime) {
window.removeEventListener('resize', resize);
// 只让 🎉 渐隐
if (hintUI) hintUI.fadeOutAndRemove();
// canvas 直接移除(不做渐变)
if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);
return;
}
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
function run() {
if (!shouldRun()) return;
function start() {
setTimeout(function () {
if (!document.body) return;
launchConfettiCannons();
}, Math.max(0, CFG.startDelay | 0));
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
start();
} else {
document.addEventListener('DOMContentLoaded', start, { once: true });
}
}
run();
})();