(function () { 'use strict'; var VERSION = 63; if (window.__homeImmersiveVersion === VERSION) return; window.__homeImmersiveVersion = VERSION; var CFG = window.HOME_IMMERSIVE_CONFIG || {}; var DESKTOP_MQ = CFG.desktopMq || '(min-width: 992px) and (hover: hover) and (pointer: fine)'; var WIDTH_MQ = CFG.widthMq || '(min-width: 992px)'; var SCOPE = CFG.scope || '.hero'; var CMS_SCOPE = CFG.cmsScope || '.features_hero'; var MODE_STORAGE_KEY = CFG.modeStorageKey || 'home-hero-view-mode'; var MODE_CAROUSEL = 'carousel'; var MODE_FULLSCREEN = 'fullscreen'; var THREE_CDN = CFG.threeCdn || 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js'; var GSAP_CDN = CFG.gsapCdn || 'https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/gsap.min.js'; var TILE_GAP = CFG.tileGap != null ? CFG.tileGap : 0.1; var TILE_HEIGHT = CFG.tileHeight != null ? CFG.tileHeight : 2.25; var TILE_WIDTH = CFG.tileWidth != null ? CFG.tileWidth : TILE_HEIGHT * (16 / 9); var TILE_UNIT = TILE_WIDTH + TILE_GAP; var CAM_FOV = CFG.cameraFov != null ? CFG.cameraFov : 50; var CAM_Z = CFG.cameraZ != null ? CFG.cameraZ : 8; var SCROLL_LERP = CFG.scrollLerp != null ? CFG.scrollLerp : 0.12; var DRAG_SENS = CFG.dragSens != null ? CFG.dragSens : 0.012; var INTRO_DUR = CFG.introDur != null ? CFG.introDur : 2.0; var FEATURES_HIDE_DUR = CFG.featuresHideDur != null ? CFG.featuresHideDur : INTRO_DUR * 0.5; var FEATURES_REVEAL_DUR = CFG.featuresRevealDur != null ? CFG.featuresRevealDur : INTRO_DUR * 0.52; var SIDES_REVEAL_DUR = CFG.sidesRevealDur != null ? CFG.sidesRevealDur : INTRO_DUR * 0.38; var ENTER_MORPH_END = CFG.enterMorphEnd != null ? CFG.enterMorphEnd : 0.78; var ENTER_SIDES_START = CFG.enterSidesStart != null ? CFG.enterSidesStart : 0.12; var ENTER_SIDES_END = CFG.enterSidesEnd != null ? CFG.enterSidesEnd : 0.62; var ENTER_SPREAD_START = CFG.enterSpreadStart != null ? CFG.enterSpreadStart : 0.4; var ENTER_HANDOFF_VIDEO_AT = CFG.enterHandoffVideoAt != null ? CFG.enterHandoffVideoAt : 0.58; var EXIT_SIDES_END = CFG.exitSidesEnd != null ? CFG.exitSidesEnd : 0.48; var EXIT_HANDOFF_AT = CFG.exitHandoffAt != null ? CFG.exitHandoffAt : 0.38; var EXIT_MORPH_START = CFG.exitMorphStart != null ? CFG.exitMorphStart : 0.36; var EXIT_SIDES_FADE_END = CFG.exitSidesFadeEnd != null ? CFG.exitSidesFadeEnd : 0.62; var BG_COLOR = CFG.bgColor != null ? CFG.bgColor : 0x050505; var DRAG_LERP = CFG.dragLerp != null ? CFG.dragLerp : 0.1; var WARP_LERP = CFG.warpLerp != null ? CFG.warpLerp : 0.07; var WARP_VEL_LERP = CFG.warpVelLerp != null ? CFG.warpVelLerp : 0.16; var HOLD_MS = CFG.holdMs != null ? CFG.holdMs : 650; var HOLD_MOVE_PX = CFG.holdMovePx != null ? CFG.holdMovePx : 14; var HOLD_RING_LEN = 175.929; var CAPTION_POS_LERP = CFG.captionPosLerp != null ? CFG.captionPosLerp : 0.16; var CAPTION_POS_LERP_DRAG = CFG.captionPosLerpDrag != null ? CFG.captionPosLerpDrag : 0.28; var CAPTION_POS_LERP_INTRO = CFG.captionPosLerpIntro != null ? CFG.captionPosLerpIntro : 0.24; var CAPTION_TEXT_LERP = CFG.captionTextLerp != null ? CFG.captionTextLerp : 0.16; var CAPTION_TEXT_TRAVEL = CFG.captionTextTravel != null ? CFG.captionTextTravel : 14; var TILE_VERT = [ 'uniform float uWarpIntensity;', 'uniform float uViewportWidth;', 'uniform float uTileHalfH;', 'varying vec2 vUv;', 'void main() {', ' vUv = uv;', ' vec4 worldPos = modelMatrix * vec4(position, 1.0);', ' float screenX = worldPos.x / (uViewportWidth * 0.5);', ' float screenXClamped = clamp(screenX, -1.0, 1.0);', ' float rollCurve = exp(-screenXClamped * screenXClamped * 6.0);', ' float edgeCurve = screenXClamped * screenXClamped;', ' float edgeBend = edgeCurve * uWarpIntensity * -0.4;', ' float zRoll = rollCurve * uWarpIntensity * 0.8 + edgeBend;', ' float scaleY = 1.0 + rollCurve * uWarpIntensity * 0.15;', ' float yNorm = position.y / max(uTileHalfH, 0.001);', ' float bowCurve = 1.0 - yNorm * yNorm;', ' float bow = bowCurve * rollCurve * uWarpIntensity * 0.15;', ' vec3 p = position;', ' p.y *= scaleY;', ' p.x += bow;', ' p.z += zRoll;', ' gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);', '}' ].join('\n'); var TILE_FRAG = [ 'uniform sampler2D uVideo;', 'uniform vec2 uCoverScale;', 'uniform vec2 uCoverOffset;', 'uniform float uGray;', 'uniform float uIntroZoom;', 'uniform float uOpacity;', 'varying vec2 vUv;', 'void main() {', ' vec2 centeredUv = vUv - 0.5;', ' centeredUv /= max(uIntroZoom, 0.001);', ' centeredUv += 0.5;', ' vec2 uv = (centeredUv - 0.5) * uCoverScale + 0.5 + uCoverOffset;', ' vec4 col = texture2D(uVideo, uv);', ' float luma = dot(col.rgb, vec3(0.299, 0.587, 0.114));', ' col.rgb = mix(col.rgb, vec3(luma), clamp(uGray, 0.0, 1.0));', ' col.a *= clamp(uOpacity, 0.0, 1.0);', ' gl_FragColor = col;', '}' ].join('\n'); var desktopMq = window.matchMedia(DESKTOP_MQ); var widthMq = window.matchMedia(WIDTH_MQ); var reduceMq = window.matchMedia('(prefers-reduced-motion: reduce)'); function isHomePage() { var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/'; return path === '/' || path === '/index.html'; } function canShowHoldUi() { if (CFG.enabled === false) return false; if (!isHomePage()) return false; if (!widthMq.matches) return false; return true; } function canRunImmersive() { return canShowHoldUi(); } function readSavedMode() { try { var mode = localStorage.getItem(MODE_STORAGE_KEY); return mode === MODE_CAROUSEL ? MODE_CAROUSEL : MODE_FULLSCREEN; } catch (err) { return MODE_FULLSCREEN; } } function saveMode(mode) { try { localStorage.setItem(MODE_STORAGE_KEY, mode === MODE_CAROUSEL ? MODE_CAROUSEL : MODE_FULLSCREEN); } catch (err) {} } function canRestoreCarouselMode() { if (!canRunImmersive()) return false; if (reduceMq.matches) return false; return readSavedMode() === MODE_CAROUSEL; } function applyCarouselRestorePending() { if (!canRestoreCarouselMode()) return; document.documentElement.classList.add('is-home-carousel-restore'); document.documentElement.style.setProperty('--hero-list-reveal', '0'); document.documentElement.style.setProperty('--hero-overlay-reveal', '0'); } function clearCarouselRestorePending() { document.documentElement.classList.remove('is-home-carousel-restore'); } function updatePageModeFlags() { if (canShowHoldUi()) { document.documentElement.classList.remove('home-immersive-off'); } else { document.documentElement.classList.add('home-immersive-off'); } } function injectHoldUiStyles() { if (document.getElementById('home-immersive-hold-inline')) return; var style = document.createElement('style'); style.id = 'home-immersive-hold-inline'; style.textContent = '.home-mode-hold{--hold-p:0;--glyph-morph:0;position:fixed!important;right:0.75rem!important;bottom:0.75rem!important;z-index:10050!important;display:inline-flex!important;flex-direction:row;align-items:center;gap:0.75rem;padding:0;margin:0;border:0;background:transparent;cursor:pointer;visibility:visible!important;opacity:1!important;pointer-events:auto}' + '.home-mode-hold.is-busy{pointer-events:none}' + '.home-mode-hold-label{font:400 11px/1.2 "Fragment Mono","SFMono-Regular",ui-monospace,monospace;letter-spacing:0.04em;text-transform:uppercase;color:rgba(255,255,255,0.92);white-space:nowrap;user-select:none;text-shadow:0 1px 8px rgba(0,0,0,0.55)}' + '.home-mode-hold-dot{position:relative;flex:0 0 auto;width:56px;height:56px}' + '.home-mode-hold-ring{display:block;width:56px;height:56px;transform:rotate(-90deg)}' + '.home-mode-hold-ring circle{fill:none;stroke:rgba(255,255,255,0.28);stroke-width:1}' + '.home-mode-hold-ring .home-mode-hold-progress{stroke:rgba(255,255,255,0.92);stroke-width:1;stroke-linecap:round;stroke-dasharray:175.929;stroke-dashoffset:175.929}' + '.home-mode-hold-glyphs{--m:var(--glyph-morph,0);--spread:11px;--tight:calc(var(--spread)*(1 - var(--m)*0.52));position:absolute;left:50%;top:50%;width:22px;height:22px;margin:-11px 0 0 -11px;transform:rotate(calc(var(--m)*90deg));pointer-events:none}' + '.home-mode-hold-glyph{position:absolute;left:50%;top:50%;width:4px;height:4px;margin:-2px 0 0 -2px;border-radius:50%;background:rgba(255,255,255,0.9)}' + '.home-mode-hold-glyph--a{transform:translate(calc(-1*var(--tight)),0)}' + '.home-mode-hold-glyph--b{opacity:calc(1 - var(--m));transform:translate(0,0)}' + '.home-mode-hold-glyph--c{transform:translate(var(--tight),0)}' + '@media(max-width:991px){.home-mode-hold{display:none!important}}'; (document.head || document.documentElement).appendChild(style); } updatePageModeFlags(); applyCarouselRestorePending(); function norm(s) { return (s || '').replace(/\s+/g, ' ').trim().toLowerCase(); } function slugFromName(name) { return norm(name).replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); } function getLineName(el) { var n = el && el.querySelector('.name'); return norm(n ? n.textContent : el ? el.textContent : ''); } function getItemName(item) { var n = item && item.querySelector('.name'); if (n) return norm(n.textContent); var videos = item ? item.querySelectorAll('video') : []; for (var i = 0; i < videos.length; i++) { var label = norm(videos[i].textContent); if (label) return label; } return ''; } function getVideo(item) { if (!item) return null; var desktop = item.querySelector('video.desktop'); var mobile = item.querySelector('video.mobile'); if (desktop && mobile) { return getComputedStyle(desktop).display !== 'none' ? desktop : mobile; } return item.querySelector('video'); } function playVideoEl(video) { if (!video) return; video.muted = true; video.playsInline = true; if (!video.paused && video.readyState >= 2) return; if (typeof window.playPortfolioHeroVideo === 'function' && video.closest('.cms_background .cms_item')) { window.playPortfolioHeroVideo(video); return; } if (typeof window.startPortfolioVideo === 'function') { window.startPortfolioVideo(video); return; } video.play().catch(function () {}); } function pauseVideoEl(video) { if (video && !video.paused) video.pause(); } function buildProjects() { if (window.__portfolioPairs && window.__portfolioPairs.length) { return window.__portfolioPairs.map(function (pair, index) { return projectFromPair(pair, index); }).filter(Boolean); } var scope = document.querySelector(SCOPE) || document.querySelector(CMS_SCOPE) || document; var lineEls = Array.prototype.slice.call( scope.querySelectorAll('.list_projects .line_features, .cms_features .line_features, .line_features') ); var items = Array.prototype.slice.call( document.querySelectorAll('.background_cases .cms_background .cms_item') ); var map = {}; var pairs = []; items.forEach(function (item) { var name = getItemName(item); if (name) map[name] = item; }); lineEls.forEach(function (line) { var row = line.closest('.cms_line') || line; var item = map[getLineName(line)]; if (!item) return; pairs.push({ line: line, item: item, row: row }); }); if (!pairs.length) { var count = Math.min(lineEls.length, items.length); for (var i = 0; i < count; i++) { var ln = lineEls[i]; pairs.push({ line: ln, item: items[i], row: ln.closest('.cms_line') || ln }); } } return pairs.map(function (pair, index) { return projectFromPair(pair, index); }).filter(Boolean); } function queryProjectMeta(row, line, selectors) { var scopes = []; if (row) scopes.push(row); if (line && line !== row) scopes.push(line); for (var si = 0; si < scopes.length; si++) { var scope = scopes[si]; for (var ai = 0; ai < selectors.length; ai++) { var sel = selectors[ai]; var el = scope.querySelector(sel) || scope.querySelector('.lf-layer ' + sel) || scope.querySelector('.lf-layer--white ' + sel); if (el) return el; } } return null; } function projectFromPair(pair, index) { var row = pair.row || pair.line; var line = pair.line || row; var item = pair.item; if (!row || !item) return null; var nameEl = queryProjectMeta(row, line, ['.name']); var industryEl = queryProjectMeta(row, line, ['.industry', '.description', '.tag', '.category']); var yearEl = queryProjectMeta(row, line, ['.year']); var name = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : ''; var industry = industryEl ? industryEl.textContent.replace(/\s+/g, ' ').trim() : ''; var year = yearEl ? yearEl.textContent.replace(/\s+/g, ' ').trim() : ''; var slug = slugFromName(name); return { index: index, name: name, industry: industry, year: year, slug: slug, href: slug ? '/work/' + slug : '', item: item, row: row, line: line, video: getVideo(item) }; } function getActiveProjectIndex(projects) { var active = document.querySelector('.background_cases .cms_item.is-active'); if (!active) return 0; for (var i = 0; i < projects.length; i++) { if (projects[i].item === active) return i; } return 0; } function isBlockedTarget(el) { if (!el || !el.closest) return true; return !!el.closest( 'a, button, input, textarea, select, .fixed_menu, .bottom_fixed, .menu_expanded, .expand_btn_primary, .home-mode-hold' ); } function loadThree() { if (typeof THREE !== 'undefined') return Promise.resolve(); if (loadThree.promise) return loadThree.promise; loadThree.promise = new Promise(function (resolve, reject) { var s = document.createElement('script'); s.src = THREE_CDN; s.async = true; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); return loadThree.promise; } function loadGsap() { if (typeof window.gsap !== 'undefined') return Promise.resolve(window.gsap); if (loadGsap.promise) return loadGsap.promise; loadGsap.promise = new Promise(function (resolve, reject) { var s = document.createElement('script'); s.src = GSAP_CDN; s.async = true; s.onload = function () { if (window.gsap) resolve(window.gsap); else reject(new Error('GSAP failed to load')); }; s.onerror = function () { reject(new Error('GSAP script error')); }; document.head.appendChild(s); }); return loadGsap.promise; } var projects = []; var scopeEl = null; var immersiveRoot = null; var modeHoldEl = null; var holdProgressEl = null; var metaNameEl = null; var metaYearEl = null; var metaSubEl = null; var metaCaptionEl = null; var metaNameLayers = null; var metaYearLayers = null; var metaSubLayers = null; var featuresHeroEl = null; var captionVecA = null; var captionVecB = null; var captionVecC = null; var hold = { active: false, startedAt: 0, originX: 0, originY: 0, raf: 0, glyphLocked: null, introGlyphDone: false }; var modeHoldBound = false; var immersiveBooted = false; var immersive = { active: false, entering: false, exiting: false, renderer: null, scene: null, camera: null, track: null, tiles: [], tileW: TILE_WIDTH, tileH: TILE_HEIGHT, viewportWidth: 20, raf: 0, selectedIndex: 0, lastSyncedCmsIndex: -1, playbackIndex: -1, videoTextures: null, scrollX: 0, targetScrollX: 0, lastScrollX: 0, scrollVel: 0, warpIntensity: 0, dragging: false, dragX: 0, dragScrollStart: 0, dragMoved: false, dragVel: 0, dragLastX: 0, dragLastT: 0, intro: null, skipEnterIntro: false, restoreEnter: false, introZoom: 1, shellProgress: 0, heroRect: null, loopWidth: 0, eventsBound: false, casesEl: null, casesMorphOrigin: null, casesFromRect: null, casesToRect: null, casesHandoffDone: false, casesExitHandoff: false, casesExitTileRect: null, casesPin: null, casesHandoffFinalized: false, handoffVideoSynced: false, enterMorphLocked: false, enterMorphFrom: null, enterMorphTo: null, enterMorphAspect: 0, featuresTween: null, featuresRevealStarted: false, featuresPin: null, caption: { left: 50, top: 85, width: 40, textLayer: 0, textBlend: 1, textIndex: -1, swapFrom: null, swapTo: null, swapping: false, swapFreezeLeft: 50, swapFreezeTop: 85, swapFreezeWidth: 40, anchorLeft: 50, anchorTop: 85, anchorWidth: 40, layoutDirty: true, captionTween: null, enterFaded: false } }; function getFeaturesHeroEl() { if (featuresHeroEl && featuresHeroEl.isConnected) return featuresHeroEl; featuresHeroEl = document.querySelector(CMS_SCOPE); return featuresHeroEl; } function captureFeaturesHeroRect() { var el = getFeaturesHeroEl(); if (!el) return null; var r = el.getBoundingClientRect(); if (r.width < 1 || r.height < 1) return null; return { left: r.left, top: r.top, width: r.width, height: r.height }; } function applyFeaturesPinnedLayout(rect) { var el = getFeaturesHeroEl(); if (!el || !rect) return; el.style.position = 'fixed'; el.style.left = rect.left + 'px'; el.style.top = rect.top + 'px'; el.style.width = rect.width + 'px'; el.style.height = rect.height + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; el.style.margin = '0'; el.style.boxSizing = 'border-box'; el.style.zIndex = '170'; el.classList.add('is-home-features-pinned'); document.documentElement.classList.add('is-home-features-pinned'); } function pinFeaturesHeroForTransition() { var el = getFeaturesHeroEl(); if (!el) return false; if (el.classList.contains('is-home-features-pinned') && el.parentNode === document.body) { if (immersive.featuresPin && immersive.featuresPin.rect) { applyFeaturesPinnedLayout(immersive.featuresPin.rect); } return true; } var rect = captureFeaturesHeroRect(); if (!rect) return false; if (!immersive.featuresPin) { immersive.featuresPin = { placeholder: document.createComment('home-features-pin'), parent: null, rect: null }; } var pin = immersive.featuresPin; pin.parent = el.parentNode; pin.parent.insertBefore(pin.placeholder, el); pin.rect = rect; document.body.appendChild(el); applyFeaturesPinnedLayout(rect); return true; } function restoreFeaturesHeroFromPin() { var el = getFeaturesHeroEl(); if (!el || !immersive.featuresPin || !immersive.featuresPin.parent) return; if (el.parentNode !== document.body) return; var pin = immersive.featuresPin; pin.parent.insertBefore(el, pin.placeholder); pin.placeholder.remove(); pin.parent = null; pin.rect = null; el.classList.remove('is-home-features-pinned'); document.documentElement.classList.remove('is-home-features-pinned'); el.style.removeProperty('position'); el.style.removeProperty('left'); el.style.removeProperty('top'); el.style.removeProperty('width'); el.style.removeProperty('height'); el.style.removeProperty('right'); el.style.removeProperty('bottom'); el.style.removeProperty('margin'); el.style.removeProperty('box-sizing'); el.style.removeProperty('z-index'); } function killFeaturesTween() { if (!immersive.featuresTween) return; if (typeof window.gsap !== 'undefined' && immersive.featuresTween.kill) { immersive.featuresTween.kill(); } immersive.featuresTween = null; } function setFeaturesHeroHidden(instant) { var el = getFeaturesHeroEl(); if (!el) return; document.documentElement.classList.add('is-home-features-hidden'); if (immersive.featuresTween && !instant) return; killFeaturesTween(); if (typeof window.gsap !== 'undefined') { window.gsap.killTweensOf(el); window.gsap.set(el, { autoAlpha: 0, y: instant ? 12 : 0, pointerEvents: 'none' }); return; } el.style.opacity = '0'; el.style.visibility = 'hidden'; el.style.pointerEvents = 'none'; } function resetFeaturesHeroVisible() { var el = getFeaturesHeroEl(); if (!el) return; killFeaturesTween(); document.documentElement.classList.remove('is-home-features-hidden'); if (typeof window.gsap !== 'undefined') { window.gsap.killTweensOf(el); window.gsap.set(el, { clearProps: 'transform,opacity,visibility,pointerEvents' }); return; } el.style.removeProperty('opacity'); el.style.removeProperty('transform'); el.style.removeProperty('visibility'); el.style.removeProperty('pointer-events'); } function animateFeaturesHeroHide(introStartTime) { var el = getFeaturesHeroEl(); if (!el) return Promise.resolve(); document.documentElement.classList.add('is-home-features-hidden'); immersive.featuresRevealStarted = false; return loadGsap().then(function (gsap) { killFeaturesTween(); gsap.killTweensOf(el); var elapsed = introStartTime ? (performance.now() - introStartTime) / 1000 : 0; var duration = Math.max(0.15, FEATURES_HIDE_DUR - elapsed); immersive.featuresTween = gsap.to(el, { autoAlpha: 0, duration: duration, ease: 'power2.inOut', overwrite: 'auto', onComplete: function () { gsap.set(el, { pointerEvents: 'none' }); immersive.featuresTween = null; } }); }).catch(function () { el.style.opacity = '0'; el.style.visibility = 'hidden'; el.style.pointerEvents = 'none'; }); } function animateFeaturesHeroReveal() { var el = getFeaturesHeroEl(); if (!el || immersive.featuresRevealStarted) return Promise.resolve(); immersive.featuresRevealStarted = true; return loadGsap().then(function (gsap) { killFeaturesTween(); gsap.killTweensOf(el); immersive.featuresTween = gsap.fromTo(el, { autoAlpha: 0 }, { autoAlpha: 1, duration: FEATURES_REVEAL_DUR, ease: 'power2.out', overwrite: 'auto', onComplete: function () { gsap.set(el, { clearProps: 'transform,opacity,visibility,pointerEvents' }); document.documentElement.classList.remove('is-home-features-hidden'); immersive.featuresTween = null; } }); }).catch(function () { el.style.opacity = '1'; el.style.visibility = 'visible'; el.style.transform = ''; el.style.pointerEvents = ''; document.documentElement.classList.remove('is-home-features-hidden'); }); } function makeCubicBezier(x1, y1, x2, y2) { var ax = 3 * x1 - 3 * x2 + 1; var bx = 3 * x2 - 6 * x1; var cx = 3 * x1; var ay = 3 * y1 - 3 * y2 + 1; var by = 3 * y2 - 6 * y1; var cy = 3 * y1; function sampleCurveX(t) { return ((ax * t + bx) * t + cx) * t; } function sampleCurveY(t) { return ((ay * t + by) * t + cy) * t; } function sampleCurveDX(t) { return (3 * ax * t + 2 * bx) * t + cx; } function solveCurveX(x) { var t2 = x; var i; for (i = 0; i < 8; i++) { var dx = sampleCurveX(t2) - x; if (Math.abs(dx) < 1e-7) return t2; var d = sampleCurveDX(t2); if (Math.abs(d) < 1e-6) break; t2 -= dx / d; } var t0 = 0; var t1 = 1; t2 = x; while (t0 < t1) { var sx = sampleCurveX(t2); if (Math.abs(sx - x) < 1e-7) return t2; if (x > sx) t0 = t2; else t1 = t2; t2 = (t0 + t1) * 0.5; } return t2; } return function (t) { if (t <= 0) return 0; if (t >= 1) return 1; return sampleCurveY(solveCurveX(t)); }; } var EASE_LOCO = makeCubicBezier(0.22, 1, 0.36, 1); function smoothRange(t, start, end) { return EASE_LOCO(clamp((t - start) / Math.max(0.0001, end - start), 0, 1)); } function applyFeaturesHeroEnterProgress(rawP) { var el = getFeaturesHeroEl(); if (!el) return; killFeaturesTween(); pinFeaturesHeroForTransition(); var hideP = immersive.restoreEnter ? 1 : smoothRange(rawP, 0, 0.34); var op = 1 - hideP; el.style.opacity = String(op); el.style.visibility = op < 0.02 ? 'hidden' : 'visible'; el.style.transform = ''; el.style.pointerEvents = 'none'; if (hideP > 0.02) { document.documentElement.classList.add('is-home-features-hidden'); } } function applyFeaturesHeroExitProgress(rawP) { var el = getFeaturesHeroEl(); if (!el) return; killFeaturesTween(); pinFeaturesHeroForTransition(); var revealP = smoothRange(rawP, 0.48, 0.98); el.style.opacity = String(revealP); el.style.visibility = revealP < 0.02 ? 'hidden' : 'visible'; el.style.transform = ''; el.style.pointerEvents = revealP > 0.55 ? 'auto' : 'none'; if (revealP > 0.98) { document.documentElement.classList.remove('is-home-features-hidden'); } } function clearFeaturesHeroInlineStyles() { restoreFeaturesHeroFromPin(); var el = getFeaturesHeroEl(); if (!el) return; el.style.removeProperty('opacity'); el.style.removeProperty('visibility'); el.style.removeProperty('transform'); el.style.removeProperty('pointer-events'); document.documentElement.classList.remove('is-home-features-hidden'); } function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } function lerp(a, b, t) { return a + (b - a) * t; } function cubicEaseInOut(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } function visibleWorldHeight(fovDeg, z) { return 2 * Math.tan((fovDeg * Math.PI / 180) / 2) * z; } function updateHoldGlyphMorph(holdP) { if (!modeHoldEl) return; if (hold.glyphLocked != null && !hold.active) { modeHoldEl.style.setProperty('--glyph-morph', String(hold.glyphLocked)); return; } var carousel = !!immersive.active; var morph; if (hold.active && holdP != null) { morph = carousel ? (1 - holdP) : holdP; } else { morph = carousel ? 1 : 0; } modeHoldEl.style.setProperty('--glyph-morph', String(clamp(morph, 0, 1))); } function updateHoldGlyphFromIntro(rawP, closing) { if (!modeHoldEl || hold.active) return; if (hold.glyphLocked != null) { modeHoldEl.style.setProperty('--glyph-morph', String(hold.glyphLocked)); return; } if (hold.introGlyphDone) return; if (immersive.restoreEnter && !closing) { modeHoldEl.style.setProperty('--glyph-morph', '1'); return; } var morph = closing ? (1 - rawP) : rawP; modeHoldEl.style.setProperty('--glyph-morph', String(clamp(morph, 0, 1))); } function updateModeHoldState() { if (!modeHoldEl) return; var busy = immersive.entering || immersive.exiting || (immersive.intro && immersive.intro.active); modeHoldEl.classList.toggle('is-busy', !!busy); modeHoldEl.classList.toggle('is-carousel-mode', !!immersive.active); updateHoldGlyphMorph(null); } function ensureModeHoldUi() { if (!canShowHoldUi()) return null; injectHoldUiStyles(); if ( modeHoldEl && (!modeHoldEl.querySelector('.home-mode-hold-glyphs') || !modeHoldEl.querySelector('.home-mode-hold-label')) ) { if (modeHoldEl.parentNode) modeHoldEl.parentNode.removeChild(modeHoldEl); modeHoldEl = null; holdProgressEl = null; } if (modeHoldEl) return modeHoldEl; modeHoldEl = document.createElement('button'); modeHoldEl.type = 'button'; modeHoldEl.className = 'home-mode-hold'; modeHoldEl.setAttribute('aria-label', 'Click and hold to switch view mode'); modeHoldEl.innerHTML = 'Click and Hold' + '' + '' + '' + ''; holdProgressEl = modeHoldEl.querySelector('.home-mode-hold-progress'); var mount = document.body || document.documentElement; if (mount && !modeHoldEl.parentNode) mount.appendChild(modeHoldEl); bindModeHold(); updateModeHoldState(); return modeHoldEl; } function setHoldProgress(p) { if (!holdProgressEl) return; var clamped = clamp(p, 0, 1); holdProgressEl.style.strokeDashoffset = String(HOLD_RING_LEN * (1 - clamped)); modeHoldEl.style.setProperty('--hold-p', String(clamped)); updateHoldGlyphMorph(clamped); } function cancelHold() { if (!hold.active) return; hold.active = false; hold.startedAt = 0; hold.glyphLocked = null; if (hold.raf) { cancelAnimationFrame(hold.raf); hold.raf = 0; } if (modeHoldEl) modeHoldEl.classList.remove('is-holding'); setHoldProgress(0); } function finishHoldSwitch() { var targetMorph = immersive.active ? 0 : 1; hold.glyphLocked = targetMorph; hold.introGlyphDone = true; hold.active = false; hold.startedAt = 0; if (hold.raf) { cancelAnimationFrame(hold.raf); hold.raf = 0; } if (modeHoldEl) { modeHoldEl.classList.remove('is-holding'); modeHoldEl.style.setProperty('--glyph-morph', String(targetMorph)); } if (holdProgressEl) holdProgressEl.style.strokeDashoffset = String(HOLD_RING_LEN); if (modeHoldEl) modeHoldEl.style.setProperty('--hold-p', '0'); } function completeHoldSwitch() { finishHoldSwitch(); if (immersive.active) { saveMode(MODE_FULLSCREEN); exitImmersive(); return; } saveMode(MODE_CAROUSEL); enterImmersive(); } function tickHold() { if (!hold.active) { hold.raf = 0; return; } var p = (performance.now() - hold.startedAt) / HOLD_MS; setHoldProgress(p); if (p >= 1) { hold.active = false; hold.raf = 0; completeHoldSwitch(); return; } hold.raf = requestAnimationFrame(tickHold); } function startHold(e) { if (immersive.entering || immersive.exiting) return; if (immersive.intro && immersive.intro.active) return; ensureModeHoldUi(); hold.active = true; hold.startedAt = performance.now(); hold.originX = e.clientX; hold.originY = e.clientY; modeHoldEl.classList.add('is-holding'); if (!hold.raf) hold.raf = requestAnimationFrame(tickHold); } function onModeHoldMove(e) { if (!hold.active) return; var dx = e.clientX - hold.originX; var dy = e.clientY - hold.originY; if (dx * dx + dy * dy > HOLD_MOVE_PX * HOLD_MOVE_PX) cancelHold(); } function bindModeHold() { if (modeHoldBound || !modeHoldEl) return; modeHoldBound = true; modeHoldEl.addEventListener('pointerdown', function (e) { e.preventDefault(); startHold(e); }); modeHoldEl.addEventListener('pointermove', onModeHoldMove); modeHoldEl.addEventListener('pointerup', cancelHold); modeHoldEl.addEventListener('pointercancel', cancelHold); modeHoldEl.addEventListener('pointerleave', cancelHold); } function scrollForRawIndex(rawIndex) { return (-rawIndex + (projects.length - 1) / 2) * TILE_UNIT; } function rawIndexFromScroll(scrollX) { return Math.round(-scrollX / TILE_UNIT + (projects.length - 1) / 2); } function logicalIndexFromRaw(rawIndex) { var count = projects.length; if (!count) return 0; return ((rawIndex % count) + count) % count; } function scrollBounds() { var count = projects.length; if (count <= 1) { var s = count ? scrollForRawIndex(0) : 0; return { min: s, max: s }; } return { min: scrollForRawIndex(count - 1), max: scrollForRawIndex(0) }; } function clampScrollValue(scrollX) { var b = scrollBounds(); return clamp(scrollX, b.min, b.max); } function indexFromScroll(scrollX) { var count = projects.length; if (!count) return 0; return clamp(rawIndexFromScroll(scrollX), 0, count - 1); } function setSelectedIndex(index) { var count = projects.length; if (!count) return; var next = clamp(index, 0, count - 1); if (next === immersive.selectedIndex && Math.abs(immersive.targetScrollX - scrollForRawIndex(next)) < 0.001) { return; } immersive.selectedIndex = next; immersive.targetScrollX = scrollForRawIndex(next); syncCmsIfCarouselSettled(); } function syncCmsIfCarouselSettled() { if (!immersive.active || (immersive.intro && immersive.intro.active)) return; if (!immersive.casesHandoffDone) return; if (immersive.lastSyncedCmsIndex === immersive.selectedIndex) return; immersive.lastSyncedCmsIndex = immersive.selectedIndex; syncCmsToProject(projects[immersive.selectedIndex], { skipAutoplayUi: true }); } function setHeroListReveal(p) { document.documentElement.style.setProperty('--hero-list-reveal', String(clamp(p, 0, 1))); } function setHeroOverlayReveal(p) { document.documentElement.style.setProperty('--hero-overlay-reveal', String(clamp(p, 0, 1))); } function setHeroUiReveal(listP, overlayP) { setHeroListReveal(listP); setHeroOverlayReveal(overlayP != null ? overlayP : listP); } function captureHeroRect() { var hero = scopeEl || document.querySelector(SCOPE); if (hero) { var r = hero.getBoundingClientRect(); immersive.heroRect = { left: r.left, top: r.top, width: r.width, height: r.height }; return; } immersive.heroRect = { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }; } function captureCasesFromRect() { var bg = document.querySelector('.background_cases'); if (!bg) { captureHeroRect(); immersive.casesFromRect = immersive.heroRect; return; } var r = bg.getBoundingClientRect(); immersive.casesFromRect = { left: r.left, top: r.top, width: r.width, height: r.height }; } function copyRect(r) { return { left: r.left, top: r.top, width: r.width, height: r.height }; } function tileAspect() { var w = immersive.tileW || TILE_WIDTH; var h = immersive.tileH || TILE_HEIGHT; return w / Math.max(h, 0.001); } function cropRectToAspect(rect, aspect) { if (!rect || !aspect || aspect <= 0) return rect; var w = rect.width; var h = rect.height; if (w <= 0 || h <= 0) return rect; var rectAspect = w / h; var outW; var outH; if (rectAspect > aspect) { outH = h; outW = h * aspect; } else { outW = w; outH = w / aspect; } return { left: rect.left + (w - outW) * 0.5, top: rect.top + (h - outH) * 0.5, width: outW, height: outH }; } function snapRectToAspect(rect, aspect) { if (!rect || !aspect || aspect <= 0) return rect; var cx = rect.left + rect.width * 0.5; var cy = rect.top + rect.height * 0.5; var w = Math.max(8, rect.width); var h = w / aspect; return { left: cx - w * 0.5, top: cy - h * 0.5, width: w, height: h }; } function clearEnterMorphRects() { immersive.enterMorphLocked = false; immersive.enterMorphFrom = null; immersive.enterMorphTo = null; immersive.enterMorphAspect = 0; } function lockEnterMorphRects(activeIdx, count) { if (immersive.enterMorphLocked) return; prepareActiveTileForProjection(activeIdx, count); var to = projectActiveTileScreenRect(); if (!to) { prepareCasesTargetRect(); to = immersive.casesToRect || computeCarouselCenterTileRect(); } if (!to) return; var fromSource = immersive.casesMorphOrigin || immersive.casesFromRect; if (!fromSource) return; immersive.enterMorphFrom = copyRect(fromSource); immersive.enterMorphTo = copyRect(to); immersive.enterMorphAspect = to.width / Math.max(to.height, 1); immersive.enterMorphLocked = true; } function morphEnterRects(activeIdx, count) { if (activeIdx != null && count != null) { lockEnterMorphRects(activeIdx, count); } if (immersive.enterMorphLocked && immersive.enterMorphFrom && immersive.enterMorphTo) { return { from: immersive.enterMorphFrom, to: immersive.enterMorphTo }; } var fromSource = immersive.casesMorphOrigin || immersive.casesFromRect; var to = projectActiveTileScreenRect(); if (!to) { prepareCasesTargetRect(); to = immersive.casesToRect || computeCarouselCenterTileRect(); } if (!to) return { from: null, to: null }; var from = fromSource ? copyRect(fromSource) : null; return { from: from, to: to }; } function applyCasesMorphLayout(rect) { var el = immersive.casesEl; if (!el || !rect) return; el.style.position = 'fixed'; el.style.left = rect.left + 'px'; el.style.top = rect.top + 'px'; el.style.width = rect.width + 'px'; el.style.height = rect.height + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; el.style.margin = '0'; el.style.zIndex = '160'; el.style.overflow = 'hidden'; el.style.transformOrigin = 'center center'; el.style.transform = 'none'; el.style.visibility = 'visible'; el.style.pointerEvents = 'none'; } function applyCasesMorphTransform(from, to, t) { var el = immersive.casesEl; if (!el || !from || !to) return; if (t <= 0) { applyCasesMorphLayout(from); return; } if (t >= 1) { applyCasesMorphLayout(to); return; } applyCasesMorphLayout({ left: lerp(from.left, to.left, t), top: lerp(from.top, to.top, t), width: lerp(from.width, to.width, t), height: lerp(from.height, to.height, t) }); } function applyCasesMorph(from, to, t) { applyCasesMorphTransform(from, to, t); } function projectWorldTileRect(cx, cy, cz, tileW, tileH) { if (!immersive.renderer || !immersive.camera || typeof THREE === 'undefined') return null; if (!captionVecA) { captionVecA = new THREE.Vector3(); captionVecB = new THREE.Vector3(); captionVecC = new THREE.Vector3(); } var pts = [ [cx - tileW / 2, cy + tileH / 2, cz], [cx + tileW / 2, cy + tileH / 2, cz], [cx - tileW / 2, cy - tileH / 2, cz], [cx + tileW / 2, cy - tileH / 2, cz] ]; var canvas = immersive.renderer.domElement; var cr = canvas.getBoundingClientRect(); if (!cr.width || !cr.height) { cr = { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }; } var minX = Infinity; var minY = Infinity; var maxX = -Infinity; var maxY = -Infinity; pts.forEach(function (p) { captionVecA.set(p[0], p[1], p[2]).project(immersive.camera); var px = (captionVecA.x * 0.5 + 0.5) * cr.width + cr.left; var py = (-captionVecA.y * 0.5 + 0.5) * cr.height + cr.top; minX = Math.min(minX, px); minY = Math.min(minY, py); maxX = Math.max(maxX, px); maxY = Math.max(maxY, py); }); return { left: minX, top: minY, width: Math.max(8, maxX - minX), height: Math.max(8, maxY - minY) }; } function computeCarouselCenterTileRect() { return projectWorldTileRect(0, 0, 0, immersive.tileW, immersive.tileH); } function prepareCasesTargetRect() { if (!immersive.track) return null; var idx = immersive.selectedIndex; var scrollTarget = scrollForRawIndex(idx); immersive.track.position.x = scrollTarget; immersive.scrollX = scrollTarget; immersive.targetScrollX = scrollTarget; updateCamera(); immersive.casesToRect = computeCarouselCenterTileRect(); return immersive.casesToRect; } function setCanvasMorphOpacity(alpha) { if (!immersive.renderer || !immersive.renderer.domElement) return; var a = clamp(alpha, 0, 1); immersive.renderer.domElement.style.opacity = String(a); immersive.renderer.domElement.style.visibility = a > 0.001 ? 'visible' : 'hidden'; } function setCanvasDuringMorph(visible) { setCanvasMorphOpacity(visible ? 1 : 0); } function setIntroTilesVisible(visible) { immersive.tiles.forEach(function (tile) { setTileOpacity(tile, visible ? 1 : 0); }); } function detachCasesForMorph(el) { if (!el || el.parentNode === document.body) return; if (!immersive.casesPin) { immersive.casesPin = { placeholder: document.createComment('home-cases-pin'), parent: null }; } var pin = immersive.casesPin; pin.parent = el.parentNode; pin.parent.insertBefore(pin.placeholder, el); document.body.appendChild(el); } function restoreCasesFromMorph(el) { if (!el || !immersive.casesPin || !immersive.casesPin.parent) return; if (el.parentNode !== document.body) return; var pin = immersive.casesPin; pin.parent.insertBefore(el, pin.placeholder); pin.placeholder.remove(); pin.parent = null; } function pinBackgroundCases(atRect) { var el = document.querySelector('.background_cases'); if (!el) return false; immersive.casesEl = el; var r = atRect ? { left: atRect.left, top: atRect.top, width: atRect.width, height: atRect.height } : (function () { var box = el.getBoundingClientRect(); return { left: box.left, top: box.top, width: box.width, height: box.height }; })(); immersive.casesMorphOrigin = r; if (!atRect) immersive.casesFromRect = r; detachCasesForMorph(el); document.documentElement.classList.add('is-home-cases-morph'); el.classList.add('is-home-cases-morphing'); el.classList.remove('is-home-cases-in-carousel'); applyCasesMorphLayout(r); return true; } function markCasesEnterHandoff() { if (immersive.casesHandoffDone) return; immersive.casesHandoffDone = true; syncHandoffVideo(); immersive.handoffVideoSynced = true; document.documentElement.classList.add('is-home-carousel-view'); pauseNonSelectedCmsVideos(); } function finalizeCasesHandoffDom() { if (immersive.casesHandoffFinalized || !immersive.casesEl) return; immersive.casesHandoffFinalized = true; immersive.casesEl.classList.remove('is-home-cases-morphing'); immersive.casesEl.classList.add('is-home-cases-in-carousel'); immersive.casesEl.style.cssText = ''; restoreCasesFromMorph(immersive.casesEl); document.documentElement.classList.remove('is-home-cases-morph'); setCanvasMorphOpacity(1); } function hideBackgroundCasesForCarousel() { markCasesEnterHandoff(); finalizeCasesHandoffDom(); } function prepareActiveTileForProjection(activeIdx, count) { var scrollTarget = scrollForRawIndex(activeIdx); immersive.track.position.x = scrollTarget; immersive.scrollX = scrollTarget; immersive.targetScrollX = scrollTarget; immersive.tiles.forEach(function (tile) { if (tile.rep !== 0 || tile.logicalIndex !== activeIdx) return; applyCarouselTileLayout(tile, (activeIdx - (count - 1) / 2) * TILE_UNIT); }); updateCamera(); } function performEnterHandoff() { if (immersive.casesHandoffDone) return true; prepareActiveTileForProjection(immersive.selectedIndex, projects.length); var rects = morphEnterRects(immersive.selectedIndex, projects.length); if (rects.to) immersive.handoffTargetRect = copyRect(rects.to); if (rects.from && rects.to) applyCasesMorphTransform(rects.from, rects.to, 1); markCasesEnterHandoff(); setActiveTileOpacity(1); if (immersive.casesEl) immersive.casesEl.style.opacity = '0'; setCanvasMorphOpacity(1); return true; } function crossfadeEnterHandoff(morphP) { if (morphP > ENTER_HANDOFF_VIDEO_AT && !immersive.handoffVideoSynced) { syncHandoffVideo(); immersive.handoffVideoSynced = true; } if (!immersive.casesHandoffDone && immersive.casesEl) { immersive.casesEl.style.opacity = '1'; } } function getProjectVideoTexture(project, logicalIndex) { if (!immersive.videoTextures) immersive.videoTextures = {}; if (immersive.videoTextures[logicalIndex]) return immersive.videoTextures[logicalIndex]; var texture = new THREE.VideoTexture(project.video); texture.colorSpace = THREE.SRGBColorSpace; texture.minFilter = THREE.LinearFilter; texture.magFilter = THREE.LinearFilter; texture.generateMipmaps = false; immersive.videoTextures[logicalIndex] = texture; return texture; } function disposeVideoTextures() { if (!immersive.videoTextures) return; Object.keys(immersive.videoTextures).forEach(function (key) { var texture = immersive.videoTextures[key]; if (texture) texture.dispose(); }); immersive.videoTextures = null; } function updateCarouselVideoTextures() { if (!immersive.videoTextures) return; immersive.tiles.forEach(function (tile) { if (tile.rep !== 0 || !tile.texture || !tile.project || !tile.project.video) return; if (tile.material.uniforms.uOpacity.value < 0.02) return; if (tile.project.video.readyState >= 2) tile.texture.needsUpdate = true; }); } function updateActiveVideoTexture() { updateCarouselVideoTextures(); } function ensureCarouselVideoAlive() { if (!immersive.active || !immersive.casesHandoffDone) return; var project = projects[immersive.selectedIndex]; if (!project || !project.video) return; if (project.video.paused) playVideoEl(project.video); } function releaseBackgroundCasesForFullscreen() { var el = immersive.casesEl || document.querySelector('.background_cases'); if (!el) return; if (immersive.heroRect && el.classList.contains('is-home-cases-morphing')) { applyCasesMorphLayout(immersive.heroRect); } el.classList.remove('is-home-cases-morphing', 'is-home-cases-in-carousel'); el.style.cssText = ''; restoreCasesFromMorph(el); immersive.casesEl = el; document.documentElement.classList.remove('is-home-cases-morph', 'is-home-carousel-view', 'is-home-exiting-to-fullscreen'); setHeroUiReveal(1, 1); clearFeaturesHeroInlineStyles(); killFeaturesTween(); immersive.casesHandoffDone = false; immersive.casesHandoffFinalized = false; immersive.handoffVideoSynced = false; immersive.casesExitHandoff = false; immersive.casesExitTileRect = null; clearEnterMorphRects(); } function getCenterTile() { var centerTile = null; var minDist = Infinity; immersive.tiles.forEach(function (tile) { if (tile.rep !== 0) return; var wx = tile.mesh.position.x + immersive.scrollX; if (Math.abs(wx) < minDist) { minDist = Math.abs(wx); centerTile = tile; } }); return centerTile; } function projectActiveTileScreenRect() { var centerTile = getCenterTile(); if (!centerTile || !immersive.renderer || !immersive.camera || typeof THREE === 'undefined') { return null; } if (!captionVecA) { captionVecA = new THREE.Vector3(); captionVecB = new THREE.Vector3(); captionVecC = new THREE.Vector3(); } var tileW = immersive.tileW * centerTile.mesh.scale.x; var tileH = immersive.tileH * centerTile.mesh.scale.y; var cx = centerTile.mesh.position.x + immersive.scrollX; var cy = centerTile.mesh.position.y; var cz = centerTile.mesh.position.z; var pts = [ [cx - tileW / 2, cy + tileH / 2, cz], [cx + tileW / 2, cy + tileH / 2, cz], [cx - tileW / 2, cy - tileH / 2, cz], [cx + tileW / 2, cy - tileH / 2, cz] ]; var canvas = immersive.renderer.domElement; var cr = canvas.getBoundingClientRect(); var minX = Infinity; var minY = Infinity; var maxX = -Infinity; var maxY = -Infinity; pts.forEach(function (p) { captionVecA.set(p[0], p[1], p[2]).project(immersive.camera); var px = (captionVecA.x * 0.5 + 0.5) * cr.width + cr.left; var py = (-captionVecA.y * 0.5 + 0.5) * cr.height + cr.top; minX = Math.min(minX, px); minY = Math.min(minY, py); maxX = Math.max(maxX, px); maxY = Math.max(maxY, py); }); return { left: minX, top: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) }; } function setActiveTileOpacity(opacity) { var idx = immersive.selectedIndex; immersive.tiles.forEach(function (tile) { if (tile.rep !== 0 || tile.logicalIndex !== idx) return; setTileOpacity(tile, opacity); }); } function syncHandoffVideo() { var project = projects[immersive.selectedIndex]; if (!project || !project.video) return; var video = project.video; video.muted = true; video.playsInline = true; if (video.paused) playVideoEl(video); immersive.tiles.forEach(function (tile) { if (tile.rep !== 0 || tile.logicalIndex !== immersive.selectedIndex) return; if (!tile.texture) return; if (tile.texture.image !== video) tile.texture.image = video; tile.texture.needsUpdate = true; var cover = coverUv(video, immersive.tileW, immersive.tileH); tile.material.uniforms.uCoverScale.value.set(cover.scale[0], cover.scale[1]); tile.material.uniforms.uCoverOffset.value.set(cover.offset[0], cover.offset[1]); }); } function primeVideoDecode(video, keepPlaying) { if (!video) return; video.muted = true; video.playsInline = true; if (typeof window.bufferPortfolioHeroVideo === 'function') { window.bufferPortfolioHeroVideo(video); } else if (typeof window.warmPortfolioVideo === 'function') { window.warmPortfolioVideo(video, !!keepPlaying); return; } if (video.readyState < 2 && !video.dataset.hlsInit && !video._portfolioHls) { try { video.load(); } catch (err) {} } if (keepPlaying) { if (video.paused) playVideoEl(video); return; } if (video.readyState >= 2) { if (!video.paused) video.pause(); return; } if (video.dataset.carouselPrimed === '1') { if (video.paused) video.play().catch(function () {}); return; } video.dataset.carouselPrimed = '1'; function pauseAfterDecode() { video.removeEventListener('loadeddata', pauseAfterDecode); video.removeEventListener('canplay', pauseAfterDecode); try { if (video.currentTime < 0.001) video.currentTime = 0.001; } catch (err) {} if (!keepPlaying && !video.paused) video.pause(); refreshProjectVideoCover(video); updateCarouselVideoTextures(); } video.addEventListener('loadeddata', pauseAfterDecode, { once: true }); video.addEventListener('canplay', pauseAfterDecode, { once: true }); if (video.paused) video.play().catch(function () {}); } function refreshProjectVideoCover(video) { if (!video || !immersive.videoTextures) return; projects.forEach(function (project, index) { if (project.video !== video) return; var texture = immersive.videoTextures[index]; if (!texture) return; immersive.tiles.forEach(function (tile) { if (tile.logicalIndex !== index || tile.rep !== 0 || !tile.material) return; var cover = coverUv(video, immersive.tileW, immersive.tileH); tile.material.uniforms.uCoverScale.value.set(cover.scale[0], cover.scale[1]); tile.material.uniforms.uCoverOffset.value.set(cover.offset[0], cover.offset[1]); if (texture) texture.needsUpdate = true; }); }); } function primeAllCarouselVideos() { var activeIdx = immersive.selectedIndex; if (typeof window.bufferAllPortfolioHeroVideos === 'function') { window.bufferAllPortfolioHeroVideos(); } projects.forEach(function (project, index) { primeVideoDecode(project && project.video, index === activeIdx); }); } function prewarmAdjacentVideos() { primeAllCarouselVideos(); } function pauseNonSelectedCmsVideos() { projects.forEach(function (project, index) { if (index === immersive.selectedIndex) return; pauseVideoEl(project.video); }); } function setActiveTileVisible(visible) { setActiveTileOpacity(visible ? 1 : 0); } function applyShellTransition(wi) { if (immersiveRoot) immersiveRoot.style.transform = ''; } function applyTransitionBackdrop(carouselSolid) { applyTransitionBackdropAlpha(carouselSolid ? 1 : 0); } function applyTransitionBackdropAlpha(alpha) { if (!immersive.renderer || !immersive.scene || typeof THREE === 'undefined') return; var a = clamp(alpha, 0, 1); immersive.renderer.setClearColor(BG_COLOR, a); immersive.scene.background = a > 0.01 ? new THREE.Color(BG_COLOR) : null; if (immersiveRoot) { immersiveRoot.style.background = a > 0.01 ? 'rgba(5,5,5,' + a + ')' : 'transparent'; immersiveRoot.classList.toggle('is-carousel-solid', a > 0.96 && immersive.active); } } function wrappedDelta(fromIndex, toIndex, count) { var delta = toIndex - fromIndex; if (delta > count / 2) delta -= count; if (delta < -count / 2) delta += count; return delta; } function setTileOpacity(tile, opacity) { tile.material.uniforms.uOpacity.value = clamp(opacity, 0, 1); } function coverUv(video, tileW, tileH) { var vw = video.videoWidth || 16; var vh = video.videoHeight || 9; var va = vw / vh; var ta = tileW / tileH; if (va > ta) { var sx = ta / va; return { scale: [sx, 1], offset: [(1 - sx) / 2, 0] }; } var sy = va / ta; return { scale: [1, sy], offset: [0, (1 - sy) / 2] }; } function startIntro(closing) { if (hold.glyphLocked == null) hold.introGlyphDone = false; var now = performance.now(); immersive.intro = { active: true, closing: !!closing, startTime: now, exitScrollStart: closing ? immersive.scrollX : 0 }; killFeaturesTween(); updateModeHoldState(); if (!closing) { immersive.shellProgress = 0; immersive.casesHandoffDone = false; immersive.casesHandoffFinalized = false; immersive.handoffVideoSynced = false; immersive.casesExitHandoff = false; immersive.casesExitTileRect = null; clearEnterMorphRects(); setHeroUiReveal(0, 0); document.documentElement.classList.remove('is-home-exiting-to-fullscreen'); if (metaCaptionEl) { metaCaptionEl.style.transform = ''; metaCaptionEl.style.opacity = '0'; } immersive.caption.textIndex = -1; immersive.caption.swapping = false; immersive.caption.enterFaded = false; prewarmAdjacentVideos(); if (immersive.restoreEnter) { hold.glyphLocked = 1; hold.introGlyphDone = true; if (modeHoldEl) modeHoldEl.style.setProperty('--glyph-morph', '1'); } pinFeaturesHeroForTransition(); applyFeaturesHeroEnterProgress(0); captureCasesFromRect(); immersive.casesMorphOrigin = copyRect(immersive.casesFromRect); pinBackgroundCases(copyRect(immersive.casesFromRect)); setCanvasMorphOpacity(0); } else { immersive.casesHandoffDone = false; immersive.casesHandoffFinalized = false; immersive.handoffVideoSynced = false; immersive.casesExitHandoff = false; immersive.casesExitTileRect = null; immersive.featuresRevealStarted = false; setHeroUiReveal(0, 0); pinFeaturesHeroForTransition(); var featEl = getFeaturesHeroEl(); if (featEl) { featEl.style.opacity = '0'; featEl.style.visibility = 'hidden'; featEl.style.transform = ''; featEl.style.pointerEvents = 'none'; } document.documentElement.classList.add('is-home-exiting-to-fullscreen'); } } function applySideTileEnter(tile, baseX, spreadT) { var eased = 1 - Math.pow(1 - spreadT, 2.2); var dir = baseX > 0 ? 1 : baseX < 0 ? -1 : 0; var outward = TILE_UNIT * 0.5 + Math.abs(baseX) * 0.08; var fromX = baseX + dir * outward; tile.mesh.position.x = lerp(fromX, baseX, eased); tile.mesh.position.y = 0; tile.mesh.position.z = 0; var sc = lerp(0.9, 1, eased); tile.mesh.scale.setScalar(sc); setTileOpacity(tile, eased); tile.material.uniforms.uGray.value = 1; } function applySideTileExit(tile, baseX, fadeT) { tile.mesh.position.x = baseX; tile.mesh.position.y = 0; tile.mesh.position.z = 0; tile.mesh.scale.setScalar(1); setTileOpacity(tile, 1 - EASE_LOCO(clamp(fadeT, 0, 1))); tile.material.uniforms.uGray.value = 1; } function applyCarouselTileLayout(tile, baseX) { tile.mesh.position.x = baseX; tile.mesh.position.y = 0; tile.mesh.position.z = 0; tile.mesh.scale.setScalar(1); } function applyIntroEnter(rawP) { var count = projects.length; var activeIdx = immersive.selectedIndex; var morphP = smoothRange(rawP, 0, ENTER_MORPH_END); var sideRevealP = smoothRange(rawP, ENTER_SIDES_START, ENTER_SIDES_END); var spreadP = smoothRange(rawP, ENTER_SPREAD_START, 1); prepareActiveTileForProjection(activeIdx, count); crossfadeEnterHandoff(morphP); var morphRects = morphEnterRects(activeIdx, count); var from = morphRects.from; var to = morphRects.to; immersive.shellProgress = morphP; immersive.introZoom = 1; applyFeaturesHeroEnterProgress(rawP); setHeroUiReveal(0, 0); applyShellTransition(0); applyTransitionBackdropAlpha(spreadP * 0.92); setCanvasMorphOpacity(immersive.casesHandoffDone ? 1 : Math.max(0.06, sideRevealP)); if (from && to) { applyCasesMorphTransform(from, to, morphP); } immersive.warpIntensity = lerp(immersive.warpIntensity, 0, spreadP); immersive.tiles.forEach(function (tile) { tile.material.uniforms.uIntroZoom.value = 1; tile.material.uniforms.uWarpIntensity.value = 0; if (tile.rep !== 0) { setTileOpacity(tile, 0); tile.mesh.scale.setScalar(0.001); return; } var ji = tile.logicalIndex; var baseX = (ji - (count - 1) / 2) * TILE_UNIT; var isActive = ji === activeIdx; applyCarouselTileLayout(tile, baseX); if (isActive) { setTileOpacity(tile, immersive.casesHandoffDone ? 1 : 0); tile.material.uniforms.uGray.value = 0; } else { setTileOpacity(tile, sideRevealP); tile.material.uniforms.uGray.value = 1; } }); if (metaCaptionEl) { updateCaptionContent(activeIdx); if (immersive.casesHandoffDone) { if (!immersive.caption.enterFaded) { immersive.caption.enterFaded = true; metaCaptionEl.style.opacity = '1'; fadeInCaptionLayers(); } } else { metaCaptionEl.style.opacity = '0'; } } var scrollTarget = scrollForRawIndex(activeIdx); immersive.track.position.x = scrollTarget; immersive.scrollX = scrollTarget; immersive.targetScrollX = scrollTarget; if (morphP >= 0.995 && !immersive.casesHandoffDone) { performEnterHandoff(); } syncPlayback(); } function resolveExitProject() { var centerTile = getCenterTile(); if (centerTile && centerTile.logicalIndex != null) { return projects[centerTile.logicalIndex]; } return projects[immersive.selectedIndex]; } function beginCasesExitHandoff() { captureHeroRect(); prepareCasesTargetRect(); immersive.casesExitTileRect = immersive.casesToRect || computeCarouselCenterTileRect(); if (!immersive.casesExitTileRect) return false; var exitProject = immersive.exitProject || projects[immersive.selectedIndex]; syncCmsToProject(exitProject, { skipAutoplayUi: true }); var exitIdx = exitProject && exitProject.index != null ? exitProject.index : immersive.selectedIndex; if (typeof window.__portfolioCmsPrepareAutoplayLine === 'function') { window.__portfolioCmsPrepareAutoplayLine(exitIdx); } syncHandoffVideo(); pinBackgroundCases(immersive.casesExitTileRect); if (immersive.casesEl) immersive.casesEl.style.opacity = '0'; setCanvasMorphOpacity(1); immersive.casesExitHandoff = true; immersive.handoffVideoSynced = true; return true; } function applyIntroExit(rawP) { var count = projects.length; var activeIdx = immersive.selectedIndex; var sidesP = smoothRange(rawP, 0, EXIT_SIDES_END); var morphP = smoothRange(rawP, EXIT_MORPH_START, 1); var overlayP = smoothRange(rawP, 0.72, 1); var scrollStart = immersive.intro.exitScrollStart != null ? immersive.intro.exitScrollStart : immersive.scrollX; var scrollTarget = scrollForRawIndex(activeIdx); var scrollX = lerp(scrollStart, scrollTarget, sidesP); immersive.track.position.x = scrollX; immersive.scrollX = scrollX; immersive.targetScrollX = scrollTarget; if (rawP >= EXIT_HANDOFF_AT && !immersive.casesExitHandoff) { beginCasesExitHandoff(); } immersive.shellProgress = 1 - morphP; immersive.introZoom = 1; applyFeaturesHeroExitProgress(rawP); if (!immersive.casesExitHandoff) { setHeroUiReveal(0, smoothRange(rawP, 0.72, 1)); applyShellTransition(0); applyTransitionBackdropAlpha(1); setCanvasMorphOpacity(1); if (metaCaptionEl) metaCaptionEl.style.opacity = String(1 - sidesP); immersive.tiles.forEach(function (tile) { tile.material.uniforms.uWarpIntensity.value = 0; tile.material.uniforms.uIntroZoom.value = 1; if (tile.rep !== 0) { setTileOpacity(tile, 0); tile.mesh.scale.setScalar(0.001); return; } var ji = tile.logicalIndex; var baseX = (ji - (count - 1) / 2) * TILE_UNIT; var isActive = ji === activeIdx; var delta = wrappedDelta(activeIdx, ji, count); var ring = Math.max(1, Math.abs(delta)); var sideDelay = (ring - 1) * 0.04; var sideFade = smoothRange(rawP, sideDelay, EXIT_SIDES_FADE_END); if (isActive) { applyCarouselTileLayout(tile, baseX); setTileOpacity(tile, 1); tile.material.uniforms.uGray.value = 0; } else { applySideTileExit(tile, baseX, sideFade); } }); } else { var cmsVisible = morphP >= 0.1; var revealP = morphP >= 0.94 ? overlayP : 0; setHeroUiReveal(revealP, revealP); applyShellTransition(0); applyTransitionBackdropAlpha(1 - smoothRange(morphP, 0.55, 1)); if (immersive.casesExitTileRect && immersive.heroRect) { applyCasesMorphTransform(immersive.casesExitTileRect, immersive.heroRect, morphP); if (immersive.casesEl) immersive.casesEl.style.opacity = cmsVisible ? '1' : '0'; setActiveTileOpacity(cmsVisible ? 0 : 1); setCanvasMorphOpacity(cmsVisible ? 0 : 1); } immersive.tiles.forEach(function (tile) { if (tile.rep !== 0) { setTileOpacity(tile, 0); return; } if (tile.logicalIndex !== activeIdx) { var ji = tile.logicalIndex; var baseX = (ji - (count - 1) / 2) * TILE_UNIT; var delta = wrappedDelta(activeIdx, ji, count); var ring = Math.max(1, Math.abs(delta)); var sideDelay = (ring - 1) * 0.04; var sideFade = smoothRange(rawP, sideDelay, EXIT_SIDES_FADE_END); if (sideFade < 1) { applySideTileExit(tile, baseX, sideFade); } else { setTileOpacity(tile, 0); tile.mesh.scale.setScalar(0.84); } } }); if (metaCaptionEl) { metaCaptionEl.style.opacity = '0'; metaCaptionEl.style.transform = ''; } } syncPlayback(); } function applyInstantCarouselState() { immersive.intro = null; immersive.shellProgress = 1; immersive.introZoom = 1; immersive.casesHandoffDone = false; immersive.casesHandoffFinalized = false; immersive.handoffVideoSynced = false; clearEnterMorphRects(); immersive.casesEl = document.querySelector('.background_cases'); prepareActiveTileForProjection(immersive.selectedIndex, projects.length); syncHandoffVideo(); immersive.tiles.forEach(function (tile) { tile.material.uniforms.uIntroZoom.value = 1; tile.material.uniforms.uWarpIntensity.value = 0; if (tile.rep !== 0) { setTileOpacity(tile, 0); tile.mesh.scale.setScalar(0.001); return; } var ji = tile.logicalIndex; var baseX = (ji - (projects.length - 1) / 2) * TILE_UNIT; applyCarouselTileLayout(tile, baseX); setTileOpacity(tile, 1); tile.material.uniforms.uGray.value = ji === immersive.selectedIndex ? 0 : 1; }); immersive.track.position.x = immersive.scrollX; hideBackgroundCasesForCarousel(); setCanvasMorphOpacity(1); applyShellTransition(1); applyTransitionBackdropAlpha(1); setHeroUiReveal(0, 0); document.documentElement.classList.add('is-home-features-hidden'); var featEl = getFeaturesHeroEl(); if (featEl) { featEl.style.opacity = '0'; featEl.style.visibility = 'hidden'; featEl.style.pointerEvents = 'none'; } hold.glyphLocked = 1; hold.introGlyphDone = true; if (modeHoldEl) modeHoldEl.style.setProperty('--glyph-morph', '1'); immersive.restoreEnter = false; clearCarouselRestorePending(); immersive.lastSyncedCmsIndex = immersive.selectedIndex; syncCmsIfCarouselSettled(); immersive.caption.textIndex = -1; if (metaCaptionEl) metaCaptionEl.style.opacity = '1'; updateCaptionAnimated({ layout: true }); } function applyIntro() { if (!immersive.track || !immersive.intro || !immersive.intro.active) { applyShellTransition(immersive.shellProgress); return false; } var elapsed = (performance.now() - immersive.intro.startTime) / 1000; var rawP = clamp(elapsed / INTRO_DUR, 0, 1); var closing = immersive.intro.closing; if (closing) applyIntroExit(rawP); else applyIntroEnter(rawP); updateHoldGlyphFromIntro(rawP, closing); if (rawP >= 1) { hold.glyphLocked = null; hold.introGlyphDone = false; immersive.restoreEnter = false; clearCarouselRestorePending(); if (closing) { finishExit(); return true; } immersive.intro.active = false; immersive.shellProgress = 1; immersive.introZoom = 1; applyFeaturesHeroEnterProgress(1); restoreFeaturesHeroFromPin(); var featDone = getFeaturesHeroEl(); if (featDone) { featDone.style.opacity = '0'; featDone.style.visibility = 'hidden'; featDone.style.pointerEvents = 'none'; } if (!immersive.casesHandoffDone) { performEnterHandoff(); } finalizeCasesHandoffDom(); if (immersive.renderer && immersive.renderer.domElement) { immersive.renderer.domElement.style.opacity = '1'; immersive.renderer.domElement.style.visibility = 'visible'; } applyShellTransition(1); applyTransitionBackdropAlpha(1); immersive.tiles.forEach(function (tile) { tile.material.uniforms.uIntroZoom.value = 1; tile.material.uniforms.uWarpIntensity.value = 0; if (tile.rep !== 0) { setTileOpacity(tile, 0); tile.mesh.scale.setScalar(0.001); return; } var ji = tile.logicalIndex; var baseX = (ji - (projects.length - 1) / 2) * TILE_UNIT; applyCarouselTileLayout(tile, baseX); setTileOpacity(tile, 1); tile.material.uniforms.uGray.value = ji === immersive.selectedIndex ? 0 : 1; }); immersive.track.position.x = immersive.scrollX; immersive.lastSyncedCmsIndex = immersive.selectedIndex; syncCmsIfCarouselSettled(); updateModeHoldState(); immersive.caption.textIndex = -1; updateCaptionAnimated({ layout: true }); } return immersive.intro.active; } function ensureImmersiveUi() { if (immersiveRoot) { var captionMain = immersiveRoot.querySelector('.home-immersive-caption-main'); var subFirst = captionMain && captionMain.children[1]; if (!immersiveRoot.querySelector('.home-immersive-year') || (subFirst && !subFirst.classList.contains('home-immersive-sub'))) { if (immersiveRoot.parentNode) immersiveRoot.parentNode.removeChild(immersiveRoot); immersiveRoot = null; metaNameEl = null; metaYearEl = null; metaSubEl = null; metaCaptionEl = null; metaNameLayers = null; metaYearLayers = null; metaSubLayers = null; } } if (immersiveRoot) return immersiveRoot; immersiveRoot = document.createElement('div'); immersiveRoot.className = 'home-immersive-root'; immersiveRoot.innerHTML = '