1/* INFI â Mobile MotionFX 2 * Brings the desktop pointer-driven 3D effects to touch devices using the 3 * gyroscope (deviceorientation), direct touch-drag gestures, and haptics. 4 * 5 * Desktop inline scripts bail on `(hover:hover) and (pointer:fine)` and the 6 * mobile CSS flattens the same elements (perspective:none + transform:none). 7 * This module is the COMPLEMENT: it only runs on coarse/touch devices, and 8 * re-enables those exact effects driven by device tilt + finger, never on 9 * desktop (so it can't fight the inline pointer code). 10 * 11 * Targets (mirrors of the desktop effects): 12 * #fomDev .............. home hero device tilt 13 * .prob__card .......... home "peak rush" photo tilt 14 * .eco3d__scene ........ home POS-stack 3D rotate 15 * .clog__shot[data-tilt] home changelog photo tilt 16 * .cph ................. feature phone mockups (hero + rows) 17 * .vs-col .............. platform comparison cards (gentle tilt + glow) 18 * .hub__scene .......... home/platform spine constellation (radial 3D rotate) 19 * Plus: global tap haptics, and the [data-csphere] constellation already 20 * drags on touch (pointer events) â we only add haptics there. 21 */ 22(function () { 23 'use strict'; 24 var mm = function (q) { return window.matchMedia && window.matchMedia(q).matches; }; 25 var COARSE = mm('(pointer:coarse)') || !mm('(hover:hover) and (pointer:fine)') || ('ontouchstart' in window); 26 if (!COARSE) return; // desktop is owned by the inline pointer scripts 27 28 var REDUCE = mm('(prefers-reduced-motion: reduce)'); 29 var HAS_VIBRATE = 'vibrate' in navigator; 30 var docEl = document.documentElement; 31 var clamp = function (v, a, b) { return v < a ? a : (v > b ? b : v); }; 32 var now = function () { return (window.performance && performance.now) ? performance.now() : +new Date(); }; 33 34 /* ============================== HAPTICS ============================== */ 35 /* Light, debounced tap feedback. (navigator.vibrate is Android-only; on 36 * iOS Safari it's a graceful no-op â there is no web haptics API there.) */ 37 var lastBuzz = 0; 38 function buzz(ms) { 39 if (!HAS_VIBRATE) return; 40 var t = now(); 41 if (t - lastBuzz < 35) return; 42 lastBuzz = t; 43 try { navigator.vibrate(ms); } catch (e) {} 44 } 45 var TAP_SEL = 'a.pcard,.pcard,.hub__node,.hub-m__n,.btn,.btn--primary,.btn--ghost,.btn--lg,' + 46 '#themeTg,#burger,#mnavx,.mnav a,.csphere__tag,.vs-col,[data-tilt],.faq__q,.slab,.stk__back,' + 47 '.sqbadge,.clog__shot,.eco3d__cta,.eco-cta,.dl-btn,.fombar a,.foot a'; 48 document.addEventListener('touchstart', function (e) { 49 var t = e.target && e.target.closest ? e.target.closest(TAP_SEL) : null; 50 if (!t) return; 51 var strong = t.id === 'themeTg' || t.id === 'burger' || t.classList.contains('btn--primary'); 52 buzz(strong ? 13 : 8); 53 }, { passive: true }); 54 55 /* Reduced-motion: keep the (non-visual) tap haptics, skip all tilt/gyro. */ 56 if (REDUCE) return; 57 58 /* ============================ TILT TARGETS ========================== */ 59 /* Each recipe maps a normalised tilt vector (nx, ny in [-1, 1]) to the 60 * element's transform, written as CSS custom props consumed by injected 61 * rules. nx = yaw (device tilted left/right), ny = pitch (top/bottom). */ 62 var setRX = function (el, deg) { el.style.setProperty('--mfx-rx', deg.toFixed(2) + 'deg'); }; 63 var setRY = function (el, deg) { el.style.setProperty('--mfx-ry', deg.toFixed(2) + 'deg'); }; 64 /* inner-screen parallax offset (px) â the frame rotates, the screen content 65 * shifts the opposite way, which is the strongest "depth" cue for a flat 66 * mockup ("looking into" the phone). */ 67 var setP = function (el, x, y) { el.style.setProperty('--mfx-px', x.toFixed(1) + 'px'); el.style.setProperty('--mfx-py', y.toFixed(1) + 'px'); }; 68 69 /* Punchy base poses + wide amplitudes so the 3D reads strongly on a phone. 70 * Combined with the lowered perspective (see injected CSS) and the inner 71 * parallax, even a small device tilt produces a clear, deep 3D shift. 72 * The CSS fallbacks below mirror the base poses. */ 73 var RECIPES = [ 74 { sel: '#fomDev', apply: function (el, nx, ny) { setRX(el, 3 - ny * 17); setRY(el, -7 + nx * 19); setP(el, -nx * 10, ny * 10); } }, 75 { sel: '.prob__card', apply: function (el, nx, ny) { setRX(el, -7 - ny * 10); setRY(el, 9 + nx * 14); } }, 76 { sel: '.eco3d__scene', apply: function (el, nx, ny) { setRX(el, 14 - ny * 10); setRY(el, nx * 16); } }, 77 { sel: '.clog__shot[data-tilt]',apply: function (el, nx, ny) { setRX(el, -ny * 12); setRY(el, -6 + nx * 13); } }, 78 { sel: '.cph', apply: function (el, nx, ny) { setRX(el, 4 - ny * 17); setRY(el, -8 + nx * 19); setP(el, -nx * 9, ny * 9); } }, 79 { sel: '.vs-col', apply: function (el, nx, ny) { 80 setRX(el, -ny * 9); setRY(el, -5 + nx * 11); 81 el.style.setProperty('--mx', ((nx * 0.5 + 0.5) * 100).toFixed(1) + '%'); 82 el.style.setProperty('--my', ((ny * 0.5 + 0.5) * 100).toFixed(1) + '%'); 83 } },
84 /* platform hero stack â the 3 layers (flattened to a tappable column on 85 * mobile) now read as a cohesive set of 3D cards leaning back together. */ 86 { sel: '.slab', apply: function (el, nx, ny) { setRX(el, -7 - ny * 9); setRY(el, nx * 13); } }, 87 /* spine constellation (home + platform) â reuse the hub's own --hrx/--hry so 88 * the same radial 3D the desktop pointer drives now follows finger-drag + tilt. */ 89 { sel: '.hub__scene', apply: function (el, nx, ny) { 90 el.style.setProperty('--hry', (nx * 17).toFixed(2) + 'deg'); 91 el.style.setProperty('--hrx', (-ny * 13).toFixed(2) + 'deg'); 92 } } 93 ]; 94 95 var TARGETS = []; 96 RECIPES.forEach(function (r) { 97 var els = document.querySelectorAll(r.sel); 98 for (var i = 0; i < els.length; i++) { 99 TARGETS.push({ el: els[i], apply: r.apply, vis: true, drag: false, blend: 0, mx: 0, my: 0 }); 100 } 101 }); 102 103 if (!TARGETS.length) return; // page has no tiltable effects â haptics already wired 104 105 docEl.classList.add('mfx-on'); 106 injectCSS(); 107 108 /* ====================== GYRO (deviceorientation) ==================== */ 109 var gx = 0, gy = 0; // smoothed target from device tilt, [-1, 1] 110 var baseB = null, baseG = null; 111 var RANGE = 16; // degrees of tilt for full deflection (lower = more sensitive) 112 function onOrient(e) { 113 if (e.beta === null || e.gamma === null) return; 114 var ang = (window.screen && screen.orientation && screen.orientation.angle != null) 115 ? screen.orientation.angle : (window.orientation || 0); 116 var b = e.beta, g = e.gamma, ax, ay; 117 if (ang === 90) { ax = b; ay = -g; }
118 else if (ang === -90 || ang === 270) { ax = -b; ay = g; } 119 else { ax = g; ay = b; } // portrait 120 if (baseG === null) { // auto-calibrate to how it's held 121 baseG = ax; baseB = ay; 122 docEl.classList.add('mfx-gyro-live'); 123 hidePrime(); 124 } 125 gx = clamp((ax - baseG) / RANGE, -1, 1); 126 gy = clamp((ay - baseB) / RANGE, -1, 1); 127 if (!raf && (Math.abs(gx - curX) > WAKE || Math.abs(gy - curY) > WAKE)) wake(); 128 } 129 var gyroOn = false; 130 function startGyro() { 131 if (gyroOn) return; gyroOn = true; 132 try { window.addEventListener('deviceorientation', onOrient, true); } catch (e) {} 133 } 134 135 /* Remember a granted motion permission across visits, so a return visit never 136 re-prompts â and never even shows the pill. */ 137 var GRANT_KEY = 'mfx-motion-ok'; 138 function rememberedGrant() { try { return localStorage.getItem(GRANT_KEY) === '1'; } catch (e) { return false; } } 139 function rememberGrant() { try { localStorage.setItem(GRANT_KEY, '1'); } catch (e) {} } 140 141 /* iOS 13+ needs DeviceOrientationEvent.requestPermission() from a gesture. */ 142 function needsPerm() { 143 return typeof DeviceOrientationEvent !== 'undefined' && 144 typeof DeviceOrientationEvent.requestPermission === 'function'; 145 } 146 function requestGyro() { 147 if (needsPerm()) { 148 DeviceOrientationEvent.requestPermission() 149 .then(function (s) { if (s === 'granted') { rememberGrant(); startGyro(); } hidePrime(); }) 150 .catch(function () {}); 151 } else { startGyro(); } 152 } 153 154 /* The "Tilt for 3D" pill is shown lazily â only when a tiltable module is 155 scrolled into view (see IntersectionObserver below). On iOS, motion 156 permission is requested ONLY when the user taps that pill â never on an 157 incidental tap or scroll. Platforms that don't gate motion start directly. */ 158 var wantPrime = needsPerm(); 159 var primeDone = false; // user tapped the pill (granted OR denied) â never offer again 160 var engaged = false; // user has started scrolling â ok to surface the hint 161 if (!wantPrime) { 162 startGyro(); // no permission gate (Android etc.) 163 } else { 164 /* iOS. requestPermission() is NEVER called except from a tap on the pill. 165 If motion was granted before, optimistically add the listener now (this is 166 NOT a permission request): on an iOS that delivers motion to an already- 167 granted page, the gyro goes live and the pill never appears. Otherwise the 168 pill shows as usual â and tapping it is the only thing that ever asks. */ 169 if (rememberedGrant()) startGyro(); 170 addEventListener('scroll', function onFirstScroll() { 171 engaged = true; 172 removeEventListener('scroll', onFirstScroll); 173 syncPrime(); 174 }, { passive: true }); 175 } 176 177 /* ===================== TOUCH-DRAG (grab to tilt) =================== */ 178 /* Touching a 3D element lets the finger drive its tilt (like the desktop 179 * cursor); on release it eases back to the ambient gyro pose. Passive â 180 * never blocks scrolling. */ 181 function localVec(el, touch) { 182 var r = el.getBoundingClientRect(); 183 var px = (touch.clientX - r.left) / r.width; 184 var py = (touch.clientY - r.top) / r.height; 185 return { nx: clamp((px - 0.5) * 2, -1.3, 1.3), ny: clamp((py - 0.5) * 2, -1.3, 1.3) }; 186 } 187 TARGETS.forEach(function (t) { 188 t.el.addEventListener('touchstart', function (e) { 189 if (!e.touches || !e.touches[0]) return; 190 var v = localVec(t.el, e.touches[0]); 191 t.mx = v.nx; t.my = v.ny; t.blend = 1; t.drag = true; buzz(5); wake(); 192 }, { passive: true }); 193 t.el.addEventListener('touchmove', function (e) { 194 if (!t.drag || !e.touches || !e.touches[0]) return; 195 var v = localVec(t.el, e.touches[0]); 196 t.mx = v.nx; t.my = v.ny; wake(); 197 }, { passive: true }); 198 var rel = function () { t.drag = false; }; 199 t.el.addEventListener('touchend', rel, { passive: true }); 200 t.el.addEventListener('touchcancel', rel, { passive: true }); 201 }); 202 203 /* Decide whether the hint shows: 204 * ⢠not tapped + permission not granted + â¥1 3D module in view â show it, 205 * pinned at bottom-centre (it stays put; it does NOT track any module). 206 * ⢠no 3D module in view â fade out. 207 * ⢠tapped â granted OR denied (primeDone) â gone for good. 208 * ⢠motion live â gone for good. 209 * The hero on first load doesn't pop; it waits for the first scroll. */ 210 function firstVisibleTarget() { 211 for (var i = 0; i < TARGETS.length; i++) if (TARGETS[i].vis) return TARGETS[i].el; 212 return null; 213 } 214 function syncPrime() { 215 if (!wantPrime || primeDone || docEl.classList.contains('mfx-gyro-live')) { 216 if (primeEl) hidePrime(); 217 return; 218 } 219 if (firstVisibleTarget()) { 220 if (!engaged) return; // don't pop until the user scrolls 221 if (!primeEl) buildPrime(); // a 3D module is in view â show (pinned) 222 } else if (primeEl) { 223 hidePrime(); // no 3D module in view â dismiss 224 } 225 } 226 227 /* Only animate on-screen targets, and re-evaluate the hint on every change. */ 228 if ('IntersectionObserver' in window) { 229 var io = new IntersectionObserver(function (es) {
230 es.forEach(function (en) { 231 for (var i = 0; i < TARGETS.length; i++) { 232 if (TARGETS[i].el === en.target) { TARGETS[i].vis = en.isIntersecting; break; } 233 } 234 }); 235 syncPrime(); 236 wake(); // a target scrolled in â apply its current pose once 237 }, { rootMargin: '12% 0px' }); 238 TARGETS.forEach(function (t) { io.observe(t.el); }); 239 } 240 241 /* ============================== LOOP =============================== */ 242 /* IDLE-OUT (battery/heat): the rAF only runs while something is actually 243 moving â settling toward a new gyro pose, or a finger drag/blend in flight. 244 When the phone is still and untouched, it stops scheduling frames entirely, 245 so a page left open on a table costs ~0 CPU/GPU instead of compositing 246 6-9 tilted 3D layers at 60fps forever. It re-arms on a real tilt (past 247 sensor noise), a touch, a scroll-into-view, or tab-visible. */ 248 var curX = 0, curY = 0, raf = 0; 249 var SETTLE = 0.0016; // |gyro target â current| below this â arrived (stop) 250 var WAKE = 0.04; // gyro must move at least this (beyond still-phone noise) to re-arm 251 function anyVis() { for (var i = 0; i < TARGETS.length; i++) if (TARGETS[i].vis) return true; return false; } 252 function frame() { 253 curX += (gx - curX) * 0.14; // smooth the ambient gyro pose (snappier) 254 curY += (gy - curY) * 0.14; 255 var moving = false; 256 for (var i = 0; i < TARGETS.length; i++) { 257 var t = TARGETS[i]; 258 if (!t.vis) continue; 259 var nx = curX, ny = curY; 260 if (t.blend > 0) { // blend finger override over gyro 261 nx = curX + (t.mx - curX) * t.blend; 262 ny = curY + (t.my - curY) * t.blend; 263 if (!t.drag) { t.blend *= 0.90; if (t.blend < 0.01) t.blend = 0; } 264 } 265 t.apply(t.el, nx, ny); 266 if (t.drag || t.blend > 0) moving = true; 267 } 268 if (Math.abs(gx - curX) > SETTLE || Math.abs(gy - curY) > SETTLE) moving = true; 269 if (moving && !document.hidden) raf = requestAnimationFrame(frame); 270 else raf = 0; // settled & untouched â stop ticking 271 } 272 function play() { if (!raf && !document.hidden) raf = requestAnimationFrame(frame); } 273 function pause() { if (raf) { cancelAnimationFrame(raf); raf = 0; } } 274 function wake() { if (anyVis()) play(); } // re-arm only if a 3D module is on-screen 275 document.addEventListener('visibilitychange', function () { document.hidden ? pause() : wake(); }); 276 play(); 277 278 /* ============================ PRIME PILL =========================== */ 279 /* The hint is pinned at a fixed spot (bottom-centre of the viewport, via CSS) 280 so it never moves while you scroll. It simply appears whenever any 3D module 281 is in view and fades out once none are â no per-scroll tracking, no jumping 282 between modules. */ 283 var primeEl = null; 284 function buildPrime() { 285 if (primeEl) return; 286 var b = document.createElement('button'); 287 b.type = 'button'; 288 b.className = 'mfx-prime'; 289 b.setAttribute('aria-label', 'Enable motion-driven 3D'); 290 b.innerHTML = '<span class="mfx-prime__ico" aria-hidden="true">' + 291 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">' + 292 '<rect x="6" y="2" width="12" height="20" rx="3"/><path d="M12 6v4l3 1.5"/></svg></span>' + 293 '<span>Tilt for 3D</span>'; 294 b.addEventListener('click', function (e) { e.stopPropagation(); primeDone = true; hidePrime(); requestGyro(); }); 295 (document.body || docEl).appendChild(b); 296 primeEl = b; 297 } 298 function hidePrime() { 299 if (!primeEl) return; 300 var el = primeEl; primeEl = null; 301 el.classList.add('mfx-prime--out'); 302 setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 420); 303 } 304 305 /* ========================== INJECTED CSS ========================== */ 306 function injectCSS() { 307 if (document.getElementById('mfx-css')) return; 308 var s = document.createElement('style'); 309 s.id = 'mfx-css'; 310 s.textContent = 311 /* Re-enable the 3D the mobile breakpoints flatten, scoped to mfx-on and 312 driven by --mfx-rx / --mfx-ry. Higher specificity + !important so it 313 beats the `transform:none!important` mobile rules. */ 314 'html.mfx-on #fomDev{transform:perspective(680px) translateZ(20px) rotateX(var(--mfx-rx,3deg)) rotateY(var(--mfx-ry,-7deg))!important;transition:transform .09s linear;will-change:transform;backface-visibility:hidden}' + 315 'html.mfx-on #fomDev .scr img{transform:scale(1.13) translate(var(--mfx-px,0px),var(--mfx-py,0px))!important;transition:transform .09s linear;will-change:transform}' + 316 'html.mfx-on .prob__media{perspective:760px!important}' + 317 'html.mfx-on .prob__card{transform:rotateX(var(--mfx-rx,-7deg)) rotateY(var(--mfx-ry,9deg))!important;transition:transform .11s linear;animation:none!important;will-change:transform}' + 318 'html.mfx-on .eco3d{perspective:1000px!important}' + 319 'html.mfx-on .eco3d__scene{transform:rotateX(var(--mfx-rx,
31914deg)) rotateY(var(--mfx-ry,0deg))!important;transition:transform .14s linear;will-change:transform}' + 320 'html.mfx-on .clog__shot[data-tilt]{transform:perspective(640px) rotateX(var(--mfx-rx,0deg)) rotateY(var(--mfx-ry,-6deg))!important;transition:transform .11s linear;will-change:transform}' + 321 'html.mfx-on .cph{transform:perspective(700px) translateZ(16px) rotateX(var(--mfx-rx,4deg)) rotateY(var(--mfx-ry,-8deg))!important;transition:transform .1s linear;animation:none!important;will-change:transform}' + 322 'html.mfx-on .cph .cph__s img{transform:scale(1.11) translate(var(--mfx-px,0px),var(--mfx-py,0px))!important;transition:transform .1s linear;will-change:transform}' + 323 'html.mfx-on .vs-col{transform:perspective(820px) rotateX(var(--mfx-rx,0deg)) rotateY(var(--mfx-ry,-5deg))!important;transition:transform .13s linear;will-change:transform}' + 324 'html.mfx-on .vs-col::after{display:block!important;opacity:.55!important}' + 325 'html.mfx-on .stk__scene{perspective:1100px!important;perspective-origin:50% 40%}' + 326 'html.mfx-on .slab{transform:perspective(880px) rotateX(var(--mfx-rx,-7deg)) rotateY(var(--mfx-ry,0deg))!important;transition:transform .12s linear;will-change:transform;backface-visibility:hidden}' + 327 /* Prime pill */ 328 /* Pinned at bottom-centre of the viewport so it never moves on scroll; 329 transform only handles the horizontal centring + entrance/exit. */ 330 '.mfx-prime{position:fixed;left:50%;bottom:calc(22px + env(safe-area-inset-bottom,0px));top:auto;transform:translateX(-50%);z-index:120;' + 331 'display:inline-flex;align-items:center;gap:7px;padding:9px 15px 9px 11px;border-radius:999px;border:1px solid rgba(120,150,255,.34);' +
332 'background:color-mix(in srgb, var(--bg, #0b0f1a) 78%, transparent);backdrop-filter:blur(12px) saturate(1.3);-webkit-backdrop-filter:blur(12px) saturate(1.3);' + 333 'color:var(--text,#eef);font:600 13px/1 var(--font-ui,system-ui),system-ui,sans-serif;letter-spacing:.01em;' + 334 'box-shadow:0 10px 30px -12px rgba(30,60,160,.6);cursor:pointer;animation:mfxPrimeIn .5s cubic-bezier(.2,.8,.2,1) both;-webkit-tap-highlight-color:transparent}' + 335 '.mfx-prime__ico{display:inline-flex;width:18px;height:18px;color:var(--blue-400,#6f9bff);animation:mfxTilt 2.6s ease-in-out infinite}' + 336 '.mfx-prime__ico svg{width:18px;height:18px}' + 337 '.mfx-prime--out{animation:mfxPrimeOut .4s ease forwards}' + 338 '@keyframes mfxPrimeIn{from{opacity:0;transform:translateX(-50%) translateY(12px) scale(.96)}to{opacity:1;transform:translateX(-50%) translateY(0) scale(1)}}' + 339 '@keyframes mfxPrimeOut{to{opacity:0;transform:translateX(-50%) translateY(10px) scale(.96)}}' + 340 '@keyframes mfxTilt{0%,100%{transform:rotate(-12deg)}50%{transform:rotate(12deg)}}' + 341 '@media (prefers-reduced-motion: reduce){.mfx-prime,.mfx-prime__ico{animation:none}}'; 342 (document.head || docEl).appendChild(s); 343 } 344})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.