Feature: DJ with colorful lights

This commit is contained in:
Dejvino 2025-12-30 06:56:14 +00:00
parent cb9a6c4a48
commit 5d3a05ec69
8 changed files with 378 additions and 0 deletions

138
party-stage/src/scene/dj.js Normal file
View File

@ -0,0 +1,138 @@
import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
import { applyVibrancyToMaterial } from '../shaders/vibrant-billboard-shader.js';
const djTextureUrls = [
'/textures/musician1.png',
'/textures/musician2.png',
'/textures/musician3.png',
'/textures/musician4.png',
'/textures/musician5.png',
];
export class DJ extends SceneFeature {
constructor() {
super();
sceneFeatureManager.register(this);
}
async init() {
// Reuse the texture processing logic from MedievalMusicians for consistency
const processTexture = (texture) => {
const image = texture.image;
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
const keyPixelData = context.getImageData(0, 0, 1, 1).data;
const keyColor = { r: keyPixelData[0], g: keyPixelData[1], b: keyPixelData[2] };
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const threshold = 25;
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i + 1], b = data[i + 2];
const distance = Math.sqrt(Math.pow(r - keyColor.r, 2) + Math.pow(g - keyColor.g, 2) + Math.pow(b - keyColor.b, 2));
if (distance < threshold) data[i + 3] = 0;
}
context.putImageData(imageData, 0, 0);
return new THREE.CanvasTexture(canvas);
};
this.materials = await Promise.all(djTextureUrls.map(async (url) => {
const texture = await state.loader.loadAsync(url);
const processedTexture = processTexture(texture);
const material = new THREE.MeshStandardMaterial({
map: processedTexture,
side: THREE.DoubleSide,
alphaTest: 0.5,
roughness: 0.7,
metalness: 0.1,
});
applyVibrancyToMaterial(material, processedTexture);
return material;
}));
const height = 2.5;
const width = 2.5;
const geometry = new THREE.PlaneGeometry(width, height);
this.mesh = new THREE.Mesh(geometry, this.materials[0]);
// Position behind the console (console is at z=-16.5, depth 0.8)
// We place the DJ slightly behind the console
this.baseY = 1.5 + height / 2;
this.mesh.position.set(0, this.baseY, -17.3);
this.mesh.castShadow = true;
this.mesh.receiveShadow = true;
this.mesh.visible = false; // Start invisible until party starts
state.scene.add(this.mesh);
// Movement State
this.state = 'IDLE';
this.targetX = 0;
this.moveTimer = 0;
this.canSwitchTexture = true;
}
update(deltaTime) {
if (!this.mesh || !state.partyStarted) return;
const time = state.clock.getElapsedTime();
// Billboard: Always face the camera
const cameraPos = new THREE.Vector3();
state.camera.getWorldPosition(cameraPos);
this.mesh.lookAt(cameraPos.x, this.mesh.position.y, cameraPos.z);
// Bobbing to music
let bobAmount = 0;
if (state.music) {
const beat = state.music.beatIntensity;
// Bob faster and deeper with the beat
bobAmount = Math.sin(time * 15) * 0.05 * (0.5 + beat);
// Texture switching on beat
if (beat > 0.8 && this.canSwitchTexture) {
const randomMat = this.materials[Math.floor(Math.random() * this.materials.length)];
this.mesh.material = randomMat;
this.canSwitchTexture = false;
} else if (beat < 0.2) {
this.canSwitchTexture = true;
}
}
this.mesh.position.y = this.baseY + bobAmount;
// Movement Logic: Slide along the console
if (this.state === 'IDLE') {
this.moveTimer -= deltaTime;
if (this.moveTimer <= 0) {
this.state = 'MOVING';
// Pick random spot behind console (width ~4.0, so range +/- 1.5 is safe)
this.targetX = (Math.random() - 0.5) * 3.0;
}
} else if (this.state === 'MOVING') {
const speed = 1.5;
if (Math.abs(this.mesh.position.x - this.targetX) < 0.1) {
this.state = 'IDLE';
this.moveTimer = 2 + Math.random() * 5; // Wait 2-7 seconds before moving again
} else {
const dir = Math.sign(this.targetX - this.mesh.position.x);
this.mesh.position.x += dir * speed * deltaTime;
}
}
}
onPartyStart() {
if(this.mesh) this.mesh.visible = true;
}
onPartyEnd() {
// Optional: Hide DJ or stop movement
}
}
new DJ();

View File

