Feature: DJ with colorful lights
138
party-stage/src/scene/dj.js
Normal 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();
|
||||
238
party-stage/src/scene/music-console.js
Normal 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();
|
||||
@ -13,6 +13,8 @@ import { MusicPlayer } from './music-player.js';
|
||||
import { WallCurtain } from './wall-curtain.js';
|
||||
import { ReproWall } from './repro-wall.js';
|
||||
import { StageLights } from './stage-lights.js';
|
||||
import { MusicConsole } from './music-console.js';
|
||||
import { DJ } from './dj.js';
|
||||
// Scene Features ^^^
|
||||
|
||||
// --- Scene Modeling Function ---
|
||||
|
||||
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 841 KiB |
|
Before Width: | Height: | Size: 878 KiB After Width: | Height: | Size: 909 KiB |
|
Before Width: | Height: | Size: 819 KiB After Width: | Height: | Size: 899 KiB |
|
Before Width: | Height: | Size: 906 KiB After Width: | Height: | Size: 914 KiB |
BIN
party-stage/textures/musician5.png
Normal file
|
After Width: | Height: | Size: 945 KiB |