Feature: stage lights
This commit is contained in:
parent
ccd52ba00a
commit
ab8334f9ab
@ -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,
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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 ---
|
||||
|
||||
@ -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();
|
||||
@ -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();
|
||||
141
party-stage/src/scene/stage-lights.js
Normal file
141
party-stage/src/scene/stage-lights.js
Normal 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();
|
||||
Loading…
Reference in New Issue
Block a user