@ -0,0 +1,238 @@
import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
export class MusicConsole extends SceneFeature {
constructor() {
super();
this.lights = [];
sceneFeatureManager.register(this);
}
init() {
// Stage Y top is 1.5
const stageY = 1.5;
const consoleWidth = 4.0;
const consoleDepth = 0.8;
const consoleHeight = 1.2;
const group = new THREE.Group();
// Position on stage, centered
group.position.set(0, stageY, -16.5);
state.scene.add(group);
// 1. The Stand/Table Body
const standGeo = new THREE.BoxGeometry(consoleWidth, consoleHeight, consoleDepth);
const standMat = new THREE.MeshStandardMaterial({
color: 0x1a1a1a,
roughness: 0.3,
metalness: 0.6
});
const stand = new THREE.Mesh(standGeo, standMat);
stand.position.y = consoleHeight / 2;
stand.castShadow = true;
stand.receiveShadow = true;
group.add(stand);
// 2. Control Surface (Top Plate)
const topGeo = new THREE.BoxGeometry(consoleWidth + 0.1, 0.05, consoleDepth + 0.1);
const topMat = new THREE.MeshStandardMaterial({ color: 0x050505, roughness: 0.8 });
const topPlate = new THREE.Mesh(topGeo, topMat);
topPlate.position.y = consoleHeight + 0.025;
topPlate.receiveShadow = true;
group.add(topPlate);
const surfaceY = consoleHeight + 0.05;
// 3. Knobs (Left side)
const knobGeo = new THREE.CylinderGeometry(0.015, 0.015, 0.02, 12);
const knobMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.8, roughness: 0.2 });
const tinyLedGeo = new THREE.CircleGeometry(0.005, 8);
tinyLedGeo.rotateX(-Math.PI / 2);
for(let row=0; row<4; row++) {
for(let col=0; col<16; col++) {
const knob = new THREE.Mesh(knobGeo, knobMat);
const x = -consoleWidth/2 + 0.3 + (col * 0.06);
const z = -0.25 + (row * 0.08);
knob.position.set(x, surfaceY + 0.01, z);
group.add(knob);
// Add tiny LED next to some knobs
if (Math.random() > 0.5) {
const ledMat = new THREE.MeshBasicMaterial({ color: 0x222222 });
const led = new THREE.Mesh(tinyLedGeo, ledMat);
led.position.set(x + 0.02, surfaceY + 0.001, z + 0.02);
group.add(led);
this.lights.push({
mesh: led,
onColor: new THREE.Color().setHSL(Math.random(), 1.0, 0.5),
offColor: new THREE.Color(0x222222),
active: false,
nextToggle: Math.random()
});
}
}
}
// 4. Sliders (Middle)
const sliderTrackGeo = new THREE.BoxGeometry(0.01, 0.005, 0.25);
const sliderHandleGeo = new THREE.BoxGeometry(0.025, 0.03, 0.015);
const trackMat = new THREE.MeshBasicMaterial({ color: 0x111111 });
const handleMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
for(let i=0; i<24; i++) {
const x = -0.5 + (i * 0.05);
const track = new THREE.Mesh(sliderTrackGeo, trackMat);
track.position.set(x, surfaceY, 0.1);
group.add(track);
const handle = new THREE.Mesh(sliderHandleGeo, handleMat);
// Randomize slider positions slightly
const slidePos = (Math.random() - 0.5) * 0.2;
handle.position.set(x, surfaceY + 0.015, 0.1 + slidePos);
group.add(handle);
// LED above slider
const ledMat = new THREE.MeshBasicMaterial({ color: 0x222222 });
const led = new THREE.Mesh(tinyLedGeo, ledMat);
led.position.set(x, surfaceY + 0.001, -0.05);
group.add(led);
this.lights.push({
mesh: led,
onColor: new THREE.Color().setHSL(Math.random(), 1.0, 0.5),
offColor: new THREE.Color(0x222222),
active: false,
nextToggle: Math.random()
});
}
// 5. Blinky Lights (Right side grid)
const lightGeo = new THREE.PlaneGeometry(0.03, 0.03);
lightGeo.rotateX(-Math.PI / 2); // Lay flat
const rows = 8;
const cols = 16;
for(let r=0; r<rows; r++) {
for(let c=0; c<cols; c++) {
const lightMat = new THREE.MeshBasicMaterial({ color: 0x222222 });
const lightMesh = new THREE.Mesh(lightGeo, lightMat);
const x = consoleWidth/2 - 1.0 + (c * 0.05);
const z = -0.25 + (r * 0.05);
lightMesh.position.set(x, surfaceY + 0.01, z);
group.add(lightMesh);
this.lights.push({
mesh: lightMesh,
// Assign random colors (Red, Green, Blue, Amber)
onColor: new THREE.Color().setHSL(Math.random(), 1.0, 0.5),
offColor: new THREE.Color(0x222222),
active: false,
nextToggle: Math.random()
});
}
}
// 6. Front LED Array (InstancedMesh)
const ledRows = 10;
const ledCols = 60;
this.ledRows = ledRows;
this.ledCols = ledCols;
const ledCount = ledRows * ledCols;
const ledDisplayGeo = new THREE.PlaneGeometry(0.04, 0.04);
const ledDisplayMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
this.frontLedMesh = new THREE.InstancedMesh(ledDisplayGeo, ledDisplayMat, ledCount);
const dummy = new THREE.Object3D();
let idx = 0;
const spacing = 0.06;
const gridWidth = ledCols * spacing;
const gridHeight = ledRows * spacing;
// Center on the front face of the stand
const startX = -gridWidth / 2 + spacing / 2;
const startY = (consoleHeight / 2) - gridHeight / 2 + spacing / 2;
const zPos = consoleDepth / 2 + 0.01; // Slightly in front of the stand
for(let r=0; r<ledRows; r++) {
for(let c=0; c<ledCols; c++) {
dummy.position.set(startX + c * spacing, startY + r * spacing, zPos);
dummy.updateMatrix();
this.frontLedMesh.setMatrixAt(idx, dummy.matrix);
this.frontLedMesh.setColorAt(idx, new THREE.Color(0x000000));
idx++;
}
}
this.frontLedMesh.instanceMatrix.needsUpdate = true;
if (this.frontLedMesh.instanceColor) this.frontLedMesh.instanceColor.needsUpdate = true;
group.add(this.frontLedMesh);
}
update(deltaTime) {
if (!state.partyStarted || !state.music) return;
const time = state.clock.getElapsedTime();
const beatIntensity = state.music.beatIntensity;
this.lights.forEach(light => {
if (time > light.nextToggle) {
// Toggle state
light.active = !light.active;
// Determine next toggle time based on music intensity
// Higher intensity = faster toggles
const speed = 0.1 + (1.0 - beatIntensity) * 0.5;
light.nextToggle = time + speed + Math.random() * 0.2;
}
if (light.active) {
light.mesh.material.color.copy(light.onColor);
// Pulse brightness with beat
light.mesh.material.color.multiplyScalar(1 + beatIntensity * 2);
} else {
light.mesh.material.color.copy(light.offColor);
}
});
// Update Front LED Array
if (this.frontLedMesh) {
const color = new THREE.Color();
let idx = 0;
for(let r=0; r<this.ledRows; r++) {
for(let c=0; c<this.ledCols; c++) {
const u = c / this.ledCols;
const v = r / this.ledRows;
// Dynamic wave pattern
const wave1 = Math.sin(u * 12 + time * 3);
const wave2 = Math.cos(v * 8 - time * 2);
const wave3 = Math.sin((u + v) * 5 + time * 5);
const intensity = (wave1 + wave2 + wave3) / 3 * 0.5 + 0.5;
// Color palette shifting
const hue = (time * 0.1 + u * 0.3 + intensity * 0.2) % 1;
const sat = 0.9;
const light = intensity * (0.1 + beatIntensity * 0.9); // Pulse brightness with beat
color.setHSL(hue, sat, light);
this.frontLedMesh.setColorAt(idx, color);
idx++;
}
}
if (this.frontLedMesh.instanceColor) this.frontLedMesh.instanceColor.needsUpdate = true;
}
}
}
new MusicConsole();

View File

@ -13,6 +13,8 @@ import { MusicPlayer } from './music-player.js';
import { WallCurtain } from './wall-curtain.js'; import { WallCurtain } from './wall-curtain.js';
import { ReproWall } from './repro-wall.js'; import { ReproWall } from './repro-wall.js';
import { StageLights } from './stage-lights.js'; import { StageLights } from './stage-lights.js';
import { MusicConsole } from './music-console.js';
import { DJ } from './dj.js';
// Scene Features ^^^ // Scene Features ^^^
// --- Scene Modeling Function --- // --- Scene Modeling Function ---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 KiB

After

Width:  |  Height:  |  Size: 841 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 KiB

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 KiB

After

Width:  |  Height:  |  Size: 914 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 KiB