Bri-Tunes/public/js/overlay-objects.js

271 lines
9.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as THREE from 'three';
const POOL = ['torusKnot','icosahedron','hyperboloid','mobius','globe','gyroscope','trefoil','saturn'];
const COLORS = [0x00e5ff, 0x8f5bff, 0xff2bd6];
const MIN_VW = 1280; // viewport width below which we hide
const FADE = 800; // ms — CSS opacity transition duration
const cycleMs = () => 20000 + Math.random() * 10000; // 2030 s
// Calculate canvas size and edge offset so the object center sits exactly
// halfway between the screen edge and the lightbox edge.
function getSizing() {
const vw = window.innerWidth;
const modal = Math.min(940, 0.92 * vw); // mirrors #np-modal width rule
const sideWidth = (vw - modal) / 2; // available space each side
const size = Math.min(480, Math.floor(sideWidth * 1.4));
const offset = Math.max(8, Math.floor(sideWidth / 2 - size / 2));
return { size, offset };
}
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
// ── Geometry builders ────────────────────────────────────────────────────────
function buildObject(name, color) {
const mat = new THREE.MeshBasicMaterial({ color, wireframe: true });
const lmat = () => new THREE.LineBasicMaterial({ color });
const g = new THREE.Group();
switch (name) {
case 'torusKnot':
g.add(new THREE.Mesh(new THREE.TorusKnotGeometry(0.8, 0.28, 120, 16), mat));
break;
case 'trefoil':
g.add(new THREE.Mesh(new THREE.TorusKnotGeometry(0.8, 0.28, 120, 16, 2, 3), mat));
break;
case 'icosahedron':
g.add(new THREE.Mesh(new THREE.IcosahedronGeometry(1, 1), mat));
break;
case 'globe':
g.add(new THREE.Mesh(new THREE.SphereGeometry(1, 18, 12), mat));
break;
case 'hyperboloid': {
const N = 24;
const struts = [], topRing = [], botRing = [];
for (let i = 0; i < N; i++) {
const a0 = (i / N) * Math.PI * 2;
const a1 = ((i + 0.5) / N) * Math.PI * 2;
struts.push(Math.cos(a0), 1, Math.sin(a0), Math.cos(a1), -1, Math.sin(a1));
topRing.push(Math.cos(a0), 1, Math.sin(a0));
botRing.push(Math.cos(a1), -1, Math.sin(a1));
}
// close rings
topRing.push(topRing[0], topRing[1], topRing[2]);
botRing.push(botRing[0], botRing[1], botRing[2]);
const sg = new THREE.BufferGeometry();
sg.setAttribute('position', new THREE.Float32BufferAttribute(struts, 3));
g.add(new THREE.LineSegments(sg, lmat()));
const tg = new THREE.BufferGeometry();
tg.setAttribute('position', new THREE.Float32BufferAttribute(topRing, 3));
g.add(new THREE.Line(tg, lmat()));
const bg = new THREE.BufferGeometry();
bg.setAttribute('position', new THREE.Float32BufferAttribute(botRing, 3));
g.add(new THREE.Line(bg, lmat()));
break;
}
case 'mobius': {
const uSeg = 80, vSeg = 8;
const verts = [], idx = [];
for (let ui = 0; ui <= uSeg; ui++) {
const u = (ui / uSeg) * Math.PI * 2;
for (let vi = 0; vi <= vSeg; vi++) {
const v = (vi / vSeg - 0.5) * 0.8;
verts.push(
(1 + v * Math.cos(u / 2)) * Math.cos(u),
(1 + v * Math.cos(u / 2)) * Math.sin(u),
v * Math.sin(u / 2)
);
}
}
for (let ui = 0; ui < uSeg; ui++) {
for (let vi = 0; vi < vSeg; vi++) {
const a = ui * (vSeg + 1) + vi;
const b = (ui + 1) * (vSeg + 1) + vi;
const c = b + 1, d = a + 1;
idx.push(a, b, c, a, c, d);
}
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
geo.setIndex(idx);
g.add(new THREE.Mesh(geo, mat));
break;
}
case 'gyroscope': {
const ringGeo = new THREE.TorusGeometry(1, 0.03, 6, 64);
const speeds = [
{ rx: 0, ry: 0, rz: 0, ax: 0, ay: 0.009, az: 0 },
{ rx: Math.PI / 2, ry: 0, rz: 0, ax: 0.006,ay: 0, az: 0 },
{ rx: Math.PI / 4, ry: Math.PI / 4, rz: 0, ax: 0, ay: 0, az: 0.012 },
];
speeds.forEach(s => {
const ring = new THREE.Mesh(ringGeo, mat.clone());
ring.rotation.set(s.rx, s.ry, s.rz);
ring.userData = { ax: s.ax, ay: s.ay, az: s.az };
g.add(ring);
});
g.userData.isGyroscope = true;
break;
}
case 'saturn': {
g.add(new THREE.Mesh(new THREE.SphereGeometry(0.55, 16, 12), mat.clone()));
const ring = new THREE.Mesh(new THREE.TorusGeometry(1.05, 0.04, 6, 64), mat.clone());
ring.rotation.x = Math.PI * 0.28;
g.add(ring);
break;
}
}
g.scale.setScalar(1.1);
return g;
}
// ── SideCanvas ───────────────────────────────────────────────────────────────
class SideCanvas {
constructor(side) {
this.side = side;
this.alive = false;
this.animId = null;
this.timer = null;
this.object = null;
this.canvas = document.createElement('canvas');
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
this.camera.position.z = 4.2; // far enough that rotating objects don't clip
this.renderer = null; // created in mount() once size is known
}
mount() {
this.alive = true;
const { size, offset } = getSizing();
this.canvas.width = size;
this.canvas.height = size;
this.canvas.className = 'oo-canvas'; // z-index managed by CSS sibling rule
this.canvas.style.cssText = [
'position:fixed',
'top:50%',
'transform:translateY(-50%)',
this.side === 'left' ? `left:${offset}px` : `right:${offset}px`,
`width:${size}px`,
`height:${size}px`,
'pointer-events:none',
'opacity:0',
`transition:opacity ${FADE}ms ease`,
].join(';');
this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, alpha: true, antialias: true });
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.setSize(size, size);
this.renderer.setClearColor(0x000000, 0);
document.body.appendChild(this.canvas);
this._spawn();
this._tick();
}
unmount() {
this.alive = false;
cancelAnimationFrame(this.animId);
clearTimeout(this.timer);
this.canvas.style.opacity = '0';
setTimeout(() => {
this.canvas.remove();
this.renderer.dispose();
}, FADE);
}
_spawn() {
if (!this.alive) return;
if (this.object) { this.scene.remove(this.object); this.object = null; }
this.object = buildObject(pick(POOL), pick(COLORS));
// Give each side a slightly different initial rotation so they don't mirror each other
this.object.rotation.y = Math.random() * Math.PI * 2;
this.scene.add(this.object);
requestAnimationFrame(() => { this.canvas.style.opacity = '1'; });
this.timer = setTimeout(() => this._cycle(), cycleMs());
}
_cycle() {
if (!this.alive) return;
this.canvas.style.opacity = '0';
setTimeout(() => { if (this.alive) this._spawn(); }, FADE);
}
_tick() {
if (!this.alive) return;
this.animId = requestAnimationFrame(() => this._tick());
if (this.object) {
if (this.object.userData.isGyroscope) {
this.object.children.forEach(ring => {
ring.rotation.x += ring.userData.ax;
ring.rotation.y += ring.userData.ay;
ring.rotation.z += ring.userData.az;
});
this.object.rotation.y += 0.002;
} else {
this.object.rotation.y += 0.005;
this.object.rotation.x += 0.003;
}
}
this.renderer.render(this.scene, this.camera);
}
}
// ── Orchestration ────────────────────────────────────────────────────────────
let left = null, right = null;
function isWide() { return window.innerWidth >= MIN_VW; }
function open() {
if (left || !isWide()) return;
left = new SideCanvas('left');
right = new SideCanvas('right');
left.mount();
right.mount();
}
function close() {
if (!left) return;
left.unmount();
right.unmount();
left = right = null;
}
function init() {
const lb = document.getElementById('lb-overlay');
const np = document.getElementById('np-overlay');
if (!lb || !np) { requestAnimationFrame(init); return; }
const check = () => {
const anyOpen = lb.classList.contains('lb-open') || np.classList.contains('np-open');
if (anyOpen && !left) open();
if (!anyOpen && left) close();
};
const obs = new MutationObserver(check);
obs.observe(lb, { attributes: true, attributeFilter: ['class'] });
obs.observe(np, { attributes: true, attributeFilter: ['class'] });
window.addEventListener('resize', () => { if (!isWide() && left) close(); });
}
init();