Files

204 lines
12 KiB
JavaScript

'use strict';
(function mountPremiumProjectIllustrations() {
const targets = Object.freeze({
'games-empty': 'library',
'backups-empty': 'protection',
'transfers-empty': 'transfer',
'media-empty': 'media',
});
const illustrations = Object.freeze({
'library': `<div class="premium-project-illustration" data-variant="library" aria-hidden="true">
<span class="premium-project-glow"></span>
<svg viewBox="0 0 320 180" focusable="false">
<path class="premium-project-path path-a" d="M31 124C76 50 122 57 159 88s75 37 130-23"/>
<path class="premium-project-path path-b" d="M37 58c42 18 65 4 96 34s70 44 151 27"/>
<g class="premium-project-source library-source"><rect x="24" y="82" width="38" height="54" rx="6"/><rect x="31" y="74" width="38" height="54" rx="6"/><path d="M39 91h22M39 100h16"/></g>
<g class="premium-project-surface library-index"><rect x="119" y="39" width="83" height="104" rx="11"/><path d="M135 63h51M135 79h34M135 105h18M160 105h26M135 119h18M160 119h26"/></g>
<g class="premium-project-node library-art"><rect x="260" y="48" width="38" height="48" rx="7"/><path d="m268 84 8-10 7 7 6-8"/><circle cx="286" cy="60" r="4"/></g>
<g class="premium-project-node library-proof"><circle cx="276" cy="126" r="15"/><path d="m269 126 5 5 9-11"/></g>
</svg>
<span class="premium-project-packet packet-a"></span><span class="premium-project-packet packet-b"></span><span class="premium-project-packet packet-c"></span>
</div>`,
'protection': `<div class="premium-project-illustration" data-variant="protection" aria-hidden="true">
<span class="premium-project-glow"></span>
<svg viewBox="0 0 320 180" focusable="false">
<path class="premium-project-path path-a" d="M35 95h79c21 0 27-30 49-30h27c24 0 29 31 49 31h47"/>
<path class="premium-project-path path-b" d="M65 131h67c18 0 25-21 42-21h35c17 0 23 21 43 21h34"/>
<g class="premium-project-source protection-copy"><rect x="27" y="61" width="46" height="68" rx="7"/><path d="M38 80h24M38 94h19M38 108h24"/></g>
<g class="premium-project-node protection-hash"><rect x="129" y="49" width="68" height="55" rx="10"/><path d="M142 68h42M142 80h32"/><text x="144" y="94">SHA</text></g>
<g class="premium-project-surface protection-vault"><path d="M238 55h55v77h-55z"/><circle cx="265" cy="93" r="14"/><path d="M265 84v18M256 93h18"/></g>
<g class="premium-project-shield"><path d="M183 112c0 19-12 28-25 34-13-6-25-15-25-34l25-9z"/><path d="m149 121 7 7 12-16"/></g>
</svg>
<span class="premium-project-packet packet-a"></span><span class="premium-project-packet packet-b"></span><span class="premium-project-packet packet-c"></span>
</div>`,
'transfer': `<div class="premium-project-illustration" data-variant="transfer" aria-hidden="true">
<span class="premium-project-glow"></span>
<svg viewBox="0 0 320 180" focusable="false">
<path class="premium-project-path path-a" d="M68 87h67c21 0 26-27 47-27h28c22 0 27 27 47 27h26"/>
<path class="premium-project-path path-b" d="M68 119h63c22 0 31 25 53 25h30c21 0 29-25 47-25h22"/>
<g class="premium-project-source transfer-console"><path d="M27 61h46l8 65H19z"/><path d="M37 78h27M42 111h17"/><circle cx="50" cy="93" r="5"/></g>
<g class="premium-project-surface transfer-router"><rect x="133" y="42" width="69" height="78" rx="11"/><path d="M146 64h43M146 79h43M146 94h31"/><circle cx="186" cy="95" r="3"/></g>
<g class="premium-project-node transfer-vault"><rect x="251" y="61" width="48" height="65" rx="7"/><path d="M260 78h30M260 91h30M260 104h21"/></g>
<g class="premium-project-progress"><path d="M126 144h82"/><path d="M126 144h55"/></g>
</svg>
<span class="premium-project-packet packet-a"></span><span class="premium-project-packet packet-b"></span><span class="premium-project-packet packet-c"></span>
</div>`,
'media': `<div class="premium-project-illustration" data-variant="media" aria-hidden="true">
<span class="premium-project-glow"></span>
<svg viewBox="0 0 320 180" focusable="false">
<path class="premium-project-path path-a" d="M85 90h58c21 0 24-28 46-28h23c21 0 25 28 44 28h35"/>
<path class="premium-project-path path-b" d="M84 120h57c22 0 27 22 47 22h26c20 0 25-22 44-22h33"/>
<g class="premium-project-source media-console"><rect x="22" y="48" width="70" height="84" rx="9"/><path d="m34 111 15-18 12 12 18-24"/><circle cx="71" cy="66" r="6"/></g>
<g class="premium-project-wave"><path d="M119 92h8l5-17 9 35 8-25 8 13 7-6h8"/></g>
<g class="premium-project-surface media-gallery"><rect x="225" y="43" width="67" height="47" rx="7"/><rect x="217" y="99" width="75" height="43" rx="7"/><path d="m235 80 10-11 8 8 11-14 17 17M227 132l11-12 9 8 11-13 22 17"/></g>
<g class="premium-project-node media-play"><circle cx="184" cy="95" r="19"/><path d="m180 86 12 9-12 9z"/></g>
</svg>
<span class="premium-project-packet packet-a"></span><span class="premium-project-packet packet-b"></span><span class="premium-project-packet packet-c"></span>
</div>`,
'trophies': `<div class="premium-project-illustration" data-variant="trophies" aria-hidden="true">
<span class="premium-project-glow"></span>
<svg viewBox="0 0 320 180" focusable="false">
<path class="premium-project-path path-a" d="M48 126c39-72 80-80 119-45s65 40 111-25"/>
<path class="premium-project-path path-b" d="M46 66c43 5 66 23 91 59s80 32 139 4"/>
<g class="premium-project-source trophy-game"><rect x="27" y="54" width="52" height="75" rx="7"/><path d="M38 75h30M38 88h20M38 108h30"/></g>
<g class="premium-project-surface trophy-cup"><path d="M139 49h46v22c0 23-9 36-23 36s-23-13-23-36z"/><path d="M139 58h-14v8c0 13 7 20 19 21M185 58h14v8c0 13-7 20-19 21M162 107v17M145 126h34"/></g>
<g class="premium-project-node trophy-locked"><circle cx="257" cy="62" r="19"/><path d="M250 62v-5a7 7 0 0 1 14 0v5M248 62h18v14h-18z"/></g>
<g class="premium-project-node trophy-earned"><circle cx="268" cy="127" r="17"/><path d="m259 127 6 6 12-14"/></g>
<g class="premium-project-progress trophy-progress"><circle cx="162" cy="84" r="56"/><path d="M162 28a56 56 0 0 1 51 79"/></g>
</svg>
<span class="premium-project-packet packet-a"></span><span class="premium-project-packet packet-b"></span><span class="premium-project-packet packet-c"></span>
</div>`,
});
const reducedMotion = typeof window.matchMedia === 'function'
? window.matchMedia('(prefers-reduced-motion: reduce)')
: { matches: false, addListener() {}, removeListener() {} };
const mounted = new Set();
const visibilityObserver = 'IntersectionObserver' in window ? new IntersectionObserver(entries => {
for (const entry of entries) {
entry.target.classList.toggle('is-active', entry.isIntersecting);
syncMotion(entry.target);
}
}, { threshold: .16 }) : null;
function motionPaused() {
return reducedMotion.matches || document.visibilityState !== 'visible';
}
function syncMotion(element) {
if (!element) return;
element.classList.toggle('is-paused', motionPaused());
}
function syncAllMotion() {
for (const element of mounted) {
if (!element.isConnected) {
visibilityObserver?.unobserve(element);
mounted.delete(element);
continue;
}
syncMotion(element);
}
}
function bind(illustration) {
if (!illustration || mounted.has(illustration)) return;
syncAllMotion();
mounted.add(illustration);
visibilityObserver?.observe(illustration);
if (!visibilityObserver) illustration.classList.add('is-active');
syncMotion(illustration);
let pointerFrame = 0;
let pointerX = 0;
let pointerY = 0;
illustration.addEventListener('pointermove', event => {
if (motionPaused() || event.pointerType === 'touch') return;
pointerX = event.clientX;
pointerY = event.clientY;
if (pointerFrame) return;
pointerFrame = window.requestAnimationFrame(() => {
pointerFrame = 0;
const rect = illustration.getBoundingClientRect();
illustration.style.setProperty('--illustration-x', `${((pointerX - rect.left) / rect.width - .5) * 7}deg`);
illustration.style.setProperty('--illustration-y', `${((pointerY - rect.top) / rect.height - .5) * -5}deg`);
});
}, { passive: true });
illustration.addEventListener('pointerleave', () => {
if (pointerFrame) window.cancelAnimationFrame(pointerFrame);
pointerFrame = 0;
illustration.style.removeProperty('--illustration-x');
illustration.style.removeProperty('--illustration-y');
});
}
function bindHomeTopology() {
const home = document.getElementById('chimera-home-command-center');
if (!home) return;
home.classList.add('premium-topology-motion');
bind(home);
}
function dockImportLauncher() {
const launcher = document.getElementById('chimera-import-open');
const sidebarDonate = document.getElementById('sidebar-donate');
if (!launcher || !sidebarDonate) return false;
launcher.classList.add('chimera-import-launcher-docked');
sidebarDonate.after(launcher);
return true;
}
function ensureTrophyIllustration() {
const target = document.querySelector('.trophy-library-empty');
if (!target) return;
let illustration = target.querySelector('.premium-project-illustration');
if (!illustration) {
target.insertAdjacentHTML('afterbegin', illustrations.trophies);
illustration = target.querySelector('.premium-project-illustration');
}
bind(illustration);
}
for (const [id, variant] of Object.entries(targets)) {
const target = document.getElementById(id);
if (!target) continue;
const ensure = () => {
let illustration = target.querySelector('.premium-project-illustration');
if (!illustration) {
target.insertAdjacentHTML('afterbegin', illustrations[variant]);
illustration = target.querySelector('.premium-project-illustration');
}
bind(illustration);
};
ensure();
new MutationObserver(ensure).observe(target, { childList: true });
}
// Trophy content is rendered by a route module and the page node may be
// replaced while the feature is loading. Observe the stable content root so
// both a late render and a replacement page receive the illustration.
const trophyHost = document.getElementById('content') || document.body;
const touchesTrophyLibrary = mutation => {
const target = mutation.target?.nodeType === Node.ELEMENT_NODE ? mutation.target : mutation.target?.parentElement;
if (target?.matches?.('#page-trophies, #page-trophies *')) return true;
return [...mutation.addedNodes].some(node => node.nodeType === Node.ELEMENT_NODE && (
node.matches?.('#page-trophies, .trophy-library-empty')
|| node.querySelector?.('#page-trophies, .trophy-library-empty')
));
};
ensureTrophyIllustration();
new MutationObserver(mutations => {
if (mutations.some(touchesTrophyLibrary)) ensureTrophyIllustration();
}).observe(trophyHost, { childList: true, subtree: true });
bindHomeTopology();
if (!dockImportLauncher()) {
const importLauncherObserver = new MutationObserver(() => {
if (!dockImportLauncher()) return;
importLauncherObserver.disconnect();
});
importLauncherObserver.observe(document.body, { childList: true });
}
document.addEventListener('visibilitychange', syncAllMotion);
if (typeof reducedMotion.addEventListener === 'function') reducedMotion.addEventListener('change', syncAllMotion);
else reducedMotion.addListener(syncAllMotion);
})();