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 { MusicConsole } from './music-console.js';
|
||||||
import { DJ } from './dj.js';
|
import { DJ } from './dj.js';
|
||||||
import { ProjectionScreen } from './projection-screen.js';
|
import { ProjectionScreen } from './projection-screen.js';
|
||||||
|
import { StageLasers } from './stage-lasers.js';
|
||||||
// Scene Features ^^^
|
// Scene Features ^^^
|
||||||
|
|
||||||
// --- Scene Modeling Function ---
|
// --- 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