70 lines
4.5 KiB
TypeScript
70 lines
4.5 KiB
TypeScript
/** Decorative geometry uses the PNG's exact 2172×724 pixel coordinate system. */
|
||
export const heroMotionMarkup = (imageUrl: string): string => `
|
||
<div class="landing-scan-mask" style="mask-image:url('${imageUrl}');-webkit-mask-image:url('${imageUrl}')">
|
||
<div class="landing-terrain-grid"></div><div class="landing-scan-beam"></div>
|
||
</div>
|
||
<svg class="landing-model-motion" viewBox="0 0 2172 724" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
|
||
<defs>
|
||
<filter id="landing-energy-glow" x="-30%" y="-200%" width="160%" height="500%"><feGaussianBlur stdDeviation="3"/></filter>
|
||
<linearGradient id="landing-flow-fade"><stop offset="0" stop-color="#087fcb" stop-opacity="0"/><stop offset=".12" stop-color="#087fcb"/><stop offset=".88" stop-color="#087fcb"/><stop offset="1" stop-color="#087fcb" stop-opacity="0"/></linearGradient>
|
||
<!-- Trace only the clearly visible pale alignment. Fade the uncertain ends instead of inventing bridge geometry. -->
|
||
<path id="landing-alignment-path" pathLength="1000" d="M600 335 C650 341 700 367 750 368.5 S850 370 950 370.5 S1020 379 1050 382 S1120 398 1150 398 S1220 395 1250 395.5 S1320 396 1350 395 S1420 392 1450 390 S1520 384 1550 378.5 S1610 352 1650 350"/>
|
||
</defs>
|
||
<use href="#landing-alignment-path" class="landing-energy-track"/>
|
||
<use href="#landing-alignment-path" class="landing-energy-haze" filter="url(#landing-energy-glow)"/>
|
||
<use href="#landing-alignment-path" class="landing-energy-flow"/>
|
||
${[[546,331],[774,335],[996,344],[1238,346],[1478,334],[1745,332]].map(([x,y],i)=>`
|
||
<g class="landing-pier-beacon" style="--beacon-delay:${(i*.72+1.6).toFixed(2)}s" transform="translate(${x} ${y})">
|
||
<line x1="0" y1="-31" x2="0" y2="-8"/>
|
||
<circle class="landing-pier-pulse" r="11"/><circle class="landing-pier-point" r="3.5"/>
|
||
<circle class="landing-pier-spark" cx="8" cy="-15" r="1.5"/>
|
||
</g>`).join('')}
|
||
<path class="landing-hud-bracket" d="M460 260h-24v22 M1775 260h24v22 M460 485h-24v-22 M1775 485h24v-22"/>
|
||
</svg>`;
|
||
|
||
export function createHeroMotion(root: HTMLElement): void {
|
||
const hero = root.querySelector<HTMLElement>('.landing-hero');
|
||
if (!hero) return;
|
||
const reduced = matchMedia('(prefers-reduced-motion: reduce)');
|
||
const finePointer = matchMedia('(hover: hover) and (pointer: fine)');
|
||
let frame = 0;
|
||
let currentX = 0;
|
||
let currentY = 0;
|
||
let targetX = 0;
|
||
let targetY = 0;
|
||
const update = (): void => {
|
||
currentX += (targetX-currentX)*.075;
|
||
currentY += (targetY-currentY)*.075;
|
||
hero.style.setProperty('--landing-parallax-x', `${currentX.toFixed(2)}px`);
|
||
hero.style.setProperty('--landing-parallax-y', `${currentY.toFixed(2)}px`);
|
||
frame = Math.abs(targetX-currentX)+Math.abs(targetY-currentY)>.08 ? requestAnimationFrame(update) : 0;
|
||
};
|
||
const reset = (): void => {
|
||
targetX = targetY = currentX = currentY = 0;
|
||
cancelAnimationFrame(frame);frame=0;
|
||
hero.style.setProperty('--landing-parallax-x','0px');hero.style.setProperty('--landing-parallax-y','0px');
|
||
};
|
||
hero.addEventListener('pointermove', (event) => {
|
||
if (reduced.matches || !finePointer.matches || root.hidden || root.classList.contains('guide-open')) return;
|
||
const bounds=hero.getBoundingClientRect();
|
||
targetX=((event.clientX-bounds.left)/bounds.width-.5)*18;
|
||
targetY=((event.clientY-bounds.top)/bounds.height-.5)*12;
|
||
if (!frame) frame=requestAnimationFrame(update);
|
||
},{passive:true});
|
||
hero.addEventListener('pointerleave',reset,{passive:true});
|
||
reduced.addEventListener('change',()=>{if(reduced.matches)reset();});
|
||
document.addEventListener('visibilitychange',()=>{root.classList.toggle('landing-paused',document.hidden);if(document.hidden)reset();});
|
||
// Native import dialogs are outside the overlay. Pause all ambient work while
|
||
// an upload/guide panel is active or when the main application is showing.
|
||
const synchronize = (): void => {
|
||
const blocked=!!root.hidden||!!document.querySelector('#import-dialog[open]')||!!root.querySelector('.landing-panel.visible')||root.classList.contains('guide-open');
|
||
if (root.classList.contains('landing-inactive')!==blocked) root.classList.toggle('landing-inactive',blocked);
|
||
if (blocked)reset();
|
||
};
|
||
const observer=new MutationObserver(synchronize);
|
||
observer.observe(root,{attributes:true,subtree:true,attributeFilter:['hidden','class']});
|
||
const importDialog=document.querySelector('#import-dialog');
|
||
if(importDialog)observer.observe(importDialog,{attributes:true,attributeFilter:['open']});
|
||
synchronize();
|
||
}
|