MediaWiki:KuranokaFX.js
MediaWiki界面页面
更多操作
注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
- Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
- Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5。
/*
* KURANOKA / STORMBREAK v2
* File page: MediaWiki:KuranokaFX.js
* Only operates on the Kuranoka view page. Never rewrites / filters article text.
* Canvas-based cinematic storm / rain / spatial tear / glass rifts / clearing.
*/
(function () {
'use strict';
if (window.__kkStormbreakLoaded) return;
if (window.mw && (
mw.config.get('wgPageName') !== 'Kuranoka' ||
mw.config.get('wgAction') !== 'view' ||
mw.config.get('wgNamespaceNumber') !== 0
)) return;
function start() {
var ids = ['kk-fx-start','kk-fx-rift','kk-fx-void','kk-fx-clear'];
var markers = ids.map(function(id) { return document.getElementById(id); });
if (markers.some(function(e) { return !e; })) return;
window.__kkStormbreakLoaded = true;
document.documentElement.classList.add('kk-stormbreak-enabled');
var canvas = document.createElement('canvas');
canvas.id = 'kk-stormbreak-canvas';
canvas.setAttribute('aria-hidden','true');
var veil = document.createElement('div');
veil.id = 'kk-stormbreak-atmosphere';
veil.setAttribute('aria-hidden','true');
// Put FX outside article. Never wrap, filter, scale or re-style article DOM.
document.body.appendChild(veil);
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d', { alpha: true });
if (!ctx) return;
var ctrl = document.createElement('div');
ctrl.id = 'kk-stormbreak-controls';
ctrl.innerHTML = '<span aria-hidden="true">◈ STORMBREAK</span>' +
'<label for="kk-stormbreak-power">强度</label>' +
'<input id="kk-stormbreak-power" type="range" min="25" max="100" value="100" step="5" aria-label="调整特效强度">' +
'<button type="button" id="kk-stormbreak-toggle" aria-pressed="false">关闭特效</button>';
document.body.appendChild(ctrl);
var slider = ctrl.querySelector('input');
var button = ctrl.querySelector('button');
var media = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
var disabled = !!(media && media.matches);
var power = 1;
var dpr = 1, w = 0, h = 0;
var lastTime = 0, raf = 0, ticking = false, active = false;
var geom = [], drops = [];
var pos = [];
var probe = { intensity:0, storm:0, rift:0, void:0, clear:0 };
function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }
function smooth(a,b,x) { if (b<=a) return x>=b ? 1 : 0; var t=clamp((x-a)/(b-a),0,1); return t*t*(3-2*t); }
function rng(seed) { return function () { seed |= 0; seed = seed+0x6D2B79F5|0; var t=Math.imul(seed^(seed>>>15),1|seed); t=t+Math.imul(t^(t>>>7),61|t)^t; return ((t^(t>>>14))>>>0)/4294967296; }; }
var random = rng(2241984);
for (var i=0;i<95;i++) drops.push({x:random(),y:random(),v:.7+random()*1.5,len:12+random()*40,weight:.45+random()*1.7});
for (var j=0;j<18;j++) geom.push({x:random(),y:random(),len:random(),phase:random()*6.28});
function measure() {
dpr = Math.min(window.devicePixelRatio||1, window.innerWidth < 700 ? 1.2 : 1.5);
w = Math.max(1,window.innerWidth); h=Math.max(1,window.innerHeight);
canvas.width=Math.round(w*dpr); canvas.height=Math.round(h*dpr);
canvas.style.width=w+'px'; canvas.style.height=h+'px';
ctx.setTransform(dpr,0,0,dpr,0,0);
pos = markers.map(function(el) {return el.getBoundingClientRect().top+window.scrollY;});
}
function updateScroll() {
ticking=false;
var center=window.scrollY+h*.55;
var s=pos[0], r=pos[1], v=pos[2], c=pos[3];
var inChapter=center>=s-h*.35 && center<c+h*.72;
document.documentElement.classList.toggle('kk-stormbreak-inchapter',inChapter);
if (disabled) { disableVisuals(); return; }
var enter=smooth(s-h*.35,s+h*.32,center);
var leave=1-smooth(c+h*.05,c+h*.94,center);
var intensity=enter*leave*power;
probe.intensity=intensity;
probe.storm=smooth(s+h*.05, r, center);
probe.rift=smooth(r-h*.1, v, center);
probe.void=smooth(v-h*.18, v+h*.42, center);
probe.clear=smooth(c-h*.45,c+h*.25, center);
active=intensity>.008;
document.documentElement.classList.toggle('kk-stormbreak-active',active);
veil.style.setProperty('--kk-atmosphere',String(clamp(intensity*(.78+.24*probe.void)*(1-.98*probe.clear),0,1)));
if (active && !raf) raf=window.requestAnimationFrame(tick);
}
function scrollListener() { if (!ticking) {ticking=true; requestAnimationFrame(updateScroll);} }
function disableVisuals() {
active=false;
document.documentElement.classList.remove('kk-stormbreak-active');
veil.style.setProperty('--kk-atmosphere','0');
ctx.clearRect(0,0,w,h);
button.textContent='开启特效';
button.setAttribute('aria-pressed','true');
}
function setEnabled() {
button.textContent=disabled ? '开启特效' : '关闭特效';
button.setAttribute('aria-pressed',String(disabled));
updateScroll();
}
button.addEventListener('click',function(){ disabled=!disabled; setEnabled(); });
slider.addEventListener('input',function(){ power=Number(slider.value)/100; updateScroll(); });
if (media && media.addEventListener) media.addEventListener('change',function(e){disabled=e.matches; setEnabled();});
function gradEllipse(x,y,rX,rY,inner,outer,alpha) {
ctx.save();ctx.translate(x,y);ctx.scale(1,rY/rX);
var g=ctx.createRadialGradient(0,0,0,0,0,rX);
g.addColorStop(0,inner);g.addColorStop(1,outer);
ctx.globalAlpha=alpha;ctx.fillStyle=g;
ctx.beginPath();ctx.arc(0,0,rX,0,Math.PI*2);ctx.fill();ctx.restore();
}
function linePath(points,color,width,alpha) {
ctx.save();ctx.globalAlpha=alpha;ctx.lineWidth=width;ctx.strokeStyle=color;
ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();
points.forEach(function(p,i){if(i===0)ctx.moveTo(p[0],p[1]);else ctx.lineTo(p[0],p[1]);});
ctx.stroke();ctx.restore();
}
function drawClouds(t, strength) {
// Storm walls press inward from all four sides, leaving the article's central text clear.
var n=24;
for (var i=0;i<n;i++) {
var side=i%4, a=i/n*7.2, xx,yy;
var drift=Math.sin(t*.00018+i*3.4)*28;
if(side===0){xx=((i*109)%w)+drift;yy=-45+Math.sin(a)*75;}
if(side===1){xx=w+55+drift;yy=((i*153)%h);}
if(side===2){xx=((i*159)%w)+drift;yy=h+15+Math.sin(a)*90;}
if(side===3){xx=-55+drift;yy=((i*149)%h);}
var r=160+((i*71)%190);
var color=i%3===0?'rgba(78,18,103,.51)':'rgba(9,38,115,.57)';
gradEllipse(xx,yy,r,r*.46,color,'rgba(4,9,22,0)',strength*1.15);
}
gradEllipse(w*.51,h*.52,w*.58,h*.54,'rgba(6,14,40,.23)','rgba(6,14,40,0)',strength*.65);
}
function drawRain(t, strength) {
ctx.save();ctx.beginPath();ctx.rect(0,0,w,h);ctx.clip();
var offset=t*.095;
for(var i=0;i<drops.length;i++) {
var d=drops[i], px=d.x*w, py=((d.y*h+offset*d.v)%(h+100))-50;
var edge=Math.abs(px-w/2)/(w/2);
var a=(.11+.6*Math.pow(edge,1.6))*strength;
ctx.strokeStyle=i%8===0?'rgba(175,110,192,'+a+')':'rgba(115,182,238,'+a+')';
ctx.lineWidth=d.weight;
ctx.beginPath();ctx.moveTo(px,py);ctx.lineTo(px-9-d.len*.15,py+d.len);ctx.stroke();
}ctx.restore();
}
function drawSignal(t, strength) {
// 180 BPM concept is translated to an ECG-like trace running around the edges,
// with NO rapid full-screen flashes and NO article text modifications.
var yy=h*.84, seg=17, points=[];
var shift=(t*.055)%(w+140);
for (var x=-140;x<=w+150;x+=seg) {
var u=(x+shift)%185;
var peak= u>75&&u<90 ? -50*Math.sin((u-75)/15*Math.PI):0;
var y=yy+Math.sin(x*.018+t*.0013)*3+peak;
points.push([x,y]);
}
linePath(points,'#69d6ed',1.5,strength*.7);
linePath(points.map(function(p){return [p[0]+4,p[1]+1];}),'#e65d91',1.2,strength*.36);
// Blunt angular bands, suggesting a mismatched signal.
ctx.save();ctx.globalAlpha=strength*.75;
ctx.fillStyle='rgba(218,74,111,.28)';
var k=Math.floor(t/220);
for(var i=0;i<7;i++){
var seed=(k*19+i*317)>>>0, xx=((seed*73)%1301)/1301*w;
var y=((seed*139)%997)/997*h;
var rw=35+(seed%100);
ctx.fillRect(xx,y,rw,2+seed%5);
}ctx.restore();
}
function drawFracture(t, strength) {
// Large static-ish fractures, spanning view edges (never filters text).
var centers=[[-w*.08,h*.23],[w*1.06,h*.42],[w*.47,-h*.13],[w*.58,h*1.08]];
var now=Math.sin(t*.0006);
centers.forEach(function(base,index){
var local=rng(24111+index*441);
var maxR=Math.min(w,h)*(.60 + .13*index);
var points=[];
var heading=(index*1.57)+.06;
for(var j=0;j<9;j++) {
var rad=j/8*maxR;
var a=heading+Math.sin(j*.63+index)*.42+(local()-.5)*.2;
points.push([base[0]+Math.cos(a)*rad,base[1]+Math.sin(a)*rad]);
}
linePath(points,'rgba(9,13,37,1)',8,strength*.75);
linePath(points,'#88d8f8',2.5,strength*(.92+now*.06));
linePath(points.map(function(p){return[p[0]+5,p[1]-2];}),'#fe638e',1.9,strength*.93);
for(var k=2;k<8;k+=2) {
var c=points[k], b=points[Math.min(k+1,8)], a=Math.atan2(b[1]-c[1],b[0]-c[0])+((k%4)?1:-1)*.8;
var len=40+local()*100;
linePath([c,[c[0]+Math.cos(a)*len,c[1]+Math.sin(a)*len]],'#a1b6f0',.8,strength*.55);
}
});
// Screen-tear slices shift translucent atmosphere only (never page DOM).
var index=Math.floor(t/360);
var tear=rng(index*5923+772);
for(var z=0;z<7;z++){
var ty=tear()*h, tw=40+tear()*Math.min(w*.45,380), tx=tear()*w;
ctx.save();ctx.globalAlpha=strength*(.18+tear()*.28);
ctx.fillStyle=z%2?'#ff4b90':'#34bde3';
ctx.fillRect(tx,ty,tw,4+tear()*18);
ctx.restore();
}
}
function drawAbyss(t, strength) {
// A ring / distorted horizon instead of replacing or morphing paragraphs.
var cx=w*.5,cy=h*.49,base=Math.max(w,h)*.46;
gradEllipse(cx,cy,base,base*.74,'rgba(4,4,19,.14)','rgba(4,4,19,0)',strength);
var rings=3;
for (var ring=0;ring<rings;ring++) {
ctx.save();ctx.translate(cx,cy);ctx.scale(1,.70);
var radius=base*(.72+ring*.17)+Math.sin(t*.00048+ring)*7;
ctx.strokeStyle=ring===1?'rgba(255,55,121,.82)':'rgba(68,197,255,.58)';
ctx.lineWidth=ring===1?3.4:2;
ctx.globalAlpha=strength;
ctx.beginPath();
for(var a=0;a<Math.PI*2;a+=.035) {
var rr=radius+Math.sin(a*13+t*.00054+ring)*7+Math.cos(a*37+ring)*4;
if(a===0) ctx.moveTo(Math.cos(a)*rr,Math.sin(a)*rr);
else ctx.lineTo(Math.cos(a)*rr,Math.sin(a)*rr);
}ctx.closePath();ctx.stroke();ctx.restore();
}
// Staggered geometric shards in peripheral vision.
ctx.save();ctx.globalAlpha=strength*.7;
for(var i=0;i<geom.length;i++){
var d=geom[i];var x=d.x*w,y=d.y*h;
var edge=Math.max(Math.abs(d.x-.5)*2,Math.abs(d.y-.5)*2);
if(edge<.55)continue;
var spread=20+d.len*85;
var sway=Math.sin(t*.00032+d.phase)*8;
ctx.fillStyle=i%3?'rgba(80,145,232,.09)':'rgba(232,59,112,.13)';
ctx.strokeStyle=i%2?'rgba(114,195,252,.35)':'rgba(243,93,148,.3)';
ctx.lineWidth=1.0;
ctx.beginPath();ctx.moveTo(x+sway,y-spread);ctx.lineTo(x+spread*.9+sway,y+spread*.3);ctx.lineTo(x-spread*.4+sway,y+spread);ctx.closePath();ctx.fill();ctx.stroke();
}ctx.restore();
}
function drawClear(t, strength) {
// Light begins to reclaim the storm without a full-screen white flash.
ctx.save();ctx.globalAlpha=strength;
for(var i=0;i<9;i++){
var x=w*.27+i*w*.075;
var g=ctx.createLinearGradient(x-150,-100,x+50,h+100);
g.addColorStop(0,'rgba(119,213,255,.0)');
g.addColorStop(.30,'rgba(145,224,255,.14)');
g.addColorStop(.7,'rgba(126,201,255,.045)');
g.addColorStop(1,'rgba(160,237,255,0)');
ctx.fillStyle=g;
ctx.beginPath();ctx.moveTo(x-22,-60);ctx.lineTo(x+26,-60);ctx.lineTo(x+220,h+60);ctx.lineTo(x+80,h+60);ctx.fill();
}
for(var j=0;j<40;j++){
var p=drops[j], px=p.x*w, py=((p.y*h -t*.022*p.v)%(h+60)+h+60)%(h+60);
ctx.fillStyle='rgba(181,230,255,'+(.08+.2*((j%9)/9))+')';
ctx.fillRect(px,py,1.2,1.2);
}ctx.restore();
}
function render(t) {
ctx.clearRect(0,0,w,h);
var q=probe, mag=q.intensity;
if(mag<=.008) return;
var intensity=Math.min(1,mag);
drawClouds(t,intensity*(.72+.9*q.storm)*(1-q.clear*.93));
drawRain(t,intensity*(.6+.8*q.rift)*(1-q.clear*.97));
drawSignal(t,intensity*smooth(.25,.75,q.rift)*(.8+.3*q.void)*(1-q.clear));
drawFracture(t,intensity*q.rift*(1-q.clear*.98));
drawAbyss(t,intensity*q.void*(1-q.clear*.98));
if(q.clear>0) drawClear(t,intensity*q.clear);
}
function tick(t) {
raf=0;
if(!active||disabled||document.hidden) return;
// 30 FPS cap, plus device-pixel-ratio cap, for CPU-friendly effects.
if(t-lastTime>=32){ lastTime=t;render(t); }
raf=requestAnimationFrame(tick);
}
window.addEventListener('scroll',scrollListener,{passive:true});
window.addEventListener('resize',function(){measure();updateScroll();},{passive:true});
window.addEventListener('load',function(){measure();updateScroll();},{once:true});
document.addEventListener('visibilitychange',function(){if(!document.hidden)updateScroll();});
measure(); setEnabled();
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});
else start();
}());