// 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(15000, 30000), 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]); entry.group.position.set(goLeft ? 65 : -65, rnd(7, 12), rnd(-30, -45)); entry.group.rotation.set(0, 0, 0); entry.group.visible = true; flyby.active = entry; flyby.xVel = goLeft ? -20 : 20; } // ---------- 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(15000, 30000); } } // 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); } }