跳转到内容
打开/关闭搜索
搜索
打开/关闭菜单
450
条目
805
文件
22
用户
9101
编辑
Blockland Wiki
导航
首页
最近更改
随机页面
MediaWiki帮助
特殊页面
打开/关闭外观设置菜单
无法加载外观设置。请检查您的网络连接并重试。
重试
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。
user-interface-preferences
个人工具
创建账号
登录
查看“︁MediaWiki:KuranokaPageTransition.js”︁的源代码
MediaWiki界面页面
查看
阅读
查看源代码
查看历史
相关页面
系统消息
讨论
更多操作
更多
工具
链入页面
相关更改
页面信息
Cargo数据
←
MediaWiki:KuranokaPageTransition.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
此页面为本wiki上的软件提供界面文本,并受到保护以防止滥用。如欲修改所有wiki的翻译,请访问
translatewiki.net
上的MediaWiki本地化项目。
您无权编辑此JavaScript页面,因为编辑此页面可能会影响所有访问者。
您可以查看和复制此页面的源代码。
/* ================================================================= KURANOKA PAGE TRANSITION V3 — ASHES / REBIRTH Paste into MediaWiki:KuranokaPageTransition.js (replace V2). Load ONLY on wiki pages Kuranoka and Kuronata via Citizen.js. Needs the matching KuranokaPageTransition.css. No libraries. ================================================================= */ (function () { 'use strict'; // The destination iframe loads this script too. NEVER nest previews. if (window !== window.top || window.__kkptV3) return; window.__kkptV3 = true; if (!window.mw || !mw.config) return; if (mw.config.get('wgNamespaceNumber') !== 0 || mw.config.get('wgAction') !== 'view') return; const page = mw.config.get('wgPageName'); if (page !== 'Kuranoka' && page !== 'Kuronata') return; const burn = page === 'Kuranoka'; const target = burn ? 'Kuronata' : 'Kuranoka'; const targetUrl = '/wiki/' + encodeURIComponent(target); const DURATION = burn ? 3900 : 3200; const startClip = burn ? 'inset(0 0 0 100%)' : 'inset(0 100% 0 0)'; const warmedImages = new Map(); const rand = (min, max) => min + Math.random() * (max - min); const smooth = t => t * t * (3 - 2 * t); const pause = ms => new Promise(resolve => setTimeout(resolve, ms)); const frame = () => new Promise(resolve => requestAnimationFrame(resolve)); let iframe = null; let warmPromise = null; let loaded = false; let busy = false; let button = null; // Look at actual CSS in BOTH documents. This finds Background template // body backgrounds as well as CSS pseudo-element backdrops. function findBackgroundUrls(doc) { const urls = new Set(); const nodes = [doc.documentElement, doc.body]; const selectors = [ '#mw-content-text', '.mw-parser-output', '.citizen-page-container', '.citizen-body-container', '.mw-page-container', '.background-theme', '[class*="background"]', '[id*="background"]', '[style*="background"]', '[data-background]' ]; selectors.forEach(selector => { try { doc.querySelectorAll(selector).forEach((node, index) => { if (index < 45 && !nodes.includes(node)) nodes.push(node); }); } catch (e) { /* selector may not exist in an older skin */ } }); function add(value) { if (!value || typeof value !== 'string') return; const urlPattern = /url\(\s*(['"]?)(.*?)\1\s*\)/gi; let match; let hasCssUrl = false; while ((match = urlPattern.exec(value))) { hasCssUrl = true; if (match[2]) { try { urls.add(new URL(match[2], doc.baseURI).href); } catch (e) { /* ignore */ } } } if (!hasCssUrl && /^(https?:\/\/|\/|\.\/|\.\.\/).+\.(?:png|jpe?g|webp|avif|gif|svg)(?:[?#].*)?$/i.test(value)) { try { urls.add(new URL(value, doc.baseURI).href); } catch (e) { /* ignore */ } } } nodes.forEach(node => { if (!node) return; try { const win = doc.defaultView; [null, '::before', '::after'].forEach(pseudo => { const style = win.getComputedStyle(node, pseudo); if (style) { add(style.backgroundImage); add(style.maskImage); add(style.getPropertyValue('--background-image')); add(style.getPropertyValue('--bg-image')); } }); } catch (e) { /* ignore unsupported pseudo styles */ } // MediaWiki Background templates often expose URLs as data-*. if (node.dataset) Object.entries(node.dataset).forEach(([key, value]) => { if (/background|wallpaper|image|backdrop|bg/i.test(key)) add(value); }); }); return [...urls]; } function decodeBackground(url) { if (warmedImages.has(url)) return warmedImages.get(url); const image = new Image(); image.decoding = 'async'; image.fetchPriority = 'high'; image.src = url; const promise = new Promise(resolve => { if (image.complete) return resolve(image.naturalWidth > 0); image.onload = () => resolve(true); image.onerror = () => resolve(false); }).then(async success => { if (success && image.decode) { try { await image.decode(); } catch (e) { /* already loaded */ } } return success; }); // Keep an Image object alive so the browser can reuse its decoded data. warmedImages.set(url, promise); if (!window.__kkptImagePool) window.__kkptImagePool = []; window.__kkptImagePool.push(image); return promise; } async function warmBackgrounds(doc) { const urls = findBackgroundUrls(doc); if (urls.length) await Promise.all(urls.map(decodeBackground)); return urls.length; } function createPreviewEarly() { if (warmPromise) return warmPromise; iframe = document.createElement('iframe'); iframe.id = 'kkpt-preview'; iframe.title = target + ' — 预载画面'; iframe.tabIndex = -1; iframe.setAttribute('aria-hidden', 'true'); iframe.setAttribute('loading', 'eager'); iframe.style.clipPath = startClip; warmPromise = new Promise(resolve => { let settled = false; function done(ok) { if (settled) return; settled = true; loaded = ok; resolve(ok); } iframe.addEventListener('load', async function () { try { const doc = iframe.contentDocument; if (!doc || !doc.querySelector('#mw-content-text')) return done(false); // Allow background JavaScript / async MediaWiki styles to run. await pause(200); const fonts = doc.fonts && doc.fonts.ready; if (fonts) await Promise.race([fonts.catch(() => {}), pause(1300)]); await warmBackgrounds(doc); await pause(170); await warmBackgrounds(doc); // catches late body/background CSS iframe.contentWindow.scrollTo(0, 0); done(true); } catch (err) { console.warn('[KuranokaPageTransition] Unable to pre-render the next page:', err); done(false); } }, { once: true }); iframe.addEventListener('error', () => done(false), { once: true }); document.body.appendChild(iframe); iframe.src = targetUrl; }); // Warm the current page as well, in case an incoming navigation needs it. warmBackgrounds(document).catch(() => {}); setTimeout(() => warmBackgrounds(document).catch(() => {}), 500); return warmPromise; } function makeSprite(stops) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = 96; const c = canvas.getContext('2d'); const g = c.createRadialGradient(48, 48, 0, 48, 48, 48); stops.forEach(stop => g.addColorStop(stop[0], stop[1])); c.fillStyle = g; c.fillRect(0, 0, 96, 96); return canvas; } const SPRITES = { fire: makeSprite([[0, '#ffffff'], [.11, '#fff6b3'], [.35, '#ffb71e'], [.68, '#f7330988'], [1, '#a3000000']]), spark: makeSprite([[0, '#ffffff'], [.16, '#fff3b8'], [.48, '#ff7900bb'], [1, '#ff230000']]), smoke: makeSprite([[0, '#170c0de0'], [.36, '#201b20a1'], [.72, '#302b3260'], [1, '#11111100']]), ash: makeSprite([[0, '#d2b2a9'], [.28, '#897471bb'], [1, '#55555500']]), sky: makeSprite([[0, '#ffffff'], [.16, '#eeffff'], [.44, '#79d5ffbb'], [1, '#3c92ff00']]) }; function runAnimation() { return new Promise(resolve => { const canvas = document.createElement('canvas'); canvas.id = 'kkpt-canvas'; canvas.setAttribute('aria-hidden', 'true'); document.body.append(canvas); const ctx = canvas.getContext('2d', { alpha: true }); if (!ctx) { canvas.remove(); resolve(); return; } let width = 0, height = 0; function resize() { width = window.innerWidth; height = window.innerHeight; const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.6); canvas.width = Math.round(width * pixelRatio); canvas.height = Math.round(height * pixelRatio); ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); } resize(); const particles = []; const shockwaves = []; const maxParticles = width < 700 ? 580 : 1100; const start = performance.now(); let previous = start; let burstClock = 0; function addParticle(x, y) { if (particles.length >= maxParticles) return; const r = Math.random(); const type = burn ? (r < .49 ? 'fire' : r < .70 ? 'spark' : r < .90 ? 'smoke' : 'ash') : (r < .68 ? 'sky' : r < .85 ? 'spark' : 'smoke'); const isSmoke = type === 'smoke'; const isDust = type === 'ash'; const life = isSmoke ? rand(.9, 1.85) : isDust ? rand(.8, 2.3) : rand(.35, 1.05); particles.push({ type, x: x + rand(-24, 24), y: y + rand(-22, 22), vx: burn ? rand(-255, 96) : rand(-86, 210), vy: isSmoke ? rand(-115, -25) : isDust ? rand(-95, 25) : rand(-280, -52), size: isSmoke ? rand(38, 102) : isDust ? rand(3, 11) : type === 'spark' ? rand(3, 10) : rand(14, 42), life, remain: life, angle: rand(-Math.PI, Math.PI), spin: rand(-1.2, 1.2) }); } function paint(now) { const dt = Math.min(.05, Math.max(.001, (now - previous) / 1000)); previous = now; const progress = Math.min(1, (now - start) / DURATION); const ease = smooth(progress); const wobble = Math.pow(Math.sin(Math.PI * progress), .7); const direction = burn ? -1 : 1; const center = burn ? width + 120 - (width + 260) * ease : -120 + (width + 260) * ease; function edge(y) { return center + wobble * ( 32 * Math.sin(y * .012 + now * .0037) + 21 * Math.sin(y * .035 - now * .0061) + 12 * Math.sin(y * .079 + now * .0107) + 7 * Math.sin(y * .156 - now * .014) ); } const pts = []; const count = Math.max(35, Math.ceil(height / 17)); for (let i = 0; i <= count; i++) { const y = height * i / count; pts.push([edge(y), y]); } const pathToEdge = pts.slice().reverse().map(p => p[0].toFixed(1) + 'px ' + p[1].toFixed(1) + 'px' ).join(','); iframe.style.clipPath = burn ? 'polygon(100% 0,100% 100%,' + pathToEdge + ')' : 'polygon(0 0,0 100%,' + pathToEdge + ')'; ctx.clearRect(0, 0, width, height); // Flare spreading over the whole screen makes this feel like // the world's light changes, rather than a moving divider. const flash = .025 * (1 + Math.sin(now * .048)) + .07 * Math.pow(Math.max(0, Math.sin(now * .009)), 12); ctx.fillStyle = burn ? 'rgba(108,13,5,' + (flash * wobble).toFixed(3) + ')' : 'rgba(48,135,205,' + (flash * wobble).toFixed(3) + ')'; ctx.fillRect(0, 0, width, height); function trace(offset) { ctx.beginPath(); pts.forEach((p, i) => { if (i === 0) ctx.moveTo(p[0] + offset, p[1]); else ctx.lineTo(p[0] + offset, p[1]); }); } // A broad charred-paper zone is consumed BEFORE the bright core. // Its gradient is on the remaining old page side of the flame. ctx.save(); const x0 = burn ? -133 : 133; const x1 = burn ? 18 : -18; const dark = ctx.createLinearGradient(center + x0, 0, center + x1, 0); if (burn) { dark.addColorStop(0, '#130d0d00'); dark.addColorStop(.35, '#150c0eaa'); dark.addColorStop(.70, '#10080ce9'); dark.addColorStop(.91, '#48170df5'); dark.addColorStop(1, '#fa42156a'); } else { dark.addColorStop(0, '#0b1f3100'); dark.addColorStop(.35, '#052744aa'); dark.addColorStop(.7, '#041c35e9'); dark.addColorStop(.91, '#215b8bf5'); dark.addColorStop(1, '#8ce8ff77'); } ctx.beginPath(); pts.forEach((p, i) => { const jitter = 10 * Math.sin(p[1] * .11 + now * .007); const x = p[0] + x0 + jitter; if (i === 0) ctx.moveTo(x, p[1]); else ctx.lineTo(x, p[1]); }); pts.slice().reverse().forEach(p => ctx.lineTo(p[0] + x1, p[1])); ctx.closePath(); ctx.fillStyle = dark; ctx.fill(); ctx.restore(); // Smoke is below the light-emitting flames. ctx.save(); for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; p.remain -= dt; if (p.remain <= 0) { particles.splice(i, 1); continue; } p.x += p.vx * dt; p.y += p.vy * dt; p.vx += (p.type === 'smoke' ? -4 : direction * 15) * dt; p.vy -= 11 * dt; p.angle += p.spin * dt; if (p.type !== 'smoke' && p.type !== 'ash') continue; const age = 1 - p.remain / p.life; const size = p.size * (1 + age * (p.type === 'smoke' ? 1.7 : .6)); ctx.globalAlpha = Math.min(1, p.remain * 2) * (p.type === 'smoke' ? .52 : .35); ctx.drawImage(SPRITES[p.type], p.x - size, p.y - size, 2 * size, 2 * size); } ctx.restore(); // Broken filament cracks penetrating the unburnt page. // They flicker independently instead of making a tidy wall. if (burn) { ctx.save(); for (let y = 38; y < height; y += 103) { const a = edge(y), skew = Math.sin(y * .13 + now * .0017); ctx.lineWidth = 1.6; ctx.strokeStyle = 'rgba(38,6,4,.8)'; ctx.beginPath(); ctx.moveTo(a - 8, y); ctx.lineTo(a - 35, y - 13 * skew); ctx.lineTo(a - 59, y + 12); ctx.lineTo(a - 93 - 20 * skew, y - 19); ctx.lineTo(a - 127, y - 6); ctx.stroke(); ctx.lineWidth = .7; ctx.strokeStyle = 'rgba(255,99,23,.66)'; ctx.stroke(); ctx.beginPath(); ctx.moveTo(a - 61, y + 12); ctx.lineTo(a - 77, y + 31); ctx.lineTo(a - 91, y + 40); ctx.stroke(); } ctx.restore(); } // Blooming outer corona, molten core, then white-hot ignition. ctx.save(); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.shadowColor = burn ? '#fb2509' : '#86e6ff'; ctx.shadowBlur = burn ? 79 : 88; trace(0); ctx.strokeStyle = burn ? 'rgba(242,35,6,.6)' : 'rgba(74,168,255,.65)'; ctx.lineWidth = burn ? 65 : 73; ctx.stroke(); ctx.shadowBlur = 39; ctx.strokeStyle = burn ? 'rgba(255,110,10,.88)' : 'rgba(130,223,255,.91)'; ctx.lineWidth = burn ? 29 : 35; ctx.stroke(); ctx.shadowBlur = 18; ctx.strokeStyle = burn ? '#ffe564' : '#e5fbff'; ctx.lineWidth = 9; ctx.stroke(); ctx.shadowBlur = 9; ctx.strokeStyle = '#fffdf0'; ctx.lineWidth = 3; ctx.stroke(); ctx.restore(); // Aggressive curling tongues. Flame tips tear leftwards into // old Kuranoka; on the way back, electric shards tear right. ctx.save(); ctx.globalCompositeOperation = 'lighter'; for (let y = -60; y < height + 60; y += burn ? 17 : 26) { const x = edge(y); const flicker = Math.sin(now * .015 + y * .093); const lift = (burn ? 55 : 68) + (burn ? 65 : 43) * Math.abs(Math.sin(y * .043 - now * .009)) + 18 * (flicker + 1); const lean = (burn ? -1 : 1) * lift; ctx.fillStyle = burn ? 'rgba(255,53,8,.44)' : 'rgba(31,138,255,.33)'; ctx.beginPath(); ctx.moveTo(x - lean * .05, y + 19); ctx.bezierCurveTo(x + lean * .14, y - 18, x + lean * .88, y - lift * .47, x + lean, y - lift); ctx.bezierCurveTo(x + lean * .35, y - lift * .53, x - lean * .13, y + 13, x + 16, y + 27); ctx.closePath(); ctx.fill(); ctx.fillStyle = burn ? 'rgba(255,231,89,.47)' : 'rgba(222,250,255,.52)'; ctx.beginPath(); ctx.moveTo(x + 4, y + 11); ctx.quadraticCurveTo(x + lean * .32, y - lift * .31, x + lean * .70, y - lift * .67); ctx.quadraticCurveTo(x - 6, y - lift * .21, x + 7, y + 11); ctx.closePath(); ctx.fill(); } ctx.restore(); // Impacts regularly burst along the travelling front. burstClock += dt; if (burstClock > (burn ? .18 : .26) && progress > .05 && progress < .95) { burstClock = 0; const y = rand(0, height); shockwaves.push({x: edge(y), y, age: 0, max: rand(52, 115)}); for (let j = 0; j < (burn ? 18 : 12); j++) addParticle(edge(y), y); } for (let i = shockwaves.length - 1; i >= 0; i--) { const s = shockwaves[i]; s.age += dt; if (s.age > .52) { shockwaves.splice(i, 1); continue; } const t = s.age / .52; ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = (1 - t) * .67; ctx.strokeStyle = burn ? '#ffce75' : '#d3ffff'; ctx.lineWidth = 4 * (1 - t) + .3; ctx.shadowBlur = 32; ctx.shadowColor = burn ? '#ff3f05' : '#72d5ff'; ctx.beginPath(); ctx.ellipse(s.x, s.y, s.max * t, s.max * t * .7, 0, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } const density = width < 700 ? 13 : 29; for (let i = 0; i < density; i++) { const y = rand(0, height); addParticle(edge(y), y); } ctx.save(); ctx.globalCompositeOperation = 'lighter'; particles.forEach(p => { if (p.type === 'smoke' || p.type === 'ash') return; const age = 1 - p.remain / p.life; const fade = Math.min(1, p.remain * 3) * (1 - age * .65); ctx.globalAlpha = fade * (p.type === 'spark' ? .97 : .72); ctx.translate(p.x, p.y); ctx.rotate(p.angle); const xsize = p.size * (p.type === 'fire' ? .8 : 1); const ysize = p.size * (p.type === 'fire' ? 1.7 : 1); ctx.drawImage(SPRITES[p.type], -xsize, -ysize, xsize * 2, ysize * 2); ctx.rotate(-p.angle); ctx.translate(-p.x, -p.y); }); ctx.restore(); if (progress < 1) { requestAnimationFrame(paint); } else { // The iframe is fully visible BEFORE the old document // navigates. No handoff cover, no blue/red solid flash. iframe.style.clipPath = 'inset(0)'; canvas.style.opacity = '0'; resolve(); } } requestAnimationFrame(paint); }); } async function begin() { if (!warmPromise) createPreviewEarly(); button.classList.add('kkpt-loading'); button.querySelector('.kkpt-sub').textContent = 'PREPARING THE OTHER SIDE...'; // Usually instantaneous: the iframe and both wallpapers began loading // when the page itself was opened, not when this button was clicked. const ready = await Promise.race([warmPromise, pause(12000).then(() => false)]); if (!ready || !loaded) { button.classList.remove('kkpt-loading'); button.querySelector('.kkpt-sub').textContent = '预览未就绪 · 点击普通跳转'; // Failure path is a normal link, not a fake blue page reveal. button.dataset.kkptFallback = '1'; busy = false; return; } button.classList.remove('kkpt-loading'); button.classList.add('kkpt-running'); button.querySelector('.kkpt-sub').textContent = burn ? 'THE WORLD IS BURNING' : 'BREAK THROUGH THE DARK'; await runAnimation(); // Let the compositor present the REAL destination iframe for a few // frames. On navigation, native cross-document View Transition takes // its old snapshot from this already-revealed destination. button.classList.add('kkpt-vanish'); await frame(); await frame(); await pause(90); window.location.assign(targetUrl); } function makeButton() { if (!document.body || document.getElementById('kkpt-toggle')) return; button = document.createElement('a'); button.id = 'kkpt-toggle'; button.href = targetUrl; button.setAttribute('aria-label', burn ? '燃尽当前世界,进入 Kuronata' : '破晓,返回 Kuranoka'); const icon = document.createElement('span'); icon.className = 'kkpt-icon'; icon.setAttribute('aria-hidden', 'true'); icon.textContent = burn ? '✦' : '☀'; const label = document.createElement('span'); label.className = 'kkpt-text'; const title = document.createElement('span'); title.textContent = target.toUpperCase(); const sub = document.createElement('span'); sub.className = 'kkpt-sub'; sub.textContent = burn ? 'BURN THE WORLD' : 'RETURN TO THE SKY'; label.append(title, sub); button.append(icon, label); document.body.append(button); // Preload starts right now, not on click. createPreviewEarly(); button.addEventListener('click', event => { if (event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; if (button.dataset.kkptFallback === '1') return; // normal navigation event.preventDefault(); if (busy) return; busy = true; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { window.location.assign(targetUrl); return; } begin().catch(err => { console.error('[KuranokaPageTransition] V3 error:', err); // Unexpected errors go to the real page, never a blank cover. window.location.assign(targetUrl); }); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', makeButton, { once: true }); } else { makeButton(); } // Back/forward cache can revive a page with the iframe still fully open. // Restore the original scene when users return with the Back button. window.addEventListener('pageshow', event => { if (!event.persisted) return; document.getElementById('kkpt-canvas')?.remove(); if (iframe) iframe.style.clipPath = startClip; if (button) { button.classList.remove('kkpt-vanish', 'kkpt-running', 'kkpt-loading'); const subtitle = button.querySelector('.kkpt-sub'); if (subtitle) subtitle.textContent = burn ? 'BURN THE WORLD' : 'RETURN TO THE SKY'; } busy = false; }); }());
返回
MediaWiki:KuranokaPageTransition.js
。
查看“︁MediaWiki:KuranokaPageTransition.js”︁的源代码
MediaWiki界面页面