// Shared hooks, helpers, icons + Universe brand marks // requestAnimationFrame loop, returns elapsed seconds function useLoop() { const [t, setT] = React.useState(0); React.useEffect(() => { let raf, start = null; const step = (ts) => { if (start === null) start = ts; setT((ts - start) / 1000); raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, []); return t; } const ease = { linear: t => t, outQuad: t => t*(2-t), inOutQuad: t => t<0.5 ? 2*t*t : -1+(4-2*t)*t, outCubic: t => 1 - Math.pow(1-t,3), inOutCubic:t => t<0.5 ? 4*t*t*t : 1-Math.pow(-2*t+2,3)/2, outQuart: t => 1 - Math.pow(1-t,4), outQuint: t => 1 - Math.pow(1-t,5), inOutQuint:t => t<0.5 ? 16*t*t*t*t*t : 1-Math.pow(-2*t+2,5)/2, outBack: (t, s=1.70158) => 1 + (s+1)*Math.pow(t-1,3) + s*Math.pow(t-1,2), outExpo: t => t === 1 ? 1 : 1 - Math.pow(2, -10*t), inOutExpo: t => t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20*t-10)/2 : (2-Math.pow(2,-20*t+10))/2, }; const clamp = (v, mn=0, mx=1) => Math.max(mn, Math.min(mx, v)); const lerp = (a, b, t) => a + (b-a)*t; const mod = (a, b) => ((a%b)+b)%b; // hash router function useRoute() { const [route, setRoute] = React.useState(() => parseHash()); React.useEffect(() => { const onChange = () => setRoute(parseHash()); window.addEventListener('hashchange', onChange); return () => window.removeEventListener('hashchange', onChange); }, []); return [route, navigate]; } function parseHash() { const h = location.hash.replace(/^#\/?/, '') || 'splash'; const [path, ...rest] = h.split('/'); return { path, params: rest }; } function navigate(path) { location.hash = '/' + path; } function useRouteTransition(route) { const [phase, setPhase] = React.useState('in'); const [displayed, setDisplayed] = React.useState(route); const prevRef = React.useRef(route); React.useEffect(() => { if (route.path === prevRef.current.path && JSON.stringify(route.params) === JSON.stringify(prevRef.current.params)) return; setPhase('out'); const t = setTimeout(() => { setDisplayed(route); prevRef.current = route; setPhase('in'); }, 220); // matches the .transition-out 220ms fade return () => clearTimeout(t); }, [route]); return { route: displayed, phase }; } // Auto-fit the 2560x1600 design canvas into ANY viewport / device aspect ratio. // Full-bleed scale-to-contain: fills the limiting axis exactly (16:10 device = // perfect fill, zero bars) and centres on the other axis with a clean surround. // Never crops content. Re-measures on resize + orientation change. function useFitScale(W=2560, Hbase=1600, Hmax=1920) { const [state, setState] = React.useState({ scale: 1, tight: true, W, H: Hbase }); React.useEffect(() => { const measure = () => { const vw = window.innerWidth, vh = window.innerHeight; // ADAPTIVE design height. On viewports TALLER than the base 16:10 ratio // (e.g. iPad Pro 4:3) the canvas grows in height so screen content fills // the device instead of leaving top/bottom letterbox dead-space. Capped at // Hmax (4:3) and floored at Hbase so the primary 16:10 tablet (Tab S7) is // mathematically UNCHANGED (H stays 1600, scale stays width-bound). const H = Math.max(Hbase, Math.min(Hmax, Math.round(W * vh / vw))); const scale = Math.min(vw / W, vh / H); setState({ scale, tight: true, W, H }); // expose to screens that anchor to the live canvas height if (typeof document !== 'undefined') { document.documentElement.style.setProperty('--canvas-h', H + 'px'); document.documentElement.style.setProperty('--canvas-w', W + 'px'); } if (typeof window !== 'undefined') window.UNIVERSE_CANVAS = { W, H }; }; measure(); window.addEventListener('resize', measure); window.addEventListener('orientationchange', measure); // some tablets fire neither reliably on rotation — poll-guard via VisualViewport if (window.visualViewport) window.visualViewport.addEventListener('resize', measure); return () => { window.removeEventListener('resize', measure); window.removeEventListener('orientationchange', measure); if (window.visualViewport) window.visualViewport.removeEventListener('resize', measure); }; }, [W, Hbase, Hmax]); return state; } // ===== Icons ===== const Icons = { story: (props) => ( ), location: (props) => ( ), masterplan: (props) => ( ), residences: (props) => ( ), amenities: (props) => ( ), gallery: (props) => ( ), tools: (props) => ( ), booking: (props) => ( ), arrow: (props) => ( ), back: (props) => ( ), close: (props) => ( ), plus: (props) => , minus: (props) => , check: (props) => , filter:(props) => , }; // === The Universe — official client monogram (real image, transparent bg) === // `size` is the pixel height of the rendered logo. // `progress` (0-1) reveals the monogram top→bottom with a soft glow rise. // `color` is ignored — the image carries the brand gold. function UniverseMonogram({ size = 200, progress = 1, color }) { const p = clamp(progress); return (
);
}
window.UniverseLogoFull = UniverseLogoFull;
// "the UNIVERSE" wordmark — italic 'the' + wide-tracked uppercase serif
function UniverseWordmark({ size = 56, color = 'currentColor', tight = false }) {
return (