MediaWiki:KuranokaPageTransition.js
MediaWiki界面页面
更多操作
注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
- Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
- Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5。
/* ============================================================
Kuranoka / Kuronata — Page Transition V46 / beat-synced sunrise
V25 ORIGINAL fire edge, translucent fire pockets, smoke and dawn.
Lightweight WebGL2 is restricted to subtle ember points, NEVER the
fire curtain nor the reverse transition. Carbon sheets are 2D.
Both cues start three seconds before the V25 curtains, fade in over
those three seconds, then fade OUT over 18 seconds after reveal.
Only this page-scoped JS/CSS; never modify Citizen or loader.js.
Keep the preloaded Wiki iframe interactive after the curtain opens;
same-origin history mirrors the selected world without reloading it.
============================================================ */
(function initKkpt(retry) {
'use strict';
retry=retry||0;
if (!window.mw || !mw.config || typeof mw.config.get!=='function') {
if(retry<40)setTimeout(()=>initKkpt(retry+1),100);
return;
}
const pathMatch = location.pathname.match(/\/wiki\/(Kuranoka|Kuronata)\/?$/);
const urlPage = pathMatch ? pathMatch[1]
: new URLSearchParams(location.search).get('title');
const basePage = ['Kuranoka','Kuronata'].includes(urlPage)
? urlPage : mw.config.get('wgPageName');
const action = new URLSearchParams(location.search).get('action') || 'view';
if (!['Kuranoka','Kuronata'].includes(basePage) || action !== 'view') return;
const root=document.documentElement;
const themeNames=['skin-theme-clientpref-day','skin-theme-clientpref-os',
'skin-theme-clientpref-night','skin-theme-clientpref-black',
'skin-citizen-light'];
let wantsNight=basePage==='Kuronata';
let changingTheme=false;
function syncTheme(){
if(changingTheme)return;
const wanted=wantsNight?'skin-theme-clientpref-night':
'skin-theme-clientpref-day';
const shadow=wantsNight?'kkpt-kuronata-dark':null;
const unexpected=themeNames.filter(c=>c!==wanted)
.concat(wantsNight?[]:['kkpt-kuronata-dark']);
if(root.classList.contains(wanted)&&
(!shadow||root.classList.contains(shadow))&&
unexpected.every(c=>!root.classList.contains(c))){
return;
}
changingTheme=true;
root.classList.remove(...themeNames,'kkpt-kuronata-dark');
root.classList.add(wanted);
if(shadow)root.classList.add(shadow);
changingTheme=false;
}
function showTheme(dark){wantsNight=dark;syncTheme();}
syncTheme();
new MutationObserver(syncTheme).observe(root,{
attributes:true,attributeFilter:['class']
});
function ensureWorldStyles(){
if(!document.head || document.getElementById('kkpt-css-retry'))return;
try{
if(getComputedStyle(root).getPropertyValue('--kkpt-css-v46').trim()==='ready')return;
}catch(_){}
const link=document.createElement('link');
link.id='kkpt-css-retry';
link.rel='stylesheet';
const target=new URL('/index.php',location.origin);
target.searchParams.set('title','MediaWiki:KuranokaPageTransition.css');
target.searchParams.set('action','raw');
target.searchParams.set('ctype','text/css');
target.searchParams.set('kkptv','46');
link.href=target.href;
document.head.appendChild(link);
}
setTimeout(ensureWorldStyles,220);
function navigateRealPage(href){
window.location.assign(href);
}
function installKuronataAfterglow() {
if(basePage!=='Kuronata'||window.__kkptDarkExtrasReady)return;
window.__kkptDarkExtrasReady=true;
const prefersReduced=window.matchMedia('(prefers-reduced-motion: reduce)');
const spoilerOverlays=new WeakMap();
const renderedSpoilerLines=(node)=>{
const lines=[];
const walk=document.createTreeWalker(node,NodeFilter.SHOW_TEXT);
const range=document.createRange();
let text,budget=0;
while((text=walk.nextNode())&&budget<340){
for(let i=0;i<text.length&&budget<340;i++,budget++){
const ch=text.nodeValue[i];
if(ch==='\n'||ch==='\r')continue;
range.setStart(text,i);
range.setEnd(text,i+1);
const r=range.getBoundingClientRect();
if(!r.width||!r.height)continue;
let line=lines.find(row=>Math.abs(row.top-r.top)<3);
if(!line){
line={top:r.top,left:r.left,right:r.right,
height:r.height,text:''};
lines.push(line);
}
line.left=Math.min(line.left,r.left);
line.right=Math.max(line.right,r.right);
line.height=Math.max(line.height,r.height);
line.text+=ch;
}
}
range.detach?.();
return lines.sort((a,b)=>a.top-b.top);
};
const spoilerFault=(node)=>{
if(prefersReduced.matches)return;
const rects=node.getClientRects();
if(rects.length<2)return;
const lines=renderedSpoilerLines(node);
if(lines.length<2)return;
node.setAttribute('data-kkpt-multiline','1');
spoilerOverlays.get(node)?.remove();
const overlay=document.createElement('div');
overlay.className='kkpt-heimu-fault';
overlay.setAttribute('aria-hidden','true');
const font=getComputedStyle(node);
for(const row of lines){
const fragment=document.createElement('span');
fragment.className='kkpt-heimu-fault-line';
fragment.dataset.copy=row.text;
fragment.style.left=row.left+'px';
fragment.style.top=row.top+'px';
fragment.style.width=(row.right-row.left+6)+'px';
fragment.style.height=(row.height+4)+'px';
fragment.style.fontFamily=font.fontFamily;
fragment.style.fontSize=font.fontSize;
fragment.style.fontWeight=font.fontWeight;
fragment.style.fontStyle=font.fontStyle;
fragment.style.lineHeight=font.lineHeight;
fragment.style.letterSpacing=font.letterSpacing;
overlay.append(fragment);
}
document.body.append(overlay);
spoilerOverlays.set(node,overlay);
};
const clearSpoilerFault=(node)=>{
const overlay=spoilerOverlays.get(node);
if(!overlay)return;
spoilerOverlays.delete(node);
overlay.classList.add('kkpt-fault-away');
setTimeout(()=>overlay.remove(),210);
};
const decorateSpoilers=()=>{
document.querySelectorAll('span.heimu').forEach(node=>{
if(node.dataset.kkptBound)return;
node.dataset.kkptBound='1';
const label=(node.textContent||'').replace(/\s+/g,' ').trim();
if(label&&label.length<=240&&!node.children.length)
node.setAttribute('data-kkpt-glitch',label);
node.addEventListener('mouseenter',()=>spoilerFault(node));
node.addEventListener('mouseleave',()=>clearSpoilerFault(node));
});
};
const decorateLinks=()=>{
const article=document.querySelector('#mw-content-text,.mw-parser-output');
if(!article)return;
article.querySelectorAll('a[href]').forEach(link=>{
if(link.dataset.kkptLinkReady ||
link.classList.contains('kkpt-fate-gear') ||
link.querySelector('img,svg,video'))return;
link.dataset.kkptLinkReady='1';
link.classList.add('kkpt-link-glitch');
try{
const url=new URL(link.href,document.baseURI);
const path=url.pathname.match(/\/wiki\/([^/?#]+)/);
const name=url.searchParams.get('title')||
(path?decodeURIComponent(path[1]):'');
if(url.origin===location.origin &&
name.replace(/_/g,' ')==='Kuranoka'){
link.classList.add('kkpt-link-origin');
const label=(link.textContent||'').trim();
if(!link.children.length &&
link.getClientRects().length===1 &&
label.length>0&&label.length<100)
link.dataset.kkptLinkCopy=label;
}
}catch(_){}
});
};
const decorateIdentityMentions=()=>{
const article=document.querySelector('#mw-content-text,.mw-parser-output');
if(!article)return;
const hasIdentity=/Kuranoka|Kurano\s+Kanata|天野[ \u00a0\u3000]*彼方/i;
const exactIdentity=/Kuranoka|Kurano\s+Kanata|天野[ \u00a0\u3000]*彼方/gi;
const walker=document.createTreeWalker(article,NodeFilter.SHOW_TEXT,{
acceptNode(node){
if(!node.nodeValue||!hasIdentity.test(node.nodeValue))
return NodeFilter.FILTER_REJECT;
const parent=node.parentElement;
if(!parent||parent.closest('script,style,textarea,code,pre,'+
'input,button,select,[contenteditable="true"],'+
'.kkpt-identity-glitch,.kkpt-heimu-fault,'+
'span.heimu,a.kkpt-link-origin,'+
'#kkpt-afterglow,#kkpt-toggle'))
return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
const matches=[];
let node;
while((node=walker.nextNode())&&matches.length<1800)matches.push(node);
let index=0;
for(const textNode of matches){
const raw=textNode.nodeValue;
const fragment=document.createDocumentFragment();
let previous=0,match;
exactIdentity.lastIndex=0;
while((match=exactIdentity.exec(raw))){
if(match.index>previous)
fragment.append(document.createTextNode(raw.slice(previous,match.index)));
const el=document.createElement('span');
el.className='kkpt-identity-glitch';
el.dataset.kkptIdentity=match[0];
el.textContent=match[0];
el.style.setProperty('--kkpt-identity-delay',
(-((index++*7)%23)*.21).toFixed(2)+'s');
fragment.append(el);
previous=match.index+match[0].length;
}
if(previous<raw.length)
fragment.append(document.createTextNode(raw.slice(previous)));
textNode.replaceWith(fragment);
}
};
const createAmbience=()=>{
if(prefersReduced.matches||document.getElementById('kkpt-afterglow')||!document.body)return;
const overlay=document.createElement('div');
overlay.id='kkpt-afterglow';
overlay.setAttribute('aria-hidden','true');
const el=(type,i)=>{
const part=document.createElement('span');
part.className=type==='soot'?'kkpt-soot-cell':'kkpt-ash-cell';
const smoke=type==='soot';
const fromLeft=i%2===0;
part.style.left=(smoke
? (fromLeft?-26+Math.random()*22:79+Math.random()*21)
: Math.random()*100)+'%';
part.style.top=(smoke?8+Math.random()*94:15+Math.random()*94)+'%';
part.style.setProperty('--size',(smoke?220+Math.random()*240:2+Math.random()*4).toFixed(1)+'px');
part.style.setProperty('--delay',(-Math.random()*(smoke?29:18)).toFixed(2)+'s');
part.style.setProperty('--duration',(smoke?18+Math.random()*14:8+Math.random()*11).toFixed(2)+'s');
part.style.setProperty('--wind',((Math.random()-.5)*(smoke?100:230)).toFixed(1)+'px');
return part;
};
for(let i=0;i<(innerWidth<700?8:16);i++)overlay.append(el('soot',i));
for(let i=0;i<30;i++)overlay.append(el('ash',i));
document.body.append(overlay);
};
const init=()=>{
decorateSpoilers();
decorateLinks();
decorateIdentityMentions();
createAmbience();
mw.hook('wikipage.content').add(()=>{
decorateSpoilers();
decorateLinks();
decorateIdentityMentions();
});
};
if(document.readyState==='loading')
document.addEventListener('DOMContentLoaded',init,{once:true});
else init();
const pulse=()=>{
const inactive=document.hidden||prefersReduced.matches||
root.classList.contains('kkpt-frame-inactive')||
root.classList.contains('kkpt-world-suspended')||
(window!==window.top&&window.frameElement?.getAttribute('aria-hidden')==='true');
if(!inactive){
root.style.setProperty('--kkpt-rift-y',(14+Math.random()*70).toFixed(1)+'%');
root.classList.add('kkpt-rift');
window.setTimeout(()=>root.classList.remove('kkpt-rift'),390);
}
window.setTimeout(pulse,5000+Math.random()*15000);
};
window.setTimeout(pulse,5000+Math.random()*15000);
}
installKuronataAfterglow();
if (window!==window.top || window.__kkptV12) return;
window.__kkptV12=true;
window.__kkptVersion='46.0';
const SHOW_FLOATING_BUTTON=false;
const otherPage=basePage==='Kuranoka'?'Kuronata':'Kuranoka';
const baseUrl=location.href;
const baseTitle=document.title;
const otherUrl=new URL('/wiki/'+encodeURIComponent(otherPage),location.href);
const clipSide=basePage==='Kuranoka'?'right':'left';
const hideClip=clipSide==='right'?'inset(0 0 0 100%)':'inset(0 100% 0 0)';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const smooth=t=>{t=Math.max(0,Math.min(1,t));
return t*t*t*(t*(t*6-15)+10);};
function dawnGeometry(t,w=innerWidth,h=innerHeight){
const cx=w*.5,cy=h*.47;
const rmax=Math.hypot(Math.max(cx,w-cx),Math.max(cy,h-cy))+42;
return {cx,cy,radius:rmax*smooth(t)};
}
const imagePool=new Map();
const worldBackdrops={
Kuranoka:'https://blockland.wiki/images/0/05/%E7%A7%AF%E9%9B%A8%E4%BA%911.png',
Kuronata:'https://blockland.wiki/images/a/ab/%E9%BB%84%E6%98%8F%EF%BC%8C%E9%BB%91%E4%BA%91.png'
};
function primeWorldBackdrops(){
const current=worldBackdrops[basePage];
if(current&&document.head){
const link=document.createElement('link');
link.rel='preload';
link.as='image';
link.href=current;
link.setAttribute('fetchpriority','high');
link.dataset.kkptBackgroundPreload='1';
document.head.appendChild(link);
warmImage(current).catch(()=>{});
}
setTimeout(()=>{
const next=worldBackdrops[otherPage];
if(next)warmImage(next).catch(()=>{});
},300);
}
primeWorldBackdrops();
const ENABLE_TRANSITION_AUDIO = true;
const FIRE_VOLUME = .30;
const DAWN_VOLUME = .20;
let soundContext=null,fireBuffer=null,activeSound=null;
const REBIRTH_PAD_URL=
'https://upload.wikimedia.org/wikipedia/commons/e/ed/Padsynth_example_synth_piano.ogg';
let padBytesPromise=null,padDecodePromise=null,padBuffer=null;
function preloadRebirthPad(){
if(padBytesPromise)return padBytesPromise;
padBytesPromise=fetch(REBIRTH_PAD_URL,{mode:'cors',cache:'force-cache'})
.then(resp=>{
if(!resp.ok)throw new Error('PAD HTTP '+resp.status);
return resp.arrayBuffer();
})
.catch(err=>{
console.debug('[KKPT] PAD file unavailable:',err);
return null;
});
return padBytesPromise;
}
function decodeRebirthPad(ctx){
if(padBuffer)return Promise.resolve(padBuffer);
if(!padDecodePromise){
padDecodePromise=preloadRebirthPad()
.then(bytes=>bytes?ctx.decodeAudioData(bytes.slice(0)):null)
.then(result=>(padBuffer=result))
.catch(err=>{
console.debug('[KKPT] Cannot decode PAD:',err);
return null;
});
}
return padDecodePromise;
}
function playRebirthPadFallback(durationMs){
const sound=new Audio(REBIRTH_PAD_URL);
sound.volume=0;
sound.play().then(()=>{
const begin=performance.now();
const timer=setInterval(()=>{
const t=Math.min(1,(performance.now()-begin)/durationMs);
sound.volume=Math.max(0,Math.min(DAWN_VOLUME,
DAWN_VOLUME*Math.min(t/.27,(1-t)/.26)));
if(t>=1){clearInterval(timer);sound.pause();sound.src='';}
},80);
}).catch(err=>console.debug('[KKPT] PAD playback unavailable:',err));
}
function primeSound(){
if(!ENABLE_TRANSITION_AUDIO)return;
try{
const AudioEngine=window.AudioContext||window.webkitAudioContext;
if(!AudioEngine)return;
if(!soundContext)soundContext=new AudioEngine();
if(soundContext.state==='suspended')soundContext.resume().catch(()=>{});
decodeRebirthPad(soundContext);
}catch(err){console.debug('[KKPT] Audio unavailable:',err);}
}
function createFireAudio(ctx){
const length=Math.round(ctx.sampleRate*4.5);
const buffer=ctx.createBuffer(1,length,ctx.sampleRate);
const data=buffer.getChannelData(0);
let low=0,middle=0;
for(let i=0;i<length;i++){
const random=Math.random()*2-1;
low=low*.992+random*.008;
middle=middle*.92+random*.08;
data[i]=Math.max(-1,Math.min(1,
low*3.9+middle*.33+random*.055));
}
for(let k=0;k<350;k++){
const at=Math.floor(Math.random()*(length-1500));
const decay=36+Math.random()*330;
const duration=Math.min(1400,Math.ceil(decay*4));
const power=.20+Math.pow(Math.random(),3)*.68;
for(let j=0;j<duration;j++){
const env=Math.exp(-j/decay);
data[at+j]=Math.max(-1,Math.min(1,data[at+j]+
(Math.random()*2-1)*power*env));
}
}
return buffer;
}
function transitionSound(dark,durationMs){
const ctx=soundContext;
if(!ctx||ctx.state!=='running')return;
const start=ctx.currentTime+.018;
const span=durationMs/1000;
if(activeSound){
const old=activeSound.gain;
old.cancelScheduledValues(start);
old.setTargetAtTime(.0001,start,.05);
}
const master=ctx.createGain();
master.gain.setValueAtTime(.0001,start);
master.gain.linearRampToValueAtTime(dark?FIRE_VOLUME:DAWN_VOLUME,
start+(dark?.75:1.10));
master.gain.setValueAtTime(dark?FIRE_VOLUME:DAWN_VOLUME,
start+span-(dark?.88:1.0));
master.gain.exponentialRampToValueAtTime(.0001,start+span);
master.connect(ctx.destination);
activeSound={gain:master};
if(dark){
if(!fireBuffer)fireBuffer=createFireAudio(ctx);
const src=ctx.createBufferSource();
src.buffer=fireBuffer;src.loop=true;
const lowpass=ctx.createBiquadFilter();
lowpass.type='lowpass';lowpass.frequency.value=2050;
const bass=ctx.createBiquadFilter();
bass.type='highpass';bass.frequency.value=55;
src.connect(lowpass);lowpass.connect(bass);bass.connect(master);
src.start(start);src.stop(start+span+.12);
}else{
decodeRebirthPad(ctx).then(buffer=>{
if(!buffer){playRebirthPadFallback(durationMs);return;}
const at=Math.max(start,ctx.currentTime+.020);
if(at>start+span-.55)return;
const player=ctx.createBufferSource();
player.buffer=buffer;
player.connect(master);
player.start(at,Math.max(0,at-start)+.95);
player.stop(start+span);
}).catch(()=>{});
}
setTimeout(()=>{
if(activeSound&&activeSound.gain===master)activeSound=null;
try{master.disconnect();}catch(_){}
},durationMs+350);
}
const SCORE_KEY='kkpt:score-handoff:44';
const SCORE_MUTE_KEY='kkpt:score-muted';
const SCORES={
Kuronata:{
id:'marenoia',target:'Kuronata',
url:'https://blkl.fun/wp-content/uploads/2026/10/Marenoia.mp3?kkpt_audio=v35',
startAt:28.275,fireStart:31.275,fireEnd:34.725,
stopAt:52.725,fadeInSeconds:3,fadeSeconds:18
},
Kuranoka:{
id:'anotherme',target:'Kuranoka',
url:'https://blkl.fun/wp-content/uploads/2026/10/AnotherMe.mp3?kkpt_audio=20261024_043627',
startAt:154.45,fireStart:157.45,fireEnd:160.55,
stopAt:178.55,fadeInSeconds:3,fadeSeconds:18
}
};
const ANOTHERME_SUN_DOWNBEATS=[
158.536,159.912,161.279,162.646,164.013,
165.380,166.757,168.124,169.491,170.878,
172.244,173.611,174.978,176.355,177.722
];
function anotherMeSunPulse(time){
let strong=0,soft=0;
const envelope=(dt,decay)=>dt<-.030||dt>.55?0:
dt<.035?Math.max(0,(dt+.030)/.065):
Math.exp(-(dt-.035)*decay);
for(const beat of ANOTHERME_SUN_DOWNBEATS){
strong=Math.max(strong,envelope(time-beat,9));
soft=Math.max(soft,envelope(time-(beat+.684),12)*.48);
}
return {strong,soft};
}
let returnScore=null;
let scoreOmen=null;
let fatePrelude=null;
function userWantsMute(){
try{return sessionStorage.getItem(SCORE_MUTE_KEY)==='1';}
catch(_){return false;}
}
const AUDIO_ICONS={
speaker:'<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="M15 9a5 5 0 0 1 0 6"/><path d="M18 6a9 9 0 0 1 0 12"/>',
mute:'<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="m16 9 5 6m0-6-5 6"/>',
play:'<path d="m8 5 11 7-11 7V5Z"/>'
};
function updateAudioToggle(score){
if(!score?.hud)return;
const paused=score.blocked||score.audio.paused;
const icon=paused?'play':score.audio.muted?'mute':'speaker';
score.hud.innerHTML='<svg viewBox="0 0 24 24" fill="none" '+
'stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '+
'stroke-linejoin="round" aria-hidden="true">'+AUDIO_ICONS[icon]+'</svg>';
score.hud.setAttribute('aria-label',paused?'恢复播放':
score.audio.muted?'取消静音':'静音');
score.hud.setAttribute('aria-pressed',String(score.audio.muted));
score.hud.classList.toggle('kkpt-audio-needs-resume',paused);
}
function mountAudioToggle(score){
const old=document.getElementById('kkpt-audio-toggle');
old?.remove();
const hud=document.createElement('button');
hud.type='button';hud.id='kkpt-audio-toggle';
hud.setAttribute('aria-label','静音');
score.hud=hud;
document.body.append(hud);
const audio=score.audio;
hud.addEventListener('click',()=>{
if(audio.paused||score.blocked){
score.blocked=false;
const resume=typeof score.resume==='function'?
score.resume():audio.play();
Promise.resolve(resume).then(()=>{
updateAudioToggle(score);
}).catch(err=>{
console.info('[KKPT] Audio resume blocked:',err);
score.blocked=true;updateAudioToggle(score);
});
updateAudioToggle(score);
return;
}
audio.muted=!audio.muted;
try{sessionStorage.setItem(SCORE_MUTE_KEY,audio.muted?'1':'0');}
catch(_){}
updateAudioToggle(score);
});
audio.addEventListener('play',()=>updateAudioToggle(score));
audio.addEventListener('pause',()=>updateAudioToggle(score));
updateAudioToggle(score);
}
function removeScoreOmen(){
if(scoreOmen){
const old=scoreOmen;
const opacity=getComputedStyle(old).opacity;
scoreOmen=null;
old.style.opacity=opacity;
old.classList.add('kkpt-omen-closing');
requestAnimationFrame(()=>requestAnimationFrame(()=>{
old.style.opacity='0';
}));
setTimeout(()=>old.remove(),1540);
}
document.body.classList.remove('kkpt-omen-running');
}
function startScoreOmen(score){
if(score?.config?.id!=='marenoia'||scoreOmen)return;
const omen=document.createElement('div');
omen.id='kkpt-omen';omen.setAttribute('aria-hidden','true');
omen.innerHTML='<i class="kkpt-omen-glow"></i>'+
'<i class="kkpt-omen-line"></i>';
document.body.append(omen);
scoreOmen=omen;
document.body.classList.add('kkpt-omen-running');
}
function clearFatePrelude(){
fatePrelude?.remove();fatePrelude=null;
}
function releaseFatePrelude(){
if(!fatePrelude)return;
const node=fatePrelude;
node.classList.add('kkpt-fate-merging');
requestAnimationFrame(()=>{if(node.isConnected)node.style.opacity='0';});
setTimeout(()=>{
if(fatePrelude===node)clearFatePrelude();
else node.remove();
},1700);
}
function fateSigilMarkup(isDark){
const at=(r,d)=>{
const a=(d-90)*Math.PI/180;
return [(150+Math.cos(a)*r).toFixed(2),
(150+Math.sin(a)*r).toFixed(2)];
};
const ring=(r,cls)=>'<circle class="'+cls+
'" cx="150" cy="150" r="'+r+'"/>';
const stroke=(a,b,cls)=>'<path class="'+cls+
'" d="M'+a.join(' ')+' L'+b.join(' ')+'"/>';
const poly=(points,cls)=>'<polygon class="'+cls+'" points="'+
points.map(p=>p.join(',')).join(' ')+'"/>';
const outer=ring(133,'kkpt-sigil-outer')+
ring(128,'kkpt-sigil-orbit')+
ring(121,'kkpt-sigil-ghost')+
ring(105,'kkpt-sigil-seal')+
ring(100,'kkpt-sigil-trace')+
ring(83,'kkpt-sigil-ghost');
const glyphs=Array.from({length:48},(_,i)=>{
const angle=i*7.5;
const major=i%4===0;
return stroke(at(121,angle),at(major?132:i%2?125:128,angle),
major?'kkpt-sigil-mark kkpt-sigil-major':'kkpt-sigil-mark');
}).join('');
const cardinal=Array.from({length:8},(_,i)=>{
const a=i*45;
return poly([at(99,a),at(111,a+4.2),at(120,a),at(111,a-4.2)],
'kkpt-sigil-diamond')+
stroke(at(85,a),at(96,a),'kkpt-sigil-needle');
}).join('');
const darkDrawing=
ring(54,'kkpt-sigil-core-orbit')+
ring(43,'kkpt-sigil-inner-orbit')+
'<path class="kkpt-sigil-dark-mark" d="M150 115L170 150 150 185 130 150Z"/>'+
'<path class="kkpt-sigil-dark-cut" d="M150 130L156 150 150 170 144 150Z"/>'+
'<path class="kkpt-sigil-axis" d="M150 96V108M150 192V204"/>';
const hopeDrawing=
ring(54,'kkpt-sigil-core-orbit')+
ring(43,'kkpt-sigil-inner-orbit')+
'<path class="kkpt-sigil-light-mark" d="M150 119L158 142 181 150 158 158 150 181 142 158 119 150 142 142Z"/>'+
'<circle class="kkpt-sigil-center" cx="150" cy="150" r="7"/>'+
'<path class="kkpt-sigil-axis" d="M150 96V108M150 192V204"/>';
return '<svg class="kkpt-fate-sigil '+(isDark?'kkpt-sigil-ominous':
'kkpt-sigil-hope')+'" viewBox="0 0 300 300" '+
'xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'+
'<g class="kkpt-sigil-engraving" fill="none" stroke="'+
(isDark?'#ff8c9b':'#f7edcf')+'" stroke-width="1.3">'+
outer+glyphs+cardinal+
(isDark?darkDrawing:hopeDrawing)+'</g></svg>';
}
function startFatePrelude(anchor,targetPage){
clearFatePrelude();
const box=anchor?.getBoundingClientRect?.();
const x=box?box.left+box.width/2:innerWidth/2;
const y=box?box.top+box.height/2:innerHeight/2;
const prelude=document.createElement('div');
prelude.id='kkpt-fate-prelude';
prelude.setAttribute('aria-hidden','true');
const isDark=targetPage==='Kuronata';
prelude.className=isDark?'kkpt-fate-dark':'kkpt-fate-light';
prelude.style.setProperty('--kkpt-fate-x',x+'px');
prelude.style.setProperty('--kkpt-fate-y',y+'px');
prelude.style.setProperty('--kkpt-fate-dx',(innerWidth/2-x)+'px');
prelude.style.setProperty('--kkpt-fate-dy',(innerHeight/2-y)+'px');
prelude.style.setProperty('--kkpt-fate-dx15',((innerWidth/2-x)*.15)+'px');
prelude.style.setProperty('--kkpt-fate-dy15',((innerHeight/2-y)*.15)+'px');
prelude.style.setProperty('--kkpt-fate-dx91',((innerWidth/2-x)*.91)+'px');
prelude.style.setProperty('--kkpt-fate-dy91',((innerHeight/2-y)*.91)+'px');
prelude.innerHTML='<div class="kkpt-fate-orbit">'+
'<i class="kkpt-fate-aura"></i>'+
'<i class="kkpt-fate-ring kkpt-fate-ring-a"></i>'+
'<i class="kkpt-fate-ring kkpt-fate-ring-b"></i>'+
'<i class="kkpt-fate-arc"></i>'+
'<i class="kkpt-fate-spark"></i>'+
'<i class="kkpt-fate-wave"></i>'+
fateSigilMarkup(isDark)+'</div>';
root.append(prelude);
fatePrelude=prelude;
setTimeout(()=>{
if(fatePrelude===prelude)clearFatePrelude();
else prelude.remove();
},30000);
}
const MARENOIA_TRANSIENTS=[
[28.453,.56],[28.682,.58],[28.892,.76],[29.121,.64],
[29.341,.72],[29.570,.48],[29.780,.87],[30.009,.93],
[30.438,.44],[30.658,.77],[30.887,.83],[31.356,.68],
[31.556,.79],[31.775,.93],[32.364,.29],[32.883,.36],
[33.192,.38],[33.561,1],[33.751,.97],[34.250,.86],
[34.469,.54],[35.317,.86],[35.786,.69]
];
function createMarenoiaBeatScene(score){
if(score.config.id!=='marenoia')return null;
const lightningLayer=document.createElement('div');
lightningLayer.id='kkpt-lightning-layer';
lightningLayer.setAttribute('aria-hidden','true');
document.body.append(lightningLayer);
let live=true,raf=0,lastTick=0,nextBeat=0,progress=0;
let lastLightning=-100;
function lightning(t,strength){
if(!live||t<.11||t>.96)return;
const w=Math.max(1,innerWidth),h=Math.max(1,innerHeight);
const rng=(a,b)=>a+Math.random()*(b-a);
const edge=w*(1-t);
let x=Math.min(w*.93,Math.max(edge+35,edge+(w-edge)*.53));
const top=rng(-70,-16),bottom=rng(h*.46,h*.80);
const points=[[x,top]];
const count=9;
for(let i=1;i<=count;i++){
const yy=top+(bottom-top)*i/count;
const xx=x+rng(-39,39)*(i/count)+Math.sin(i*2.4)*14;
points.push([Math.max(10,Math.min(w-10,xx)),yy]);
}
const path=points.map((p,i)=>(i?'L':'M')+
p[0].toFixed(1)+','+p[1].toFixed(1)).join(' ');
const branchAt=Math.floor(rng(3,7));
const p=points[branchAt];
const branch='M'+p[0].toFixed(1)+','+p[1].toFixed(1)+
' l'+rng(-46,46).toFixed(1)+','+rng(45,86).toFixed(1)+
' l'+rng(-28,28).toFixed(1)+','+rng(38,71).toFixed(1);
const ns='http://www.w3.org/2000/svg';
const svg=document.createElementNS(ns,'svg');
svg.setAttribute('class','kkpt-storm-bolt');
svg.setAttribute('viewBox','0 0 '+w+' '+h);
svg.setAttribute('preserveAspectRatio','none');
svg.style.opacity=String(Math.min(1,.46+strength*.42));
svg.style.clipPath='inset(0 0 0 '+
Math.max(0,(1-t)*100).toFixed(2)+'%)';
const addPath=(d,klass)=>{
const el=document.createElementNS(ns,'path');
el.setAttribute('d',d);el.setAttribute('class',klass);
svg.append(el);
};
addPath(path,'kkpt-bolt-haze');
addPath(path,'kkpt-bolt-core');
addPath(branch,'kkpt-bolt-branch');
lightningLayer.append(svg);
setTimeout(()=>svg.remove(),390);
}
function tick(now){
if(!live)return;
raf=requestAnimationFrame(tick);
if(now-lastTick<30)return;lastTick=now;
const audio=score.audio;
const audioTime=audio.currentTime;
while(nextBeat<MARENOIA_TRANSIENTS.length&&
MARENOIA_TRANSIENTS[nextBeat][0]<=audioTime){
const [moment,strength]=MARENOIA_TRANSIENTS[nextBeat++];
if(audioTime-moment<.22){
if(progress>.11&&strength>.81&&moment-lastLightning>.18){
lightning(progress,strength);
lastLightning=moment;
}
}
}
}
raf=requestAnimationFrame(tick);
return {
update(t){progress=t;},
finish(){
live=false;cancelAnimationFrame(raf);
setTimeout(()=>lightningLayer.remove(),450);
},
dispose(){
live=false;cancelAnimationFrame(raf);
lightningLayer.remove();
}
};
}
const AFTER_DARK_KEY='kkpt:dark-afterglow:44';
const DAWN_HALO_KEY='kkpt:dawn-cloudbreak:44';
const MARENOIA_EIGHT_BEAT_KICKS=[
35.311,38.843,42.375,45.907,49.429
];
const MARENOIA_LOUDNESS=[
.61,.71,.98,1,.85,.96,.92,.72,.57,.54,.45,.46,
.53,.54,.56,.63,.92,1,.88,.95,.94,.77,.61,.61,
.52,.41,.46,.48,.44,.50,.81,1,.92,.98,.90,.80,
.63,.58,.54,.61,.65,.67,.65,.69,.83,1,.90,.93,
.94,.81,.65,.65,.66,.65,.62,.64,.60,.58,.70,1,
.91,.90,.95,.83,.62,.58,.46,.54,.47,.57,.55,.63
];
function marenoiaLoudness(time){
const pos=Math.max(0,Math.min(MARENOIA_LOUDNESS.length-1,
(time-34.75)*4));
const i=Math.floor(pos),j=Math.min(MARENOIA_LOUDNESS.length-1,i+1);
return MARENOIA_LOUDNESS[i]*(1-(pos-i))+
MARENOIA_LOUDNESS[j]*(pos-i);
}
function marenoiaGlitchEnvelope(age){
if(age<0||age>=.92)return 0;
if(age<.045)return age/.045;
if(age<.16)return 1;
return Math.pow(1-smooth((age-.16)/.76),1.5);
}
let kuronataAftermath=null;
let dawnBeatScene=null;
function mountKuronataAftermath(score){
if(kuronataAftermath){
if(score)kuronataAftermath.attach(score);
return kuronataAftermath;
}
const layer=document.createElement('div');
layer.id='kkpt-beat-scene';
layer.setAttribute('aria-hidden','true');
layer.className='kkpt-beat-after kkpt-beat-idle';
layer.innerHTML='<div class="kkpt-beat-focus"></div>'+
'<div class="kkpt-beat-lens"></div>'+
'<div class="kkpt-beat-fringe"></div>'+
'<div class="kkpt-beat-rupture"></div>'+
'<div class="kkpt-beat-slices"></div>'+
'<svg class="kkpt-beat-scars" viewBox="0 0 1440 900" '+
'preserveAspectRatio="none" aria-hidden="true">'+
'<path d="M-20 340L126 317 203 355 294 339 383 382 477 352 605 395 728 365 842 399 982 371 1125 397 1264 351 1460 390" />'+
'<path d="M-20 635L158 605 294 646 404 612 547 668 679 622 846 653 991 621 1149 668 1277 636 1460 660" />'+
'<path d="M203 355l-19 33 27 39M842 399l30 34-20 47M404 612l-44 42 23 29M1149 668l39 41-18 32"/>'+
'</svg>';
root.append(layer);
let live=true,raf=0,idleTimer=0,lastTick=0,source=null,lastKick=-100;
let index=0;
const values=Object.create(null);
const setLayer=(name,value)=>{
if(values[name]===value)return;
values[name]=value;
layer.style.setProperty(name,value);
};
const stopTicks=()=>{
cancelAnimationFrame(raf);
clearTimeout(idleTimer);
idleTimer=0;
};
const queueTick=(delay=0)=>{
if(!delay){raf=requestAnimationFrame(tick);return;}
idleTimer=setTimeout(()=>{
idleTimer=0;
if(live)raf=requestAnimationFrame(tick);
},delay);
};
const quiet=()=>{
setLayer('--kkpt-beat-dark','.67');
setLayer('--kkpt-beat-blur','0.15px');
setLayer('--kkpt-beat-glitch','.10');
setLayer('--kkpt-beat-shear','0px');
setLayer('--kkpt-beat-shear-reverse','0px');
setLayer('--kkpt-beat-scars','.04');
layer.classList.remove('kkpt-beat-stutter');
};
function tick(now){
if(!live)return;
if(now-lastTick<28){
queueTick(Math.max(0,28-(now-lastTick)));
return;
}
lastTick=now;
if(!source||source.failed||source.audio.ended||
source.audio.currentTime>=source.config.stopAt-.02){
source=null;layer.classList.add('kkpt-beat-idle');
quiet();live=false;stopTicks();
return;
}
const audioTime=source.audio.currentTime;
while(index<MARENOIA_EIGHT_BEAT_KICKS.length&&
MARENOIA_EIGHT_BEAT_KICKS[index]<=audioTime){
lastKick=MARENOIA_EIGHT_BEAT_KICKS[index++];
}
const actualGain=Number(source.audio.volume);
const playerGain=Number.isFinite(actualGain)
? Math.max(0,Math.min(1,actualGain/.78)):1;
const soundStrength=(.48+.52*marenoiaLoudness(audioTime))*
playerGain;
const rupture=marenoiaGlitchEnvelope(audioTime-lastKick)*
soundStrength;
layer.classList.toggle('kkpt-beat-stutter',rupture>.14);
setLayer('--kkpt-beat-dark',
(.67+.18*rupture).toFixed(2));
setLayer('--kkpt-beat-blur',
(.15+1.9*rupture).toFixed(1)+'px');
setLayer('--kkpt-beat-glitch',
(.10+.85*rupture).toFixed(2));
const shear=Math.sin(index*1.47+audioTime*31)*rupture*13;
setLayer('--kkpt-beat-shear',
shear.toFixed(1)+'px');
setLayer('--kkpt-beat-shear-reverse',
(-shear*.86).toFixed(1)+'px');
setLayer('--kkpt-beat-scars',
(.04+.85*rupture).toFixed(2));
const nextKick=MARENOIA_EIGHT_BEAT_KICKS[index]??Infinity;
if(rupture<.005&&nextKick-audioTime>.15)queueTick(110);
else queueTick(28);
}
quiet();
kuronataAftermath={
attach(nextScore){
source=nextScore;
nextScore.aftermath=kuronataAftermath;
stopTicks();
live=true;lastTick=0;lastKick=-100;
index=0;
layer.classList.remove('kkpt-beat-idle');
raf=requestAnimationFrame(tick);
},
settle(){
source=null;live=false;stopTicks();
layer.classList.add('kkpt-beat-idle');quiet();
},
fadeToDawn(){
source=null;live=false;stopTicks();
quiet();
layer.classList.add('kkpt-beat-outro');
const old=layer;
setTimeout(()=>{
old.remove();
if(kuronataAftermath?.layer===old)kuronataAftermath=null;
},2400);
},
dispose(){
source=null;live=false;stopTicks();
layer.remove();kuronataAftermath=null;
},
layer
};
if(score)kuronataAftermath.attach(score);
return kuronataAftermath;
}
function createCloudbreakAfterglow(continueFromNavigation=false){
const previous=document.getElementById('kkpt-cloudbreak');
previous?.remove();
let timestamp=Date.now();
if(continueFromNavigation){
try{timestamp=Number(sessionStorage.getItem(DAWN_HALO_KEY));}
catch(_){return;}
if(!Number.isFinite(timestamp)||timestamp<=0)return;
}else{
try{sessionStorage.setItem(DAWN_HALO_KEY,String(timestamp));}
catch(_){}
}
const elapsed=Math.max(0,Date.now()-timestamp);
if(elapsed>5000){
try{sessionStorage.removeItem(DAWN_HALO_KEY);}catch(_){}
return;
}
const bloom=document.createElement('div');
bloom.id='kkpt-cloudbreak';
bloom.setAttribute('aria-hidden','true');
bloom.style.setProperty('--kkpt-dawn-delay',(-elapsed)+'ms');
bloom.innerHTML='<i class="kkpt-dawn-bright-rim"></i>'+
'<i class="kkpt-dawn-cloud-sweep"></i>';
root.append(bloom);
setTimeout(()=>{
bloom.remove();
try{
if(Number(sessionStorage.getItem(DAWN_HALO_KEY))===timestamp)
sessionStorage.removeItem(DAWN_HALO_KEY);
}catch(_){}
},Math.max(0,5100-elapsed));
}
function createDawnBeatScene(score){
if(!score||score.config?.id!=='anotherme'||
matchMedia('(prefers-reduced-motion: reduce)').matches)return null;
dawnBeatScene?.dispose();
const layer=document.createElement('div');
layer.id='kkpt-sunbeat';
layer.setAttribute('aria-hidden','true');
layer.innerHTML='<i class="kkpt-sunbeat-core"></i>'+
'<i class="kkpt-sunbeat-rays"></i>';
document.body.append(layer);
let timer=0,stopped=false;
const stop=instant=>{
if(stopped)return;
stopped=true;clearTimeout(timer);
if(!instant)layer.classList.add('kkpt-sunbeat-finish');
if(instant)layer.remove();
else setTimeout(()=>layer.remove(),520);
if(dawnBeatScene===scene)dawnBeatScene=null;
if(score.dawnScene===scene)score.dawnScene=null;
};
const scene={finish:()=>stop(false),dispose:()=>stop(true)};
dawnBeatScene=score.dawnScene=scene;
const tick=()=>{
if(stopped)return;
const audio=score.audio;
if(audio.ended||audio.error||audio.currentTime>=score.config.stopAt-.035){
scene.finish();return;
}
const pulse=anotherMeSunPulse(audio.currentTime);
const level=audio.paused||audio.muted?0:
Math.max(0,Math.min(1,audio.volume/.78));
const hit=(pulse.strong*.76+pulse.soft*.17)*level;
const ray=(pulse.strong*.33+pulse.soft*.24)*level;
layer.style.setProperty('--kkpt-sun-hit',hit.toFixed(3));
layer.style.setProperty('--kkpt-sun-ray',ray.toFixed(3));
timer=setTimeout(tick,hit>.025||ray>.025?32:95);
};
tick();
return scene;
}
function createRebirthVisual(score){
const layer=document.createElement('div');
layer.id='kkpt-rebirth-visual';
layer.setAttribute('aria-hidden','true');
const drizzle=document.createElement('div');
drizzle.className='kkpt-rebirth-rain';
const rainbow=document.createElementNS('http://www.w3.org/2000/svg','svg');
rainbow.setAttribute('class','kkpt-rebirth-rainbow');
rainbow.setAttribute('viewBox','0 0 1000 650');
rainbow.setAttribute('preserveAspectRatio','none');
const svgNS='http://www.w3.org/2000/svg';
const defs=document.createElementNS(svgNS,'defs');
const gradient=document.createElementNS(svgNS,'linearGradient');
gradient.setAttribute('id','kkpt-rebirth-muted-arc');
gradient.setAttribute('x1','0%');
gradient.setAttribute('x2','100%');
const haloColors=['#c7e5ff','#e4f7e6','#f7ebd9','#e7e0f5'];
haloColors.forEach((color,i)=>{
const stop=document.createElementNS(svgNS,'stop');
stop.setAttribute('offset',(i*100/3).toFixed(1)+'%');
stop.setAttribute('stop-color',color);
gradient.append(stop);
});
defs.append(gradient);
const arc=document.createElementNS(svgNS,'path');
arc.setAttribute('d','M -100 625 Q 500 -175 1100 625');
arc.setAttribute('fill','none');
arc.setAttribute('stroke','url(#kkpt-rebirth-muted-arc)');
arc.setAttribute('stroke-width','36');
rainbow.append(defs,arc);
const light=document.createElement('div');
light.className='kkpt-rebirth-sunlight';
const essence=document.createElement('div');
essence.className='kkpt-rebirth-essence';
const count=innerWidth<700?14:24;
for(let i=0;i<count;i++){
const mote=document.createElement('i');
mote.className='kkpt-rebirth-mote';
mote.style.left=(5+Math.random()*88).toFixed(1)+'%';
mote.style.top=(22+Math.random()*73).toFixed(1)+'%';
mote.style.animationDelay=(-Math.random()*2.8).toFixed(2)+'s';
mote.style.animationDuration=(2.3+Math.random()*1.8).toFixed(2)+'s';
essence.append(mote);
}
const field=document.createElement('div');
field.className='kkpt-rebirth-field';
field.append(drizzle,light,rainbow,essence);
const aperture=document.createElement('div');
aperture.className='kkpt-rebirth-aperture';
layer.append(field,aperture);
document.body.append(layer);
let last=-1,alive=true;
return {
update(t){
if(!alive||Math.abs(t-last)<.018&&t<1)return;
last=t;
const pulse=score?.config?.id==='anotherme'?
anotherMeSunPulse(score.audio.currentTime):null;
const bright=pulse?.strong||0;
const {radius}=dawnGeometry(t);
field.style.clipPath='circle('+radius.toFixed(1)+'px at 50% 47%)';
aperture.style.transform='translate(-50%,-50%) scale('+
(radius/520).toFixed(4)+')';
aperture.style.opacity=Math.min(1,
Math.sin(Math.PI*t)*.65+bright*.22).toFixed(3);
drizzle.style.opacity=(.30*Math.pow(1-t,1.4)).toFixed(3);
light.style.opacity=(Math.max(0,Math.min(1,(t-.12)/.67))*.36+
bright*.16).toFixed(3);
rainbow.style.opacity=(Math.max(0,Math.min(1,(t-.48)/.40))*.105).toFixed(3);
essence.style.opacity=(Math.max(0,Math.min(1,(t-.22)/.55))*.72).toFixed(3);
},
finish(){
alive=false;layer.classList.add('kkpt-rebirth-finish');
setTimeout(()=>layer.remove(),1250);
},
dispose(){alive=false;layer.remove();}
};
}
function clearReturnScore(score){
if(!score)return;
clearInterval(score.displayTimer);
score.hud?.remove();
score.beatScene?.dispose();
score.dawnScene?.finish();
score.aftermath?.settle();
try{
score.audio.pause();
score.audio.removeAttribute('src');score.audio.load();
}catch(_){}
if(returnScore===score)returnScore=null;
if(score.config?.id==='marenoia')removeScoreOmen();
}
function watchScore(score){
const audio=score.audio,config=score.config;
score.displayTimer=setInterval(()=>{
if(audio.ended||audio.error||audio.currentTime>=config.stopAt-.018){
clearReturnScore(score);return;
}
const elapsed=Math.max(0,audio.currentTime-config.startAt);
const remaining=Math.max(0,config.stopAt-audio.currentTime);
const fadeIn=config.fadeInSeconds?
Math.min(1,elapsed/config.fadeInSeconds):1;
const fadeOut=config.fadeSeconds?
Math.min(1,remaining/config.fadeSeconds):1;
const volume=.78*Math.min(fadeIn,fadeOut);
if(Math.abs(audio.volume-volume)>.0008)audio.volume=volume;
},60);
audio.addEventListener('ended',()=>clearReturnScore(score),{once:true});
audio.addEventListener('error',()=>{
score.failed=true;clearReturnScore(score);
},{once:true});
}
function createScore(config){
const audio=new Audio(config.url);
audio.preload='auto';audio.playsInline=true;
audio.volume=0;audio.muted=userWantsMute();
const score={audio,config,failed:false,blocked:false,
displayTimer:null,hud:null,playPromise:null};
returnScore=score;
mountAudioToggle(score);
watchScore(score);
return score;
}
function armReturnScore(targetPage){
const config=SCORES[targetPage];
if(!config)return null;
if(returnScore&&!returnScore.failed&&
returnScore.config.id===config.id)return returnScore;
if(returnScore)clearReturnScore(returnScore);
const score=createScore(config);
try{score.audio.currentTime=config.startAt;}catch(_){}
score.audio.addEventListener('loadedmetadata',()=>{
if(score.audio.currentTime<config.startAt-.2){
try{score.audio.currentTime=config.startAt;}catch(_){}
}
},{once:true});
startScoreOmen(score);
try{
score.playPromise=Promise.resolve(score.audio.play()).then(()=>{
return true;
},err=>{
console.warn('[KKPT] Soundtrack playback blocked:',err);
score.failed=true;clearReturnScore(score);return false;
});
}catch(err){
console.warn('[KKPT] Soundtrack playback failed:',err);
score.failed=true;clearReturnScore(score);
score.playPromise=Promise.resolve(false);
}
return score;
}
function awaitScoreTime(score,seconds){
if(!score||score.failed)return Promise.resolve(false);
return new Promise(resolve=>{
const audio=score.audio;
let timer=null,lastTime=audio.currentTime;
let lastMovement=performance.now();
let settled=false;
const done=ok=>{
if(settled)return;
settled=true;clearInterval(timer);
audio.removeEventListener('timeupdate',check);
audio.removeEventListener('ended',check);
audio.removeEventListener('error',check);
resolve(ok);
};
const check=()=>{
if(audio.currentTime>=seconds-.018){done(true);return;}
if(score.failed||audio.error||audio.ended){done(false);return;}
if(audio.currentTime>lastTime+.015){
lastTime=audio.currentTime;lastMovement=performance.now();
}
if(performance.now()-lastMovement>20000)done(false);
};
audio.addEventListener('timeupdate',check);
audio.addEventListener('ended',check);
audio.addEventListener('error',check);
timer=setInterval(check,30);
check();
});
}
function saveReturnScoreHandoff(score){
if(!score||score.failed||score.audio.ended||
score.audio.currentTime>=score.config.stopAt-.15)return;
try{
sessionStorage.setItem(SCORE_KEY,JSON.stringify({
stamp:Date.now(),time:score.audio.currentTime,
target:score.config.target,
expires:Date.now()+115000,revision:46
}));
if(score.config.id==='marenoia'){
sessionStorage.setItem(AFTER_DARK_KEY,'1');
}
}catch(err){
console.warn('[KKPT] Cannot preserve soundtrack position:',err);
}
}
function restoreReturnScoreHandoff(){
if(window!==window.top)return false;
let saved=null;
try{
saved=JSON.parse(sessionStorage.getItem(SCORE_KEY));
sessionStorage.removeItem(SCORE_KEY);
}catch(_){}
if(!saved||saved.revision!==46||saved.target!==basePage||
Date.now()>saved.expires)return false;
const config=SCORES[saved.target];
if(!config)return false;
const currentTarget=()=>saved.time+
(Date.now()-saved.stamp)/1000;
if(currentTarget()>=config.stopAt-.15)return false;
const score=createScore(config);
const audio=score.audio;
let attempted=false,blockedAt=null;
function playFromPosition(){
const requested=blockedAt===null?currentTarget():blockedAt;
const position=Math.min(config.stopAt-.12,requested);
if(position>=config.stopAt-.15){clearReturnScore(score);return;}
try{audio.currentTime=Math.max(config.startAt,position);}catch(_){}
Promise.resolve(audio.play()).then(()=>{
blockedAt=null;
score.blocked=false;updateAudioToggle(score);
}).catch(err=>{
console.info('[KKPT] Browser requires an audio click:',err);
blockedAt=position;score.blocked=true;
updateAudioToggle(score);
});
}
score.resume=()=>{
if(blockedAt!==null){
try{audio.currentTime=blockedAt;}catch(_){}
}
return Promise.resolve(audio.play()).then(value=>{
blockedAt=null;return value;
});
};
const autoplay=()=>{
if(attempted)return;attempted=true;
playFromPosition();
};
audio.addEventListener('loadedmetadata',autoplay,{once:true});
audio.load();
setTimeout(()=>{if(audio.readyState>=1)autoplay();},1100);
return true;
}
let active = false; // true while the interactive target iframe owns the viewport
let animating = false;
let gearPending = false;
let ready = false;
let loading = null;
let button, labelTitle, labelSub, labelIcon, frame;
let otherTitle = otherPage + ' - Blockland Wiki';
const COOLDOWN_KEY='kkpt:switch-ready:46';
const COOLDOWN_MS=18000;
let cooldownUntil=0,cooldownTicker=0;
function cooldownSeconds(){
return Math.max(0,Math.ceil((cooldownUntil-Date.now())/1000));
}
function isCoolingDown(){return cooldownSeconds()>0;}
function refreshCooldownUI(){
const seconds=cooldownSeconds();
const locked=seconds>0;
if(button){
button.classList.toggle('kkpt-cooldown',locked);
button.setAttribute('aria-disabled',String(locked));
if(locked)labelSub.textContent='RECONNECT IN '+seconds+'S';
}
const docs=[document];
try{if(frame?.contentDocument)docs.push(frame.contentDocument);}catch(_){}
for(const doc of docs){
doc.documentElement.classList.toggle('kkpt-cooldown-active',locked);
doc.querySelectorAll('.kkpt-fate-gear').forEach(gear=>{
gear.disabled=locked;
gear.setAttribute('aria-disabled',String(locked));
});
}
if(!locked){
try{sessionStorage.removeItem(COOLDOWN_KEY);}catch(_){}
updateButton();
}
}
function scheduleCooldown(){
clearTimeout(cooldownTicker);
refreshCooldownUI();
if(isCoolingDown())cooldownTicker=setTimeout(scheduleCooldown,
Math.min(1000,Math.max(25,cooldownUntil-Date.now())));
}
function beginCooldown(){
cooldownUntil=Date.now()+COOLDOWN_MS;
try{sessionStorage.setItem(COOLDOWN_KEY,String(cooldownUntil));}catch(_){}
scheduleCooldown();
}
function restoreCooldown(){
try{cooldownUntil=Number(sessionStorage.getItem(COOLDOWN_KEY))||0;}
catch(_){cooldownUntil=0;}
if(isCoolingDown())scheduleCooldown();
}
function whichPage() { return active ? otherPage : basePage; }
function destination() { return active ? basePage : otherPage; }
function isDark(page) { return page === 'Kuronata'; }
function pathPage() {
const url = new URL(location.href);
if (url.pathname.startsWith('/wiki/')) {
try { return decodeURIComponent(url.pathname.slice(6)).replace(/_/g, ' '); }
catch (e) { return ''; }
}
return url.searchParams.get('title') || '';
}
function commitVirtualWorld(nextActive){
const url=nextActive?otherUrl.href:baseUrl;
const page=nextActive?otherPage:basePage;
if(pathPage()!==page){
const previous=history.state;
const state=previous&&typeof previous==='object'
?{...previous,kkptIframeWorld:page}
:{kkptIframeWorld:page};
history.pushState(state,'',url);
}
}
function updateButton() {
if (!button) return;
const to = destination();
button.href = to === basePage ? baseUrl : otherUrl.href;
labelTitle.textContent = to.toUpperCase();
labelSub.textContent = isDark(to) ? 'BURN THE SKY' : 'RECLAIM THE SKY';
if(isCoolingDown())labelSub.textContent='RECONNECT IN '+cooldownSeconds()+'S';
labelIcon.textContent = isDark(to) ? '✦' : '❄';
button.classList.toggle('kkpt-dark', isDark(whichPage()));
button.setAttribute('aria-label', isDark(to) ? '燃尽天空,进入 Kuronata' : '驱散黑暗,回到 Kuranoka');
}
function wikiLinkPage(anchor, doc) {
try {
const url = new URL(anchor.href, doc.baseURI);
if (url.origin !== location.origin) return null;
const action = url.searchParams.get('action');
if (action && action !== 'view') return null;
let title = url.searchParams.get('title');
if (!title) {
const matched = url.pathname.match(/^\/wiki\/([^/?#]+)\/?$/);
if (matched) title = decodeURIComponent(matched[1]);
}
title = (title || '').replace(/_/g, ' ');
return title === 'Kuranoka' || title === 'Kuronata' ? title : null;
} catch (_) {
return null;
}
}
function decoratePortalLinks(doc, targetPage, changedRoots) {
const content = doc.querySelector('#mw-content-text,.mw-parser-output');
if (!content) return;
const decorate=anchor=>{
if(anchor.classList.contains('kkpt-fate-gear'))return;
if (wikiLinkPage(anchor, doc) !== targetPage) return;
anchor.classList.add('kkpt-portal-link');
anchor.classList.toggle('kkpt-portal-red', targetPage === 'Kuronata');
anchor.classList.toggle('kkpt-portal-blue', targetPage === 'Kuranoka');
const caption = (anchor.textContent || '').trim();
if (caption) anchor.dataset.kkptText = caption.slice(0, 120);
anchor.setAttribute('aria-label', caption + ',切换至 ' + targetPage);
};
for(const node of changedRoots||[content]){
if(node.matches?.('a[href]'))decorate(node);
node.querySelectorAll?.('a[href]').forEach(decorate);
}
}
function watchPortalLinks(doc, targetPage) {
decoratePortalLinks(doc, targetPage);
if (doc.__kkptPortalObserver) return;
const root = doc.querySelector('#mw-content-text,.mw-parser-output');
if (!root) return;
let pending=false;
const changed=new Set();
const observer = new MutationObserver(records => {
for(const record of records){
for(const node of record.addedNodes){
if(node.nodeType===1)changed.add(node);
else if(node.parentElement?.matches('a[href]'))
changed.add(node.parentElement);
}
}
if(pending)return;
pending=true;
setTimeout(()=>{
pending=false;
const roots=[...changed];changed.clear();
if(roots.length)decoratePortalLinks(doc,targetPage,roots);
},60);
});
observer.observe(root, {childList:true,subtree:true});
doc.__kkptPortalObserver = observer;
}
function isPlainClick(event) {
return !event.defaultPrevented && event.button === 0 &&
!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey;
}
function findAnchor(event, doc) {
const target = event.target;
return target && target.nodeType === 1 &&
typeof target.closest === 'function'
? target.closest('a[href],button.kkpt-fate-gear') : null;
}
function portalClick(event, doc, targetPage, nextActive) {
if (!isPlainClick(event)) return false;
const anchor = findAnchor(event, doc);
const gear=anchor?.classList.contains('kkpt-fate-gear')&&
anchor.dataset.kkptTargetPage===targetPage;
if (!anchor || (anchor.target && anchor.target !== '_self') ||
anchor.hasAttribute('download') ||
(!gear&&wikiLinkPage(anchor, doc)!==targetPage)) return false;
event.preventDefault();
if(animating||gearPending||isCoolingDown())return true;
const score=armReturnScore(targetPage);
if(targetPage==='Kuronata'&&score&&!score.beatScene){
score.beatScene=createMarenoiaBeatScene(score);
}
startFatePrelude(anchor,targetPage);
primeSound(); // Synchronous click activation unlocks Web Audio.
if(gear) {
beginFateSwap(anchor,nextActive);
} else {
requestSwap(nextActive);
}
return true;
}
function beginFateSwap(anchor,nextActive) {
if(animating||gearPending||isCoolingDown()||nextActive===active)return;
gearPending=true;
anchor.classList.add('kkpt-fate-gear-turning');
requestAnimationFrame(()=>{
gearPending=false;
requestSwap(nextActive);
});
setTimeout(()=>anchor.classList.remove('kkpt-fate-gear-turning'),3300);
}
function fateGearShape() {
const parts=[];
for(let i=0;i<48;i++) {
const a=i/48*Math.PI*2-Math.PI/2;
const r=(i%4===0||i%4===3)?21:16.8;
parts.push((i?'L':'M')+(24+Math.cos(a)*r).toFixed(2)+' '+
(24+Math.sin(a)*r).toFixed(2));
}
return parts.join(' ')+' Z';
}
function mountFateGear(doc,targetPage) {
if(doc.querySelector('.kkpt-fate-gear'))return;
const content=doc.querySelector('#mw-content-text,.mw-parser-output');
if(!content)return;
const box=content.querySelector('table.infobox.vcard,table.infobox');
const host=box?.caption ||
box?.querySelector('th.infobox-above,th.infobox-title,th[colspan]')||
doc.querySelector('#firstHeading .mw-page-title-main,'+
'.mw-page-title-main,#firstHeading');
if(!host)return;
host.classList.add('kkpt-fate-host');
const anchor=doc.createElement('button');
anchor.type='button';
anchor.className='kkpt-fate-gear';
anchor.dataset.kkptTargetPage=targetPage;
const gearLabel=targetPage==='Kuronata'
?'命运的齿轮 · 唤醒 Kuronata':'命运的齿轮 · 回到 Kuranoka';
anchor.setAttribute('aria-label',gearLabel);
anchor.innerHTML='<svg class="kkpt-fate-gear-svg" viewBox="0 0 48 48" '+
'aria-hidden="true" focusable="false"><path d="'+fateGearShape()+
'"/><circle cx="24" cy="24" r="8.5"/>'+
'<circle cx="24" cy="24" r="3"/></svg>';
host.appendChild(anchor);
}
function backgroundUrls(doc) {
const urls = new Set();
const selector = [doc.documentElement, doc.body];
try {
doc.querySelectorAll('.background-theme,[class*="background"],[style*="background"],#mw-content-text,.citizen-page-container')
.forEach((el, i) => { if (i < 18) selector.push(el); });
} catch (e) { /* use html/body */ }
function add(value) {
if (!value) return;
const pattern = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
let match;
while ((match = pattern.exec(value))) {
try { urls.add(new URL(match[2], doc.baseURI).href); } catch (e) { /* ignore */ }
}
if (/^https?:\/\/\S+\.(?:jpg|jpeg|png|webp|avif)(?:\?\S*)?$/i.test(value)) {
try { urls.add(new URL(value, doc.baseURI).href); } catch (e) { /* ignore */ }
}
}
selector.forEach(el => {
if (!el) return;
try {
[null, '::before', '::after'].forEach(pseudo => {
const style = doc.defaultView.getComputedStyle(el, pseudo);
if (!style) return;
add(style.backgroundImage);
add(style.getPropertyValue('--bg-image'));
add(style.getPropertyValue('--background-image'));
});
if (el.dataset) add(el.dataset.bgImage);
} catch (e) { /* style not yet ready */ }
});
return [...urls];
}
function warmImage(url) {
if (imagePool.has(url)) return imagePool.get(url).promise;
const image = new Image();
const promise = new Promise(resolve => {
let finished = false;
const timeout = setTimeout(() => done(false), 5000);
function done(ok) {
if (finished) return;
finished = true;
clearTimeout(timeout);
resolve(ok);
}
image.onload = () => done(true);
image.onerror = () => done(false);
image.src = url;
if (image.complete) done(image.naturalWidth > 0);
}).then(ok => ok && image.decode
? Promise.race([image.decode().then(() => true).catch(() => true),sleep(900).then(() => true)])
: ok);
imagePool.set(url, { image, promise });
return promise;
}
async function warmBackgrounds(doc) {
const urls = backgroundUrls(doc);
await Promise.allSettled(urls.map(warmImage));
}
function wireFrame() {
try {
const doc = frame.contentDocument;
if (!doc) return;
otherTitle = doc.title || otherTitle;
doc.documentElement.classList.add('kkpt-frame-inactive');
watchPortalLinks(doc, basePage);
mountFateGear(doc, basePage);
if (doc.__kkptFrameBridge) return;
doc.__kkptFrameBridge = true;
doc.addEventListener('click', event => {
if (!active || !isPlainClick(event)) return;
if (portalClick(event, doc, basePage, false)) return;
const el = findAnchor(event, doc);
if (!el || (el.target && el.target !== '_self')) return;
const href = el.href;
if (!href || href.startsWith('javascript:')) return;
const next = new URL(href, doc.baseURI);
const currentFrame = new URL(frame.contentWindow.location.href);
if (next.origin === currentFrame.origin &&
next.pathname === currentFrame.pathname &&
next.search === currentFrame.search && next.hash) {
history.replaceState(history.state, '', next.href);
return;
}
event.preventDefault();
navigateRealPage(next.href);
}, true);
doc.addEventListener('submit', event => {
if (active && event.target instanceof doc.defaultView.HTMLFormElement) {
event.target.target = '_top';
}
}, true);
} catch (err) {
console.warn('[KuranokaPageTransition] Frame link bridge unavailable:', err);
}
}
function warmFrame() {
if (loading) return loading;
frame = document.createElement('iframe');
frame.id = 'kkpt-preview';
frame.src = otherUrl.href; // set BEFORE insertion, avoids extra initial history hop
frame.title = otherPage + ' 预载页面';
frame.tabIndex = -1;
frame.setAttribute('aria-hidden', 'true');
frame.setAttribute('loading', 'eager');
frame.style.clipPath = hideClip;
frame.style.visibility = 'visible';
const targetBackdrop=worldBackdrops[otherPage];
if(targetBackdrop){
frame.style.backgroundImage='url("'+targetBackdrop+'")';
frame.style.backgroundPosition='center center';
frame.style.backgroundSize='cover';
frame.style.backgroundRepeat='no-repeat';
}
frame.addEventListener('load',()=>{
if(!active)return;
try{
const next=new URL(frame.contentWindow.location.href);
if(next.origin!==location.origin||
wikiLinkPage({href:next.href},frame.contentDocument)!==otherPage)
navigateRealPage(next.href);
}catch(_){}
});
loading = new Promise(resolve => {
let settled = false;
let preparing = false;
let poll = null;
const frameCssUrl=new URL('/index.php',location.origin);
frameCssUrl.searchParams.set('title','MediaWiki:KuranokaPageTransition.css');
frameCssUrl.searchParams.set('action','raw');
frameCssUrl.searchParams.set('ctype','text/css');
frameCssUrl.searchParams.set('kkptv','46');
function finish(ok, reason) {
if (settled) return;
settled = true;
clearInterval(poll);
ready = ok;
if (!ok) console.warn('[KuranokaPageTransition] Preview:', reason);
resolve(ok);
}
async function inspect() {
if (settled || preparing) return;
try {
const doc = frame.contentDocument;
if (!doc || doc.readyState!=='complete'||
!doc.querySelector('#mw-content-text,.mw-parser-output')) return;
preparing = true;
clearInterval(poll);
const styled=()=>doc.defaultView?.getComputedStyle(
doc.documentElement).getPropertyValue('--kkpt-css-v46').trim()==='ready';
if(!styled()&&!doc.getElementById('kkpt-frame-css-guarantee')){
const style=document.createElement('link');
style.id='kkpt-frame-css-guarantee';
style.rel='stylesheet';style.href=frameCssUrl.href;
doc.head?.appendChild(style);
}
await Promise.race([doc.fonts?.ready||Promise.resolve(),sleep(1300)]);
await Promise.race([warmBackgrounds(doc),sleep(3300)]);
const visibleImages=Array.from(doc.images).filter(image=>{
const rect=image.getBoundingClientRect();
return rect.top<innerHeight*1.45&&rect.bottom>-innerHeight*.3;
}).slice(0,32);
visibleImages.forEach(image=>{
if(image.loading==='lazy')image.loading='eager';
});
await Promise.race([Promise.allSettled(visibleImages.map(image=>
image.complete?Promise.resolve():new Promise(resolve=>{
image.addEventListener('load',resolve,{once:true});
image.addEventListener('error',resolve,{once:true});
}))),sleep(2600)]);
for(let i=0;i<12&&!styled();i++)await sleep(110);
if(!styled()){
finish(false,'目标页特效 CSS 未完成加载');return;
}
await sleep(100);
frame.contentWindow.scrollTo(0, 0);
wireFrame();
finish(true);
} catch (err) { finish(false, String(err)); }
}
frame.addEventListener('load', () => {
if (!settled && !preparing) inspect();
});
frame.addEventListener('error', () => finish(false,'iframe 网络加载错误'));
document.body.append(frame);
poll = setInterval(inspect, 130);
setTimeout(() => { if (!settled) finish(false,'7 秒内目标页仍未完成加载'); }, 7000);
});
warmBackgrounds(document).catch(() => {});
setTimeout(() => warmBackgrounds(document).catch(() => {}), 700);
return loading;
}
let atmosphereAssetCache=null;
const atmosphereNoiseCache=new Map();
function createAtmosphere(canvas,buildOneOnly=false) {
const ctx = canvas.getContext('2d', {alpha:true});
const rand = n => {
const cached=atmosphereNoiseCache.get(n);
if(cached!==undefined)return cached;
const x = Math.sin(n*127.1+311.7)*43758.5453123;
const value=x-Math.floor(x);
atmosphereNoiseCache.set(n,value);
return value;
};
const clamp01 = n => Math.max(0,Math.min(1,n));
function valueNoise(x,y,seed) {
const ix=Math.floor(x), iy=Math.floor(y);
let fx=x-ix,fy=y-iy;
fx=fx*fx*(3-2*fx);fy=fy*fy*(3-2*fy);
const h=(a,b)=>rand(a*171.3+b*513.7+seed*89.1);
const a=h(ix,iy)*(1-fx)+h(ix+1,iy)*fx;
const b=h(ix,iy+1)*(1-fx)+h(ix+1,iy+1)*fx;
return a*(1-fy)+b*fy;
}
function smokeTexture(seed,dark) {
const tex=document.createElement('canvas');
tex.width=tex.height=112;
const g=tex.getContext('2d');
const data=g.createImageData(112,112);
for(let y=0;y<112;y++) for(let x=0;x<112;x++){
const u=(x-56)/56,v=(y-56)/56;
const curl=valueNoise(u*2.9+11,v*2.9+1,seed);
const curl2=valueNoise(u*3.4+3,v*3.4+7,seed+7);
const wx=u+.47*(curl-.5),wy=v+.40*(curl2-.5);
const radius=Math.sqrt(wx*wx*.91+wy*wy*1.13);
const envelope=clamp01((1.05-radius)/.56);
const low=valueNoise(u*4.1+4,v*4.1-9,seed+2);
const high=valueNoise(u*11.4-3,v*11.4+6,seed+5);
const fibers=valueNoise(u*22.0+1,v*16.0+5,seed+9);
const density=clamp01((low*.62+high*.28+fibers*.10-.28)*2.0)*
envelope*envelope;
const i=(y*112+x)*4;
const variation=low*28+high*15;
if(dark){
data.data[i]=5+variation*.37;
data.data[i+1]=5+variation*.33;
data.data[i+2]=8+variation*.38;
}else{
data.data[i]=44+variation;
data.data[i+1]=91+variation;
data.data[i+2]=125+variation;
}
data.data[i+3]=Math.round(240*density);
}
g.putImageData(data,0,0);
return tex;
}
function glowTexture(seed,mode) {
const tex=document.createElement('canvas');
tex.width=96;tex.height=128;
const g=tex.getContext('2d');
const gradient=g.createRadialGradient(48,68,0,48,68,64);
const colors=mode==='fire'
? [[0,'#fffef1'],[.14,'#fff4bb'],[.34,'#ffd04d'],
[.55,'#ff841bcc'],[.80,'#bc290749'],[1,'#6e030000']]
: mode==='blue'
? [[0,'#ffffff'],[.19,'#f1ffff'],[.40,'#a2efff'],
[.68,'#5298ff99'],[1,'#345bf000']]
: [[0,'#ffffff'],[.19,'#fff8cd'],[.49,'#ff9b28cc'],
[1,'#ff380000']];
colors.forEach(([p,c])=>gradient.addColorStop(p,c));
g.fillStyle=gradient;g.fillRect(0,0,96,128);
const img=g.getImageData(0,0,96,128);
for(let y=0;y<128;y++)for(let x=0;x<96;x++){
const u=(x-48)/48,v=(y-64)/64;
const cloud=valueNoise(u*4+seed*1.9,v*4+seed*.7,seed+18);
const fiber=valueNoise(u*11+3,v*8+17,seed+41);
const shifted=u+.18*(cloud-.5);
const irregular=clamp01((1.15-Math.abs(shifted))/.52)*
clamp01((1.05-Math.abs(v+.12*(fiber-.5)))/.42);
const n=.59+.77*cloud+.28*fiber;
const alphaIndex=(y*96+x)*4+3;
img.data[alphaIndex]=Math.round(img.data[alphaIndex]*
irregular*clamp01(n));
}
g.putImageData(img,0,0);
return tex;
}
const cache=atmosphereAssetCache ||= {
sootSprites:[],mistSprites:[],FIRE_GLOW:[],BLUE_GLOW:[],
EMBER_GLOW:null
};
function buildNextAsset(){
let n=cache.sootSprites.length;
if(n<4){cache.sootSprites.push(smokeTexture(n+17,true));return true;}
n=cache.mistSprites.length;
if(n<4){cache.mistSprites.push(smokeTexture(n+51,false));return true;}
n=cache.FIRE_GLOW.length;
if(n<4){cache.FIRE_GLOW.push(glowTexture(n+1,'fire'));return true;}
n=cache.BLUE_GLOW.length;
if(n<4){cache.BLUE_GLOW.push(glowTexture(n+8,'blue'));return true;}
if(!cache.EMBER_GLOW){
cache.EMBER_GLOW=glowTexture(21,'ember');return true;
}
return false;
}
if(buildOneOnly)return buildNextAsset();
while(buildNextAsset()){}
const {sootSprites,mistSprites,FIRE_GLOW,BLUE_GLOW,EMBER_GLOW}=cache;
const combustion=[];
const residualSoot=[];
const tornPaper=[];
let lastElapsed=0,spawnRemainder=0,sootRemainder=0,shardRemainder=0;
function advanceCombustion(seconds,progress,dark,elapsed,w,h) {
const dt=Math.min(.065,Math.max(0,elapsed-lastElapsed));
lastElapsed=elapsed;
const live=progress>.01&&progress<.985;
if(live){
const flow=(dark?132:64)*Math.min(1.5,h/720)*
(.75+.25*Math.sin(Math.PI*progress));
spawnRemainder+=dt*flow;
const births=Math.min(65,Math.floor(spawnRemainder));
spawnRemainder-=births;
for(let j=0;j<births&&combustion.length<1320;j++){
const y=Math.random()*h;
const edge=edgeAt(y,seconds,progress,w);
const r=Math.random();
const kind=dark
? r<.38?'flame':r<.91?'ember':'ash'
: r<.37?'glow':r<.81?'leaf':'pollen';
const small=kind==='ember'||kind==='pollen';
const soot=kind==='ash';
const life=dark
? (soot?.7+Math.random()*1.1:
small?.9+Math.random()*1.55:.42+Math.random()*.76)
: 1.15+Math.random()*1.55;
const dawn=dark?null:dawnGeometry(progress,w,h);
const angle=Math.random()*Math.PI*2;
combustion.push({
kind,x:dark?edge+Math.random()*47-8:
dawn.cx+Math.cos(angle)*dawn.radius,
y:dark?y+(Math.random()-.5)*25:
dawn.cy+Math.sin(angle)*dawn.radius,
vx:dark?(small?65+Math.random()*245:
14+Math.random()*154):
Math.cos(angle)*(20+Math.random()*88),
vy:dark?-(small?12+Math.random()*190:
32+Math.random()*171):
Math.sin(angle)*(20+Math.random()*60)-33,
life,remaining:life,
radius:dark
? (soot?1.2+Math.random()*2.7:
small?2+Math.random()*5:9+Math.random()*19)
: kind==='glow'?26+Math.random()*49:
kind==='leaf'?4+Math.random()*6:
2+Math.random()*3,
angle:(Math.random()-.5)*1.1,
sprite:(Math.random()*4)|0,
seed:Math.random()*6.28
});
}
if(dark) {
sootRemainder+=dt*(h<680?32:46);
const sootBirths=Math.min(11,Math.floor(sootRemainder));
sootRemainder-=sootBirths;
for(let i=0;i<sootBirths&&residualSoot.length<(w<700?92:155);i++){
const y=Math.random()*h;
const life=2.15+Math.random()*1.60;
residualSoot.push({
x:edgeAt(y,seconds,progress,w)+5+Math.random()*83,
y:y+(Math.random()-.5)*48,
vx:8+Math.random()*39,vy:-18-Math.random()*83,
size:155+Math.random()*195,life,remaining:life,
sprite:(Math.random()*4)|0,
spin:(Math.random()-.5)*.8,
seed:Math.random()*6.28
});
}
shardRemainder+=dt*(h<680?18:30);
const shardBirths=Math.min(7,Math.floor(shardRemainder));
shardRemainder-=shardBirths;
for(let i=0;i<shardBirths&&tornPaper.length<175;i++){
const y=Math.random()*h;
const life=.90+Math.random()*1.85;
const points=8+Math.floor(Math.random()*7);
const outline=Array.from({length:points},(_,j)=>({
a:Math.PI*2*j/points+(Math.random()-.5)*.50,
r:.25+Math.random()*1.06
}));
tornPaper.push({
x:edgeAt(y,seconds,progress,w)+(Math.random()-.20)*58,
y, vx:55+Math.random()*295,
vy:-190+Math.random()*270,
gravity:90+Math.random()*150,
radius:2+Math.pow(Math.random(),2.5)*22,
angle:Math.random()*6.28,
spin:(Math.random()-.5)*7,
outline,life,remaining:life,
seed:Math.random()*6.28
});
}
}
}
for(let i=combustion.length-1;i>=0;i--){
const p=combustion[i];
p.remaining-=dt;
if(p.remaining<=0){combustion.splice(i,1);continue;}
p.x+=(p.vx+Math.sin(elapsed*(dark?5:1.6)+p.seed)*
(dark?22:24))*dt;
p.y+=p.vy*dt;
p.vy-=(dark?17:7)*dt;
}
for(let i=residualSoot.length-1;i>=0;i--){
const p=residualSoot[i];
p.remaining-=dt;
if(p.remaining<=0){residualSoot.splice(i,1);continue;}
p.x+=(p.vx+Math.sin(elapsed*2+p.seed)*27)*dt;
p.y+=p.vy*dt;
p.size+=32*dt;
}
for(let i=tornPaper.length-1;i>=0;i--){
const p=tornPaper[i];
p.remaining-=dt;
if(p.remaining<=0){tornPaper.splice(i,1);continue;}
p.x+=(p.vx+Math.sin(elapsed*4+p.seed)*42)*dt;
p.y+=p.vy*dt;
p.vy+=p.gravity*dt;
p.angle+=p.spin*dt;
}
}
const particles=Array.from({length:innerWidth<700?40:62},(_,i)=>({
seed:rand(i*4+71),height:rand(i*5+13),
lifetime:.5+rand(i*8+37)*.7,
phase:rand(i*9+21),sprite:i%4,
scale:.72+rand(i*7+9)*1.25,
bend:rand(i*15+23)*6.28
}));
const motes=Array.from({length:innerWidth<700?56:115},(_,i)=>({
y:rand(i*3+17),phase:rand(i*9+43),
speed:100+rand(i*12+5)*340,
drift:82+rand(i*15+2)*330,
length:3+rand(i*17+7)*16
}));
const shockDebris=Array.from({length:innerWidth<700?32:66},(_,i)=>({
angle:rand(i*17+131)*Math.PI*2,
velocity:190+rand(i*11+51)*900,
seed:rand(i*5+79),
length:6+rand(i*7+17)*26
}));
function pressureWave(x,y,radius,power,dark,time) {
if(power<=.005)return;
const core=ctx.createRadialGradient(x,y,0,x,y,radius*.75);
if(dark){
core.addColorStop(0,'rgba(255,231,169,'+(.20*power)+')');
core.addColorStop(.17,'rgba(253,88,18,'+(.21*power)+')');
core.addColorStop(.60,'rgba(106,16,12,'+(.09*power)+')');
core.addColorStop(1,'rgba(30,0,0,0)');
}else{
core.addColorStop(0,'rgba(237,255,255,'+(.24*power)+')');
core.addColorStop(.26,'rgba(98,193,255,'+(.18*power)+')');
core.addColorStop(1,'rgba(0,29,100,0)');
}
ctx.globalCompositeOperation='screen';
ctx.fillStyle=core;
ctx.fillRect(x-radius,y-radius,radius*2,radius*2);
ctx.globalCompositeOperation='source-over';
}
return function drawAtmosphere(seconds,progress,dark,elapsed,cells) {
const w=innerWidth,h=innerHeight;
const dawn=dark?null:dawnGeometry(progress,w,h);
const dawnPoint=(angle,offset=0)=>({
x:dawn.cx+(dawn.radius+offset)*Math.cos(angle),
y:dawn.cy+(dawn.radius+offset)*Math.sin(angle)
});
const scale=dark?(w<700?.43:.55):(w<700?.35:.45);
const bw=Math.max(1,Math.round(w*scale));
const bh=Math.max(1,Math.round(h*scale));
if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;}
ctx.setTransform(scale,0,0,scale,0,0);
ctx.clearRect(0,0,w,h);
const envelope=Math.sin(Math.PI*progress);
const fade=clamp01(progress/.075)*clamp01((1-progress)/.065);
advanceCombustion(seconds,progress,dark,elapsed,w,h);
const sprites=dark?sootSprites:mistSprites;
const direction=dark?1:-1;
ctx.globalCompositeOperation='source-over';
const bands=dark?Math.max(8,Math.ceil(h/116)):
Math.max(6,Math.ceil(h/136));
for(let i=0;i<bands;i++){
const baseY=(i+.15+rand(i*11+39)*.70)*h/bands;
const angle=i/bands*Math.PI*2+.12;
const place=dawn?dawnPoint(angle,98+32*Math.sin(seconds+i)):
null;
const y=dawn?place.y:baseY+31*Math.sin(seconds*1.6+i*1.47);
const x=dawn?place.x:edgeAt(y,seconds,progress,w)+direction*
(76+rand(i*19+6)*260)+
Math.sin(seconds*.93+i*1.3)*37;
const width=dark?170+rand(i*29+12)*220:250;
const height=dark?165+rand(i*7+5)*170:245;
ctx.save();
ctx.globalAlpha=(dark?.34:.21)*fade*(.55+.45*envelope);
ctx.translate(x,y);
ctx.rotate(.27*Math.sin(i*2.1+seconds*.9));
ctx.drawImage(sprites[i%4],-width/2,-height/2,width,height);
ctx.restore();
}
for(let particleIndex=0;particleIndex<particles.length;
particleIndex+=dark?1:2){
const item=particles[particleIndex];
const age=((elapsed/item.lifetime+item.phase)%1);
const baseY=item.height*h;
const y=baseY-age*(dark?230:128)+(Math.sin(elapsed*2+item.bend)*15);
if(dark&&(y< -230||y>h+230))continue;
const angle=item.seed*Math.PI*2+item.bend*.25;
const place=dawn?dawnPoint(angle,40+age*145):null;
const frontier=edgeAt(baseY,seconds,progress,w);
const x=dawn?place.x:frontier+direction*(age*225+
30*Math.sin(age*6.3+item.bend));
const particleY=dawn?place.y-age*28:y;
if(particleY< -230||particleY>h+230)continue;
const size=(dark?174:145)*item.scale*(.68+age*1.9);
const opacity=(dark?.43:.26)*Math.sin(Math.PI*age)*fade*
(.55+.45*envelope);
if(opacity<=.005)continue;
ctx.save();
ctx.globalAlpha=opacity;
ctx.translate(x,particleY);
ctx.rotate((item.bend-3.14)*.14+age*.85*direction);
ctx.scale(1.18,.84);
ctx.drawImage(sprites[item.sprite],-size/2,-size/2,size,size);
ctx.restore();
}
if(dark){
ctx.save();
ctx.globalCompositeOperation='source-over';
for(const p of residualSoot){
const age=1-p.remaining/p.life;
const opacity=clamp01(age/.14)*
Math.pow(1-age,.49)*(.66+.24*Math.sin(p.seed*3)**2)*fade;
if(opacity<.009)continue;
ctx.save();
ctx.globalAlpha=opacity;
ctx.translate(p.x,p.y);
ctx.rotate(p.spin+age*.33*Math.sin(p.seed));
ctx.scale(1.20,.92);
const s=p.size*(1+age*.68);
ctx.drawImage(sootSprites[p.sprite],-s/2,-s/2,s,s);
ctx.restore();
}
ctx.restore();
}else{
ctx.save();
ctx.globalCompositeOperation='screen';
for(let n=0;n<2;n++){
const place=dawnPoint(n*Math.PI+.7,42);
const yy=place.y+Math.sin(seconds*.8+n*2)*30;
const xx=place.x;
const radius=145+65*Math.sin(seconds*.7+n*1.7)**2;
const bloom=ctx.createRadialGradient(xx,yy,8,xx,yy,radius);
bloom.addColorStop(0,'rgba(250,255,255,.10)');
bloom.addColorStop(.33,'rgba(164,237,255,.07)');
bloom.addColorStop(.74,'rgba(117,198,255,.04)');
bloom.addColorStop(1,'rgba(117,198,255,0)');
ctx.globalAlpha=fade;
ctx.fillStyle=bloom;
ctx.fillRect(xx-radius,yy-radius,radius*2,radius*2);
}
ctx.restore();
}
for(const moment of (dark?[.10,.48]:[])){
const age=(progress-moment)/.125;
if(age<0||age>1)continue;
const intensity=(1-age)*(1-age)*fade;
ctx.globalCompositeOperation='screen';
ctx.fillStyle=dark
? 'rgba(255,81,26,'+(.095*intensity)+')'
: 'rgba(150,219,255,'+(.080*intensity)+')';
ctx.fillRect(0,0,w,h);
ctx.globalCompositeOperation='source-over';
const x=edgeAt(h*.51,seconds,progress,w);
const y=h*.51;
const radius=95+age*Math.max(w,h)*.9;
pressureWave(x,y,radius,intensity,dark,seconds);
ctx.globalAlpha=(dark?.20:.28)*intensity;
ctx.lineWidth=(dark?2.5:2)*Math.max(.4,1-age);
ctx.strokeStyle=dark?'#ffb36c':'#b5eaff';
ctx.beginPath();
for(let i=0;i<=110;i++){
const a=i/110*Math.PI*2;
const jag=1+.03*Math.sin(a*29+seconds*12)+
.05*Math.sin(a*13-seconds*5);
const rx=Math.cos(a)*radius*jag,ry=Math.sin(a)*radius*jag;
if(i===0)ctx.moveTo(x+rx,y+ry);else ctx.lineTo(x+rx,y+ry);
}
ctx.stroke();
ctx.globalAlpha=1;
if(dark){
ctx.globalCompositeOperation='screen';
for(const bit of shockDebris){
const f=age*(.5+bit.seed*.5);
const travel=bit.velocity*f;
const px=x+Math.cos(bit.angle)*travel;
const py=y+Math.sin(bit.angle)*travel;
const vx=Math.cos(bit.angle)*bit.length*(1-age);
const vy=Math.sin(bit.angle)*bit.length*(1-age);
ctx.globalAlpha=(dark?.82:.68)*intensity*(.3+bit.seed*.7);
ctx.strokeStyle=dark?'#ffdd9b':'#e0faff';
ctx.lineWidth=.6+bit.seed*1.3;
ctx.beginPath();
ctx.moveTo(px,py);
ctx.lineTo(px-vx,py-vy);
ctx.stroke();
}
}
ctx.globalCompositeOperation='source-over';
ctx.globalAlpha=1;
}
if(dark){
ctx.save();
ctx.globalCompositeOperation='screen';
const count=Math.ceil(h/17);
for(let j=0;j<count;j++){
const seed=rand(j*13+167);
const y=(j+seed*.78)*11.5-5;
const x=edgeAt(y,seconds,progress,w);
if(x < -90 || x > w+90)continue;
const ripple=Math.sin(seconds*(3.4+rand(j*11+4)*2.4)+
j*2.09+seed*3.1);
const heat=clamp01(.58+ripple*.34+
.16*Math.sin(j*1.17-seconds*7.1));
if(heat<.34)continue;
const length=(22+rand(j*23+19)*75)*(.5+heat*.94);
const bend=.18+rand(j*17+42)*.66;
const tipX=x-length*(.76+.19*seed);
const tipY=y-length*bend;
const thick=(3.2+rand(j*31+11)*10.3)*(.5+heat);
const gradient=ctx.createLinearGradient(x,y,tipX,tipY);
gradient.addColorStop(0,'rgba(255,255,242,.99)');
gradient.addColorStop(.28,'rgba(255,224,84,.96)');
gradient.addColorStop(.59,'rgba(255,76,10,.64)');
gradient.addColorStop(1,'rgba(182,17,6,0)');
ctx.beginPath();
ctx.moveTo(x+2,y+thick*.5);
ctx.bezierCurveTo(
x-length*.22,y-thick*.25,
tipX+length*.36,tipY+thick*.68,tipX,tipY);
ctx.bezierCurveTo(
tipX+length*.32,tipY-thick*.22,
x-length*.17,y-thick*.74,
x+2,y-thick*.65);
ctx.closePath();
ctx.globalAlpha=Math.min(1,heat*1.08)*fade;
ctx.fillStyle=gradient;
ctx.shadowBlur=0;
ctx.fill();
if(heat>.66){
ctx.globalAlpha=(heat-.54)*1.40*fade;
ctx.strokeStyle='#fffce0';ctx.lineWidth=.9+heat*1.1;
ctx.beginPath();ctx.moveTo(x,y);
ctx.quadraticCurveTo(x-length*.3,y-length*.11,
tipX+length*.19,tipY+thick*.18);
ctx.stroke();
}
}
ctx.restore();
}
ctx.save();
ctx.beginPath();
if(dark){
for(let y=-12,i=0;y<=h+12;y+=3.6,i++){
const x=edgeAt(y,seconds,progress,w);
if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
}
}else{
for(let i=0;i<=96;i++){
const a=i/96*Math.PI*2;
const flutter=Math.sin(Math.PI*progress)*(
3.4*Math.sin(a*7+seconds*1.8)+
1.8*Math.sin(a*15-seconds*2.3));
const r=Math.max(0,dawn.radius+flutter);
const x=dawn.cx+Math.cos(a)*r;
const y=dawn.cy+Math.sin(a)*r;
if(!i)ctx.moveTo(x,y);else ctx.lineTo(x,y);
}
ctx.closePath();
}
ctx.lineCap='round';ctx.lineJoin='round';
if(dark){
ctx.strokeStyle='rgba(9,5,7,.81)';
ctx.lineWidth=18;ctx.shadowBlur=0;
ctx.stroke();
ctx.strokeStyle='rgba(210,45,9,.53)';
ctx.lineWidth=6;
ctx.stroke();
}else{
ctx.strokeStyle='rgba(27,95,190,.22)';
ctx.lineWidth=22;ctx.shadowColor='#bcefff';ctx.shadowBlur=16;
ctx.stroke();
ctx.strokeStyle='rgba(173,234,255,.54)';
ctx.lineWidth=9;ctx.shadowColor='#d2faff';ctx.shadowBlur=10;
ctx.stroke();
ctx.strokeStyle='rgba(249,255,255,.78)';
ctx.lineWidth=2.4;ctx.shadowBlur=3;
ctx.stroke();
}
ctx.restore();
if(dark){
ctx.save();
ctx.globalCompositeOperation='screen';
ctx.lineCap='round';
for(let y=-8;y<h+8;y+=5.8){
const intensity=clamp01(.58+
.29*Math.sin(y*.054+seconds*3.7)+
.21*Math.sin(y*.147-seconds*8.9)+
.13*Math.sin(y*.337+seconds*16.4));
if(intensity<.47)continue;
const x=edgeAt(y,seconds,progress,w);
const nextX=edgeAt(y+5.8,seconds,progress,w);
ctx.globalAlpha=intensity*fade;
ctx.lineWidth=1.4+intensity*4.6;
ctx.shadowBlur=0;
ctx.strokeStyle=intensity>.75?'#fff8d7':
intensity>.58?'#ffd369':'#ff831b';
ctx.beginPath();ctx.moveTo(x,y);
ctx.lineTo(nextX,y+5.8);ctx.stroke();
}
ctx.restore();
ctx.save();
ctx.globalCompositeOperation='screen';
ctx.shadowBlur=0;
for (let i=0; i<cells.length; i++) {
const c=cells[i];
ctx.globalAlpha=(.42+.34*c.heat)*fade;
ctx.strokeStyle=c.heat>.73?'#fff6bf':
c.heat>.38?'#ffd166':'#ff621c';
ctx.lineWidth=.8+Math.min(2.6,c.radius*.24);
const n=c.points.length;
ctx.beginPath();
for (let j=0;j<n;j++) {
if ((j+i*3)%5>2)continue;
const a=c.points[j],b=c.points[(j+1)%n];
ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);
}
ctx.stroke();
}
ctx.restore();
ctx.save();
for(let j=0;j<Math.ceil(h/10);j++){
const y=(j+rand(j*5+61)*.8)*10;
const x=edgeAt(y,seconds,progress,w)-
(5+rand(j*17+38)*24);
const size=.5+rand(j*29+11)*2.5;
const visible=.25+.59*rand(j*13+7);
ctx.globalAlpha=visible*fade;
ctx.fillStyle='#150b0a';
ctx.beginPath();
ctx.ellipse(x,y,size*.71,size,rand(j*7+31)*3.14,
0,Math.PI*2);
ctx.fill();
}
ctx.restore();
}
ctx.save();
ctx.globalCompositeOperation='lighter';
for(const p of combustion){
if(p.kind==='ash'||p.kind==='leaf'||p.kind==='pollen')continue;
const age=1-p.remaining/p.life;
const opacity=clamp01(p.remaining/.22)*
clamp01(age/.08)*(1-age*.30)*
(p.kind==='ember'?.74:p.kind==='glow'?.13:.66)*fade*
(dark?Math.exp(-Math.max(0,p.x-
edgeAt(p.y,seconds,progress,w))/70):1);
if(opacity<.012)continue;
ctx.save();
ctx.globalAlpha=opacity;
ctx.translate(p.x,p.y);
ctx.rotate(p.angle+Math.sin(elapsed*4+p.seed)*.13);
const large=p.kind==='flame';
const sx=p.radius*(p.kind==='glow'?1.7:large?.96:1.32)*(1+age*.32);
const sy=p.radius*(p.kind==='glow'?1.4:large?1.78:1.35)*(1+age*.57);
const sprite=p.kind==='flame'?FIRE_GLOW[p.sprite]:
p.kind==='glow'?BLUE_GLOW[p.sprite]:EMBER_GLOW;
ctx.drawImage(sprite,-sx,-sy,sx*2,sy*2);
ctx.restore();
}
ctx.restore();
if(!dark){
ctx.save();
ctx.globalCompositeOperation='screen';
for(const p of combustion){
if(p.kind!=='leaf'&&p.kind!=='pollen')continue;
const age=1-p.remaining/p.life;
const opacity=clamp01(age/.17)*clamp01(p.remaining/.55)*
(p.kind==='leaf'?.47:.28)*fade;
if(opacity<.012)continue;
ctx.save();
ctx.globalAlpha=opacity;
ctx.translate(p.x+11*Math.sin(elapsed*1.6+p.seed),p.y);
ctx.rotate(p.angle+age*1.65*Math.sin(p.seed*2.5));
if(p.kind==='leaf'){
const r=p.radius;
ctx.shadowBlur=5;
ctx.shadowColor='#bbf4ff';
ctx.fillStyle=p.sprite%2?'#e6ffff':'#ffffff';
ctx.beginPath();
ctx.moveTo(0,-r*1.65);
ctx.bezierCurveTo(r*.97,-r*.48,r*.85,r*.60,0,r*1.35);
ctx.bezierCurveTo(-r*.92,r*.28,-r*.94,-r*.57,0,-r*1.65);
ctx.fill();
ctx.globalAlpha=opacity*.38;
ctx.strokeStyle='#70c9ea';
ctx.lineWidth=.65;
ctx.beginPath();ctx.moveTo(0,-r*1.15);
ctx.quadraticCurveTo(-r*.17,0,0,r*.95);ctx.stroke();
}else{
const r=p.radius;
ctx.globalAlpha=opacity*.72;
ctx.drawImage(BLUE_GLOW[p.sprite],-r*3,-r*3,r*6,r*6);
}
ctx.restore();
}
ctx.restore();
}
ctx.save();
ctx.globalCompositeOperation='source-over';
for(const p of combustion){
if(p.kind!=='ash')continue;
ctx.globalAlpha=clamp01(p.remaining/.25)*.7*fade;
ctx.fillStyle='#190d10';
ctx.save();ctx.translate(p.x,p.y);ctx.rotate(elapsed*2+p.angle);
ctx.fillRect(-p.radius*.5,-p.radius*.5,p.radius,p.radius*1.8);
ctx.restore();
}
ctx.restore();
if(dark){
ctx.save();
ctx.globalCompositeOperation='source-over';
for(const p of tornPaper){
const age=1-p.remaining/p.life;
const opacity=clamp01(age/.08)*clamp01(p.remaining/.4)*fade;
if(opacity<.01)continue;
ctx.save();
ctx.globalAlpha=opacity;
ctx.translate(p.x,p.y);
ctx.rotate(p.angle);
ctx.beginPath();
p.outline.forEach((v,i)=>{
const px=Math.cos(v.a)*v.r*p.radius;
const py=Math.sin(v.a)*v.r*p.radius*.74;
if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);
});
ctx.closePath();
ctx.shadowBlur=7;
ctx.shadowColor='#090506';
ctx.fillStyle=p.seed>3?'#190d11':'#302025';
ctx.fill();
ctx.shadowBlur=0;
ctx.lineWidth=.8+Math.sin(p.seed)**2*.8;
ctx.strokeStyle=age<.48?'rgba(255,107,35,.48)':
'rgba(49,20,19,.47)';
ctx.stroke();
if(p.seed>4.5&&age<.52){
ctx.globalAlpha*=.45*(1-age);
ctx.strokeStyle='#c5e7f7';
ctx.beginPath();
ctx.moveTo(-p.radius*.42,-p.radius*.16);
ctx.quadraticCurveTo(0,p.radius*.25,p.radius*.28,0);
ctx.stroke();
}
ctx.restore();
}
ctx.restore();
}
if(dark){ctx.save();
ctx.globalCompositeOperation='screen';
for(const mote of motes){
const age=(elapsed*.74+mote.phase)%1;
const yy=mote.y*h-age*mote.speed+Math.sin(age*12+mote.phase*8)*9;
const x=edgeAt(yy,seconds,progress,w)+direction*age*mote.drift;
if(x<-25||x>w+25||yy<-25||yy>h+25)continue;
const opacity=(1-age)*fade*.89;
ctx.globalAlpha=opacity;
ctx.strokeStyle='#ffb45d';
ctx.lineWidth=.8;
ctx.beginPath();
ctx.moveTo(x,yy);
ctx.lineTo(x-direction*mote.length*.4,yy+mote.length);
ctx.stroke();
}
ctx.restore();}
};
}
let atmosphereWarmPromise=null;
function prewarmAtmosphere() {
if(atmosphereWarmPromise)return atmosphereWarmPromise;
atmosphereWarmPromise=new Promise(resolve=>{
const scratchCanvas=document.createElement('canvas');
const schedule=()=>{
if(typeof requestIdleCallback==='function')
requestIdleCallback(step,{timeout:70});
else setTimeout(step,24);
};
function step(){
if(atmosphereAssetCache?.EMBER_GLOW){resolve();return;}
createAtmosphere(scratchCanvas,true);
if(atmosphereAssetCache?.EMBER_GLOW){resolve();return;}
schedule();
}
setTimeout(schedule,40);
});
return atmosphereWarmPromise;
}
let atmosphereEdge=null;
function edgeAt(y,seconds,progress,width){
return atmosphereEdge
? atmosphereEdge(y)
: makeEdge(progress,seconds,Boolean(window.__kkptVisualDark),width)(y);
}
let sprites=null;
function makeSprites() {
if (sprites) return sprites;
function radial(stops) {
const el=document.createElement('canvas');
el.width=el.height=96;
const c=el.getContext('2d');
const g=c.createRadialGradient(48,48,0,48,48,48);
stops.forEach(pair=>g.addColorStop(pair[0],pair[1]));
c.fillStyle=g;c.fillRect(0,0,96,96);
return el;
}
sprites={
flame:radial([[0,'#fffbed'],[.18,'#fff2a0'],
[.43,'#ffb42b'],[.70,'#ff360088'],[1,'#8f050000']]),
ember:radial([[0,'#ffffff'],[.15,'#fff6a8'],
[.44,'#ff9900cc'],[1,'#ef280000']]),
smoke:radial([[0,'#080608dd'],[.30,'#100b10bb'],
[.62,'#160f1677'],[1,'#08050800']]),
sky:radial([[0,'#ffffff'],[.16,'#e6ffff'],
[.46,'#94deffe0'],[1,'#4ab5ff00']]),
mist:radial([[0,'#f2fdffb0'],[.4,'#abdffff0'],
[1,'#a9e7ff00']])
};
return sprites;
}
function makeEdge(t,seconds,dark,w) {
const center=dark
? w+105-(w+210)*smooth(t)
: -105+(w+210)*smooth(t);
const wobble=Math.sin(Math.PI*t);
return y=>center+wobble*(
17*Math.sin(y*.023+seconds*5.5)+
9*Math.sin(y*.063-seconds*8.4)+
4*Math.sin(y*.13+seconds*12)
);
}
function createGpuEmbers(){
const canvas=document.createElement('canvas');
canvas.id='kkpt-webgl';
canvas.setAttribute('aria-hidden','true');
let gl,program,buffer;
try{
gl=canvas.getContext('webgl2',{
alpha:true,antialias:false,depth:false,stencil:false,
premultipliedAlpha:false,preserveDrawingBuffer:false,
powerPreference:'low-power'
});
if(!gl)throw new Error('WebGL2 context unavailable');
const vert=`#version 300 es
precision mediump float;
layout(location=0) in vec4 aSpark;
uniform vec2 uSize;
uniform float uScale;
out float vOpacity;
void main(){
vec2 xy=aSpark.xy/uSize*2.0-1.0;
gl_Position=vec4(xy.x,-xy.y,0.0,1.0);
gl_PointSize=aSpark.z*uScale;
vOpacity=aSpark.w;
}`;
const frag=`#version 300 es
precision mediump float;
in float vOpacity;
out vec4 fragColor;
void main(){
vec2 p=gl_PointCoord*2.0-1.0;
float r=length(p);
if(r>=1.0)discard;
float a=pow(1.0-r,1.65)*vOpacity;
vec3 c=mix(vec3(1.0,.30,.035),
vec3(1.0,.93,.57),smoothstep(.40,.92,1.0-r));
fragColor=vec4(c,a);
}`;
function shader(type,source){
const s=gl.createShader(type);
gl.shaderSource(s,source);gl.compileShader(s);
if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))
throw new Error(gl.getShaderInfoLog(s));
return s;
}
const vs=shader(gl.VERTEX_SHADER,vert);
const fs=shader(gl.FRAGMENT_SHADER,frag);
program=gl.createProgram();
gl.attachShader(program,vs);gl.attachShader(program,fs);
gl.linkProgram(program);
gl.deleteShader(vs);gl.deleteShader(fs);
if(!gl.getProgramParameter(program,gl.LINK_STATUS))
throw new Error(gl.getProgramInfoLog(program));
buffer=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
gl.bufferData(gl.ARRAY_BUFFER,170*4*4,gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0,4,gl.FLOAT,false,0,0);
gl.useProgram(program);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
gl.disable(gl.DEPTH_TEST);
}catch(err){
console.warn('[KKPT] Ember-only WebGL disabled:',err);
canvas.remove();
return null;
}
document.body.append(canvas);
const sizeUniform=gl.getUniformLocation(program,'uSize');
const scaleUniform=gl.getUniformLocation(program,'uScale');
const max=innerWidth<700?80:170;
const data=new Float32Array(max*4);
const sparks=[];
let carry=0,disabled=false,lastW=0,lastH=0;
canvas.addEventListener('webglcontextlost',event=>{
event.preventDefault();disabled=true;
window.__kkptRenderer='Canvas2D (GPU context lost)';
},{once:true});
const rand=(min,max)=>min+Math.random()*(max-min);
function draw(edge,seconds,dt,fade,progress){
if(disabled)return;
const w=innerWidth,h=innerHeight;
const scale=w<700?.67:.75;
const cw=Math.round(w*scale),ch=Math.round(h*scale);
if(cw!==lastW||ch!==lastH){
canvas.width=cw;canvas.height=ch;
gl.viewport(0,0,cw,ch);lastW=cw;lastH=ch;
}
gl.clearColor(0,0,0,0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.uniform2f(sizeUniform,w,h);
gl.uniform1f(scaleUniform,scale);
carry+=dt*(w<700?27:56)*Math.max(0,fade);
const births=Math.min(5,Math.floor(carry));carry-=births;
for(let i=0;i<births&&sparks.length<max;i++){
const y=rand(0,h),life=rand(.55,1.65);
sparks.push({x:edge(y)+rand(-7,23),y,
vx:rand(48,265),vy:rand(-195,-26),life,left:life,
size:rand(3,13),alpha:rand(.24,.60)});
}
let count=0;
for(let i=sparks.length-1;i>=0;i--){
const p=sparks[i];
p.left-=dt;
if(p.left<=0){sparks[i]=sparks[sparks.length-1];sparks.pop();continue;}
p.x+=p.vx*dt;p.y+=p.vy*dt;
if(p.x < -30||p.x>w+30||p.y < -30||p.y>h+30)continue;
const offset=count++*4;
data[offset]=p.x;data[offset+1]=p.y;
data[offset+2]=p.size;
data[offset+3]=p.alpha*fade*Math.min(1,p.left/.24);
}
if(!count)return;
gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
gl.bufferSubData(gl.ARRAY_BUFFER,0,data.subarray(0,count*4));
gl.drawArrays(gl.POINTS,0,count);
}
function dispose(){
disabled=true;
if(!gl.isContextLost()){
gl.deleteBuffer(buffer);gl.deleteProgram(program);
}
canvas.remove();
}
return {canvas,draw,dispose};
}
function clipFrame(edge) {
const w=innerWidth,h=innerHeight;
const rows=window.__kkptVisualDark?
Math.max(24,Math.ceil(h/30)):
Math.max(16,Math.ceil(h/61));
const pts=[];
for(let i=rows;i>=0;i--) {
const y=h*i/rows;
pts.push(edge(y).toFixed(1)+'px '+y.toFixed(1)+'px');
}
frame.style.clipPath=clipSide==='right'
? 'polygon(100% 0,100% 100%,'+pts.join(',')+')'
: 'polygon(0 0,0 100%,'+pts.join(',')+')';
}
function clipDawnFrame(t){
const {radius}=dawnGeometry(t);
const near=Math.max(0,radius-2).toFixed(1);
const far=(radius+2).toFixed(1);
const mask='radial-gradient(circle at 50% 47%, transparent '+
near+'px, #000 '+far+'px)';
const darkLens=document.getElementById('kkpt-beat-scene');
if(darkLens){
darkLens.style.maskImage=mask;
darkLens.style.webkitMaskImage=mask;
}
if(basePage==='Kuronata'){
frame.style.clipPath='circle('+radius.toFixed(1)+'px at 50% 47%)';
return;
}
frame.style.clipPath='inset(0)';
frame.style.maskImage=mask;
frame.style.webkitMaskImage=mask;
}
function canvasFire(canvas,dark,duration,drawAtmosphere,done,gpuEmbers,score,extraVisuals) {
const ctx=dark&&canvas
? canvas.getContext('2d',{alpha:true,desynchronized:true})
: null;
if(dark&&!ctx){done();return;}
const art=dark?makeSprites():null;
const w=innerWidth,h=innerHeight;
if(ctx){
const scale=Math.min(window.devicePixelRatio||1,w<650?.69:.80);
canvas.width=Math.max(1,Math.round(w*scale));
canvas.height=Math.max(1,Math.round(h*scale));
ctx.setTransform(scale,0,0,scale,0,0);
}
const rand=(a,b)=>a+Math.random()*(b-a);
const mobile=w<650;
const maxCount=mobile?160:250;
const p=[];
const accum={flame:0,ember:0,shard:0,sky:0,mist:0,pollen:0};
const rates=dark
? {flame:mobile?80:105,ember:mobile?72:96,
shard:mobile?26:42}
: {sky:mobile?50:80,mist:mobile?13:20,pollen:mobile?27:50};
const started=performance.now();
let previous=started;
let lastPaint=0;
function spawn(kind,edge,seconds) {
if(p.length>=maxCount)return;
const y=rand(-25,h+25);
const x=edge(y);
const type=kind;
const smoke=type==='smoke';
const shard=type==='shard';
const ember=type==='ember'||type==='pollen';
const blue=type==='sky'||type==='mist'||type==='pollen';
let radius,life,vx,vy;
if(shard){
radius=rand(4,18);life=rand(.6,1.65);
vx=rand(-130,65);vy=rand(-125,30);
}else if(smoke){
radius=rand(32,85);life=rand(1.1,2.5);
vx=rand(-20,65);vy=rand(-88,-22);
}else if(type==='mist'){
radius=rand(45,100);life=rand(.9,1.8);
vx=rand(-35,45);vy=rand(-75,-15);
}else if(type==='flame'){
radius=rand(20,57);life=rand(.3,.82);
vx=rand(-100,30);vy=rand(-180,-45);
}else{
radius=ember?rand(2.5,9):rand(18,47);
life=ember?rand(.45,1.1):rand(.35,.85);
vx=rand(-95,75);vy=rand(-150,-25);
}
p.push({
type,life,left:life,
x:x+rand(smoke?12:shard?-35:-27,smoke?140:shard?36:64),
y:y+rand(-14,14),vx,vy,radius,
angle:rand(-Math.PI,Math.PI),spin:rand(-5,5),
drift:rand(-.65,.65),blue
});
}
function spawnAtRate(dt,edge,seconds,strength){
for(const type in rates){
accum[type]+=rates[type]*dt*strength;
const n=Math.min(11,Math.floor(accum[type]));
accum[type]-=n;
for(let i=0;i<n;i++)spawn(type,edge,seconds);
}
}
function advance(dt){
for(let i=p.length-1;i>=0;i--) {
const q=p[i];
q.left-=dt;
if(q.left<=0){p[i]=p[p.length-1];p.pop();continue;}
q.x+=q.vx*dt;
q.y+=q.vy*dt;
q.vx+=q.drift*dt*35;
q.vy-=10*dt;
q.angle+=q.spin*dt;
}
}
function spriteDraw(image,x,y,sx,sy,alpha){
ctx.globalAlpha=alpha;
ctx.drawImage(image,x-sx,y-sy,sx*2,sy*2);
}
function drawSmoke(fade){
ctx.globalCompositeOperation='source-over';
for(const q of p){
if(q.type!=='smoke'&&q.type!=='mist')continue;
const age=1-q.left/q.life;
const radius=q.radius*(1+age*.98);
const alpha=(q.type==='smoke'?.76:.32)*
Math.min(1,age*5)*Math.min(1,q.left/.55)*fade;
spriteDraw(q.type==='smoke'?art.smoke:art.mist,
q.x,q.y,radius*1.7,radius*1.3,alpha);
}
ctx.globalAlpha=1;
}
function drawShards(fade) {
if(!dark)return;
ctx.save();
ctx.globalCompositeOperation='source-over';
for(const q of p){
if(q.type!=='shard')continue;
const age=1-q.left/q.life;
const a=Math.min(1,q.left/.38)*(.90-age*.28)*fade;
ctx.save();
ctx.translate(q.x,q.y);
ctx.rotate(q.angle);
ctx.globalAlpha=a;
const r=q.radius;
ctx.beginPath();
ctx.moveTo(-r*.9,-r*.3);
ctx.lineTo(r*.7,-r*.75);
ctx.lineTo(r*.48,r*.42);
ctx.lineTo(-r*.35,r*.8);
ctx.closePath();
ctx.fillStyle=age<.3?'#371d22':'#140d13';
ctx.fill();
ctx.strokeStyle=age<.42?'#f57d3c':'#5a2c2b';
ctx.lineWidth=age<.42?1.6:.75;
ctx.stroke();
ctx.restore();
}
ctx.restore();
}
function drawCore(edge,seconds,t,fade){
const waviness=Math.sin(Math.PI*t);
ctx.save();
ctx.beginPath();
const count=Math.max(35,Math.ceil(h/19));
for(let i=0;i<=count;i++){
const y=h*i/count;
if(i===0)ctx.moveTo(edge(y),y);
else ctx.lineTo(edge(y),y);
}
ctx.lineCap='round';
ctx.lineJoin='round';
if(dark){
ctx.globalCompositeOperation='lighter';
ctx.globalAlpha=fade;
ctx.shadowColor='#ff3903';
ctx.shadowBlur=25;
ctx.strokeStyle='rgba(255,55,5,.82)';
ctx.lineWidth=43;
ctx.stroke();
ctx.shadowBlur=16;
ctx.strokeStyle='#ff9b17';
ctx.lineWidth=20;
ctx.stroke();
ctx.shadowBlur=7;
ctx.strokeStyle='#ffe45e';
ctx.lineWidth=9;
ctx.stroke();
ctx.shadowBlur=4;
ctx.strokeStyle='#fffcde';
ctx.lineWidth=3.8;
ctx.stroke();
}else{
ctx.globalAlpha=fade*.64;
ctx.shadowColor='#8ccfff';
ctx.shadowBlur=29;
ctx.strokeStyle='rgba(139,219,255,.68)';
ctx.lineWidth=28;
ctx.stroke();
ctx.shadowBlur=10;
ctx.strokeStyle='#e7f8ff';
ctx.lineWidth=9;
ctx.stroke();
ctx.shadowBlur=0;
ctx.strokeStyle='white';
ctx.lineWidth=2;
ctx.stroke();
}
ctx.restore();
if(dark){
ctx.save();
ctx.globalCompositeOperation='lighter';
for(let y=-70;y<h+80;y+=48){
const x=edge(y);
const pulse=.65+.35*Math.sin(seconds*8.5+y*.083);
spriteDraw(art.flame,x+rand(-8,30),y,77,70,
fade*(.26+.16*pulse));
}
for(let y=-20;y<h+52;y+=36){
const x=edge(y);
const flicker=Math.sin(seconds*13+y*.19);
const length=52+30*flicker+17*Math.sin(seconds*6-y*.11);
const lean=15+18*Math.sin(seconds*9+y*.069);
ctx.globalAlpha=.62*fade;
ctx.fillStyle='#ff5705';
ctx.beginPath();
ctx.moveTo(x+18,y+19);
ctx.bezierCurveTo(x-18,y+2,x-lean-24,y-length*.57,
x-lean,y-length);
ctx.bezierCurveTo(x+2,y-length*.57,x+13,y-12,x+18,y+19);
ctx.fill();
ctx.globalAlpha=.58*fade;
ctx.fillStyle='#ffdb58';
ctx.beginPath();
ctx.moveTo(x+7,y+8);
ctx.quadraticCurveTo(x-10,y-length*.33,
x-lean*.8,y-length*.72);
ctx.quadraticCurveTo(x+3,y-length*.30,x+8,y+8);
ctx.fill();
ctx.globalAlpha=.38*fade;
ctx.fillStyle='#fffced';
ctx.beginPath();
ctx.moveTo(x+3,y+5);
ctx.quadraticCurveTo(x-5,y-length*.2,
x-lean*.5,y-length*.48);
ctx.quadraticCurveTo(x+4,y-length*.19,x+5,y+5);
ctx.fill();
}
ctx.restore();
}else{
ctx.save();
ctx.globalCompositeOperation='lighter';
for(let y=-50;y<h+55;y+=88){
spriteDraw(art.sky,edge(y),y,89,109,.27*fade);
}
ctx.restore();
}
}
function drawBrightParticles(fade) {
ctx.save();
ctx.globalCompositeOperation='lighter';
for(const q of p){
if(q.type==='smoke'||q.type==='mist'||q.type==='shard')continue;
const age=1-q.left/q.life;
const brightness=Math.min(1,q.left/.25)*(1-age*.25)*fade;
let artName='flame',sx=q.radius,sy=q.radius;
if(q.type==='ember'){artName='ember';sx*=1.2;sy*=1.2;}
else if(q.type==='pollen'){artName='sky';sx*=1.15;sy*=1.15;}
else if(q.type==='sky'){artName='sky';sx*=1.1;sy*=1.55;}
else {sx*=.9;sy*=1.65;}
spriteDraw(art[artName],q.x,q.y,sx,sy,
brightness*(q.type==='ember'?.93:.78));
}
ctx.restore();
ctx.globalAlpha=1;
ctx.globalCompositeOperation='source-over';
}
function paint(now) {
const t=score
? Math.max(0,Math.min(1,(score.audio.currentTime-
score.config.fireStart)/
(score.config.fireEnd-score.config.fireStart)))
: Math.min(1,(now-started)/duration);
extraVisuals?.update(t);
const seconds=now/1000;
const edge=dark?makeEdge(t,seconds,true,w):
y=>{
const dawn=dawnGeometry(t,w,h);
const dy=y-dawn.cy;
const reach=Math.sqrt(Math.max(0,dawn.radius*dawn.radius-dy*dy));
return dawn.cx+Math.cos(y*.018+seconds*.35)*reach;
};
const paintStep=dark?(mobile?35:32):(mobile?52:41);
if(now-lastPaint<paintStep && t<1){
requestAnimationFrame(paint);return;
}
atmosphereEdge=edge;
window.__kkptVisualDark=dark;
if(dark)clipFrame(edge);
else clipDawnFrame(t);
const dt=Math.min(.070,(now-previous)/1000);
previous=now;lastPaint=now;
const fade=Math.min(1,t/.055,(1-t)/.045);
drawAtmosphere(seconds,dark?Math.min(t,.925):t,
dark,(now-started)/1000,[]);
if(dark&&gpuEmbers)gpuEmbers.draw(edge,seconds,dt,fade,t);
if(dark){
advance(dt);
if(t<1)spawnAtRate(dt,edge,seconds,Math.max(0,fade));
ctx.clearRect(0,0,w,h);
drawShards(fade);
drawCore(edge,seconds,t,fade);
drawBrightParticles(fade);
}
if(t<1)requestAnimationFrame(paint);
else done();
}
requestAnimationFrame(paint);
}
function setDisplayed(nextActive) {
active=nextActive;
frame.classList.toggle('kkpt-active',active);
document.body.classList.toggle('kkpt-other-active',active);
frame.style.clipPath=active?'inset(0)':hideClip;
frame.style.maskImage='none';
frame.style.webkitMaskImage='none';
frame.style.visibility='visible'; // hidden solely by clip-path
frame.style.pointerEvents='';
frame.tabIndex=active?0:-1;
frame.setAttribute('aria-hidden',active?'false':'true');
document.title=active?otherTitle:baseTitle;
root.classList.toggle('kkpt-iframe-owns-viewport',active);
const articleShell=document.querySelector('.citizen-page-container');
if(articleShell)articleShell.inert=active;
if(active)frame.focus({preventScroll:true});
root.classList.toggle('kkpt-world-suspended',basePage==='Kuronata'&&active);
try{
frame.contentDocument?.documentElement.classList.toggle(
'kkpt-frame-inactive',!active);
}catch(_){}
updateButton();
}
async function animateSwap(nextActive) {
if(animating)return;
animating=true;
button.classList.add('kkpt-busy');
labelSub.textContent='PREPARING THE OTHER SIDE...';
const dark=(nextActive?otherPage:basePage)==='Kuronata';
const targetPage=nextActive?otherPage:basePage;
let score=returnScore?.config?.target===targetPage?
returnScore:null;
if(!loading)warmFrame();
let framePrepared=false;
const preparation=Promise.all([
loading,
score?prewarmAtmosphere():
Promise.race([prewarmAtmosphere(),sleep(160)])
]).then(([isReady])=>{framePrepared=Boolean(isReady);});
if(score){
const started=await Promise.race([
score.playPromise,sleep(6500).then(()=>false)
]);
const onCue=started ? await Promise.all([
preparation,awaitScoreTime(score,score.config.fireStart)
]).then(([,hit])=>hit) : false;
if(!onCue){
await preparation;
clearReturnScore(score);
score=null; // Preserve the exact V25 visuals on audio failure.
}
} else await preparation;
if(!framePrepared){
removeScoreOmen();releaseFatePrelude();
saveReturnScoreHandoff(score);
navigateRealPage(nextActive?otherUrl.href:baseUrl);
return;
}
if(score&&score.audio.currentTime>score.config.fireStart+.35){
try{score.audio.currentTime=score.config.fireStart;}catch(_){}
}
removeScoreOmen();
frame.style.visibility='visible';
frame.classList.add('kkpt-animating');
await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));
labelSub.textContent=dark?'THE SKY IS BURNING':'DAWN IS BREAKING';
frame.classList.remove('kkpt-active');
frame.style.pointerEvents='none';
frame.style.clipPath=active?'inset(0)':hideClip;
const atmosphere=document.createElement('canvas');
atmosphere.id='kkpt-atmosphere';
atmosphere.setAttribute('aria-hidden','true');
document.body.append(atmosphere);
const drawAtmosphere=createAtmosphere(atmosphere);
const canvas=dark?document.createElement('canvas'):null;
if(canvas){
canvas.id='kkpt-vfx';
canvas.setAttribute('aria-hidden','true');
document.body.append(canvas);
}
const gpuEmbers=dark?createGpuEmbers():null;
const extraVisuals=dark?(score?.beatScene||null):createRebirthVisual(score);
window.__kkptRenderer=dark
? gpuEmbers?'WebGL2 embers + original V25 Canvas fire':
'Original V25 Canvas fire (WebGL fallback)'
: 'Original V25 Canvas dawn';
const duration=dark?3450:3100;
if(!score)transitionSound(dark,duration);
const curtainFinished=new Promise(resolve=>canvasFire(
canvas,dark,duration,drawAtmosphere,resolve,gpuEmbers,
score,extraVisuals));
requestAnimationFrame(releaseFatePrelude);
await curtainFinished;
setDisplayed(nextActive);
commitVirtualWorld(nextActive);
if(dark){
dawnBeatScene?.dispose();
try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
mountKuronataAftermath(score);
}else{
kuronataAftermath?.fadeToDawn();
createCloudbreakAfterglow();
createDawnBeatScene(score);
}
extraVisuals?.finish();
if(canvas){
canvas.style.opacity='0';
setTimeout(()=>canvas.remove(),210);
}
if(gpuEmbers){
gpuEmbers.canvas.style.transition='opacity 280ms ease-out';
gpuEmbers.canvas.style.opacity='0';
setTimeout(()=>gpuEmbers.dispose(),320);
}
const tail=dark?2000:1320;
atmosphere.style.transition='opacity '+tail+
'ms ease-out, transform '+tail+'ms ease-out';
await new Promise(resolve=>requestAnimationFrame(resolve));
atmosphere.style.opacity='0';
atmosphere.style.transform='translate3d(0,-22px,0) scale(1.035)';
await sleep(tail+70);
atmosphere.remove();
animating=false;
frame.classList.remove('kkpt-animating');
button.classList.remove('kkpt-busy','kkpt-quiet');
beginCooldown();
}
function requestSwap(nextActive) {
if (animating || isCoolingDown() || nextActive === active) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
animating=true;
let succeeded=false;
(loading||warmFrame()).then(ok=>{
if(!ok){
saveReturnScoreHandoff(returnScore);
navigateRealPage(nextActive?otherUrl.href:baseUrl);
return;
}
setDisplayed(nextActive);
commitVirtualWorld(nextActive);
succeeded=true;
if((nextActive?otherPage:basePage)==='Kuronata'){
dawnBeatScene?.dispose();
try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
mountKuronataAftermath(returnScore?.config?.id==='marenoia'
?returnScore:null);
}else{
kuronataAftermath?.fadeToDawn();
createDawnBeatScene(returnScore);
}
}).finally(()=>{
animating=false;
if(succeeded)beginCooldown();
});
return;
}
animateSwap(nextActive).catch(err => {
console.error('[KuranokaPageTransition] V46:',err);
removeScoreOmen();
clearFatePrelude();
document.getElementById('kkpt-vfx')?.remove();
document.getElementById('kkpt-atmosphere')?.remove();
document.getElementById('kkpt-webgl')?.remove();
document.getElementById('kkpt-rebirth-visual')?.remove();
returnScore?.beatScene?.dispose();
document.getElementById('kkpt-lightning-layer')?.remove();
if (frame) frame.style.filter = '';
if((nextActive?otherPage:basePage)==='Kuronata'){
try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
}
saveReturnScoreHandoff(returnScore);
navigateRealPage(nextActive ? otherUrl.href : baseUrl);
});
}
function createButton() {
if (!document.body || document.getElementById('kkpt-toggle')) return;
button=document.createElement('a');
button.id='kkpt-toggle';
labelIcon=document.createElement('span');
labelIcon.className='kkpt-icon';
labelIcon.setAttribute('aria-hidden','true');
const text=document.createElement('span');
text.className='kkpt-text';
labelTitle=document.createElement('span');
labelSub=document.createElement('span');
labelSub.className='kkpt-sub';
text.append(labelTitle,labelSub);
button.append(labelIcon,text);
document.body.append(button);
button.hidden=!SHOW_FLOATING_BUTTON;
updateButton();
watchPortalLinks(document, otherPage);
mountFateGear(document,otherPage);
const continuingScore=restoreReturnScoreHandoff();
if(basePage==='Kuranoka')createCloudbreakAfterglow(true);
if(basePage==='Kuronata'){
let hadDarkHandoff=false;
try{hadDarkHandoff=sessionStorage.getItem(AFTER_DARK_KEY)==='1';}
catch(_){}
if(continuingScore||hadDarkHandoff){
const aftermath=mountKuronataAftermath(returnScore);
aftermath.layer.classList.add('kkpt-beat-resumed');
}
}
if(continuingScore)setTimeout(warmFrame,3000);
else warmFrame();
prewarmAtmosphere();
preloadRebirthPad();
button.addEventListener('click', event => {
if (!isPlainClick(event)) return;
event.preventDefault();
if(animating||isCoolingDown())return;
armReturnScore(destination());
primeSound();
requestSwap(!active);
});
restoreCooldown();
}
document.addEventListener('click', event => {
if (active) return;
portalClick(event, document, otherPage, true);
}, true);
mw.hook('wikipage.content').add(() => {
if (document.readyState !== 'loading') {
watchPortalLinks(document, otherPage);
mountFateGear(document, otherPage);
}
});
function syncWorldFromHistory(){
if(animating)return;
const page=pathPage();
if(page!==basePage&&page!==otherPage)return;
const wantOther=page===otherPage;
if(wantOther&&!ready){
(loading||warmFrame()).then(ok=>{
if(ok&&pathPage()===otherPage)setDisplayed(true);
else if(!ok)navigateRealPage(location.href);
});
return;
}
if(wantOther===active)return;
setDisplayed(wantOther);
if(page==='Kuronata'){
const after=mountKuronataAftermath(
returnScore?.config?.id==='marenoia'?returnScore:null);
after.layer.classList.add('kkpt-beat-resumed');
}else{
kuronataAftermath?.fadeToDawn();
createCloudbreakAfterglow();
}
}
window.addEventListener('popstate',syncWorldFromHistory);
window.addEventListener('pageshow', event => {
if (!event.persisted) return;
animating=false;
document.getElementById('kkpt-vfx')?.remove();
document.getElementById('kkpt-atmosphere')?.remove();
document.getElementById('kkpt-rebirth-visual')?.remove();
document.getElementById('kkpt-cloudbreak')?.remove();
document.getElementById('kkpt-beat-scene')?.remove();
kuronataAftermath?.dispose();
document.getElementById('kkpt-lightning-layer')?.remove();
if(frame){
frame.style.filter='';
setDisplayed(pathPage()===otherPage);
frame.classList.remove('kkpt-animating');
}
if(pathPage()==='Kuronata'){
let entered=false;
try{entered=sessionStorage.getItem(AFTER_DARK_KEY)==='1';}
catch(_){}
if(entered){
const aftermath=mountKuronataAftermath(
returnScore?.config?.id==='marenoia'?returnScore:null);
aftermath.layer.classList.add('kkpt-beat-resumed');
}
}
button?.classList.remove('kkpt-busy','kkpt-quiet');
restoreCooldown();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded',createButton,{once:true});
} else {
createButton();
}
}());