跳转到内容
打开/关闭搜索
搜索
打开/关闭菜单
451
条目
806
文件
22
用户
9166
编辑
Blockland Wiki
导航
首页
最近更改
随机页面
MediaWiki帮助
特殊页面
打开/关闭外观设置菜单
无法加载外观设置。请检查您的网络连接并重试。
重试
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。
user-interface-preferences
个人工具
创建账号
登录
查看“︁MediaWiki:KuranokaPageTransition.js”︁的源代码
MediaWiki界面页面
查看
阅读
查看源代码
查看历史
相关页面
系统消息
讨论
更多操作
更多
工具
链入页面
相关更改
页面信息
Cargo数据
←
MediaWiki:KuranokaPageTransition.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
此页面为本wiki上的软件提供界面文本,并受到保护以防止滥用。如欲修改所有wiki的翻译,请访问
translatewiki.net
上的MediaWiki本地化项目。
您无权编辑此JavaScript页面,因为编辑此页面可能会影响所有访问者。
您可以查看和复制此页面的源代码。
/* ============================================================ Kuranoka / Kuronata — Page Transition V24 V2-inspired REAL CANVAS FIRE: bright flames, smoke, fragments. V18 smooths the mask, uses sampled PADsynth and stabilizes scroll, with capped particle work and infrequent spatial glitches. Only this page-scoped JS/CSS; never modify Citizen or loader.js. Real page navigation after the transition; iframe is visual-only. ============================================================ */ (function () { 'use strict'; if (!window.mw || !mw.config) 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; // V24: the two article worlds have deliberate, deterministic palettes. // "os" is NOT a light theme: it can resolve to dark on the reader's // computer. Session storage may also be unavailable. Therefore, never // restore a stale "os" token when landing back on Kuranoka. 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(); // Citizen's later initialization may reinstate the OS-preference class. // Keep the intended world palette without touching the user's global // account preference or other MediaWiki pages. new MutationObserver(syncTheme).observe(root,{ attributes:true,attributeFilter:['class'] }); function navigateRealPage(href){ window.location.assign(href); } // V17: the dark-world extras run in *both* the visible document and // its same-origin preview iframe. They never alter other Wiki pages. function installKuronataAfterglow() { if(basePage!=='Kuronata'||window.__kkptDarkExtrasReady)return; window.__kkptDarkExtrasReady=true; const prefersReduced=window.matchMedia('(prefers-reduced-motion: reduce)'); // Multiline .heimu cannot use a single nowrap pseudo-element: // it piles all copied text onto line one. Project each rendered // line into two SHORT clipped chromatic slices only on hover. // Range measurements leave the original article DOM and wrapping // entirely untouched, including italic/link children. 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'); // Copy a link only if it truly occupies one // rendered line. Multiline links keep real text. 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 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'; // All sprites are static layers with compositor-only // transform/opacity animation. No persistent canvas or RAF. // The soot is a side-bank haze, not a random full-screen // spread of isolated circular clouds. 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(); createAmbience(); mw.hook('wikipage.content').add(()=>{ decorateSpoilers(); decorateLinks(); }); }; if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init,{once:true}); else init(); // One brief spatial fault roughly every 14-36 seconds, never a // full-screen white strobe. Pause while the iframe is hidden. 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(); // The nested preview only applies its own theme; no recursive iframe. if (window!==window.top || window.__kkptV12) return; window.__kkptV12=true; window.__kkptVersion='24.0'; // Easter eggs + Infobox gear are the default. No floating pill. 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*t*(3-2*t); const imagePool=new Map(); // V24: preload both original background photographs. The target // image is decoded while its real-document iframe is preparing, // reducing flashes of the bare background-color at navigation. 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(()=>{}); } // Give the current photo first priority; then prepare the next // world so the iframe and the eventual real page can reuse it. 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; // Public-domain PADsynth recording, ~8.4s / 120KB. Change this // to a same-origin Wiki-upload URL if external media are blocked. 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; } // A plain media element can still play the recording when a site's // CORS policy disallows reading the file into Web Audio. 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)); } // Micro-crackles: impulsive bursts with different decay times. 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{ // A REAL recorded PADsynth sample; no generated wind-chimes. 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); } let active = false; // true only during the visual handoff; destination navigates natively let animating = false; let gearPending = false; let ready = false; let loading = null; let button, labelTitle, labelSub, labelIcon, frame; let otherTitle = otherPage + ' - Blockland Wiki'; 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 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'; labelIcon.textContent = isDark(to) ? '✦' : '❄'; button.classList.toggle('kkpt-dark', isDark(whichPage())); button.setAttribute('aria-label', isDark(to) ? '燃尽天空,进入 Kuronata' : '驱散黑暗,回到 Kuranoka'); } // Accept the two standard MediaWiki link forms, while rejecting // edit/action links, external sites and similarly named articles. 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) { // Use only the article: sidebar, edit tools and site-wide links // must retain their usual navigation behavior. const content = doc.querySelector('#mw-content-text,.mw-parser-output'); if (!content) return; content.querySelectorAll('a[href]').forEach(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); }); } 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 observer = new MutationObserver(() => { if(pending)return; pending=true; setTimeout(()=>{ pending=false; decoratePortalLinks(doc,targetPage); },120); }); 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]') : null; } function portalClick(event, doc, targetPage, nextActive) { if (!isPlainClick(event)) return false; const anchor = findAnchor(event, doc); if (!anchor || (anchor.target && anchor.target !== '_self') || anchor.hasAttribute('download') || wikiLinkPage(anchor, doc) !== targetPage) return false; event.preventDefault(); primeSound(); // Synchronous click activation unlocks Web Audio. if(anchor.classList.contains('kkpt-fate-gear')) { beginFateSwap(anchor,nextActive); } else { requestSwap(nextActive); } return true; } // The Infobox gear begins to turn before the actual world changes. function beginFateSwap(anchor,nextActive) { if(animating||gearPending||nextActive===active)return; gearPending=true; anchor.classList.add('kkpt-fate-gear-turning'); setTimeout(()=>{ gearPending=false; requestSwap(nextActive); },570); setTimeout(()=>anchor.classList.remove('kkpt-fate-gear-turning'),4800); } 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; // The fate wheel stays inside the native Infobox caption, but is // positioned independently: it must NEVER push the title off-center. 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('a'); anchor.className='kkpt-fate-gear'; anchor.href=new URL('/wiki/'+targetPage,doc.baseURI).href; anchor.title=targetPage==='Kuronata' ?'命运的齿轮 · 唤醒 Kuronata':'命运的齿轮 · 回到 Kuranoka'; anchor.setAttribute('aria-label',anchor.title); 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); // Fixed corner placement: no title measurements, ResizeObserver // or reflow during the animation. } // Read backgrounds after background-template code populates --bg-image. 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; // The preview exists only for the animated reveal. Pausing // its hidden ambience avoids doubling ongoing render work. doc.documentElement.classList.add('kkpt-frame-inactive'); watchPortalLinks(doc, basePage); mountFateGear(doc, basePage); if (doc.__kkptFrameBridge) return; doc.__kkptFrameBridge = true; // Non-anchor navigation should leave the iframe and open normally // in the top window, so the browser's URL doesn't become stale. 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); // Same-document fragments stay inside the temporary preview. 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; loading = new Promise(resolve => { let settled = false; let preparing = false; let poll = null; 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.querySelector('#mw-content-text,.mw-parser-output')) return; preparing = true; clearInterval(poll); await sleep(320); // Background.js / wiki style scripts await Promise.race([warmBackgrounds(doc), sleep(3600)]); await sleep(120); await Promise.race([warmBackgrounds(doc), sleep(1300)]); 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 && !preparing) finish(false,'18 秒仍未找到目标正文'); }, 18000); }); warmBackgrounds(document).catch(() => {}); setTimeout(() => warmBackgrounds(document).catch(() => {}), 700); return loading; } // Shared, low-resolution atmospherics: return of V14's dark smoke, // long-lived torn paper, shockwaves, blue-white petals and pollen. // Only the combustion CURTAIN is replaced by the bright V2 engine. let atmosphereAssetCache=null; function createAtmosphere(canvas) { const ctx = canvas.getContext('2d', {alpha:true}); const rand = n => { const x = Math.sin(n*127.1+311.7)*43758.5453123; return x-Math.floor(x); }; 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; } const sootSprites=atmosphereAssetCache?.sootSprites || [0,1,2,3].map(n=>smokeTexture(n+17,true)); const mistSprites=atmosphereAssetCache?.mistSprites || [0,1,2,3].map(n=>smokeTexture(n+51,false)); // V2-inspired white-hot additive sprites, but with noise-eroded // silhouettes instead of repetitive smooth, elliptical balls. 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 FIRE_GLOW=atmosphereAssetCache?.FIRE_GLOW || [1,2,3,4].map(seed=>glowTexture(seed,'fire')); const BLUE_GLOW=atmosphereAssetCache?.BLUE_GLOW || [1,2,3,4].map(seed=>glowTexture(seed+7,'blue')); const EMBER_GLOW=atmosphereAssetCache?.EMBER_GLOW || glowTexture(21,'ember'); atmosphereAssetCache ||= {sootSprites,mistSprites,FIRE_GLOW,BLUE_GLOW,EMBER_GLOW}; 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?70:96)*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<.55?'flame':r<.90?'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?.65+Math.random()*1.1:.42+Math.random()*.76) : 1.15+Math.random()*1.55; combustion.push({ kind,x:edge+(dark?1:-1)*(Math.random()*47-8), y:y+(Math.random()-.5)*25, vx:(dark?1:-1)*(dark?14+Math.random()*154: 16+Math.random()*69), vy:-(dark?32+Math.random()*171:12+Math.random()*57), 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) { // Unlike the traveling front, these black smoke plumes // are born in world-space and remain over burnt paper. 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 }); } // Charcoal sheets tear off in irregular polygons, not // squares or narrow glitch streaks. shardRemainder+=dt*(h<680?9:15); const shardBirths=Math.min(5,Math.floor(shardRemainder)); shardRemainder-=shardBirths; for(let i=0;i<shardBirths&&tornPaper.length<135;i++){ const y=Math.random()*h; const life=.82+Math.random()*1.55; const points=6+Math.floor(Math.random()*4); const outline=Array.from({length:points},(_,j)=>({ a:Math.PI*2*j/points+(Math.random()-.5)*.22, r:.50+Math.random()*.61 })); tornPaper.push({ x:edgeAt(y,seconds,progress,w)+(Math.random()-.5)*47, y, vx:34+Math.random()*210, vy:-170+Math.random()*260, gravity:90+Math.random()*150, radius:3+Math.pow(Math.random(),1.6)*15, 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?37:72},(_,i)=>({ y:rand(i*3+17),phase:rand(i*9+43), speed:85+rand(i*12+5)*260, drift:75+rand(i*15+2)*170, length:3+rand(i*17+7)*11 })); 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 scale=w<700?.43:.55; 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; // New-world side: red fire trails to the right when burning, // blue dawn trails to the left when returning. const direction=dark?1:-1; // Broken smoke banks with different depths and sizes. The old // aligned rows created an artificial horizontal fog ribbon. ctx.globalCompositeOperation='source-over'; const bands=Math.max(8,Math.ceil(h/(dark?116:91))); for(let i=0;i<bands;i++){ const baseY=(i+.15+rand(i*11+39)*.70)*h/bands; const y=baseY+31*Math.sin(seconds*1.6+i*1.47); const x=edgeAt(y,seconds,progress,w)+direction* (dark?76+rand(i*19+6)*260:120)+ 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(); } // Independent turbulent billows peel away and rise. for(const item of particles){ 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(y< -230||y>h+230)continue; const frontier=edgeAt(baseY,seconds,progress,w); const x=frontier+direction*(age*(dark?225:135)+ 30*Math.sin(age*6.3+item.bend)); 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,y); 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(); } // Residual devastation: coal-black plumes stay pinned to their // original burn positions, expanding long after the tear passes. // Composited *before* fire, so the hot core cuts through smoke. 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{ // The inverse is a slow, organic unfurling: broad milky // radiance behind the rebuilt sky, not a cold explosion. ctx.save(); ctx.globalCompositeOperation='screen'; for(let n=0;n<3;n++){ const yy=(n+.43)*h/3+Math.sin(seconds*.8+n*2)*30; const xx=edgeAt(yy,seconds,progress,w)- (88+55*Math.sin(n*2.1+seconds*.7)); 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(); } // The impact happens at ignition and again as the rupture reaches // the middle of the screen; pulses are intentionally fleeting. 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; // A single-frame-ish exposure bloom punches through // the entire page; the tone is red or icy blue by direction. 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; // Only combustion throws straight, fiery projectile trails. // Return-to-blue has no streaks or sparks whatsoever. 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; } // Real flame tongues break OUT of the burn front into the sky. // They are thin, tapered Bezier ribbons with an unstable pulse, // rather than round glowing particles or a uniform bright line. 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(); } // Carbonized paper has a fine, ragged edge. The charcoal layer // persists continuously, but the white-hot core does NOT. ctx.save(); ctx.beginPath(); 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); } 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=27;ctx.shadowColor='#56bdff';ctx.shadowBlur=40; ctx.stroke(); ctx.strokeStyle='rgba(132,225,255,.51)'; ctx.lineWidth=11;ctx.shadowColor='#8aeaff';ctx.shadowBlur=30; ctx.stroke(); ctx.strokeStyle='rgba(249,255,255,.65)'; ctx.lineWidth=2.7;ctx.shadowBlur=11; ctx.stroke(); } ctx.restore(); if(dark){ // Intermittent ignition cells: varying width/color/intensity // make the bright edge resemble burning fibres, not neon. 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(); // Fragmented ignition on real microscopic perforations. // Missing arc sections stop them resembling punched circles. 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(); // Small carbon flecks stick to the still-blue paper just // ahead of the bite; no large rectangular glitch debris. 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(); } // The key property V3-V6 lost: particles remain at their // birth position while the front advances. A wide, incandescent // WAKE builds behind the tearing page instead of following // it as one thin, plastic-looking stripe. 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(); // Rebirth uses soft, rotating, organic leaf/petal silhouettes, // minute circular pollen and spacious white-blue bokeh. No // rectangular ions, no warm textures and NO glowing streaks. 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=13; 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; const halo=ctx.createRadialGradient(0,0,0,0,0,r*3); halo.addColorStop(0,'rgba(255,255,255,.78)'); halo.addColorStop(.25,'rgba(225,250,255,.40)'); halo.addColorStop(1,'rgba(150,215,255,0)'); ctx.fillStyle=halo; ctx.beginPath();ctx.arc(0,0,r*3,0,Math.PI*2);ctx.fill(); } ctx.restore(); } ctx.restore(); } // Minute black flecks, never rectangular shards on the blue side. 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(); // Irregular burnt page sections tumble away and reveal their // blackened underside, with an occasional incandescent rim. 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,98,34,.29)': 'rgba(49,20,19,.47)'; ctx.stroke(); // Thin blue-white unburnt fibers still visible on a // few ripped faces, before the back chars completely. 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(); } // Sharp, directional embers / crystalline fragments, not blobs. 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();} }; } function prewarmAtmosphere() { if(atmosphereAssetCache)return; const prepare=()=>{ if(!atmosphereAssetCache) createAtmosphere(document.createElement('canvas')); }; if('requestIdleCallback' in window) requestIdleCallback(prepare,{timeout:6000}); else setTimeout(prepare,1400); } let atmosphereEdge=null; function edgeAt(y,seconds,progress,width){ return atmosphereEdge ? atmosphereEdge(y) : makeEdge(progress,seconds,Boolean(window.__kkptVisualDark),width)(y); } // Original V2's moving flame sprites and three bright contours, rebuilt // with bounded particle counts. No WebGL, no full-screen noise pass, // no 1,000-point polygon or per-frame image-texture upload. 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; } // V2's simple advancing boundary. The reader asked for the // vibrant fire curtain, not a mathematically fractal tearing wire. 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) ); } // Clip the ACTUAL preloaded Wiki frame, precisely where the fire is. function clipFrame(edge) { const w=innerWidth,h=innerHeight; const rows=Math.max(24,Math.ceil(h/30)); 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 canvasFire(canvas,dark,duration,drawAtmosphere,done) { 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(); // Dense GOLD / WHITE rolling fire, including on both sides // of the border. These are the luminous shapes from V2. 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=Math.min(1,(now-started)/duration); const seconds=now/1000; const edge=makeEdge(t,seconds,dark,w); // Update the actual Wiki page reveal at display refresh rate. // The costly flame/smoke pass has an independent 30fps budget. atmosphereEdge=edge; window.__kkptVisualDark=dark; clipFrame(edge); if(now-lastPaint<(mobile?35:32) && t<1){ requestAnimationFrame(paint);return; } const dt=Math.min(.070,(now-previous)/1000); previous=now;lastPaint=now; const fade=Math.min(1,t/.055,(1-t)/.045); // Restore V14's broad smoke banks, long-lived char, pressure // waves and delicate blue leaves. V2 only paints the FIRE. drawAtmosphere(seconds,Math.min(t,.925),dark,(now-started)/1000,[]); 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.pointerEvents=''; frame.tabIndex=active?0:-1; frame.setAttribute('aria-hidden',active?'false':'true'); document.title=active?otherTitle:baseTitle; showTheme((active?otherPage:basePage)==='Kuronata'); // Stop the invisible Kuronata ambience in either document. 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...'; if(!loading)warmFrame(); const ok=await Promise.race([loading,sleep(11000).then(()=>false)]); if(!ok||!ready){ navigateRealPage(nextActive?otherUrl.href:baseUrl);return; } const dark=(nextActive?otherPage:basePage)==='Kuronata'; labelSub.textContent=dark?'THE SKY IS BURNING':'DAWN IS BREAKING'; // When returning to the blue page, force the Day palette // BEFORE unmasking it. The dark iframe still fully covers it. if(!dark)showTheme(false); 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 duration=dark?3450:3100; transitionSound(dark,duration); await new Promise(resolve=>canvasFire( canvas,dark,duration,drawAtmosphere,resolve)); setDisplayed(nextActive); if(canvas){ canvas.style.opacity='0'; setTimeout(()=>canvas.remove(),210); } // The last frame holds living smoke and drifting fragments. // Let it disperse with a lightweight compositor fade AFTER the // destination is revealed; no costly extra canvas render loop. 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; button.classList.remove('kkpt-busy','kkpt-quiet'); // The iframe was only the moving scenery during the reveal. // Hand off to the ACTUAL Wiki page: native sidebars, scrollbar, // history, refresh, accessibility and links all work normally. navigateRealPage(otherUrl.href); } function requestSwap(nextActive) { if (animating || nextActive === active) return; if (matchMedia('(prefers-reduced-motion: reduce)').matches) { navigateRealPage(otherUrl.href); return; } animateSwap(nextActive).catch(err => { console.error('[KuranokaPageTransition] V12:',err); document.getElementById('kkpt-vfx')?.remove(); document.getElementById('kkpt-atmosphere')?.remove(); if (frame) frame.style.filter = ''; 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); warmFrame(); prewarmAtmosphere(); preloadRebirthPad(); button.addEventListener('click', event => { if (!isPlainClick(event)) return; event.preventDefault(); primeSound(); requestSwap(!active); }); } // A plain [[Kuronata|your mysterious text]] now triggers everything. // The inverse [[Kuranoka|...]] uses the same transition, then real navigation. 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); }); // Browser history is now real document history. A bfcache return // must restore THIS document, not leave its animated preview visible. window.addEventListener('pageshow', event => { if (!event.persisted) return; animating=false; document.getElementById('kkpt-vfx')?.remove(); document.getElementById('kkpt-atmosphere')?.remove(); if(frame){ frame.style.filter=''; setDisplayed(false); } button?.classList.remove('kkpt-busy','kkpt-quiet'); }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded',createButton,{once:true}); } else { createButton(); } }());
返回
MediaWiki:KuranokaPageTransition.js
。
查看“︁MediaWiki:KuranokaPageTransition.js”︁的源代码
MediaWiki界面页面