import { heroCapture } from './assets/okseong-hero-capture';
const flowPath = (() => {
let connected = false;
return heroCapture.terrainFlow.map(point => {
if (point === null) { connected = false; return ''; }
const command = connected ? 'L' : 'M'; connected = true;
return `${command}${point.x} ${point.y}`;
}).join(' ');
})();
const frame = {
left: Math.min(...heroCapture.points.map(p=>p.x))-50,
right: Math.max(...heroCapture.points.map(p=>p.x))+50,
top: Math.min(...heroCapture.points.map(p=>p.y))-60,
bottom: Math.max(...heroCapture.points.map(p=>p.y))+70,
};
/** Markers share the exact camera projection used to capture the model. */
export const heroMotionMarkup = (): string => `
`;
export function createHeroMotion(root: HTMLElement): void {
const hero = root.querySelector('.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();
}