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 { state } from '../state.js';
|
||||||
import { SceneFeature } from './SceneFeature.js';
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
import sceneFeatureManager from './SceneFeatureManager.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 {
|
export class DJ extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -18,116 +9,199 @@ export class DJ extends SceneFeature {
|
|||||||
sceneFeatureManager.register(this);
|
sceneFeatureManager.register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
init() {
|
||||||
// Reuse the texture processing logic from MedievalMusicians for consistency
|
this.group = new THREE.Group();
|
||||||
const processTexture = (texture) => {
|
|
||||||
const image = texture.image;
|
// Position behind the console (console is at z=-16.5)
|
||||||
const canvas = document.createElement('canvas');
|
this.baseY = 1.8; // Stage height
|
||||||
canvas.width = image.width;
|
this.group.position.set(0, this.baseY, -17.5);
|
||||||
canvas.height = image.height;
|
|
||||||
const context = canvas.getContext('2d');
|
// 1. Body (Blobby)
|
||||||
context.drawImage(image, 0, 0);
|
const bodyGeo = new THREE.CapsuleGeometry(0.3, 0.8, 4, 8);
|
||||||
const keyPixelData = context.getImageData(0, 0, 1, 1).data;
|
const bodyMat = new THREE.MeshStandardMaterial({
|
||||||
const keyColor = { r: keyPixelData[0], g: keyPixelData[1], b: keyPixelData[2] };
|
color: 0x3355ff, // Bright Blue
|
||||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
roughness: 0.6,
|
||||||
const data = imageData.data;
|
metalness: 0.1
|
||||||
const threshold = 25;
|
});
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
||||||
const r = data[i], g = data[i + 1], b = data[i + 2];
|
body.position.y = 0.7;
|
||||||
const distance = Math.sqrt(Math.pow(r - keyColor.r, 2) + Math.pow(g - keyColor.g, 2) + Math.pow(b - keyColor.b, 2));
|
body.castShadow = true;
|
||||||
if (distance < threshold) data[i + 3] = 0;
|
body.receiveShadow = true;
|
||||||
}
|
this.group.add(body);
|
||||||
context.putImageData(imageData, 0, 0);
|
|
||||||
return new THREE.CanvasTexture(canvas);
|
// 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) => {
|
this.leftArm = createArm(true);
|
||||||
const texture = await state.loader.loadAsync(url);
|
this.rightArm = createArm(false);
|
||||||
const processedTexture = processTexture(texture);
|
this.group.add(this.leftArm);
|
||||||
|
this.group.add(this.rightArm);
|
||||||
|
|
||||||
const material = new THREE.MeshStandardMaterial({
|
this.group.visible = false;
|
||||||
map: processedTexture,
|
state.scene.add(this.group);
|
||||||
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
|
// Movement State
|
||||||
this.state = 'IDLE';
|
this.state = 'IDLE';
|
||||||
this.targetX = 0;
|
this.targetX = 0;
|
||||||
this.moveTimer = 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) {
|
update(deltaTime) {
|
||||||
if (!this.mesh || !state.partyStarted) return;
|
if (!this.group || !state.partyStarted) return;
|
||||||
|
|
||||||
const time = state.clock.getElapsedTime();
|
const time = state.clock.getElapsedTime();
|
||||||
|
|
||||||
// Billboard: Always face the camera
|
// Bobbing
|
||||||
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;
|
let bobAmount = 0;
|
||||||
|
let beatIntensity = 0;
|
||||||
if (state.music) {
|
if (state.music) {
|
||||||
const beat = state.music.beatIntensity;
|
beatIntensity = state.music.beatIntensity;
|
||||||
// Bob faster and deeper with the beat
|
bobAmount = Math.sin(time * 15) * 0.05 * (0.5 + beatIntensity);
|
||||||
bobAmount = Math.sin(time * 15) * 0.05 * (0.5 + beat);
|
}
|
||||||
|
this.group.position.y = this.baseY + bobAmount;
|
||||||
|
|
||||||
// Texture switching on beat
|
// Arms Up Animation
|
||||||
if (beat > 0.8 && this.canSwitchTexture) {
|
// Update Arm State
|
||||||
const randomMat = this.materials[Math.floor(Math.random() * this.materials.length)];
|
this.armTimer -= deltaTime;
|
||||||
this.mesh.material = randomMat;
|
if (this.armTimer <= 0) {
|
||||||
this.canSwitchTexture = false;
|
this.armState = Math.floor(Math.random() * 5);
|
||||||
} else if (beat < 0.2) {
|
this.armTimer = 2 + Math.random() * 4;
|
||||||
this.canSwitchTexture = true;
|
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.mesh.position.y = this.baseY + bobAmount;
|
|
||||||
|
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
|
// Movement Logic: Slide along the console
|
||||||
if (this.state === 'IDLE') {
|
if (this.state === 'IDLE') {
|
||||||
this.moveTimer -= deltaTime;
|
this.moveTimer -= deltaTime;
|
||||||
if (this.moveTimer <= 0) {
|
if (this.moveTimer <= 0) {
|
||||||
this.state = 'MOVING';
|
this.state = 'MOVING';
|
||||||
// Pick random spot behind console (width ~4.0, so range +/- 1.5 is safe)
|
this.targetX = (Math.random() - 0.5) * 2.5;
|
||||||
this.targetX = (Math.random() - 0.5) * 3.0;
|
|
||||||
}
|
}
|
||||||
} else if (this.state === 'MOVING') {
|
} else if (this.state === 'MOVING') {
|
||||||
const speed = 1.5;
|
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.state = 'IDLE';
|
||||||
this.moveTimer = 2 + Math.random() * 5; // Wait 2-7 seconds before moving again
|
this.moveTimer = 2 + Math.random() * 5; // Wait 2-7 seconds before moving again
|
||||||
} else {
|
} else {
|
||||||
const dir = Math.sign(this.targetX - this.mesh.position.x);
|
const dir = Math.sign(this.targetX - this.group.position.x);
|
||||||
this.mesh.position.x += dir * speed * deltaTime;
|
this.group.position.x += dir * speed * deltaTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPartyStart() {
|
onPartyStart() {
|
||||||
if(this.mesh) this.mesh.visible = true;
|
if(this.group) this.group.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onPartyEnd() {
|
onPartyEnd() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user