c8ad2d9989
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
380 lines
22 KiB
JavaScript
380 lines
22 KiB
JavaScript
/* 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;
|
|
const T = (s) => (window.__i18n && window.__i18n[s]) || s;
|
|
|
|
/* ---------- hero icons: one per venture ---------- */
|
|
const svg = (p) => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + p + '</svg>';
|
|
const ICONS = [
|
|
{ n: 'AI Software', side: 'l', x: '8%', y: '24%', r: '6deg', d: '0s', p: '<rect x="6" y="6" width="12" height="12" rx="2.5"/><path d="M9 3v3M15 3v3M9 18v3M15 18v3M3 9h3M3 15h3M18 9h3M18 15h3"/><path d="M9.6 15v-3.6L12 9.4l2.4 2V15M9.6 13.4h4.8"/>' },
|
|
{ n: 'Marketing & AI Search', side: 'l', x: '15%', y: '50%', r: '-7deg', d: '-2s', p: '<circle cx="10.5" cy="10.5" r="6.2"/><path d="m15.2 15.2 5 5"/><path d="m7.6 12.6 2.2-2.4 1.8 1.6 2.6-3"/>' },
|
|
{ n: 'Education Technology', side: 'r', x: '8%', y: '30%', r: '-6deg', d: '-1s', p: '<path d="m2.5 9.5 9.5-5 9.5 5-9.5 5z"/><path d="M6.5 12v4.2c0 1.2 2.5 2.8 5.5 2.8s5.5-1.6 5.5-2.8V12"/><path d="M21.5 9.5v6"/>' },
|
|
{ n: 'Cybersecurity', side: 'r', x: '12%', y: '62%', r: '7deg', d: '-3s', p: '<path d="M12 3 4.5 6v5.5c0 4.5 3.2 7.8 7.5 9.5 4.3-1.7 7.5-5 7.5-9.5V6z"/><rect x="9" y="10.8" width="6" height="5" rx="1"/><path d="M10.2 10.8V9.4a1.8 1.8 0 0 1 3.6 0v1.4"/>' },
|
|
{ n: 'E-Commerce', side: 'l', x: '6%', y: '73%', r: '8deg', d: '-5s', p: '<path d="M5 8h14l-1 12H6z"/><path d="M9 8V7a3 3 0 0 1 6 0v1"/>' },
|
|
];
|
|
const tileHTML = (i) => '<div class="tile">' + svg(i.p) + '</div><span class="tl">' + T(i.n).replace('&', '&') + '</span>';
|
|
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: '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 = T(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 = T(V[vi].n); vdesc.textContent = T(V[vi].d); vnum.textContent = '0' + (vi + 1) + ' / 0' + V.length; 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');
|
|
// In Arabic (right to left) "next" is to the left.
|
|
const fwd = document.documentElement.dir === 'rtl' ? -1 : 1;
|
|
vpanel.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight') setV(vi + fwd); if (e.key === 'ArrowLeft') setV(vi - fwd); });
|
|
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 ? fwd : -fwd)); 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;
|
|
},
|
|
// 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);
|
|
})();
|
|
})();
|