(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 = '
' + '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
' + '
'; document.body.appendChild(immersiveRoot); metaNameEl = immersiveRoot.querySelector('.home-immersive-name'); metaYearEl = immersiveRoot.querySelector('.home-immersive-year'); metaSubEl = immersiveRoot.querySelector('.home-immersive-sub'); metaCaptionEl = immersiveRoot.querySelector('.home-immersive-caption'); metaNameLayers = metaNameEl ? metaNameEl.querySelectorAll('.home-immersive-text') : null; metaYearLayers = metaYearEl ? metaYearEl.querySelectorAll('.home-immersive-text') : null; metaSubLayers = metaSubEl ? metaSubEl.querySelectorAll('.home-immersive-text') : null; return immersiveRoot; } function queryCaptionPart(row, line, selectors) { if (!Array.isArray(selectors)) selectors = [selectors]; return queryProjectMeta(row, line, selectors); } function formatCaptionYear(year) { if (!year) return ''; return String(year).replace(/^[—–\-]\s*/, '').trim(); } function projectCaptionText(index) { var project = projects[index]; if (!project) return { name: '', year: '', industry: '' }; var row = project.row || project.line; var line = project.line || row; var nameEl = queryCaptionPart(row, line, ['.name']); var industryEl = queryCaptionPart(row, line, ['.industry', '.description', '.tag', '.category']); var yearEl = queryCaptionPart(row, line, ['.year']); var name = project.name || (nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : ''); var industry = project.industry || (industryEl ? industryEl.textContent.replace(/\s+/g, ' ').trim() : ''); var yearRaw = project.year || (yearEl ? yearEl.textContent.replace(/\s+/g, ' ').trim() : ''); return { name: name, year: formatCaptionYear(yearRaw), industry: industry }; } function captionLayerEls(layer) { if (!metaNameLayers || !metaYearLayers || !metaSubLayers) return []; return [metaNameLayers[layer], metaYearLayers[layer], metaSubLayers[layer]]; } function captionAllTextEls() { if (!metaNameLayers || !metaYearLayers || !metaSubLayers) return []; return [ metaNameLayers[0], metaNameLayers[1], metaYearLayers[0], metaYearLayers[1], metaSubLayers[0], metaSubLayers[1] ]; } function killCaptionTween() { var cap = immersive.caption; if (!cap.captionTween) return; if (typeof window.gsap !== 'undefined' && cap.captionTween.kill) { cap.captionTween.kill(); } cap.captionTween = null; } function setCaptionLayerVisual(el, opacity, yPercent, clipTop, clipBottom) { if (!el) return; el.style.opacity = String(opacity); el.style.transform = 'translate3d(0,' + yPercent + '%,0)'; el.style.clipPath = 'inset(' + clipTop + '% 0 ' + clipBottom + '% 0)'; } function applyCaptionTextBlend(fromLayer, toLayer, t) { if (!metaNameLayers || !metaYearLayers || !metaSubLayers) return; var outY = -110 * t; var inY = -110 * (1 - t); var outClip = t * 100; var inClip = (1 - t) * 100; setCaptionLayerVisual(metaNameLayers[fromLayer], 1 - t, outY, 0, outClip); setCaptionLayerVisual(metaNameLayers[toLayer], t, inY, inClip, 0); setCaptionLayerVisual(metaYearLayers[fromLayer], 1 - t, outY, 0, outClip); setCaptionLayerVisual(metaYearLayers[toLayer], t, inY, inClip, 0); setCaptionLayerVisual(metaSubLayers[fromLayer], 1 - t, outY, 0, outClip); setCaptionLayerVisual(metaSubLayers[toLayer], t, inY, inClip, 0); } function showCaptionLayerOnly(layer) { if (!metaNameLayers || !metaYearLayers || !metaSubLayers) return; var show = layer === 1 ? 1 : 0; var hide = 1 - show; setCaptionLayerVisual(metaNameLayers[show], 1, 0, 0, 0); setCaptionLayerVisual(metaYearLayers[show], 1, 0, 0, 0); setCaptionLayerVisual(metaSubLayers[show], 1, 0, 0, 0); setCaptionLayerVisual(metaNameLayers[hide], 0, 0, 100, 0); setCaptionLayerVisual(metaYearLayers[hide], 0, 0, 100, 0); setCaptionLayerVisual(metaSubLayers[hide], 0, 0, 100, 0); } function fillCaptionLayer(layer, index) { if (!metaNameLayers || !metaYearLayers || !metaSubLayers || layer < 0 || layer > 1) return; var copy = projectCaptionText(index); metaNameLayers[layer].textContent = copy.name; metaYearLayers[layer].textContent = copy.year; metaSubLayers[layer].textContent = copy.industry; } function fadeInCaptionLayers() { if (!metaNameLayers || !metaYearLayers || !metaSubLayers) return; var layer = immersive.caption.textLayer; var els = captionLayerEls(layer); function run(gsap) { gsap.set(els, { y: 10, opacity: 0 }); gsap.to(els, { y: 0, opacity: 1, duration: 0.48, ease: 'power2.out', stagger: 0.03 }); } if (typeof window.gsap !== 'undefined') run(window.gsap); else loadGsap().then(run).catch(function () {}); } function primeCaptionForEnter(index) { if (!metaCaptionEl) return; resetCaptionText(index); metaCaptionEl.style.opacity = '0'; immersive.caption.enterFaded = false; } function resetCaptionText(index) { immersive.caption.textIndex = -1; updateCaptionContent(index); } function computeActiveTileCaptionRect() { var px = projectActiveTileScreenRect(); if (!px) return null; var em = 11; if (metaCaptionEl) { var fs = parseFloat(getComputedStyle(metaCaptionEl).fontSize); if (fs) em = fs; } return { left: (px.left / window.innerWidth) * 100, top: ((px.top + px.height + em) / window.innerHeight) * 100, width: (px.width / window.innerWidth) * 100 }; } function layoutCaptionAnchor() { var rect = computeActiveTileCaptionRect(); if (!rect || !metaCaptionEl) return; metaCaptionEl.style.left = rect.left + '%'; metaCaptionEl.style.top = rect.top + '%'; metaCaptionEl.style.width = rect.width + '%'; metaCaptionEl.style.bottom = 'auto'; metaCaptionEl.style.transform = ''; } function updateCaptionContent(index) { var cap = immersive.caption; if (index == null || index < 0) return; if (cap.textIndex === index) return; killCaptionTween(); cap.textIndex = index; cap.textLayer = 0; cap.swapping = false; cap.swapFrom = null; cap.swapTo = null; cap.textBlend = 1; fillCaptionLayer(0, index); showCaptionLayerOnly(0); if (typeof window.gsap !== 'undefined') { window.gsap.set(captionAllTextEls(), { clearProps: 'transform,opacity,clipPath' }); window.gsap.set(captionLayerEls(0), { opacity: 1, y: 0 }); } } function startCaptionGsapSwap(toIndex) { var cap = immersive.caption; if (cap.swapping || toIndex === cap.textIndex) return; cap.swapping = true; cap.swapFrom = null; cap.swapTo = null; var fromLayer = cap.textLayer; var toLayer = 1 - fromLayer; fillCaptionLayer(toLayer, toIndex); function finishSwap() { cap.textLayer = toLayer; cap.textIndex = toIndex; cap.swapping = false; cap.captionTween = null; showCaptionLayerOnly(toLayer); } function runGsap(gsap) { if (!metaNameLayers || !metaYearLayers || !metaSubLayers) { finishSwap(); return; } killCaptionTween(); var outEls = captionLayerEls(fromLayer); var inEls = captionLayerEls(toLayer); var travel = CAPTION_TEXT_TRAVEL; gsap.set(inEls, { y: -travel, opacity: 0 }); gsap.set(outEls, { y: 0, opacity: 1 }); cap.captionTween = gsap.timeline({ onComplete: function () { gsap.set(outEls, { y: 0, opacity: 0 }); gsap.set(inEls, { y: 0, opacity: 1 }); finishSwap(); } }); cap.captionTween.to(outEls, { y: -travel, opacity: 0, duration: 0.38, ease: 'power2.in' }, 0); cap.captionTween.to(inEls, { y: 0, opacity: 1, duration: 0.44, ease: 'power2.out' }, 0.08); } loadGsap().then(runGsap).catch(function () { cap.swapFrom = fromLayer; cap.swapTo = toLayer; cap.textBlend = 0; }); } function updateCaptionAnimated(options) { options = options || {}; if (!metaCaptionEl || !immersive.active) return; layoutCaptionAnchor(); var centerTile = getCenterTile(); var focusIndex = centerTile ? centerTile.logicalIndex : immersive.selectedIndex; if (options.intro) { updateCaptionContent(focusIndex); return; } updateCaptionContent(focusIndex); } function syncPlayback() { var idx = immersive.selectedIndex; if (immersive.playbackIndex !== idx) { projects.forEach(function (project, index) { var video = project.video; if (!video) return; if (index === idx) playVideoEl(video); else primeVideoDecode(video, false); }); immersive.playbackIndex = idx; } else { var active = projects[idx]; if (active && active.video) { if (active.video.paused) playVideoEl(active.video); } projects.forEach(function (project, index) { if (index === idx) return; var video = project.video; if (!video) return; if (video.readyState >= 2) { if (!video.paused) video.pause(); } else { primeVideoDecode(video, false); } }); } updateCarouselVideoTextures(); updateCaptionAnimated({ intro: !!(immersive.intro && immersive.intro.active) }); } function disposeObject3D(obj) { if (!obj) return; obj.traverse(function (child) { if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) child.material.forEach(function (m) { m.dispose(); }); else child.material.dispose(); } }); } function disposeImmersiveScene() { if (immersive.raf) { cancelAnimationFrame(immersive.raf); immersive.raf = 0; } immersive.tiles.forEach(function (tile) { disposeObject3D(tile.mesh); }); disposeVideoTextures(); immersive.tiles = []; if (immersive.track) { disposeObject3D(immersive.track); immersive.track = null; } if (immersive.renderer) { immersive.renderer.dispose(); if (immersive.renderer.domElement && immersive.renderer.domElement.parentNode) { immersive.renderer.domElement.parentNode.removeChild(immersive.renderer.domElement); } } immersive.renderer = null; immersive.scene = null; immersive.camera = null; immersive.playbackIndex = -1; immersive.dragging = false; projects.forEach(function (project) { if (project.video) delete project.video.dataset.carouselPrimed; }); immersive.selectedIndex = 0; immersive.scrollX = 0; immersive.targetScrollX = 0; immersive.exiting = false; immersive.intro = null; immersive.shellProgress = 0; immersive.introZoom = 1; if (immersiveRoot) immersiveRoot.style.transform = ''; setHeroListReveal(0); setHeroOverlayReveal(0); immersive.caption.textIndex = -1; immersive.caption.swapping = false; killCaptionTween(); killFeaturesTween(); } function getPickables() { return immersive.tiles.map(function (t) { return t.mesh; }); } function resetTileLayout() { immersive.tiles.forEach(function (tile) { tile.mesh.position.x = (tile.rep * projects.length + tile.logicalIndex - (projects.length - 1) / 2) * TILE_UNIT; tile.mesh.scale.setScalar(1); }); } function applyScrollBounds() { immersive.scrollX = clampScrollValue(immersive.scrollX); immersive.targetScrollX = clampScrollValue(immersive.targetScrollX); immersive.lastScrollX = clampScrollValue(immersive.lastScrollX); } function layoutCarousel() { if (!immersive.track || !immersive.camera || typeof THREE === 'undefined') return; if (applyIntro()) return; ensureCarouselVideoAlive(); if (!immersive.dragging) { immersive.scrollX = lerp(immersive.scrollX, immersive.targetScrollX, DRAG_LERP); } applyScrollBounds(); var delta = immersive.scrollX - immersive.lastScrollX; immersive.lastScrollX = immersive.scrollX; immersive.scrollVel = lerp(immersive.scrollVel, delta, WARP_VEL_LERP); var ai = clamp(immersive.scrollVel * 12, -1.5, 1.5); var warpTarget = Math.abs(ai) > 0.001 ? Math.sign(ai) * Math.pow(Math.abs(ai), 1.85) * 0.46 : 0; var warpLerp = immersive.dragging ? 0.11 : WARP_LERP; immersive.warpIntensity = lerp(immersive.warpIntensity, warpTarget, warpLerp); immersive.selectedIndex = indexFromScroll(immersive.scrollX); syncCmsIfCarouselSettled(); immersive.track.position.x = immersive.scrollX; immersive.tiles.forEach(function (tile) { var worldX = tile.mesh.position.x + immersive.scrollX; var isCenter = Math.abs(worldX) < TILE_UNIT * 0.35; tile.material.uniforms.uWarpIntensity.value = immersive.warpIntensity; tile.material.uniforms.uViewportWidth.value = immersive.viewportWidth; tile.material.uniforms.uGray.value = lerp(tile.material.uniforms.uGray.value, isCenter ? 0 : 1, 0.12); tile.mesh.position.y = 0; tile.mesh.position.z = 0; }); syncPlayback(); } function updateCamera() { if (!immersive.camera) return; immersive.camera.position.set(0, 0, CAM_Z); immersive.camera.lookAt(0, 0, 0); } function renderImmersive() { if (!immersive.active || !immersive.renderer || !immersive.scene || !immersive.camera) return; layoutCarousel(); updateCamera(); updateActiveVideoTexture(); immersive.renderer.render(immersive.scene, immersive.camera); immersive.raf = requestAnimationFrame(renderImmersive); } function pickProject(clientX, clientY) { if (!immersive.renderer || !immersive.camera || !immersive.tiles.length || typeof THREE === 'undefined') { return -1; } var rect = immersive.renderer.domElement.getBoundingClientRect(); var mouse = new THREE.Vector2( ((clientX - rect.left) / rect.width) * 2 - 1, -(((clientY - rect.top) / rect.height) * 2 - 1) ); var raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, immersive.camera); var hits = raycaster.intersectObjects(getPickables(), false); if (!hits.length) return -1; return hits[0].object.userData.logicalIndex != null ? hits[0].object.userData.logicalIndex : -1; } function snapAfterDrag() { var momentum = immersive.dragVel * 0.01 * 3 / (1 - 0.94); var projected = clampScrollValue(immersive.scrollX + momentum); var idx = indexFromScroll(projected); immersive.targetScrollX = scrollForRawIndex(idx); immersive.selectedIndex = idx; } function createTile(project, logicalIndex, rep, tileW, tileH) { if (!project.video || typeof THREE === 'undefined') return null; var cover = coverUv(project.video, tileW, tileH); var texture = getProjectVideoTexture(project, logicalIndex); var material = new THREE.ShaderMaterial({ uniforms: { uVideo: { value: texture }, uCoverScale: { value: new THREE.Vector2(cover.scale[0], cover.scale[1]) }, uCoverOffset: { value: new THREE.Vector2(cover.offset[0], cover.offset[1]) }, uWarpIntensity: { value: 0 }, uViewportWidth: { value: immersive.viewportWidth }, uTileHalfH: { value: tileH / 2 }, uGray: { value: logicalIndex === immersive.selectedIndex ? 0 : 1 }, uIntroZoom: { value: 1.08 }, uOpacity: { value: rep === 0 ? 1 : 0 } }, vertexShader: TILE_VERT, fragmentShader: TILE_FRAG, transparent: true, depthWrite: true }); var mesh = new THREE.Mesh(new THREE.PlaneGeometry(tileW, tileH, 8, 8), material); mesh.position.x = (rep * projects.length + logicalIndex - (projects.length - 1) / 2) * TILE_UNIT; mesh.userData.logicalIndex = logicalIndex; return { mesh: mesh, material: material, texture: texture, project: project, logicalIndex: logicalIndex, rep: rep }; } function buildImmersiveScene() { if (typeof THREE === 'undefined') return false; ensureImmersiveUi(); var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance' }); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5)); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(BG_COLOR, 0); renderer.outputColorSpace = THREE.SRGBColorSpace; immersiveRoot.insertBefore(renderer.domElement, immersiveRoot.firstChild); immersiveRoot.style.background = 'transparent'; var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(CAM_FOV, window.innerWidth / window.innerHeight, 0.1, 120); camera.position.set(0, 0, CAM_Z); immersive.tileW = TILE_WIDTH; immersive.tileH = TILE_HEIGHT; immersive.viewportWidth = visibleWorldHeight(CAM_FOV, CAM_Z) * (window.innerWidth / window.innerHeight); immersive.loopWidth = projects.length * TILE_UNIT; var track = new THREE.Group(); scene.add(track); immersive.track = track; immersive.videoTextures = null; for (var rep = -1; rep <= 1; rep++) { projects.forEach(function (project, index) { var tile = createTile(project, index, rep, immersive.tileW, immersive.tileH); if (!tile) return; track.add(tile.mesh); immersive.tiles.push(tile); }); } var start = getActiveProjectIndex(projects); immersive.selectedIndex = start; immersive.targetScrollX = scrollForRawIndex(start); immersive.scrollX = immersive.targetScrollX; immersive.lastScrollX = immersive.scrollX; primeCaptionForEnter(start); immersive.renderer = renderer; immersive.scene = scene; immersive.camera = camera; updateCamera(); prepareCasesTargetRect(); setCanvasDuringMorph(false); primeAllCarouselVideos(); if (immersive.skipEnterIntro || reduceMq.matches) { immersive.skipEnterIntro = false; immersive.restoreEnter = false; clearCarouselRestorePending(); applyInstantCarouselState(); } else { clearCarouselRestorePending(); startIntro(false); } layoutCarousel(); syncPlayback(); return true; } function onImmersiveResize() { if (!immersive.renderer || !immersive.camera) return; immersive.camera.aspect = window.innerWidth / window.innerHeight; immersive.camera.updateProjectionMatrix(); immersive.renderer.setSize(window.innerWidth, window.innerHeight); immersive.viewportWidth = visibleWorldHeight(CAM_FOV, CAM_Z) * immersive.camera.aspect; if (immersive.intro && immersive.intro.active) { if (!immersive.intro.closing) { prepareCasesTargetRect(); } else if (immersive.casesExitHandoff) { captureHeroRect(); } } updateCaptionAnimated({ intro: !!(immersive.intro && immersive.intro.active) }); } function bindImmersiveEvents() { if (immersive.eventsBound) return; immersive.eventsBound = true; window.addEventListener('resize', onImmersiveResize); immersiveRoot.addEventListener('pointerdown', function (e) { if (!immersive.active) return; if (immersive.intro && immersive.intro.active) return; immersive.dragging = true; immersive.dragMoved = false; immersive.dragX = e.clientX; immersive.dragScrollStart = immersive.scrollX; immersive.dragLastX = e.clientX; immersive.dragLastT = performance.now(); immersive.dragVel = 0; }); window.addEventListener('pointerup', function () { if (!immersive.dragging) return; immersive.dragging = false; snapAfterDrag(); }); immersiveRoot.addEventListener('pointerleave', function () { if (!immersive.dragging) return; immersive.dragging = false; snapAfterDrag(); }); immersiveRoot.addEventListener('pointermove', function (e) { if (!immersive.active || !immersive.dragging) return; var dx = e.clientX - immersive.dragX; if (Math.abs(dx) > 2) immersive.dragMoved = true; var now = performance.now(); var dt = now - immersive.dragLastT; if (dt > 0) { var inst = (e.clientX - immersive.dragLastX) / dt; immersive.dragVel = immersive.dragVel * 0.85 + inst * 0.15; } immersive.dragLastX = e.clientX; immersive.dragLastT = now; var nextScroll = clampScrollValue(immersive.dragScrollStart + dx * DRAG_SENS); immersive.scrollX = nextScroll; immersive.targetScrollX = nextScroll; immersive.selectedIndex = indexFromScroll(nextScroll); }); immersiveRoot.addEventListener('wheel', function (e) { if (!immersive.active || (immersive.intro && immersive.intro.active)) return; e.preventDefault(); var delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY; if (delta > 0) setSelectedIndex(immersive.selectedIndex + 1); else if (delta < 0) setSelectedIndex(immersive.selectedIndex - 1); }, { passive: false }); immersiveRoot.addEventListener('click', function (e) { if (!immersive.active || immersive.dragMoved || (immersive.intro && immersive.intro.active)) return; var index = pickProject(e.clientX, e.clientY); if (index >= 0 && index !== immersive.selectedIndex) { setSelectedIndex(index); } }); } function enterImmersive(options) { options = options || {}; if (immersive.active || immersive.entering) return; immersive.fromRestore = !!options.fromRestore; immersive.restoreEnter = !!options.fromRestore; immersive.skipEnterIntro = !!options.skipIntro; if (immersive.fromRestore) applyCarouselRestorePending(); pauseCmsAutoplay(); immersive.entering = true; updateModeHoldState(); captureHeroRect(); loadThree() .then(function () { return loadGsap().catch(function () { return null; }); }) .then(function () { projects = buildProjects(); if (!projects.length) throw new Error('no projects'); ensureImmersiveUi(); immersiveRoot.classList.add('is-active'); document.documentElement.classList.add('is-home-immersive-active'); bindImmersiveEvents(); if (!buildImmersiveScene()) throw new Error('scene failed'); pauseLenis(); immersive.active = true; immersive.entering = false; saveMode(MODE_CAROUSEL); updateModeHoldState(); renderImmersive(); console.info('[home-immersive v' + VERSION + '] entered —', projects.length, 'projects'); }) .catch(function (err) { immersive.entering = false; immersive.restoreEnter = false; clearCarouselRestorePending(); updateModeHoldState(); console.warn('[home-immersive v' + VERSION + ']', err); }); } function finishExit() { var exitProject = immersive.exitProject || projects[immersive.selectedIndex]; var exitIndex = exitProject ? exitProject.index : immersive.selectedIndex; immersive.exitProject = null; immersive.active = false; immersive.exiting = false; immersive.entering = false; immersive.intro = null; hold.glyphLocked = null; hold.introGlyphDone = false; immersive.lastSyncedCmsIndex = -1; disposeImmersiveScene(); if (immersiveRoot) immersiveRoot.classList.remove('is-active'); document.documentElement.classList.remove('is-home-immersive-active'); syncCmsToProject(exitProject, { skipAutoplayUi: true }); if (typeof window.__portfolioCmsPrepareAutoplayLine === 'function') { window.__portfolioCmsPrepareAutoplayLine(exitIndex); } releaseBackgroundCasesForFullscreen(); saveMode(MODE_FULLSCREEN); if (metaCaptionEl) { metaCaptionEl.style.transform = ''; } updateModeHoldState(); if (typeof window.__portfolioCmsOnWake === 'function') { window.__portfolioCmsOnWake({ skipAutoplayUi: true }); } resumeCmsAutoplay(exitIndex); resumeLenis(); console.info('[home-immersive v' + VERSION + '] exited'); } function exitImmersive() { if (!immersive.active || immersive.entering || immersive.exiting) return; if (reduceMq.matches || !immersive.track) { immersive.exitProject = resolveExitProject(); finishExit(); return; } immersive.exitProject = resolveExitProject(); immersive.exiting = true; updateModeHoldState(); captureHeroRect(); startIntro(true); } function onKeyDown(e) { if (immersive.active) { if (e.key === 'Escape' || e.key === 'Esc') { e.preventDefault(); e.stopPropagation(); return; } if (immersive.intro && immersive.intro.active) return; if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex(immersive.selectedIndex + 1); return; } if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex(immersive.selectedIndex - 1); return; } } } function bindCore() { window.addEventListener('keydown', onKeyDown, true); } function boot() { updatePageModeFlags(); if (!canRunImmersive()) return false; if (immersiveBooted) return projects.length > 0; scopeEl = document.querySelector(SCOPE); if (!scopeEl) { console.warn('[home-immersive v' + VERSION + '] scope not found:', SCOPE); return false; } projects = buildProjects(); if (!projects.length) { console.warn('[home-immersive v' + VERSION + '] no CMS projects found'); return false; } bindCore(); ensureModeHoldUi(); if (!reduceMq.matches) { loadGsap().catch(function () {}); loadThree().catch(function () {}); } if (canRestoreCarouselMode()) { enterImmersive({ fromRestore: true }); } else { updateModeHoldState(); } console.info('[home-immersive v' + VERSION + '] ready —', projects.length, 'projects'); immersiveBooted = true; return true; } function syncCmsAutoplayUi(project) { if (!project || !project.row) return; document.querySelectorAll('.cms_line').forEach(function (row) { row.classList.remove('is-autoplay-current', 'is-autoplay-running'); row.style.removeProperty('--line-fill'); var bar = row.querySelector('.line_progress'); if (bar) { bar.style.removeProperty('animation'); bar.style.removeProperty('transform'); } }); project.row.classList.add('is-autoplay-current'); void project.row.offsetWidth; project.row.classList.add('is-autoplay-running'); } function syncCmsToProject(project, options) { if (!project || !project.item) return -1; options = options || {}; if (typeof window.__portfolioCmsSetActiveProject === 'function') { var idx = window.__portfolioCmsSetActiveProject( project.item, project.line, project.row, options ); if (idx >= 0) return idx; } var prev = document.querySelector('.background_cases .cms_item.is-active'); if (prev && prev !== project.item) { prev.classList.remove('is-active', 'is-leaving', 'is-entering'); pauseVideoEl(getVideo(prev)); } document.querySelectorAll('.background_cases .cms_item').forEach(function (item) { if (item !== project.item) { item.classList.remove('is-active', 'is-leaving', 'is-entering'); } }); project.item.classList.add('is-active'); playVideoEl(project.video); document.querySelectorAll('.cms_line, .line_features').forEach(function (el) { el.classList.remove('is-active', 'is-autoplay-current', 'is-autoplay-running'); }); if (project.row) project.row.classList.add('is-active'); if (project.line) project.line.classList.add('is-active'); if (!options.skipAutoplayUi) syncCmsAutoplayUi(project); return project.index != null ? project.index : -1; } function syncCmsToSelectedIndex(index) { var idx = index != null ? index : immersive.selectedIndex; syncCmsToProject(projects[idx]); } function pauseCmsAutoplay() { if (typeof window.__portfolioCmsOnHide === 'function') window.__portfolioCmsOnHide(); } function resumeCmsAutoplay(atIndex) { if (typeof window.__portfolioCmsResumeAutoplayOnce === 'function') { window.__portfolioCmsResumeAutoplayOnce(atIndex); return; } if (typeof window.__portfolioCmsOnWake === 'function') { window.__portfolioCmsOnWake({ skipAutoplayUi: true }); } if (typeof window.__portfolioCmsRestartAutoplay === 'function') { window.__portfolioCmsRestartAutoplay(atIndex, { resume: true, consume: true }); } } function pauseLenis() { if (window.lenis && typeof window.lenis.stop === 'function') window.lenis.stop(); } function resumeLenis() { if (window.lenis && typeof window.lenis.start === 'function') window.lenis.start(); } function scheduleBoot() { updatePageModeFlags(); ensureModeHoldUi(); if (!canRunImmersive()) return; if (boot()) return; var tries = 0; var wait = window.setInterval(function () { tries++; if (boot() || tries > 120) window.clearInterval(wait); }, 250); } window.homeImmersiveView = { enabled: true, version: VERSION, isActive: function () { return !!immersive.active; }, enter: enterImmersive, exit: exitImmersive, canShowHoldUi: function () { return canShowHoldUi(); }, canRunImmersive: function () { return canRunImmersive(); }, destroy: function () { cancelHold(); finishExit(); document.documentElement.classList.remove('is-home-immersive-active'); document.documentElement.style.removeProperty('--hero-list-reveal'); document.documentElement.style.removeProperty('--hero-overlay-reveal'); document.documentElement.classList.remove('is-home-features-hidden'); clearCarouselRestorePending(); killFeaturesTween(); clearFeaturesHeroInlineStyles(); if (modeHoldEl && modeHoldEl.parentNode) modeHoldEl.parentNode.removeChild(modeHoldEl); if (immersiveRoot && immersiveRoot.parentNode) immersiveRoot.parentNode.removeChild(immersiveRoot); modeHoldEl = null; immersiveRoot = null; } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', scheduleBoot); } else { scheduleBoot(); } if (window.Webflow) { window.Webflow.push(scheduleBoot); } function onViewportModeChange() { updatePageModeFlags(); if (!canShowHoldUi()) { cancelHold(); if (immersive.active) finishExit(); return; } ensureModeHoldUi(); if (!canRunImmersive()) { updateModeHoldState(); return; } if (!immersiveBooted) { boot(); return; } if (canRestoreCarouselMode() && !immersive.active && !immersive.entering) { enterImmersive({ fromRestore: true }); } else { updateModeHoldState(); } } widthMq.addEventListener('change', onViewportModeChange); desktopMq.addEventListener('change', onViewportModeChange); reduceMq.addEventListener('change', onViewportModeChange); })();