跳转到内容
打开/关闭搜索
搜索
打开/关闭菜单
450
条目
805
文件
22
用户
9131
编辑
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 V15 V2-inspired REAL CANVAS FIRE: bright flames, smoke, fragments. Only this page-scoped JS/CSS; never modify Citizen or loader.js. Two living same-origin Wiki documents; no reload for toggles. ============================================================ */ (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; // Snapshot the user's actual Citizen theme. An entering Kuronata // is dark only for this document; returning to Kuranoka restores // exactly the former Day / OS / Night preference classes. 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 savedTheme=themeNames.filter(c=>root.classList.contains(c)); let wantsNight=basePage==='Kuronata'; let changingTheme=false; function syncTheme() { if (changingTheme) return; const expected=wantsNight ? ['skin-theme-clientpref-night','kkpt-kuronata-dark'] : savedTheme; const forbidden=wantsNight ? themeNames.filter(c=>c!=='skin-theme-clientpref-night') : ['kkpt-kuronata-dark',...themeNames.filter(c=>!savedTheme.includes(c))]; if(expected.every(c=>root.classList.contains(c)) && forbidden.every(c=>!root.classList.contains(c))) return; changingTheme=true; root.classList.remove(...themeNames,'kkpt-kuronata-dark'); root.classList.add(...expected); changingTheme=false; } function showTheme(dark) { wantsNight=dark;syncTheme(); } syncTheme(); new MutationObserver(()=>{ // Follow later preference changes while Kuranoka is visible. // Only Kuronata needs an enforced appearance. if(wantsNight)syncTheme(); else savedTheme=themeNames.filter(c=>root.classList.contains(c)); }).observe(root,{ attributes:true,attributeFilter:['class'] }); // The nested preview only applies its own theme; no recursive iframe. if (window!==window.top || window.__kkptV12) return; window.__kkptV12=true; window.__kkptVersion='15.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(); const ENABLE_TRANSITION_AUDIO = true; const FIRE_VOLUME = .30; const DAWN_VOLUME = .20; let soundContext=null,fireBuffer=null,activeSound=null; 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(()=>{}); }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{ // Return to blue: a rising E-major wind-chime motif and airy // breath, with no fire or electrical crackle artifacts. const notes=[329.63,415.30,493.88,659.25,830.61]; notes.forEach((hz,i)=>{ const at=start+.14+i*.36; const osc=ctx.createOscillator(); osc.type=i%2?'triangle':'sine'; osc.frequency.setValueAtTime(hz,at); const note=ctx.createGain(); note.gain.setValueAtTime(.0001,at); note.gain.exponentialRampToValueAtTime(.20/(1+i*.16),at+.095); note.gain.exponentialRampToValueAtTime(.0001, Math.min(start+span-.04,at+1.45)); osc.connect(note);note.connect(master); osc.start(at);osc.stop(Math.min(start+span,at+1.52)); }); // A soft, swelling harmonic makes the return sound alive and // spacious without turning it into another impact blast. const air=ctx.createOscillator(); air.type='sine';air.frequency.setValueAtTime(164.81,start); air.frequency.linearRampToValueAtTime(246.94,start+span*.82); const breath=ctx.createGain(); breath.gain.setValueAtTime(.0001,start); breath.gain.linearRampToValueAtTime(.115,start+1.0); breath.gain.exponentialRampToValueAtTime(.0001,start+span); air.connect(breath);breath.connect(master); air.start(start);air.stop(start+span+.01); } setTimeout(()=>{ if(activeSound&&activeSound.gain===master)activeSound=null; try{master.disconnect();}catch(_){} },durationMs+350); } let active = false; // false: original top-level doc; true: otherPage iframe 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; const selectors='table.infobox,.infobox-character,.portable-infobox,'+ '.infobox,[class*="infobox"]'; const box=[...content.querySelectorAll(selectors)].find(el=>{ const r=el.getBoundingClientRect(); return r.width>175&&r.width<720&&r.height>210; }); if(!box)return; const header=box.querySelector('.infobox-title,.infobox-header,'+ '.infobox-above,.infobox-heading,.infobox-head,'+ '.pi-title,caption,th[colspan]'); // Deliberately NEVER append to or restyle a native Infobox node. // A fixed-position overlay follows its visual rectangle instead. const host=header&&header.tagName!=='TR'?header:box; const anchor=doc.createElement('a'); anchor.className='kkpt-fate-gear'; anchor.hidden=true; 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>'; doc.body.appendChild(anchor); let pendingPosition=false; function positionGear(){ pendingPosition=false; if(!doc.body.contains(anchor)||!doc.body.contains(host)){ anchor.hidden=true;return; } const rect=host.getBoundingClientRect(); const view=doc.defaultView; const y=host===box?rect.top+23:rect.top+rect.height*.5; anchor.hidden=rect.width<120||rect.bottom<0||rect.top>view.innerHeight|| y<0||y>view.innerHeight; if(anchor.hidden)return; anchor.style.left=Math.round(rect.right-49)+'px'; anchor.style.top=Math.round(y-21)+'px'; } function updateGear(){ if(pendingPosition)return; pendingPosition=true; doc.defaultView.requestAnimationFrame(positionGear); } doc.defaultView.addEventListener('scroll',updateGear,{passive:true}); doc.defaultView.addEventListener('resize',updateGear,{passive:true}); updateGear(); } // 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; 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 iframe. 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(); window.location.assign(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; } // 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(32,Math.ceil(h/22)); 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,done) { const ctx=canvas.getContext('2d',{alpha:true,desynchronized:true}); if(!ctx){done();return;} const art=makeSprites(); const w=innerWidth,h=innerHeight; const scale=Math.min(window.devicePixelRatio||1,w<650?.88:1.0); 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?175:340; const p=[]; const accum={flame:0,ember:0,smoke:0,shard:0,sky:0,mist:0,pollen:0}; const rates=dark ? {flame:mobile?85:120,ember:mobile?75:113, smoke:mobile?28:43,shard:mobile?30:50} : {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=36; ctx.strokeStyle='rgba(255,55,5,.82)'; ctx.lineWidth=43; ctx.stroke(); ctx.shadowBlur=21; ctx.strokeStyle='#ff9b17'; ctx.lineWidth=20; ctx.stroke(); ctx.shadowBlur=9; 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) { if(now-lastPaint<(mobile?39:31)){ requestAnimationFrame(paint);return; } const dt=Math.min(.05,(now-previous)/1000); previous=now;lastPaint=now; const t=Math.min(1,(now-started)/duration); const seconds=now/1000; const edge=makeEdge(t,seconds,dark,w); const fade=Math.min(1,t/.055,(1-t)/.045); clipFrame(edge); advance(dt); if(t<1)spawnAtRate(dt,edge,seconds,Math.max(0,fade)); ctx.clearRect(0,0,w,h); drawSmoke(fade); 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); 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'); 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){ window.location.assign(nextActive?otherUrl.href:baseUrl);return; } const dark=(nextActive?otherPage:basePage)==='Kuronata'; 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 canvas=document.createElement('canvas'); canvas.id='kkpt-vfx'; canvas.setAttribute('aria-hidden','true'); document.body.append(canvas); const duration=dark?3450:2950; transitionSound(dark,duration); await new Promise(resolve=>canvasFire(canvas,dark,duration,resolve)); setDisplayed(nextActive); canvas.style.opacity='0'; await sleep(160); canvas.remove(); animating=false; button.classList.remove('kkpt-busy','kkpt-quiet'); // The target remains visible in a live iframe. No reload. history.pushState({kkptV12:true,active},'', active?otherUrl.href:baseUrl); } function requestSwap(nextActive) { if (animating || nextActive === active) return; if (matchMedia('(prefers-reduced-motion: reduce)').matches && ready) { setDisplayed(nextActive); history.pushState({kkptV12:true,active},'',active?otherUrl.href:baseUrl); return; } animateSwap(nextActive).catch(err => { console.error('[KuranokaPageTransition] V12:',err); document.getElementById('kkpt-vfx')?.remove(); if (frame) frame.style.filter = ''; window.location.assign(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(); 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|...]] works inside the already-loaded iframe. 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); }); // Back and Forward restore the two already-loaded documents instantly. window.addEventListener('popstate', () => { if (!frame || !ready || animating) return; const route = pathPage(); if (route === basePage || route === otherPage) { setDisplayed(route === otherPage); } }); window.addEventListener('pageshow', event => { if (!event.persisted || !frame || !ready) return; animating=false; document.getElementById('kkpt-vfx')?.remove(); frame.style.filter = ''; setDisplayed(pathPage() === otherPage); 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界面页面