跳转到内容
打开/关闭搜索
搜索
打开/关闭菜单
450
条目
805
文件
22
用户
9103
编辑
Blockland Wiki
导航
首页
最近更改
随机页面
MediaWiki帮助
特殊页面
打开/关闭外观设置菜单
无法加载外观设置。请检查您的网络连接并重试。
重试
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。
user-interface-preferences
个人工具
创建账号
登录
查看“︁MediaWiki:KuranokaPageTransition.js”︁的源代码
MediaWiki界面页面
查看
阅读
查看源代码
查看历史
相关页面
系统消息
讨论
更多操作
更多
工具
链入页面
相关更改
页面信息
Cargo数据
←
MediaWiki:KuranokaPageTransition.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
此页面为本wiki上的软件提供界面文本,并受到保护以防止滥用。如欲修改所有wiki的翻译,请访问
translatewiki.net
上的MediaWiki本地化项目。
您无权编辑此JavaScript页面,因为编辑此页面可能会影响所有访问者。
您可以查看和复制此页面的源代码。
/* ==================================================================== KURANOKA / KURONATA V4 — EMBER / AURORA MediaWiki:KuranokaPageTransition.js Same-origin iframe + history.pushState: NO full reload on toggles. Smooth SVG-path clipping + WebGL procedural flames / cold lightning. Backgrounds are prewarmed on page load. Zero external dependencies. ==================================================================== */ (function () { 'use strict'; // The preview is a complete MediaWiki page. It must NOT spawn another. if (window !== window.top || window.__kkptV4) return; if (!window.mw || !mw.config) return; // The URL is the source of truth if a skin/extension restored a page // without immediately updating mw.config (soft navigation / bfcache). 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; window.__kkptV4 = true; window.__kkptVersion = '4.0'; 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(); let active = false; // false: original top-level doc; true: otherPage iframe let animating = 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'); } // 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 < 65) 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; // 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 || event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return; const el = event.target instanceof doc.defaultView.Element ? event.target.closest('a[href]') : null; 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; } // This edge is used by BOTH the DOM clip and the shader. function edgeAt(y, seconds, progress, width) { const eased = smooth(progress); const toDark = window.__kkptVisualDark; const center = toDark ? width + 110 - (width + 220) * eased : -110 + (width + 220) * eased; const wave = Math.sin(Math.PI * progress); return center + wave * ( 28 * Math.sin(y * .012 + seconds * 3.1) + 17 * Math.sin(y * .041 - seconds * 5.7) + 7 * Math.sin(y * .10 + seconds * 10.0) + 11 * Math.sin(y * .073 + seconds * 6.13 + 2 * Math.sin(y * .017 - seconds * 2.3)) ); } function clipFrame(seconds, progress) { const width = innerWidth, height = innerHeight; const pts = []; const count = Math.max(35, Math.ceil(height / 11)); for (let i = 0; i <= count; i++) { const y = height * i / count; pts.push([edgeAt(y, seconds, progress, width), y]); } const supportsPath = CSS.supports('clip-path', 'path("M 0 0 L 8 0 L 8 8 Z")'); if (supportsPath) { const startX = clipSide === 'right' ? width + 4 : -4; let d = `M ${startX} -4 L ${startX} ${height + 4} L ${pts[count][0].toFixed(1)} ${height}`; // Quadratic smoothing eliminates the geometric triangular teeth. for (let i = count - 1; i >= 0; i--) { const point = pts[i]; const previous = pts[Math.max(0, i - 1)]; const mx = (point[0] + previous[0]) * .5; const my = (point[1] + previous[1]) * .5; d += ` Q ${point[0].toFixed(1)} ${point[1].toFixed(1)} ${mx.toFixed(1)} ${my.toFixed(1)}`; } d += ` L ${pts[0][0].toFixed(1)} 0 Z`; frame.style.clipPath = `path("${d}")`; } else { const points = pts.slice().reverse().map(p => `${p[0].toFixed(1)}px ${p[1].toFixed(1)}px`).join(','); frame.style.clipPath = clipSide === 'right' ? `polygon(100% 0,100% 100%,${points})` : `polygon(0 0,0 100%,${points})`; } } // Tiny fullscreen quad + procedural fBm fire. No sprites or dependencies. const VERTEX = ` attribute vec2 aPosition; void main() { gl_Position = vec4(aPosition, 0.0, 1.0); } `; const FRAGMENT = ` precision highp float; uniform vec2 uSize; uniform vec2 uBuffer; uniform float uTime; uniform float uProgress; uniform float uDark; float random2(vec2 p) { return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453123); } float noise(vec2 p) { vec2 i = floor(p), f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix(mix(random2(i),random2(i+vec2(1.0,0.0)),u.x), mix(random2(i+vec2(0.0,1.0)),random2(i+1.0),u.x),u.y); } float fbm(vec2 p) { float result = 0.0, amp = 0.5; for (int i = 0; i < 5; i++) { result += amp * noise(p); p = mat2(1.6,-1.2,1.2,1.6) * p; amp *= 0.5; } return result; } float smoothProgress(float t) { return t*t*(3.0-2.0*t); } float edge(float y) { float w = uSize.x; float t = smoothProgress(uProgress); float c = uDark > .5 ? w+110.0-(w+220.0)*t : -110.0+(w+220.0)*t; float wave = sin(3.14159265*uProgress); return c+wave*(28.0*sin(y*.012+uTime*3.1)+ 17.0*sin(y*.041-uTime*5.7)+7.0*sin(y*.10+uTime*10.0)+ 11.0*sin(y*.073+uTime*6.13+2.0*sin(y*.017-uTime*2.3))); } void main() { vec2 p = vec2(gl_FragCoord.x*uSize.x/uBuffer.x, uSize.y-gl_FragCoord.y*uSize.y/uBuffer.y); float d = p.x-edge(p.y); float oldSide = uDark > .5 ? -d : d; float t = uTime; float impact = sin(3.14159265*uProgress); vec3 col = vec3(0.0); float alpha = 0.0; if (uDark > .5) { // Fluid, upward-drifting fBm and domain warping. vec2 q = vec2(p.y*.012-t*.44, oldSide*.017-t*.68); float warp1 = fbm(q+vec2(0.0,t*.4)); float warp2 = fbm(q*2.3+warp1*2.2); float curl = fbm(q*1.35+vec2(warp2*2.8,-warp1*1.7)); float domain = mix(warp1,warp2,.5); float tongues = 140.0*clamp((domain*.76+curl*.53),0.0,1.0); float fire = clamp((tongues-oldSide)/68.0,0.0,1.0); fire *= smoothstep(-55.0,8.0,oldSide); float core = exp(-abs(d)/9.0); float outer = exp(-abs(d)/48.0); vec2 sq = vec2(p.y*.010+t*.14,oldSide*.013-t*.24); float sootNoise = fbm(sq+fbm(sq*2.0)); float soot = (1.0-smoothstep(45.0,270.0,oldSide))* smoothstep(-28.0,17.0,oldSide)*(sootNoise*.75+.20); vec3 sootColor = mix(vec3(.028,.015,.016),vec3(.14,.040,.027),sootNoise); float heat = clamp(fire*.85+core*.6,0.0,1.0); vec3 fireColor = mix(vec3(.36,.015,.008),vec3(1.0,.20,.025),smoothstep(.15,.58,heat)); fireColor = mix(fireColor,vec3(1.0,.79,.21),smoothstep(.55,.88,heat)); fireColor = mix(fireColor,vec3(1.0,.97,.74),smoothstep(.85,1.0,heat)); col = mix(sootColor,fireColor,clamp(fire*1.10+core,0.0,1.0)); alpha = clamp(soot*.58+fire*.90+outer*.19+core*.64,0.0,.96); // Fine orange embers, not giant elliptical blobs. vec2 cell = floor((p+vec2(t*27.0,-t*100.0))/13.0); vec2 uv = fract((p+vec2(t*27.0,-t*100.0))/13.0)-.5; float rand = random2(cell); float sparks = (1.0-smoothstep(0.02,.17,length(uv)))* step(.987,rand)*exp(-abs(d)/210.0); col = mix(col,vec3(1.0,.78,.30),sparks); alpha = max(alpha,sparks*.93); } else { // The return is ELECTRIC AURORA / COLD MIST, NOT BLUE FIRE. vec2 q = vec2(p.y*.009-t*.25,oldSide*.013+t*.18); float cloud = fbm(q+vec2(fbm(q*1.7),-t*.25)); float mist = exp(-abs(d)/115.0)*(cloud*.7+.13); float ion = exp(-abs(d)/26.0); float arcWobble = 25.0*sin(p.y*.043-t*6.0)+ 10.0*sin(p.y*.14+t*13.0)+5.0*sin(p.y*.37-t*19.0); float arc = exp(-abs(d+arcWobble)/2.7)* (.7+.3*sin(p.y*.065+t*7.0)); float branches = exp(-abs(d+52.0*sin(p.y*.018+t*2.4))/1.9)* smoothstep(.63,.92,noise(vec2(floor(p.y/54.0),floor(t*9.0)))); col = mix(vec3(.012,.10,.24),vec3(.20,.64,1.0),cloud+.27); col += vec3(.23,.45,.60)*ion; col = mix(col,vec3(.90,1.0,1.0),clamp(arc*.93+branches*.55,0.0,1.0)); alpha = clamp(mist*.70+ion*.36+arc*.95+branches*.61,0.0,.89); } // No long-lasting screen tint after the burn has finished. alpha *= (.35+.65*impact) * smoothstep(0.0,.07,uProgress) * (1.0-smoothstep(.94,1.0,uProgress)); gl_FragColor = vec4(col,alpha); } `; function createRenderer(canvas) { const gl = canvas.getContext('webgl', { alpha: true, antialias: false, depth: false, stencil: false, preserveDrawingBuffer: false, premultipliedAlpha: false }); if (!gl) return null; function compile(type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('[KKPT WebGL] Shader:', gl.getShaderInfoLog(shader)); return null; } return shader; } const vs = compile(gl.VERTEX_SHADER, VERTEX); const fs = compile(gl.FRAGMENT_SHADER, FRAGMENT); if (!vs || !fs) return null; const program = gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error('[KKPT WebGL] Program:', gl.getProgramInfoLog(program)); return null; } gl.useProgram(program); const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW); const pos = gl.getAttribLocation(program,'aPosition'); gl.enableVertexAttribArray(pos); gl.vertexAttribPointer(pos,2,gl.FLOAT,false,0,0); const sizeUniform = gl.getUniformLocation(program,'uSize'); const bufferUniform = gl.getUniformLocation(program,'uBuffer'); const timeUniform = gl.getUniformLocation(program,'uTime'); const progressUniform = gl.getUniformLocation(program,'uProgress'); const modeUniform = gl.getUniformLocation(program,'uDark'); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA); return function draw(seconds,progress,dark) { const w=innerWidth,h=innerHeight; // Keep a stable GPU budget on high-DPI/mobile displays. const scale = w < 700 ? .68 : .84; const bw=Math.round(w*scale),bh=Math.round(h*scale); if (canvas.width!==bw || canvas.height!==bh) { canvas.width=bw;canvas.height=bh;gl.viewport(0,0,bw,bh); } gl.clearColor(0,0,0,0); gl.clear(gl.COLOR_BUFFER_BIT); gl.uniform2f(sizeUniform,w,h); gl.uniform2f(bufferUniform,bw,bh); gl.uniform1f(timeUniform,seconds); gl.uniform1f(progressUniform,progress); gl.uniform1f(modeUniform,dark?1:0); gl.drawArrays(gl.TRIANGLES,0,6); }; } function createFallback(canvas) { const ctx = canvas.getContext('2d'); return function draw(seconds,progress,dark) { const w=innerWidth,h=innerHeight; if (canvas.width!==w || canvas.height!==h) {canvas.width=w;canvas.height=h;} ctx.clearRect(0,0,w,h); const x = edgeAt(h/2,seconds,progress,w); const gradient = ctx.createLinearGradient(x-115,0,x+115,0); if (dark) { gradient.addColorStop(0,'#180d0d00'); gradient.addColorStop(.40,'#67180677'); gradient.addColorStop(.50,'#ffe186cc'); gradient.addColorStop(.62,'#ef3f0955'); gradient.addColorStop(1,'#180d0d00'); } else { gradient.addColorStop(0,'#5abcf600'); gradient.addColorStop(.46,'#7cdbff77'); gradient.addColorStop(.5,'#ffffffbb'); gradient.addColorStop(.56,'#63c9ff77'); gradient.addColorStop(1,'#5abcf600'); } ctx.fillStyle=gradient;ctx.fillRect(x-115,0,230,h); }; } function setDisplayed(nextActive) { active = nextActive; frame.classList.toggle('kkpt-active', active); frame.style.clipPath = active ? 'inset(0)' : hideClip; frame.tabIndex = active ? 0 : -1; frame.setAttribute('aria-hidden', active ? 'false' : 'true'); document.title = active ? otherTitle : baseTitle; updateButton(); } async function animateSwap(nextActive) { if (animating) return; animating = true; button.classList.add('kkpt-busy'); // Warm-up should be done already; prepare if the user clicked early. labelSub.textContent = 'PREPARING THE OTHER SIDE...'; if (!loading) warmFrame(); const canUseFrame = await Promise.race([loading,sleep(11000).then(() => false)]); if (!canUseFrame || !ready) { // No mock background: fall back to the genuine Wiki page. window.location.assign(nextActive ? otherUrl.href : baseUrl); return; } const destinationDark = isDark(nextActive ? otherPage : basePage); window.__kkptVisualDark = destinationDark; button.classList.remove('kkpt-quiet'); labelSub.textContent = destinationDark ? 'THE SKY IS BURNING' : 'DAWN IS BREAKING'; // Freeze frame interaction while its silhouette is animated. 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'; document.body.append(canvas); let draw = createRenderer(canvas); if (!draw) { canvas.remove(); const fallbackCanvas = document.createElement('canvas'); fallbackCanvas.id = 'kkpt-vfx'; document.body.append(fallbackCanvas); draw = createFallback(fallbackCanvas); } const fxCanvas = document.getElementById('kkpt-vfx'); const duration = destinationDark ? 3650 : 2950; const started = performance.now(); await new Promise(resolve => { function tick(now) { const seconds = now / 1000; const t = Math.min(1,(now-started)/duration); clipFrame(seconds,t); draw(seconds,t,destinationDark); if (t < 1) requestAnimationFrame(tick); else resolve(); } requestAnimationFrame(tick); }); setDisplayed(nextActive); fxCanvas.style.opacity = '0'; await sleep(190); fxCanvas.remove(); frame.style.pointerEvents = ''; animating = false; button.classList.remove('kkpt-busy','kkpt-quiet'); // No location.assign here: the rendered target stays alive. history.pushState({ kkptV4: true, active }, '', active ? 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); updateButton(); warmFrame(); button.addEventListener('click', event => { if (event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return; event.preventDefault(); if (animating) return; if (matchMedia('(prefers-reduced-motion: reduce)').matches && ready) { setDisplayed(!active); history.pushState({kkptV4:true,active},'',active?otherUrl.href:baseUrl); return; } animateSwap(!active).catch(err => { console.error('[KuranokaPageTransition] V4:',err); window.location.assign(active ? baseUrl : otherUrl.href); }); }); } // 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(); 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界面页面