feat: add lens flare effect on bright light sources (#109)
Some checks failed
CI / validate (pull_request) Failing after 10s
CI / auto-merge (pull_request) Has been skipped

Implements procedural lens flare using Three.js Lensflare addon.
Canvas-generated textures (no external assets) are attached to
overheadLight and meterLight. Three.js handles occlusion automatically —
flares fade when the source is behind geometry and intensify when the
camera looks directly toward the light.

Fixes #109
This commit is contained in:
Alexander Whitestone
2026-03-24 00:41:06 -04:00
parent d193a89262
commit af0ff8fdae

41
app.js
View File

@@ -4,6 +4,7 @@ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { BokehPass } from 'three/addons/postprocessing/BokehPass.js';
import { LoadingManager } from 'three';
import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js';
// === COLOR PALETTE ===
const NEXUS = {
@@ -575,6 +576,46 @@ async function loadSovereigntyStatus() {
loadSovereigntyStatus();
// === LENS FLARE ===
// Procedural canvas textures — no external assets required.
// LensflareElement at distance=0 sits at the light; elements at distance>0
// streak along the lens axis, producing the classic cinematic look.
// Three.js handles occlusion automatically: flares fade when the source is
// behind geometry and brighten when the camera looks directly toward the light.
function createFlareTexture(size, inner, mid, outer) {
const c = document.createElement('canvas');
c.width = c.height = size;
const ctx = c.getContext('2d');
const cx = size / 2;
const g = ctx.createRadialGradient(cx, cx, 0, cx, cx, cx);
g.addColorStop(0, inner);
g.addColorStop(0.25, mid);
g.addColorStop(1, outer);
ctx.fillStyle = g;
ctx.fillRect(0, 0, size, size);
return new THREE.CanvasTexture(c);
}
const flareTex0 = createFlareTexture(256, 'rgba(255,255,240,1)', 'rgba(120,160,255,0.5)', 'rgba(0,0,0,0)');
const flareTex1 = createFlareTexture(128, 'rgba(180,200,255,0.7)', 'rgba(60,100,220,0.25)', 'rgba(0,0,0,0)');
const flareTex2 = createFlareTexture(64, 'rgba(255,210,110,0.6)', 'rgba(200,80,20,0.2)', 'rgba(0,0,0,0)');
function attachLensFlare(light, baseColor, mainSize) {
const lf = new Lensflare();
lf.addElement(new LensflareElement(flareTex0, mainSize, 0, baseColor));
lf.addElement(new LensflareElement(flareTex1, mainSize * 0.28, 0.55));
lf.addElement(new LensflareElement(flareTex2, mainSize * 0.18, 0.82));
lf.addElement(new LensflareElement(flareTex1, mainSize * 0.12, 1.1));
lf.addElement(new LensflareElement(flareTex2, mainSize * 0.08, 1.35));
light.add(lf);
}
// Overhead point-light — primary flare (cool blue-white)
attachLensFlare(overheadLight, new THREE.Color(0x8899bb), 350);
// Sovereignty meter glow — secondary flare (color driven by score, starts green)
attachLensFlare(meterLight, new THREE.Color(0x00ff88), 180);
// === ANIMATION LOOP ===
const clock = new THREE.Clock();