Feature: blobby DJ
This commit is contained in:
parent
32679ced8e
commit
3e773361e2
@ -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() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user