/* Homepage interactions ported from option 2: floating venture icons, giant words marquee, scroll-linked type, fireworks journey, themed 3D venture objects (three.js r128) and the scaled portfolio dashboard. */ (() => { 'use strict'; const $ = (s, c = document) => c.querySelector(s); const $$ = (s, c = document) => Array.from(c.querySelectorAll(s)); const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; const clamp = (v, a, b) => Math.min(b, Math.max(a, v)); const lerp = (a, b, t) => a + (b - a) * t; /* ---------- hero icons: one per venture ---------- */ const svg = (p) => ''; const ICONS = [ { n: 'AI Software', side: 'l', x: '8%', y: '24%', r: '6deg', d: '0s', p: '' }, { n: 'Marketing & AI Search', side: 'l', x: '15%', y: '50%', r: '-7deg', d: '-2s', p: '' }, { n: 'Spirits', side: 'l', x: '6%', y: '73%', r: '8deg', d: '-4s', p: '' }, { n: 'Education Technology', side: 'r', x: '8%', y: '22%', r: '-6deg', d: '-1s', p: '' }, { n: 'Cybersecurity', side: 'r', x: '15%', y: '50%', r: '7deg', d: '-3s', p: '' }, { n: 'E-Commerce', side: 'r', x: '6%', y: '73%', r: '-8deg', d: '-5s', p: '' }, ]; const tileHTML = (i) => '
' + svg(i.p) + '
' + i.n.replace('&', '&') + ''; const fl = $('#floaters'), ir = $('#iconRow'); if (fl && ir) { ICONS.forEach((i) => { const t = document.createElement('div'); t.className = 't ' + i.side; t.style.cssText = '--x:' + i.x + ';--y:' + i.y + ';--r:' + i.r + ';--d:' + i.d; t.innerHTML = tileHTML(i); fl.appendChild(t); const t2 = document.createElement('div'); t2.className = 't'; t2.innerHTML = tileHTML(i); ir.appendChild(t2); }); } /* emblem follows the cursor a little */ const emblemImg = $('#emblem img'); addEventListener('pointermove', (e) => { if (!emblemImg || reduce || e.pointerType !== 'mouse') return; const mx = (e.clientX / innerWidth) * 2 - 1, my = (e.clientY / innerHeight) * 2 - 1; emblemImg.style.setProperty('--ry', (mx * 16).toFixed(1) + 'deg'); emblemImg.style.setProperty('--rx', (-my * 11).toFixed(1) + 'deg'); }, { passive: true }); /* neon words switch on when they scroll into view */ const awords = $('.awords'); if (awords) new IntersectionObserver((es, o) => es.forEach((e) => { if (e.isIntersecting) { awords.classList.add('is-in'); o.disconnect(); } }), { threshold: .3 }).observe(awords); /* ---------- portfolio dashboard: scale the fixed-size cluster to its column ---------- */ const fit = $('.cluster-fit'), cl = fit && $('.cluster', fit); const sizeCluster = () => { if (!fit || !cl) return; if (innerWidth <= 1100) { fit.style.height = ''; cl.style.removeProperty('--s'); return; } const s = Math.min(1, fit.clientWidth / 760); cl.style.setProperty('--s', s.toFixed(4)); fit.style.height = Math.round(740 * s) + 'px'; }; sizeCluster(); addEventListener('resize', sizeCluster); /* ---------- scroll-linked playbook word ---------- */ const pbSec = $('#playbook'), pbWord = $('#pbword'); let ticking = false; function onScroll() { if (pbSec && pbWord) { const rp = pbSec.getBoundingClientRect(); const kp = clamp((innerHeight - rp.top) / (innerHeight + rp.height), 0, 1); pbWord.style.transform = 'translate3d(' + (reduce ? 0 : lerp(-6, -38, kp)).toFixed(1) + '%,-50%,0)'; } } addEventListener('scroll', () => { if (!ticking) { ticking = true; requestAnimationFrame(() => { ticking = false; onScroll(); }); } }, { passive: true }); addEventListener('resize', onScroll); onScroll(); const book = $('#book'); if (book) book.addEventListener('click', () => book.classList.toggle('tap')); /* ---------- giant words: moves on its own ---------- */ const wTrack = $('#wTrack'), wSpans = wTrack ? $$('span', wTrack) : [], wSec = $('#words'); let wx = 0, wVis = false; if (wSec) new IntersectionObserver((es) => { wVis = es[0].isIntersecting; }).observe(wSec); function marquee(dt) { const half = wTrack.scrollWidth / 2; if (!half) return; wx -= (reduce ? 0 : 58) * dt; if (wx <= -half) wx += half; wTrack.style.transform = 'translate3d(' + wx.toFixed(1) + 'px,0,0)'; const vw = innerWidth; wSpans.forEach((s) => { const r = s.getBoundingClientRect(); const d = Math.abs(r.left + r.width / 2 - vw / 2) / (vw * .55); s.style.opacity = (.25 + .75 * clamp(1 - d, 0, 1)).toFixed(3); }); } /* ---------- journey: fireworks, counters land in Dylan's stats row ---------- */ const jst = $('#jstage'), fw = $('#fw'); const FWOK = !!(jst && fw); const fx = FWOK ? fw.getContext('2d') : null; const pinEls = $$('.pn'), jx = $$('.jx'), jfinal = $('#jfinal'), jreplay = $('#jreplay'); const cEx = $('#cEx'), cInd = $('#cInd'), cYr = $('#cYr'); const PINX = [.12, .31, .5, .69, .88], PINF = [.22, .34, .46, .58, .72], LAUNCH = [200, 950, 1700, 2450, 3300]; const COLORS_DARK = ['#ffffff', '#9fe6ff', '#1fb7ff', '#3d8bff', '#d6ecff']; const COLORS_LIGHT = ['#1a7fff', '#0b5fd0', '#1fb7ff', '#5b6cff', '#0870c4']; const isLight = () => document.documentElement.getAttribute('data-theme') === 'light'; let FW = { w: 0, h: 0, floor: 0 }, PH = [0, 0, 0, 0, 0], rockets = [], parts = [], extra = []; let fwState = 'idle', fwStart = 0, fwVis = false, cleared = true, fwLaunched = [false, false, false, false, false], fwPlayed = false; const countUp = (el, to, suf) => { if (reduce) { el.textContent = to + (suf || ''); return; } const t0 = performance.now(), d = 1200; (function tick(n) { const k = clamp((n - t0) / d, 0, 1); el.textContent = Math.round(to * (1 - Math.pow(1 - k, 3))) + (k >= 1 ? (suf || '') : ''); if (k < 1) requestAnimationFrame(tick); })(t0); }; const pop = (el) => { el.classList.remove('pop'); void el.offsetWidth; el.classList.add('pop'); }; function sizeFW() { if (!FWOK) return; const r = jst.getBoundingClientRect(), dpr = Math.min(devicePixelRatio || 1, 2); fw.width = Math.round(r.width * dpr); fw.height = Math.round(r.height * dpr); fx.setTransform(dpr, 0, 0, dpr, 0, 0); FW.w = r.width; FW.h = r.height; FW.floor = FW.h * .83; pinEls.forEach((p, i) => { PH[i] = Math.round(FW.h * PINF[i] * .88); p.style.left = (PINX[i] * 100) + '%'; p.style.setProperty('--ph', PH[i] + 'px'); }); } function burst(x, y, n, spread) { const COLORS = isLight() ? COLORS_LIGHT : COLORS_DARK; const base = COLORS[(Math.random() * COLORS.length) | 0], sp = spread || 1; for (let i = 0; i < n; i++) { const a = Math.random() * Math.PI * 2, v = (50 + Math.random() * 190) * sp; parts.push({ x, y, px: x, py: y, vx: Math.cos(a) * v, vy: Math.sin(a) * v, life: 0, max: .85 + Math.random() * .7, c: Math.random() < .65 ? base : COLORS[(Math.random() * COLORS.length) | 0], w: Math.random() < .2 ? 2.2 : 1.3 }); } } function launch(i) { pinEls[i].classList.add('rise'); rockets.push({ i, x: PINX[i] * FW.w, y: FW.floor, ty: FW.floor - PH[i], t: 0, dur: .55, trail: [] }); } function arrive(i) { const R = { x: PINX[i] * FW.w, y: FW.floor - PH[i] }; pinEls[i].classList.add('done'); if (i < 4) { jx[i] && jx[i].classList.add('lit'); if (cEx) { cEx.textContent = i + 1; pop(cEx); } burst(R.x, R.y, 72, 1); } else { burst(R.x, R.y, 120, 1.3); extra = [{ t: .12, x: .3, y: .34 }, { t: .3, x: .68, y: .28 }, { t: .5, x: .5, y: .2 }, { t: .7, x: .15, y: .3 }, { t: .9, x: .85, y: .4 }]; jfinal.classList.add('show'); if (cInd) { countUp(cInd, 6); pop(cInd); } if (cYr) { countUp(cYr, 8, '+'); pop(cYr); } fwState = 'done'; fwPlayed = true; jreplay.classList.add('show'); } } function resetFW() { fwState = 'idle'; rockets = []; parts = []; extra = []; pinEls.forEach((p) => p.classList.remove('rise', 'done')); jx.forEach((x) => x.classList.remove('lit')); [cEx, cInd, cYr].forEach((el) => el && (el.textContent = '0')); jfinal.classList.remove('show'); jreplay.classList.remove('show'); fx.clearRect(0, 0, FW.w, FW.h); } function startFW() { if (fwState !== 'idle') return; sizeFW(); if (reduce) { pinEls.forEach((p) => p.classList.add('rise', 'done')); jx.forEach((x) => x.classList.add('lit')); cEx.textContent = '4'; cInd.textContent = '6'; cYr.textContent = '8+'; jfinal.classList.add('show'); fwState = 'done'; fwPlayed = true; return; } [cEx, cInd, cYr].forEach((el) => el && (el.textContent = '0')); fwState = 'play'; fwStart = performance.now(); fwLaunched = [false, false, false, false, false]; } if (FWOK) { new IntersectionObserver((es) => { const e = es[0]; fwVis = e.isIntersecting; if (e.intersectionRatio >= .45 && fwState === 'idle' && !fwPlayed) startFW(); }, { threshold: [0, .45] }).observe(jst); jreplay.addEventListener('click', () => { resetFW(); fwPlayed = false; startFW(); }); addEventListener('resize', sizeFW); sizeFW(); } function fwFrame(now, dt) { if (fwState === 'idle' && !parts.length && !rockets.length) { if (!cleared) { fx.clearRect(0, 0, FW.w, FW.h); cleared = true; } return; } cleared = false; if (fwState === 'play') { const el = now - fwStart; LAUNCH.forEach((t, i) => { if (!fwLaunched[i] && el >= t) { fwLaunched[i] = true; launch(i); } }); } if (extra.length) { extra.forEach((e) => { e.t -= dt; }); extra = extra.filter((e) => { if (e.t <= 0) { burst(e.x * FW.w, e.y * FW.h, 70, 1.1); return false; } return true; }); } const light = isLight(); fx.clearRect(0, 0, FW.w, FW.h); fx.globalCompositeOperation = light ? 'source-over' : 'lighter'; fx.lineCap = 'round'; rockets = rockets.filter((r) => { r.t += dt; const k = Math.min(1, r.t / r.dur), e = 1 - Math.pow(1 - k, 3); r.y = lerp(FW.floor, r.ty, e); r.trail.push({ x: r.x + (Math.random() - .5) * 1.5, y: r.y }); if (r.trail.length > 14) r.trail.shift(); for (let j = 1; j < r.trail.length; j++) { const a = j / r.trail.length; fx.strokeStyle = (light ? 'rgba(26,127,255,' : 'rgba(159,230,255,') + (a * .8) + ')'; fx.lineWidth = 2.4 * a; fx.beginPath(); fx.moveTo(r.trail[j - 1].x, r.trail[j - 1].y); fx.lineTo(r.trail[j].x, r.trail[j].y); fx.stroke(); } fx.fillStyle = light ? '#0b5fd0' : '#fff'; fx.beginPath(); fx.arc(r.x, r.y, 2.6, 0, 6.283); fx.fill(); if (k >= 1) { arrive(r.i); return false; } return true; }); parts = parts.filter((p) => { p.life += dt; if (p.life >= p.max) return false; p.vx *= Math.exp(-1.3 * dt); p.vy = p.vy * Math.exp(-1.3 * dt) + 130 * dt; p.px = p.x; p.py = p.y; p.x += p.vx * dt; p.y += p.vy * dt; fx.globalAlpha = 1 - Math.pow(p.life / p.max, 2); fx.strokeStyle = p.c; fx.lineWidth = p.w; fx.beginPath(); fx.moveTo(p.px, p.py); fx.lineTo(p.x, p.y); fx.stroke(); return true; }); fx.globalAlpha = 1; fx.globalCompositeOperation = 'source-over'; } /* ---------- main loop ---------- */ let last = performance.now(); (function loop(now) { requestAnimationFrame(loop); const dt = Math.min(.05, (now - last) / 1000); last = now; if (wVis && wTrack) marquee(dt); if (FWOK && (fwVis || fwState === 'play' || parts.length || rockets.length)) fwFrame(now, dt); })(last); /* ---------- ventures: tabs + a 3D object that matches each business ---------- */ const V = [ { n: 'MARKETING & AI SEARCH', d: 'Leading teams through a constantly changing search landscape.', c: '#1fb7ff', m: 'magnifier' }, { n: 'AI SOFTWARE', d: 'Building around new opportunities created by AI.', c: '#7a7bff', m: 'chip' }, { n: 'SPIRITS', d: 'Taking a consumer brand from idea to market.', c: '#3fa9ff', m: 'bottle' }, { n: 'EDUCATION TECHNOLOGY', d: 'Creating technology designed for real classrooms.', c: '#27d9ff', m: 'cap' }, { n: 'CYBERSECURITY', d: 'Solving security challenges for growing businesses.', c: '#8a6bff', m: 'lock' }, { n: 'E-COMMERCE', d: 'Turning product ideas into businesses built around real customers.', c: '#1a8bff', m: 'bag' }, ]; const vtabs = $('#vtabs'), vpanel = $('#vpanel'); if (!vtabs || !vpanel) return; const vt = $('#vt'), vname = $('#vname'), vdesc = $('#vdesc'), vnum = $('#vnum'); let vi = 0, vSwitch = () => {}; V.forEach((v, i) => { const b = document.createElement('button'); b.type = 'button'; b.setAttribute('role', 'tab'); b.textContent = v.n; b.addEventListener('click', () => setV(i)); vtabs.appendChild(b); }); const vbtns = $$('button', vtabs); function setV(i, first) { vi = (i + V.length) % V.length; vbtns.forEach((b, n) => b.setAttribute('aria-selected', String(n === vi))); const apply = () => { vname.textContent = V[vi].n; vdesc.textContent = V[vi].d; vnum.textContent = '0' + (vi + 1) + ' / 06'; vpanel.style.setProperty('--ac', V[vi].c); vt.classList.remove('out'); }; if (first || reduce) apply(); else { vt.classList.add('out'); setTimeout(apply, 200); } vSwitch(vi); const sel = vbtns[vi]; if (!first && sel && vtabs.scrollWidth > vtabs.clientWidth) vtabs.scrollTo({ left: sel.offsetLeft - vtabs.clientWidth / 2 + sel.clientWidth / 2, behavior: 'smooth' }); } $('#vprev').addEventListener('click', () => setV(vi - 1)); $('#vnext').addEventListener('click', () => setV(vi + 1)); vpanel.setAttribute('tabindex', '0'); vpanel.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight') setV(vi + 1); if (e.key === 'ArrowLeft') setV(vi - 1); }); let sx0 = null; vpanel.addEventListener('touchstart', (e) => { sx0 = e.touches[0].clientX; }, { passive: true }); vpanel.addEventListener('touchend', (e) => { if (sx0 === null) return; const dx = e.changedTouches[0].clientX - sx0; if (Math.abs(dx) > 50) setV(vi + (dx < 0 ? 1 : -1)); sx0 = null; }); setV(0, true); if (typeof THREE === 'undefined') return; (function () { const canvas = $('#vcanvas'); let r; try { r = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); } catch (e) { return; } r.setPixelRatio(Math.min(devicePixelRatio || 1, 2)); r.outputEncoding = THREE.sRGBEncoding; r.toneMapping = THREE.ACESFilmicToneMapping; r.toneMappingExposure = 1.2; // Studio reflections painted on a canvas, as in option 2. const ec = document.createElement('canvas'); ec.width = 1024; ec.height = 512; const e = ec.getContext('2d'); const g0 = e.createLinearGradient(0, 0, 0, 512); g0.addColorStop(0, '#1b2f5c'); g0.addColorStop(.5, '#05070f'); g0.addColorStop(1, '#000'); e.fillStyle = g0; e.fillRect(0, 0, 1024, 512); const box = (x, y, w, h, c, b) => { e.save(); e.shadowColor = c; e.shadowBlur = b; e.fillStyle = c; e.fillRect(x, y, w, h); e.restore(); }; box(120, 50, 300, 36, '#ffffff', 40); box(560, 40, 260, 30, '#d6e8ff', 40); box(250, 150, 26, 240, '#ffffff', 30); box(610, 130, 34, 260, '#2aa8ff', 34); box(860, 140, 30, 240, '#1a7fff', 34); box(40, 160, 26, 220, '#1fb7ff', 30); box(440, 200, 18, 200, '#bfe6ff', 26); const tex = new THREE.CanvasTexture(ec); tex.mapping = THREE.EquirectangularReflectionMapping; tex.encoding = THREE.sRGBEncoding; const env = new THREE.PMREMGenerator(r).fromEquirectangular(tex).texture; const scene = new THREE.Scene(), cam = new THREE.PerspectiveCamera(32, 1, .1, 100); cam.position.set(0, 0, 9); const metal = new THREE.MeshStandardMaterial({ color: 0x0b2c6e, metalness: .9, roughness: .22, envMap: env, envMapIntensity: 1.5, emissive: 0x1a7fff, emissiveIntensity: .15 }); const chrome = new THREE.MeshStandardMaterial({ color: 0xdfe8f5, metalness: 1, roughness: .12, envMap: env, envMapIntensity: 1.6 }); const glass = new THREE.MeshPhysicalMaterial({ color: 0x9fdcff, metalness: .1, roughness: .05, transmission: .6, transparent: true, opacity: .55, envMap: env, envMapIntensity: 1.4 }); const glow = new THREE.MeshBasicMaterial({ color: 0x9fe6ff }); const holder = new THREE.Group(); scene.add(holder); const key = new THREE.PointLight(0x1fb7ff, 60, 30); key.position.set(-4, 3, 6); scene.add(key); scene.add(new THREE.AmbientLight(0x2a4a99, .8)); const k2 = new THREE.DirectionalLight(0xffffff, 1.1); k2.position.set(3, 4, 5); scene.add(k2); const M = (geo, mat) => new THREE.Mesh(geo, mat); const BUILD = { // Marketing & AI Search: a magnifying glass over rising bars magnifier() { const g = new THREE.Group(); const ring = M(new THREE.TorusGeometry(1.05, .17, 24, 64), metal); const lens = M(new THREE.CircleGeometry(1.0, 48), glass); const handle = M(new THREE.CylinderGeometry(.16, .2, 1.6, 24), chrome); handle.position.set(1.15, -1.15, 0); handle.rotation.z = Math.PI / 4; const lensGroup = new THREE.Group(); lensGroup.add(ring, lens, handle); lensGroup.position.set(.25, .35, .35); [[-.75, .45], [-.25, .8], [.25, 1.15]].forEach(([x, h], i) => { const bar = M(new THREE.BoxGeometry(.32, h, .32), i === 2 ? glow : metal); bar.position.set(x - .1, -1.05 + h / 2, -.6); g.add(bar); }); g.add(lensGroup); return g; }, // AI Software: a processor chip with pins and a glowing core chip() { const g = new THREE.Group(); g.add(M(new THREE.BoxGeometry(2.2, 2.2, .32), metal)); const core = M(new THREE.BoxGeometry(1.1, 1.1, .1), glow); core.position.z = .2; g.add(core); const pinGeo = new THREE.BoxGeometry(.14, .42, .1); for (let s = 0; s < 4; s++) for (let i = 0; i < 5; i++) { const p = M(pinGeo, chrome); const off = -0.8 + i * .4; if (s === 0) p.position.set(off, 1.3, 0); if (s === 1) p.position.set(off, -1.3, 0); if (s === 2) { p.position.set(1.3, off, 0); p.rotation.z = Math.PI / 2; } if (s === 3) { p.position.set(-1.3, off, 0); p.rotation.z = Math.PI / 2; } g.add(p); } g.rotation.x = -.35; return g; }, // Spirits: a bottle (lathe profile) with a chrome cap bottle() { const g = new THREE.Group(); const pts = [[0, -1.7], [.78, -1.7], [.86, -1.55], [.86, .35], [.7, .7], [.32, 1.05], [.27, 1.55], [0, 1.55]].map(([x, y]) => new THREE.Vector2(x, y)); g.add(M(new THREE.LatheGeometry(pts, 48), glass)); const liquid = M(new THREE.CylinderGeometry(.78, .78, 1.6, 48), metal); liquid.position.y = -.85; g.add(liquid); const label = M(new THREE.CylinderGeometry(.875, .875, .55, 48, 1, true), glow); label.position.y = -.35; g.add(label); const cap = M(new THREE.CylinderGeometry(.3, .3, .4, 32), chrome); cap.position.y = 1.72; g.add(cap); g.scale.setScalar(.85); return g; }, // Education Technology: a graduation cap cap() { const g = new THREE.Group(); const board = M(new THREE.BoxGeometry(2.6, .12, 2.6), metal); board.rotation.y = Math.PI / 4; board.position.y = .45; g.add(board); g.add(M(new THREE.CylinderGeometry(.95, 1.05, .9, 40, 1, true), chrome)); const bottom = M(new THREE.CircleGeometry(.95, 40), metal); bottom.rotation.x = Math.PI / 2; bottom.position.y = -.45; g.add(bottom); const button = M(new THREE.SphereGeometry(.12, 16, 16), glow); button.position.y = .55; g.add(button); const cord = M(new THREE.CylinderGeometry(.03, .03, 1.3, 8), glow); cord.position.set(.85, -.1, .85); g.add(cord); const tassel = M(new THREE.ConeGeometry(.12, .4, 16), glow); tassel.position.set(.85, -.85, .85); g.add(tassel); g.rotation.x = .35; return g; }, // Cybersecurity: a padlock lock() { const g = new THREE.Group(); const body = M(new THREE.BoxGeometry(2, 1.6, .7), metal); body.position.y = -.5; g.add(body); const shackle = M(new THREE.TorusGeometry(.62, .16, 20, 48, Math.PI), chrome); shackle.position.y = .3; g.add(shackle); [-.62, .62].forEach((x) => { const leg = M(new THREE.CylinderGeometry(.16, .16, .5, 20), chrome); leg.position.set(x, .05, 0); g.add(leg); }); const hole = M(new THREE.CylinderGeometry(.17, .17, .1, 24), glow); hole.rotation.x = Math.PI / 2; hole.position.set(0, -.35, .36); g.add(hole); const slot = M(new THREE.BoxGeometry(.12, .4, .1), glow); slot.position.set(0, -.62, .36); g.add(slot); return g; }, // E-commerce: a shopping bag with handles bag() { const g = new THREE.Group(); g.add(M(new THREE.BoxGeometry(2, 2.2, .9), metal)); const band = M(new THREE.BoxGeometry(2.02, .28, .92), glow); band.position.y = .6; g.add(band); [-.42, .42].forEach((z) => { const h = M(new THREE.TorusGeometry(.5, .09, 16, 40, Math.PI), chrome); h.position.set(0, 1.1, z * .9); g.add(h); }); const tag = M(new THREE.BoxGeometry(.5, .32, .05), chrome); tag.position.set(.55, -.45, .48); tag.rotation.z = -.25; g.add(tag); g.rotation.x = .1; g.scale.setScalar(.9); return g; }, }; let model = null, born = 0; const dispose = (o) => o.traverse((c) => { if (c.geometry) c.geometry.dispose(); }); vSwitch = (i) => { if (model) { holder.remove(model); dispose(model); } model = BUILD[V[i].m](); holder.add(model); born = performance.now(); key.color.set(V[i].c); metal.emissive.set(V[i].c); glow.color.set(V[i].c).lerp(new THREE.Color(0xffffff), .45); }; vSwitch(vi); let vis = true; new IntersectionObserver((es) => { vis = es[0].isIntersecting; }).observe(canvas); const resize = () => { const w = canvas.clientWidth || 1, h = canvas.clientHeight || 1; r.setSize(w, h, false); cam.aspect = w / h; cam.updateProjectionMatrix(); }; addEventListener('resize', resize); resize(); let mx = 0, my = 0; vpanel.addEventListener('pointermove', (ev) => { const b = vpanel.getBoundingClientRect(); mx = ((ev.clientX - b.left) / b.width - .5) * 2; my = ((ev.clientY - b.top) / b.height - .5) * 2; }); let lt = performance.now(); (function loop(now) { requestAnimationFrame(loop); if (!vis) return; const dt = Math.min(.05, (now - lt) / 1000); lt = now; const k = reduce ? 1 : clamp((now - born) / 650, 0, 1), sc = reduce ? 1 : (.3 + .7 * (1 - Math.pow(1 - k, 3))); holder.scale.setScalar(sc); if (!reduce) { holder.rotation.y += dt * (.45 + (1 - k) * 6); } holder.rotation.x += ((my * .35) - holder.rotation.x) * .06; holder.position.x += ((mx * .5) - holder.position.x) * .06; r.render(scene, cam); })(lt); })(); })();