Bri-Tunes/public/js/bg-grid.js

424 lines
16 KiB
JavaScript
Raw 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.

// Synthwave horizon grid background. Three.js module.
import * as THREE from 'three';
const canvas = document.getElementById('bg-canvas');
if (canvas) {
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x05060d, 18, 80);
const camera = new THREE.PerspectiveCamera(70, 1, 0.1, 200);
camera.position.set(0, 2.2, 10);
camera.lookAt(0, 1.8, 0);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: false, alpha: true });
renderer.setClearColor(0x000000, 0);
function resize() {
const w = window.innerWidth;
const h = window.innerHeight;
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
renderer.setPixelRatio(dpr);
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
window.addEventListener('resize', resize);
// ---------- Sunset backdrop (a large plane far behind the grid) ----------
const sunsetGeo = new THREE.PlaneGeometry(200, 120);
const sunsetMat = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 } },
transparent: true,
depthWrite: false,
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform float uTime;
vec3 c1 = vec3(0.02, 0.01, 0.05); // deep space
vec3 c2 = vec3(0.22, 0.08, 0.36); // violet (dimmed)
vec3 c3 = vec3(0.40, 0.06, 0.34); // magenta (dimmed)
vec3 c4 = vec3(0.42, 0.26, 0.12); // amber (dimmed)
void main() {
float y = vUv.y;
// horizon around y = 0.45
vec3 col;
if (y < 0.45) {
col = mix(c1, c1 * 0.8, y / 0.45);
} else if (y < 0.58) {
col = mix(c4, c3, (y - 0.45) / 0.13);
} else if (y < 0.78) {
col = mix(c3, c2, (y - 0.58) / 0.20);
} else {
col = mix(c2, c1, (y - 0.78) / 0.22);
}
// subtle horizontal scan-bands over the sun
float band = sin((vUv.y - 0.45) * 90.0) * 0.5 + 0.5;
if (vUv.y > 0.46 && vUv.y < 0.58) {
float mask = smoothstep(0.58, 0.46, vUv.y);
col *= mix(1.0, 0.55, band * mask);
}
// Global darken so foreground text retains contrast
col *= 0.75;
gl_FragColor = vec4(col, 1.0);
}
`,
});
const sunset = new THREE.Mesh(sunsetGeo, sunsetMat);
sunset.position.set(0, 10, -60);
scene.add(sunset);
// ---------- Horizon line (thin cyan) ----------
const horizonGeo = new THREE.BufferGeometry();
horizonGeo.setAttribute(
'position',
new THREE.Float32BufferAttribute([-100, 5.5, -55, 100, 5.5, -55], 3),
);
const horizon = new THREE.Line(
horizonGeo,
new THREE.LineBasicMaterial({ color: 0x00e5ff, transparent: true, opacity: 0.9 }),
);
scene.add(horizon);
// ---------- Infinite recessing grid ----------
const GRID_SIZE = 200;
const GRID_DIVS = 40;
const gridGeo = buildGridGeometry(GRID_SIZE, GRID_DIVS);
const gridMat = new THREE.LineBasicMaterial({
color: 0x8f5bff,
transparent: true,
opacity: 0.85,
});
const grid = new THREE.LineSegments(gridGeo, gridMat);
grid.rotation.x = -Math.PI / 2;
grid.position.y = 0;
scene.add(grid);
function buildGridGeometry(size, divs) {
const verts = [];
const step = size / divs;
const half = size / 2;
for (let i = 0; i <= divs; i++) {
const p = -half + i * step;
// lines along X
verts.push(-half, p, 0, half, p, 0);
// lines along Y (which becomes Z after rotation)
verts.push(p, -half, 0, p, half, 0);
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
return geo;
}
// ---------- Palm trees ----------
const palmMat = new THREE.LineBasicMaterial({ color: 0x00e5ff, transparent: true, opacity: 0.7 });
function makePalmTree() {
const group = new THREE.Group();
// Trunk
const trunkGeo = new THREE.CylinderGeometry(0.08, 0.15, 3.2, 5, 1);
const trunk = new THREE.LineSegments(new THREE.EdgesGeometry(trunkGeo), palmMat);
trunk.rotation.z = 0.12;
group.add(trunk);
// Fronds — 5 cone leaves spread around trunk top, long and drooping
for (let i = 0; i < 5; i++) {
const frondGeo = new THREE.ConeGeometry(0.75, 2.8, 4, 1);
const frond = new THREE.LineSegments(new THREE.EdgesGeometry(frondGeo), palmMat);
frond.position.y = 1.4; // half of cone height, so base sits at pivot
const pivot = new THREE.Object3D();
pivot.position.set(0, 3.2, 0); // trunk top
pivot.rotation.z = -1.15; // droop outward ~66°
pivot.rotation.y = (Math.PI * 2 / 5) * i;
pivot.add(frond);
group.add(pivot);
}
return group;
}
// 4 columns × 4 rows = 16 trees; adjacent columns staggered by half-step
const PALM_COLS = [
{ x: +10, zOffset: 0 },
{ x: +14, zOffset: -7 },
{ x: -10, zOffset: 0 },
{ x: -14, zOffset: -7 },
];
const PALM_Z_ROWS = [-5, -19, -33, -47];
const palmTrees = [];
for (const col of PALM_COLS) {
for (const zRow of PALM_Z_ROWS) {
const tree = makePalmTree();
tree.position.set(col.x, 0, zRow + col.zOffset);
tree.userData.baseX = col.x; // restore X to this lane on recycle
scene.add(tree);
palmTrees.push(tree);
}
}
// ---------- Flyby wireframe vehicles ----------
function rnd(a, b) { return a + Math.random() * (b - a); }
function makeVehicleMat() {
// Each vehicle gets its own material so color can be changed at spawn time.
return new THREE.LineBasicMaterial({ color: 0x00e5ff, transparent: true, opacity: 0.9 });
}
function addEdges(group, geo, mat) {
group.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo), mat));
}
function makeAirplane() {
const mat = makeVehicleMat();
const g = new THREE.Group();
addEdges(g, new THREE.BoxGeometry(4.0, 0.4, 0.6), mat); // fuselage
const lw = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(2.2, 0.12, 0.9)), mat);
lw.position.set(-1.8, 0, 0); g.add(lw); // left wing
const rw = lw.clone(); rw.position.set(1.8, 0, 0); g.add(rw); // right wing
const vf = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(0.1, 0.7, 0.45)), mat);
vf.position.set(-1.9, 0.3, 0); g.add(vf); // vertical tail fin
const hs = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.0, 0.1, 0.35)), mat);
hs.position.set(-1.9, 0.1, 0); g.add(hs); // horizontal stabiliser
return { group: g, mat };
}
function makeUFO() {
const mat = makeVehicleMat();
const g = new THREE.Group();
addEdges(g, new THREE.CylinderGeometry(1.8, 1.8, 0.35, 8, 1), mat); // main disc
const dome = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.SphereGeometry(0.8, 6, 4)), mat);
dome.position.set(0, 0.35, 0); g.add(dome); // top dome
const rim = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.CylinderGeometry(2.1, 2.1, 0.1, 8, 1)), mat);
rim.position.set(0, -0.1, 0); g.add(rim); // outer rim
return { group: g, mat };
}
function makeCar() {
const mat = makeVehicleMat();
const g = new THREE.Group();
addEdges(g, new THREE.BoxGeometry(2.8, 0.55, 1.2), mat); // body
const roof = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.4, 0.5, 1.0)), mat);
roof.position.set(-0.2, 0.52, 0); g.add(roof);
const wheelPositions = [[0.9, -0.2, 0.65], [0.9, -0.2, -0.65], [-0.9, -0.2, 0.65], [-0.9, -0.2, -0.65]];
for (const [x, y, z] of wheelPositions) {
const w = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.CylinderGeometry(0.3, 0.3, 0.2, 6, 1)), mat);
w.rotation.z = Math.PI / 2;
w.position.set(x, y, z);
g.add(w);
}
return { group: g, mat };
}
function makeSpaceship() {
const mat = makeVehicleMat();
const g = new THREE.Group();
const front = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(2.0, 0.4, 0.8)), mat);
front.position.set(0.75, 0, 0); g.add(front);
const rear = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.5, 0.7, 1.4)), mat);
rear.position.set(-0.8, 0, 0); g.add(rear);
const cockpit = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(0.5, 0.3, 0.5)), mat);
cockpit.position.set(0.9, 0.35, 0); g.add(cockpit);
// swept wings
for (const side of [1, -1]) {
const wing = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.6, 0.1, 0.65)), mat);
wing.rotation.y = side * 0.35;
wing.position.set(-0.5, -0.1, side * 0.9); g.add(wing);
const nacelle = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(0.4, 0.3, 0.3)), mat);
nacelle.position.set(-1.6, 0, side * 0.55); g.add(nacelle);
}
return { group: g, mat };
}
function makeHelicopter() {
const mat = makeVehicleMat();
const g = new THREE.Group();
addEdges(g, new THREE.BoxGeometry(2.0, 0.8, 0.9), mat); // cabin
const tail = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.8, 0.2, 0.25)), mat);
tail.position.set(-1.8, 0.2, 0); g.add(tail);
const mainRotor = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.CylinderGeometry(2.0, 2.0, 0.05, 8, 1)), mat);
mainRotor.position.set(0, 0.7, 0); g.add(mainRotor);
const tailRotor = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.CylinderGeometry(0.45, 0.45, 0.06, 6, 1)), mat);
tailRotor.rotation.z = Math.PI / 2;
tailRotor.position.set(-2.7, 0.35, 0.15); g.add(tailRotor);
for (const side of [1, -1]) {
const skid = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.6, 0.1, 0.12)), mat);
skid.position.set(0, -0.5, side * 0.5); g.add(skid);
}
return { group: g, mat };
}
function makeRocket() {
const mat = makeVehicleMat();
const g = new THREE.Group();
// Build pointing along local Y, then rotate group so it flies along X
addEdges(g, new THREE.CylinderGeometry(0.22, 0.28, 3.0, 6, 1), mat);
const nose = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.ConeGeometry(0.25, 0.85, 6, 1)), mat);
nose.position.set(0, 1.93, 0); g.add(nose);
for (let i = 0; i < 3; i++) {
const fin = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(0.08, 0.7, 0.5)), mat);
fin.rotation.y = (Math.PI * 2 / 3) * i;
fin.position.set(Math.sin((Math.PI * 2 / 3) * i) * 0.28, -1.1, Math.cos((Math.PI * 2 / 3) * i) * 0.28);
g.add(fin);
}
g.rotation.z = Math.PI / 2; // point along X so xVelocity moves it naturally
return { group: g, mat };
}
function makeMotorcycle() {
const mat = makeVehicleMat();
const g = new THREE.Group();
for (const x of [0.85, -0.75]) {
const wheel = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.CylinderGeometry(0.5, 0.5, 0.2, 8, 1)), mat);
wheel.rotation.z = Math.PI / 2;
wheel.position.set(x, 0, 0); g.add(wheel);
}
const frame = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.5, 0.6, 0.25)), mat);
frame.position.set(0, 0.5, 0); g.add(frame);
const seat = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(0.7, 0.15, 0.3)), mat);
seat.position.set(-0.1, 0.9, 0); g.add(seat);
const bar = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(0.3, 0.08, 0.7)), mat);
bar.position.set(0.7, 0.85, 0); g.add(bar);
g.scale.setScalar(1.5);
return { group: g, mat };
}
const FLYBY_COLORS = [0x00e5ff, 0xff2bd6, 0x8f5bff];
const flybyPool = [makeAirplane, makeUFO, makeCar, makeSpaceship, makeHelicopter, makeRocket, makeMotorcycle]
.map(fn => {
const r = fn();
r.group.visible = false;
scene.add(r.group);
return r;
});
const flyby = {
active: null,
xVel: 0,
nextAt: performance.now() + rnd(5000, 12000),
colorIdx: 0,
};
function spawnFlyby() {
const entry = flybyPool[Math.floor(Math.random() * flybyPool.length)];
const goLeft = Math.random() < 0.5;
entry.mat.color.setHex(FLYBY_COLORS[flyby.colorIdx++ % FLYBY_COLORS.length]);
const z = rnd(-6, -45);
// Closer passes move faster so they feel properly scaled in speed.
const dist = Math.abs(camera.position.z - z);
const speed = rnd(7, 14) * (55 / dist);
entry.group.position.set(goLeft ? 65 : -65, rnd(5, 13), z);
entry.group.rotation.set(0, 0, 0);
entry.group.visible = true;
flyby.active = entry;
flyby.xVel = goLeft ? -speed : speed;
}
// ---------- Beat / bass reactivity surface ----------
const state = {
beat: 0, // smoothed bass energy 0..1
scrollX: 0, // accumulated grid scroll on X
scrollZ: 0, // accumulated grid scroll on Z
dirAngle: 0, // current heading (radians, 0 = +Z toward camera)
targetAngle: 0, // target heading we're easing toward
nextTurnAt: performance.now() + 10000,
lastT: performance.now(),
visible: true,
};
function pickNewDirection() {
const spread = Math.PI * 0.9;
state.targetAngle = (Math.random() - 0.5) * spread;
state.nextTurnAt = performance.now() + 10000;
}
window.briTunesBg = {
setBeat(v) {
state.beat = Math.max(state.beat * 0.6, Math.min(1, v));
},
};
document.addEventListener('visibilitychange', () => {
state.visible = !document.hidden;
});
function frame() {
if (!state.visible) {
requestAnimationFrame(frame);
return;
}
const now = performance.now();
const dt = Math.min(0.05, (now - state.lastT) / 1000);
state.lastT = now;
// Decay beat
state.beat *= 0.92;
// Time to pick a new heading?
if (now >= state.nextTurnAt) pickNewDirection();
// Ease current angle toward target
const angleDelta = state.targetAngle - state.dirAngle;
state.dirAngle += angleDelta * Math.min(1, dt * 1.2);
// Scroll the grid in the current heading
const baseSpeed = 6;
const speed = baseSpeed * (1 + state.beat * 1.2);
state.scrollX += dt * speed * Math.sin(state.dirAngle);
state.scrollZ += dt * speed * Math.cos(state.dirAngle);
const step = (GRID_SIZE / GRID_DIVS);
grid.position.x = ((state.scrollX % step) + step) % step;
grid.position.z = ((state.scrollZ % step) + step) % step;
// Scroll palm trees with the grid (same heading); recycle when behind camera
for (const t of palmTrees) {
t.position.x += dt * speed * Math.sin(state.dirAngle);
t.position.z += dt * speed * Math.cos(state.dirAngle);
if (t.position.z > 15) {
t.position.z = -54;
t.position.x = t.userData.baseX; // snap back to column lane on recycle
}
}
// Flyby objects
if (!flyby.active && now >= flyby.nextAt) spawnFlyby();
if (flyby.active) {
flyby.active.group.position.x += flyby.xVel * dt;
flyby.active.group.rotation.y += 0.3 * dt;
flyby.active.group.rotation.z += 0.12 * dt;
if (Math.abs(flyby.active.group.position.x) > 72) {
flyby.active.group.visible = false;
flyby.active = null;
flyby.nextAt = now + rnd(5000, 12000);
}
}
// Pulse color on beat (violet -> magenta)
const mix = Math.min(1, state.beat);
gridMat.color.setRGB(0.56 + mix * 0.44, 0.35 - mix * 0.2, 0.94);
gridMat.opacity = 0.75 + mix * 0.25;
horizon.material.opacity = 0.7 + mix * 0.3;
sunsetMat.uniforms.uTime.value = now * 0.001;
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
if (reduced) {
renderer.render(scene, camera);
} else {
requestAnimationFrame(frame);
}
}