Feature: stage lasers
This commit is contained in:
parent
48fe11bf3f
commit
3a7251e185
@ -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 ---
|
||||
|
||||
209
party-stage/src/scene/stage-lasers.js
Normal file
209
party-stage/src/scene/stage-lasers.js
Normal 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();
|
||||
Loading…
Reference in New Issue
Block a user