Feature: blobby DJ

This commit is contained in:
Dejvino 2025-12-30 23:42:57 +00:00
parent 32679ced8e
commit 3e773361e2

View File

@ -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);
this.leftArm = createArm(true);
this.rightArm = createArm(false);
this.group.add(this.leftArm);
this.group.add(this.rightArm);
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);
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() {