Feature: stage lasers

This commit is contained in:
Dejvino 2026-01-03 21:42:55 +00:00
parent 48fe11bf3f
commit 3a7251e185
2 changed files with 210 additions and 0 deletions

View File

@ -17,6 +17,7 @@ import { StageLights } from './stage-lights.js';
import { MusicConsole } from './music-console.js';
import { DJ } from './dj.js';
import { ProjectionScreen } from './projection-screen.js';
import { StageLasers } from './stage-lasers.js';
// Scene Features ^^^
// --- Scene Modeling Function ---

View File

@ -0,0 +1,209 @@
import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
export class StageLasers extends SceneFeature {
constructor() {
super();
this.lasers = [];
this.pattern = 0;
this.lastPatternChange = 0;
this.averageLoudness = 0;
this.activationState = 'IDLE';
this.stateTimer = 0;
this.initialSilenceSeconds = 10;
sceneFeatureManager.register(this);
}
init() {
// Geometry: Long thin cylinder, pivot at one end
const length = 80;
const geometry = new THREE.CylinderGeometry(0.03, 0.03, length, 8);
geometry.rotateX(-Math.PI / 2); // Align with Z axis
geometry.translate(0, 0, length / 2); // Pivot at start
// Material: Additive blending for light beam effect
const material = new THREE.MeshBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.3,
blending: THREE.AdditiveBlending,
depthWrite: false,
side: THREE.DoubleSide
});
this.sharedGeometry = geometry;
this.sharedMaterial = material;
// Fixture assets
this.fixtureGeometry = new THREE.BoxGeometry(0.2, 0.2, 0.3);
this.fixtureMaterial = new THREE.MeshStandardMaterial({
color: 0x111111,
roughness: 0.7,
metalness: 0.2
});
// Create Banks of Lasers
// 1. Left
this.createBank(new THREE.Vector3(-7, 8.2, -18), 3, 0.4, 0.3);
// 2. Right
this.createBank(new THREE.Vector3(7, 8.2, -18), 3, 0.4, -0.3);
// 3. Center
this.createBank(new THREE.Vector3(0, 8.5, -16), 8, 0.5, 0);
}
createBank(position, count, spacing, angleOffsetY) {
const group = new THREE.Group();
group.position.copy(position);
// Rotate bank slightly to face center/audience
group.rotation.y = angleOffsetY;
state.scene.add(group);
// --- Connecting Bar ---
const barWidth = (count * spacing) + 0.2;
const barGeo = new THREE.BoxGeometry(barWidth, 0.1, 0.1);
const barMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.7, metalness: 0.5 });
const bar = new THREE.Mesh(barGeo, barMat);
bar.position.set(0, 0, -0.25); // Behind the fixtures
group.add(bar);
for (let i = 0; i < count; i++) {
const mesh = new THREE.Mesh(this.sharedGeometry, this.sharedMaterial.clone());
// Center the bank
const xOff = (i - (count - 1) / 2) * spacing;
mesh.position.set(xOff, 0, 0);
// Add Static Fixture
const fixture = new THREE.Mesh(this.fixtureGeometry, this.fixtureMaterial);
fixture.position.set(xOff, 0, -0.15);
fixture.castShadow = true;
group.add(fixture);
// Add a source flare
const flare = new THREE.Mesh(
new THREE.SphereGeometry(0.06, 8, 8),
new THREE.MeshBasicMaterial({ color: 0xffffff })
);
mesh.add(flare);
group.add(mesh);
this.lasers.push({
mesh: mesh,
flare: flare,
index: i,
totalInBank: count,
bankId: position.x < 0 ? 0 : (position.x > 0 ? 1 : 2) // 0:L, 1:R, 2:C
});
}
}
update(deltaTime) {
if (!state.partyStarted) {
this.lasers.forEach(l => l.mesh.visible = false);
return;
}
const time = state.clock.getElapsedTime();
// --- Loudness Check ---
let isActive = false;
if (state.music) {
const loudness = state.music.loudness || 0;
// Update running average
this.averageLoudness = THREE.MathUtils.lerp(this.averageLoudness, loudness, deltaTime * 0.2);
if (this.activationState === 'IDLE') {
// Wait for song to pick up before first activation
if (time > this.initialSilenceSeconds && loudness > this.averageLoudness + 0.1) {
this.activationState = 'ACTIVE';
this.stateTimer = 4.0; // Active duration
}
} else if (this.activationState === 'ACTIVE') {
isActive = true;
this.stateTimer -= deltaTime;
if (this.stateTimer <= 0) {
this.activationState = 'COOLDOWN';
this.stateTimer = 4.0; // Cooldown duration
}
} else if (this.activationState === 'COOLDOWN') {
this.stateTimer -= deltaTime;
if (this.stateTimer <= 0) {
this.activationState = 'IDLE';
}
}
}
if (!isActive) {
this.lasers.forEach(l => l.mesh.visible = false);
return;
}
// --- Pattern Logic ---
if (time - this.lastPatternChange > 8) { // Change every 8 seconds
this.pattern = (this.pattern + 1) % 4;
this.lastPatternChange = time;
}
// --- Color & Intensity ---
const beat = state.music ? state.music.beatIntensity : 0;
const hue = (time * 0.1) % 1;
const color = new THREE.Color().setHSL(hue, 1.0, 0.5);
let intensity = 0.2 + beat * 0.6;
// Strobe Mode: Flash rapidly when beat intensity is high
if (beat > 0.7) {
// Rapid on/off based on time (approx 15Hz)
if (Math.sin(time * 100) < 0) {
intensity = 0.05;
} else {
intensity = 1.0;
}
}
this.lasers.forEach(l => {
l.mesh.visible = true;
l.mesh.material.color.copy(color);
l.mesh.material.opacity = intensity;
l.flare.material.color.copy(color);
// --- Movement Calculation ---
let yaw = 0;
let pitch = 0;
const t = time * 2.0;
const idx = l.index;
switch (this.pattern) {
case 0: // Lissajous / Figure 8
yaw = Math.sin(t + idx * 0.2) * 0.5;
pitch = Math.cos(t * 1.5 + idx * 0.2) * 0.3;
break;
case 1: // Horizontal Scan / Wave
yaw = Math.sin(t * 2 + idx * 0.5) * 0.8;
pitch = Math.sin(t * 0.5) * 0.1;
break;
case 2: // Tunnel / Circle
const offset = (Math.PI * 2 / l.totalInBank) * idx;
const radius = 0.4;
yaw = Math.cos(t + offset) * radius;
pitch = Math.sin(t + offset) * radius;
break;
case 3: // Chaos / Random
yaw = Math.sin(t * 3 + idx) * 0.6;
pitch = Math.cos(t * 2.5 + idx * 2) * 0.4;
break;
}
// Apply rotation
// Default points +Z.
l.mesh.rotation.set(pitch, yaw, 0);
});
}
onPartyEnd() {
this.lasers.forEach(l => l.mesh.visible = false);
}
}
new StageLasers();