diff --git a/party-stage/src/scene/dj.js b/party-stage/src/scene/dj.js index 7e0ddb4..53d1a9d 100644 --- a/party-stage/src/scene/dj.js +++ b/party-stage/src/scene/dj.js @@ -2,15 +2,6 @@ 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() { @@ -18,116 +9,199 @@ export class DJ extends SceneFeature { 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); + init() { + this.group = new THREE.Group(); + + // Position behind the console (console is at z=-16.5) + this.baseY = 1.8; // Stage height + this.group.position.set(0, this.baseY, -17.5); + + // 1. Body (Blobby) + const bodyGeo = new THREE.CapsuleGeometry(0.3, 0.8, 4, 8); + const bodyMat = new THREE.MeshStandardMaterial({ + color: 0x3355ff, // Bright Blue + roughness: 0.6, + metalness: 0.1 + }); + const body = new THREE.Mesh(bodyGeo, bodyMat); + body.position.y = 0.7; + body.castShadow = true; + body.receiveShadow = true; + this.group.add(body); + + // 2. Head + const headGeo = new THREE.SphereGeometry(0.25, 16, 16); + const head = new THREE.Mesh(headGeo, bodyMat); + head.position.y = 1.55; + head.castShadow = true; + head.receiveShadow = true; + this.group.add(head); + this.head = head; + + // 3. Glasses + const glassesGroup = new THREE.Group(); + glassesGroup.position.set(0, 0, 0.18); // On face + head.add(glassesGroup); + + const glassesMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.2, metalness: 0.8 }); + + const lensGeo = new THREE.BoxGeometry(0.13, 0.08, 0.15); + const bridgeGeo = new THREE.BoxGeometry(0.04, 0.02, 0.15); + + const leftLens = new THREE.Mesh(lensGeo, glassesMat); + leftLens.position.set(-0.085, 0, 0); + glassesGroup.add(leftLens); + + const rightLens = new THREE.Mesh(lensGeo, glassesMat); + rightLens.position.set(0.085, 0, 0); + glassesGroup.add(rightLens); + + const bridge = new THREE.Mesh(bridgeGeo, glassesMat); + glassesGroup.add(bridge); + + // 4. Headphones + const headphoneMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.3, metalness: 0.8 }); + const earCupGeo = new THREE.CylinderGeometry(0.12, 0.12, 0.1, 16); + earCupGeo.rotateZ(Math.PI / 2); + + const leftCup = new THREE.Mesh(earCupGeo, headphoneMat); + leftCup.position.set(-0.26, 0, 0); + head.add(leftCup); + + const rightCup = new THREE.Mesh(earCupGeo, headphoneMat); + rightCup.position.set(0.26, 0, 0); + head.add(rightCup); + + const bandGeo = new THREE.TorusGeometry(0.26, 0.03, 8, 24, Math.PI); + const band = new THREE.Mesh(bandGeo, headphoneMat); + band.position.set(0, 0, 0); + head.add(band); + + // 5. Arms + const armGeo = new THREE.SphereGeometry(0.12, 16, 16); + const createArm = (isLeft) => { + const pivot = new THREE.Group(); + pivot.position.set(isLeft ? -0.35 : 0.35, 1.3, 0); + + const arm = new THREE.Mesh(armGeo, bodyMat); + arm.scale.set(1, 3.5, 1); + arm.position.y = -0.4; + arm.castShadow = true; + arm.receiveShadow = true; + + pivot.add(arm); + return pivot; }; - 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; - })); + this.leftArm = createArm(true); + this.rightArm = createArm(false); + this.group.add(this.leftArm); + this.group.add(this.rightArm); - 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); + this.group.visible = false; + state.scene.add(this.group); // Movement State this.state = 'IDLE'; this.targetX = 0; this.moveTimer = 0; - this.canSwitchTexture = true; + + // Arm State + this.armState = 3; // 0: None, 1: Left, 2: Right, 3: Both, 4: Twiddling + this.armTimer = 0; + this.currentLeftAngle = Math.PI * 0.85; + this.currentRightAngle = Math.PI * 0.85; + this.currentLeftAngleX = 0; + this.currentRightAngleX = 0; } update(deltaTime) { - if (!this.mesh || !state.partyStarted) return; + if (!this.group || !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 + // Bobbing let bobAmount = 0; + let beatIntensity = 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; - } + beatIntensity = state.music.beatIntensity; + bobAmount = Math.sin(time * 15) * 0.05 * (0.5 + beatIntensity); } - this.mesh.position.y = this.baseY + bobAmount; + this.group.position.y = this.baseY + bobAmount; + + // Arms Up Animation + // Update Arm State + this.armTimer -= deltaTime; + if (this.armTimer <= 0) { + this.armState = Math.floor(Math.random() * 5); + this.armTimer = 2 + Math.random() * 4; + if (Math.random() < 0.3) this.armState = 4; // Twiddling some more + } + + const upAngle = Math.PI * 0.85; + const downAngle = 0.1; + const twiddleX = -1.2; + + let targetLeftZ = downAngle; + let targetRightZ = downAngle; + let targetLeftX = 0; + let targetRightX = 0; + + if (this.armState === 4) { + // Twiddling + targetLeftZ = 0.2; + targetRightZ = 0.2; + targetLeftX = twiddleX; + targetRightX = twiddleX; + } else { + targetLeftZ = (this.armState === 1 || this.armState === 3) ? upAngle : downAngle; + targetRightZ = (this.armState === 2 || this.armState === 3) ? upAngle : downAngle; + } + + this.currentLeftAngle = THREE.MathUtils.lerp(this.currentLeftAngle, targetLeftZ, deltaTime * 3); + this.currentRightAngle = THREE.MathUtils.lerp(this.currentRightAngle, targetRightZ, deltaTime * 3); + this.currentLeftAngleX = THREE.MathUtils.lerp(this.currentLeftAngleX, targetLeftX, deltaTime * 5); + this.currentRightAngleX = THREE.MathUtils.lerp(this.currentRightAngleX, targetRightX, deltaTime * 5); + + if (this.armState === 4) { + const t = time * 15; + this.leftArm.rotation.z = -this.currentLeftAngle + Math.cos(t) * 0.05; + this.rightArm.rotation.z = this.currentRightAngle + Math.sin(t) * 0.05; + this.leftArm.rotation.x = this.currentLeftAngleX + Math.sin(t) * 0.1; + this.rightArm.rotation.x = this.currentRightAngleX + Math.cos(t) * 0.1; + } else { + const wave = Math.sin(time * 8) * 0.1; + const beatBounce = beatIntensity * 0.2; + this.leftArm.rotation.z = -this.currentLeftAngle + wave - beatBounce; + this.rightArm.rotation.z = this.currentRightAngle - wave + beatBounce; + this.leftArm.rotation.x = this.currentLeftAngleX; + this.rightArm.rotation.x = this.currentRightAngleX; + } + + // Head Bop + this.head.rotation.x = beatIntensity * 0.2; // 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; + this.targetX = (Math.random() - 0.5) * 2.5; } } else if (this.state === 'MOVING') { const speed = 1.5; - if (Math.abs(this.mesh.position.x - this.targetX) < 0.1) { + if (Math.abs(this.group.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; + const dir = Math.sign(this.targetX - this.group.position.x); + this.group.position.x += dir * speed * deltaTime; } } } onPartyStart() { - if(this.mesh) this.mesh.visible = true; + if(this.group) this.group.visible = true; } onPartyEnd() {