// =========================================================================== // PLAIN TEXT JS SOURCE FILE — iam/marketing v5 effects (r3) // =========================================================================== // // This file contains JavaScript source code (Lenis, GSAP sync, magnetic CTAs). // It is loaded as a remote script by the front page HTML. // Do not edit directly — edit v3-effects.js in the iam-marketing-wp-theme repo // and re-upload via the deploy pipeline. // // =========================================================================== // ════════════════════════════════════════════════════════════════ // v3 effects · the $50k touches // ──────────────────────────────────────────────────────────────── // 1. Lenis smooth scroll — inertia-based, cinematic feel // 2. Spotlight borders — cursor-aware lighting on .v3-spotlight cards // 3. Magnetic CTAs — subtle pull toward cursor on .v3-magnetic buttons // 4. Count-up numbers — animate values 0 → target on viewport entry // 5. CSS injection for the above effects so v3 only needs this one file // ════════════════════════════════════════════════════════════════ (function () { 'use strict'; const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; // ── Inject CSS scaffolding ──────────────────────────────────── const style = document.createElement('style'); style.textContent = ` /* SPOTLIGHT BORDERS — cursor-aware glow on cards */ .v3-spotlight { position: relative; isolation: isolate; transition: border-color 280ms cubic-bezier(0.22,1,0.36,1); } .v3-spotlight::before { content: ''; position: absolute; inset: -1px; border-radius: inherit; pointer-events: none; background: radial-gradient( circle 400px at var(--mx, 50%) var(--my, 50%), rgba(197,216,109,0.30), transparent 40% ); opacity: 0; transition: opacity 380ms cubic-bezier(0.22,1,0.36,1); z-index: -1; -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; padding: 1px; } .v3-spotlight:hover::before { opacity: 1; } .v3-spotlight:hover { border-color: rgba(197,216,109,0.4); } /* v5+ · MAGNETIC button — Sonner-curve snap-back. cubic-bezier(0.32,0.72,0,1) is Emil's signature ease (drawer/iOS-like). Snap-back at 400ms gives weight without feeling sluggish; pull-in shortens to 180ms because that's the responsive direction. */ .v3-magnetic { transition: transform 400ms cubic-bezier(0.32, 0.72, 0, 1); will-change: transform; } .v3-magnetic:hover { transition-duration: 180ms; } /* v5+ · HERO WORD MASK REVEAL — Emil's signature ease-out (Sonner/Vaul curve). 700ms over 900ms because smaller, more deliberate motion. Stagger handled inline via --word-d (0ms / 160ms in hero). */ .v3-word-mask { display: inline-block; overflow: hidden; padding-bottom: 0.06em; vertical-align: bottom; } .v3-word-mask > span { display: inline-block; transform: translateY(102%); opacity: 0; transition: transform 700ms cubic-bezier(0.16, 1, 0.3, 1) var(--word-d, 0ms), opacity 700ms cubic-bezier(0.16, 1, 0.3, 1) var(--word-d, 0ms); } .v3-word-mask.is-revealed > span { transform: translateY(0); opacity: 1; } /* COUNTUP — tabular nums + slight glow on first reveal */ .v3-countup { font-variant-numeric: tabular-nums; } /* CUSTOM CURSOR — chartreuse dot + ring · hides native, grows on hover */ @media (hover: hover) and (pointer: fine) { .v3-cursor-on, .v3-cursor-on * { cursor: none !important; } .v3-cursor-dot, .v3-cursor-ring { position: fixed; top: 0; left: 0; pointer-events: none; z-index: 99998; border-radius: 50%; will-change: transform; transform: translate3d(-100px, -100px, 0); } .v3-cursor-dot { width: 6px; height: 6px; background: #C5D86D; margin: -3px 0 0 -3px; transition: opacity 200ms cubic-bezier(0.22,1,0.36,1); } .v3-cursor-ring { width: 36px; height: 36px; border: 1px solid rgba(197,216,109,0.55); margin: -18px 0 0 -18px; transition: width 280ms cubic-bezier(0.22,1,0.36,1), height 280ms cubic-bezier(0.22,1,0.36,1), margin 280ms cubic-bezier(0.22,1,0.36,1), background 280ms cubic-bezier(0.22,1,0.36,1), border-color 280ms cubic-bezier(0.22,1,0.36,1), opacity 200ms cubic-bezier(0.22,1,0.36,1); backdrop-filter: invert(0.04); } .v3-cursor-on.v3-cursor-near .v3-cursor-ring { width: 64px; height: 64px; margin: -32px 0 0 -32px; border-color: rgba(197,216,109,0.95); background: rgba(197,216,109,0.08); } .v3-cursor-on.v3-cursor-near .v3-cursor-dot { opacity: 0; } .v3-cursor-on.v3-cursor-cta .v3-cursor-ring { width: 56px; height: 56px; margin: -28px 0 0 -28px; background: #C5D86D; border-color: #C5D86D; } .v3-cursor-on.v3-cursor-cta .v3-cursor-ring::after { content: '→'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font: 700 16px 'JetBrains Mono', monospace; color: #050506; animation: v3-cursor-arrow 800ms ease-in-out infinite; } @keyframes v3-cursor-arrow { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(3px); } } .v3-cursor-on.v3-cursor-text .v3-cursor-ring { width: 4px; height: 28px; margin: -14px 0 0 -2px; border-radius: 1px; background: rgba(197,216,109,0.85); border: none; } } @media (prefers-reduced-motion: reduce) { .v3-magnetic { transition: none !important; transform: none !important; } .v3-word-mask > span { transform: none !important; opacity: 1 !important; transition: none !important; } .v3-cursor-dot, .v3-cursor-ring { display: none !important; } .v3-cursor-on, .v3-cursor-on * { cursor: auto !important; } } `; document.head.appendChild(style); // ── 1. LENIS SMOOTH SCROLL ──────────────────────────────────── let lenis = null; function initLenis() { if (reduced || !window.Lenis) return; lenis = new window.Lenis({ duration: 1.2, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // expo-out smoothWheel: true, wheelMultiplier: 1.0, touchMultiplier: 1.5, }); // ── Single rAF driver: gsap.ticker if present, else requestAnimationFrame. // NEVER drive lenis.raf from BOTH — double-driving stutters scrub & breaks pin math. if (window.gsap && window.ScrollTrigger) { window.gsap.ticker.add((t) => lenis.raf(t * 1000)); window.gsap.ticker.lagSmoothing(0); lenis.on('scroll', window.ScrollTrigger.update); // Refresh AFTER Lenis is wired so any ScrollTriggers created earlier // (e.g. HomeDiff §6 pin) recompute start/end against the new scroll source. window.ScrollTrigger.refresh(); } else { function raf(time) { lenis.raf(time); requestAnimationFrame(raf); } requestAnimationFrame(raf); } // Expose for debugging / refresh hooks window.__lenis = lenis; } // ── 2. SPOTLIGHT BORDERS ────────────────────────────────────── function wireSpotlights() { document.addEventListener('mousemove', (e) => { const targets = document.querySelectorAll('.v3-spotlight'); for (const el of targets) { const r = el.getBoundingClientRect(); if (e.clientX < r.left - 100 || e.clientX > r.right + 100 || e.clientY < r.top - 100 || e.clientY > r.bottom + 100) continue; const x = ((e.clientX - r.left) / r.width) * 100; const y = ((e.clientY - r.top) / r.height) * 100; el.style.setProperty('--mx', x + '%'); el.style.setProperty('--my', y + '%'); } }, { passive: true }); } // ── 3. MAGNETIC CTAs ────────────────────────────────────────── function wireMagnetic() { if (reduced) return; document.addEventListener('mouseover', (e) => { const el = e.target.closest('.v3-magnetic'); if (!el || el.dataset.magneticBound) return; el.dataset.magneticBound = '1'; const onMove = (ev) => { const r = el.getBoundingClientRect(); const dx = ev.clientX - (r.left + r.width / 2); const dy = ev.clientY - (r.top + r.height / 2); const strength = 0.30; // v5+ · 22% → 30%, more confident pull (still subtle) el.style.transform = `translate3d(${dx * strength}px, ${dy * strength}px, 0)`; }; const onLeave = () => { el.style.transform = 'translate3d(0,0,0)'; el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); delete el.dataset.magneticBound; }; el.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); }, { passive: true }); } // ── 4. COUNT-UP on viewport entry ───────────────────────────── function wireCountUps() { const elements = () => document.querySelectorAll('.v3-countup[data-target]'); const animate = (el) => { if (el.dataset.counted) return; el.dataset.counted = '1'; const target = parseFloat(el.dataset.target); const decimals = parseInt(el.dataset.decimals || '0', 10); const prefix = el.dataset.prefix || ''; const suffix = el.dataset.suffix || ''; const dur = parseInt(el.dataset.duration || '1400', 10); const start = performance.now(); const easeOutExpo = (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t); const tick = (now) => { const t = Math.min(1, (now - start) / dur); const v = target * easeOutExpo(t); el.textContent = `${prefix}${v.toFixed(decimals)}${suffix}`; if (t < 1) requestAnimationFrame(tick); else el.textContent = `${prefix}${target.toFixed(decimals)}${suffix}`; }; requestAnimationFrame(tick); }; const obs = new IntersectionObserver((entries) => { for (const e of entries) if (e.isIntersecting) animate(e.target); }, { threshold: 0.4 }); // Re-scan periodically since React mounts can be deferred let scans = 0; const scan = () => { elements().forEach((el) => { if (!el.dataset.observed) { el.dataset.observed = '1'; obs.observe(el); } }); if (scans++ < 10) setTimeout(scan, 600); }; scan(); } // ── 5. WORD-MASK REVEAL on viewport entry ───────────────────── function wireWordReveals() { if (reduced) { document.querySelectorAll('.v3-word-mask').forEach(el => el.classList.add('is-revealed')); return; } const obs = new IntersectionObserver((entries) => { for (const e of entries) if (e.isIntersecting) { e.target.classList.add('is-revealed'); obs.unobserve(e.target); } }, { threshold: 0.3 }); let scans = 0; const scan = () => { document.querySelectorAll('.v3-word-mask:not([data-mask-observed])').forEach((el) => { el.dataset.maskObserved = '1'; obs.observe(el); }); if (scans++ < 10) setTimeout(scan, 600); }; scan(); } // ── 6. CUSTOM CURSOR · chartreuse dot + ring · grows on hover of links/buttons function wireCursor() { if (reduced) return; // Skip touch devices if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches) return; const dot = document.createElement('div'); dot.className = 'v3-cursor-dot'; const ring = document.createElement('div'); ring.className = 'v3-cursor-ring'; document.body.append(dot, ring); document.documentElement.classList.add('v3-cursor-on'); let mx = -100, my = -100, rx = -100, ry = -100; const lerp = (a, b, t) => a + (b - a) * t; document.addEventListener('mousemove', (e) => { mx = e.clientX; my = e.clientY; // dot tracks cursor exactly (no smoothing) dot.style.transform = `translate3d(${mx}px, ${my}px, 0)`; // detect interactive context for state classes const t = e.target; const cta = t && t.closest('.v3-magnetic') !== null; const interactive = !cta && t && (t.closest('a, button, [role="button"], .ic-pin, label, input, textarea, select') !== null); const text = t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.closest('p, h1, h2, h3, h4, h5, h6, [contenteditable]')); const root = document.documentElement; root.classList.toggle('v3-cursor-cta', !!cta); root.classList.toggle('v3-cursor-near', !!interactive); root.classList.toggle('v3-cursor-text', !cta && !interactive && !!text); }, { passive: true }); document.addEventListener('mouseleave', () => { dot.style.opacity = '0'; ring.style.opacity = '0'; }); document.addEventListener('mouseenter', () => { dot.style.opacity = '1'; ring.style.opacity = '1'; }); // Smooth ring with rAF lerp const tick = () => { rx = lerp(rx, mx, 0.18); ry = lerp(ry, my, 0.18); ring.style.transform = `translate3d(${rx}px, ${ry}px, 0)`; requestAnimationFrame(tick); }; requestAnimationFrame(tick); } // ── Boot — wait for React to render ─────────────────────────── // Custom cursor removed per brief §9 ("perf cost, broke OS affordances"). function boot() { initLenis(); wireSpotlights(); wireMagnetic(); wireCountUps(); wireWordReveals(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(boot, 600)); } else { setTimeout(boot, 600); } })();