Merge option 2 homepage and visual language into the site
Rebuild the homepage on option 2's sections and effects (3D DNA, spinning cube, giant words, fireworks journey, neon type, chrome ribbon) with copy kept verbatim from the content document. Each venture now has its own 3D object, the portfolio dashboard sits beside the DNA, the journey uses the original stats row and the playbook keeps our book. Inner pages adopt the same navy, glass and gradient skin, with dark as the default theme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
/* 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) => '<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: 'Spirits', side: 'l', x: '6%', y: '73%', r: '8deg', d: '-4s', p: '<path d="M10 2.5h4M10.5 2.5v4.2L8.6 9.4a3 3 0 0 0-.6 1.8V20a1.5 1.5 0 0 0 1.5 1.5h5A1.5 1.5 0 0 0 16 20v-8.8a3 3 0 0 0-.6-1.8l-1.9-2.7V2.5"/><path d="M8 13.5h8v4H8z"/>' },
|
||||
{ n: 'Education Technology', side: 'r', x: '8%', y: '22%', 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: '15%', y: '50%', 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: 'r', 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">' + 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: "still building" rows + playbook word ---------- */
|
||||
const bst = $('#bstack'), rows = bst ? $$('.r', bst) : [], pbSec = $('#playbook'), pbWord = $('#pbword');
|
||||
let ticking = false;
|
||||
function onScroll() {
|
||||
if (bst) {
|
||||
const r = bst.getBoundingClientRect();
|
||||
const k = clamp((innerHeight - r.top) / (innerHeight + r.height), 0, 1) - .5;
|
||||
rows.forEach((el, i) => {
|
||||
const dir = i % 2 ? 1 : -1;
|
||||
const op = 1 - Math.abs(i - 2.5) / 3.2;
|
||||
el.style.opacity = clamp(op, .18, 1).toFixed(2);
|
||||
el.style.transform = reduce ? '' : 'translate3d(' + (k * dir * (120 + i * 14)).toFixed(1) + 'px,0,0)';
|
||||
});
|
||||
}
|
||||
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 = ['#ffffff', '#9fe6ff', '#1fb7ff', '#3d8bff', '#d6ecff'];
|
||||
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 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; }); }
|
||||
fx.clearRect(0, 0, FW.w, FW.h); fx.globalCompositeOperation = '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 = '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 = '#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';
|
||||
}
|
||||
|
||||
/* ---------- 3D DNA: fall back to the still image without WebGL ---------- */
|
||||
setTimeout(() => {
|
||||
const dna = $('#dna'), fb = $('#dnaFallback');
|
||||
const ok = dna && dna.shadowRoot && dna.shadowRoot.querySelector('canvas');
|
||||
if (!ok && fb) { if (dna) dna.hidden = true; fb.hidden = false; }
|
||||
}, 1500);
|
||||
|
||||
/* ---------- 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);
|
||||
})();
|
||||
})();
|
||||
Reference in New Issue
Block a user