Feature: stage lights

This commit is contained in:
Dejvino 2025-12-29 23:13:12 +00:00
parent ccd52ba00a
commit ab8334f9ab
6 changed files with 144 additions and 227 deletions

View File

@ -21,7 +21,7 @@ export class DustEffect {
particlesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const particleMaterial = new THREE.PointsMaterial({
color: 0xffffff,
color: 0xaaaaaa,
size: 0.015,
transparent: true,
opacity: 0.08,

View File

@ -61,7 +61,7 @@ export class PartyGuests extends SceneFeature {
const createGuests = () => {
const geometry = new THREE.PlaneGeometry(guestWidth, guestHeight);
const numGuests = 80;
const numGuests = 150;
for (let i = 0; i < numGuests; i++) {
const material = materials[i % materials.length];

View File

@ -9,11 +9,10 @@ import { Stage } from './stage.js';
import { PartyGuests } from './party-guests.js';
import { StageTorches } from './stage-torches.js';
import { MusicVisualizer } from './music-visualizer.js';
import { RoseWindowLight } from './rose-window-light.js';
import { RoseWindowLightshafts } from './rose-window-lightshafts.js';
import { MusicPlayer } from './music-player.js';
import { WallCurtain } from './wall-curtain.js';
import { ReproWall } from './repro-wall.js';
import { StageLights } from './stage-lights.js';
// Scene Features ^^^
// --- Scene Modeling Function ---

View File

@ -1,66 +0,0 @@
import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
export class RoseWindowLight extends SceneFeature {
constructor() {
super();
this.spotlight = null;
this.helper = null;
sceneFeatureManager.register(this);
}
init() {
// --- Dimensions for positioning ---
const length = 40;
const naveHeight = 15;
const stageDepth = 5;
// --- Create the spotlight ---
this.spotlight = new THREE.SpotLight(0xffffff, 100.0); // White light, high intensity
this.spotlight.position.set(0, naveHeight, -length / 2 + 10); // Position it at the rose window
this.spotlight.angle = Math.PI / 9; // A reasonably focused beam
this.spotlight.penumbra = 0.3; // Soft edges
this.spotlight.decay = 0.7;
this.spotlight.distance = 30;
this.spotlight.castShadow = false;
this.spotlight.shadow.mapSize.width = 1024;
this.spotlight.shadow.mapSize.height = 1024;
this.spotlight.shadow.camera.near = 1;
this.spotlight.shadow.camera.far = 30;
this.spotlight.shadow.focus = 1;
// --- Create a target for the spotlight to aim at ---
const targetObject = new THREE.Object3D();
targetObject.position.set(0, 0, -length / 2 + stageDepth); // Aim at the center of the stage
state.scene.add(targetObject);
this.spotlight.target = targetObject;
state.scene.add(this.spotlight);
// --- Add a debug helper ---
if (state.debugLight) {
this.helper = new THREE.SpotLightHelper(this.spotlight);
state.scene.add(this.helper);
}
}
update(deltaTime) {
if (!this.spotlight) return;
// Make the light pulse with the music
if (state.music) {
const baseIntensity = 4.0;
this.spotlight.intensity = baseIntensity + state.music.beatIntensity * 1.0;
}
// Update the helper if it exists
if (this.helper) {
this.helper.update();
}
}
}
new RoseWindowLight();

View File

@ -1,157 +0,0 @@
import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
export class RoseWindowLightshafts extends SceneFeature {
constructor() {
super();
this.shafts = [];
sceneFeatureManager.register(this);
}
init() {
// --- Dimensions for positioning ---
const length = 40;
const naveWidth = 12;
const naveHeight = 15;
const stageDepth = 5;
const stageWidth = naveWidth - 1;
const roseWindowRadius = naveWidth / 2 - 2;
const roseWindowCenter = new THREE.Vector3(0, naveHeight, -length / 2 - 1.1);
// --- Procedural Noise Texture for Light Shafts ---
const createNoiseTexture = () => {
const width = 128;
const height = 512;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
const imageData = context.createImageData(width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// Create vertical streaks of noise
const y = Math.floor((i / 4) / width);
const noise = Math.pow(Math.random(), 2.5) * (1 - y / height) * 255;
data[i] = noise; // R
data[i + 1] = noise; // G
data[i + 2] = noise; // B
data[i + 3] = 255; // A
}
context.putImageData(imageData, 0, 0);
return new THREE.CanvasTexture(canvas);
};
const baseMaterial = new THREE.MeshBasicMaterial({
//map: texture,
blending: THREE.AdditiveBlending,
transparent: true,
depthWrite: false,
opacity: 1.0,
color: 0x88aaff, // Give the light a cool blueish tint
});
// --- Create multiple thin light shafts ---
const numShafts = 16;
for (let i = 0; i < numShafts; i++) {
const texture = createNoiseTexture();
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
const material = baseMaterial.clone(); // Each shaft needs its own material for individual opacity
material.map = texture;
const startAngle = Math.random() * Math.PI * 2;
const startRadius = Math.random() * roseWindowRadius;
const startPoint = new THREE.Vector3(
roseWindowCenter.x + Math.cos(startAngle) * startRadius,
roseWindowCenter.y + Math.sin(startAngle) * startRadius,
roseWindowCenter.z
);
// Define a linear path on the floor for the beam to travel
const floorStartPoint = new THREE.Vector3(
(Math.random() - 0.5) * stageWidth * 0.75,
0,
-length / 2 + Math.random() * 8 + 0
);
const floorEndPoint = new THREE.Vector3(
(Math.random() - 0.5) * stageWidth * 0.75,
0,
-length / 2 + Math.random() * 8 + 3
);
const distance = startPoint.distanceTo(floorStartPoint);
const geometry = new THREE.CylinderGeometry(0.01, 0.5 + Math.random() * 0.5, distance, 16, 1, true);
const lightShaft = new THREE.Mesh(geometry, material);
state.scene.add(lightShaft);
this.shafts.push({
mesh: lightShaft,
startPoint: startPoint, // The stationary point in the window
endPoint: floorStartPoint.clone(), // The current position of the beam on the floor
floorStartPoint: floorStartPoint, // The start of the sweep path
floorEndPoint: floorEndPoint, // The end of the sweep path
moveSpeed: 0.01 + Math.random() * 0.5, // Each shaft has a different speed
// No 'state' needed anymore
});
}
}
update(deltaTime) {
const baseOpacity = 0.1;
this.shafts.forEach(shaft => {
const { mesh, startPoint, endPoint, floorStartPoint, floorEndPoint, moveSpeed } = shaft;
// Animate texture for dust motes
mesh.material.map.offset.y += deltaTime * 0.004;
mesh.material.map.offset.x -= deltaTime * 0.02;
if (mesh.material.map.offset.y >= 0.2) {
mesh.material.map.offset.y -= 0.2;
}
if (mesh.material.map.offset.x <= 0.0) {
mesh.material.map.offset.x += 1.0;
}
// --- Movement Logic ---
const pathDirection = floorEndPoint.clone().sub(floorStartPoint).normalize();
const pathLength = floorStartPoint.distanceTo(floorEndPoint);
// Move the endpoint along its path
endPoint.add(pathDirection.clone().multiplyScalar(moveSpeed * deltaTime));
const currentDistance = floorStartPoint.distanceTo(endPoint);
if (currentDistance >= pathLength) {
// Reached the end, reset to the start
endPoint.copy(floorStartPoint);
}
// --- Opacity based on Progress ---
const progress = Math.min(currentDistance / pathLength, 1.0);
// Use a sine curve to fade in at the start and out at the end
const fadeOpacity = Math.sin(progress * Math.PI) * baseOpacity;
// --- Update Mesh Position and Orientation ---
const distance = startPoint.distanceTo(endPoint);
mesh.scale.y = -distance/5;
mesh.position.lerpVectors(startPoint, endPoint, 0.5);
const quaternion = new THREE.Quaternion();
const cylinderUp = new THREE.Vector3(0, 1, 0);
const direction = new THREE.Vector3().subVectors(endPoint, startPoint).normalize();
quaternion.setFromUnitVectors(cylinderUp, direction);
mesh.quaternion.copy(quaternion);
// --- Music Visualization ---
const beatPulse = state.music ? state.music.beatIntensity * 0.05 : 0;
mesh.material.opacity = fadeOpacity + beatPulse;
});
}
}
new RoseWindowLightshafts();

View File

@ -0,0 +1,141 @@
import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
export class StageLights extends SceneFeature {
constructor() {
super();
this.lights = [];
this.focusPoint = new THREE.Vector3(0, 0, -10);
this.targetFocusPoint = new THREE.Vector3(0, 0, -10);
this.lastChangeTime = 0;
sceneFeatureManager.register(this);
}
init() {
// 1. Create the Steel Beam
const beamLength = 24;
const beamGeo = new THREE.BoxGeometry(beamLength, 0.5, 0.5);
const beamMat = new THREE.MeshStandardMaterial({
color: 0x444444,
metalness: 0.9,
roughness: 0.4
});
this.beam = new THREE.Mesh(beamGeo, beamMat);
// Positioned high above the front of the stage area
this.beam.position.set(0, 8, -14);
this.beam.castShadow = true;
this.beam.receiveShadow = true;
state.scene.add(this.beam);
// 2. Create Spotlights
const numLights = 8;
const spacing = beamLength / numLights;
// Geometry for the light fixture (par can style)
const fixtureGeo = new THREE.CylinderGeometry(0.2, 0.3, 0.6);
// Rotate geometry so -Y (bottom) points to +Z (lookAt direction)
fixtureGeo.rotateX(-Math.PI / 2);
const fixtureMat = new THREE.MeshStandardMaterial({ color: 0x111111 });
const lensGeo = new THREE.CircleGeometry(0.18, 32);
for (let i = 0; i < numLights; i++) {
const x = -beamLength / 2 + spacing/2 + i * spacing;
// Fixture Mesh
const fixture = new THREE.Mesh(fixtureGeo, fixtureMat);
fixture.position.set(x, 7.5, -14); // Match beam position
state.scene.add(fixture);
// Lens Mesh
const lensMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const lens = new THREE.Mesh(lensGeo, lensMat);
lens.position.set(0, 0, 0.31);
fixture.add(lens);
// SpotLight
const spotLight = new THREE.SpotLight(0xffffee, 0);
spotLight.position.copy(fixture.position);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.3;
spotLight.decay = 1.5;
spotLight.distance = 60;
spotLight.castShadow = true;
spotLight.shadow.bias = -0.0001;
spotLight.shadow.mapSize.width = 512;
spotLight.shadow.mapSize.height = 512;
// Target Object
const target = new THREE.Object3D();
state.scene.add(target);
spotLight.target = target;
state.scene.add(spotLight);
this.lights.push({
light: spotLight,
fixture: fixture,
lens: lens,
target: target,
baseX: x
});
}
}
update(deltaTime) {
const time = state.clock.getElapsedTime();
// Change target area logic
let shouldChange = false;
if (time < this.lastChangeTime) this.lastChangeTime = time;
if (state.music && state.partyStarted) {
// Change on the measure (every 4 beats) if enough time has passed
if (state.music.measurePulse > 0.5 && time - this.lastChangeTime > 1.5) {
shouldChange = true;
}
} else if (time - this.lastChangeTime > 3.0) {
shouldChange = true;
}
if (shouldChange) {
this.lastChangeTime = time;
// Randomly pick a zone: Stage or Dance Floor
if (Math.random() < 0.4) {
// Stage Area (Z: -20 to -10)
this.targetFocusPoint.set((Math.random() - 0.5) * 15, 1, -15 + (Math.random() - 0.5) * 5);
} else {
// Dance Floor / Guests (Z: -5 to 15)
this.targetFocusPoint.set((Math.random() - 0.5) * 20, 0, 5 + (Math.random() - 0.5) * 15);
}
}
// Smoothly move the focus point
this.focusPoint.lerp(this.targetFocusPoint, deltaTime * 2.0);
// Update each light
const intensity = state.music ? 20 + state.music.beatIntensity * 150 : 50;
const hue = (time * 0.2) % 1;
const color = new THREE.Color().setHSL(hue, 0.8, 0.5);
const spread = 0.2 + (state.music ? state.music.beatIntensity * 0.4 : 0);
const bounce = state.music ? state.music.beatIntensity * 0.5 : 0;
this.lights.forEach((item) => {
// Converge lights on focus point, but keep slight X offset for spread
const targetX = this.focusPoint.x + (item.baseX * spread);
item.target.position.set(targetX, this.focusPoint.y + bounce, this.focusPoint.z);
item.fixture.lookAt(targetX, this.focusPoint.y, this.focusPoint.z);
item.light.intensity = intensity;
item.light.color.copy(color);
item.lens.material.color.copy(color);
});
}
}
new StageLights();