Feature: blob party guests
This commit is contained in:
parent
eb8e74273d
commit
b76810e883
@ -2,31 +2,24 @@ 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';
|
||||||
const guestTextureUrls = [
|
|
||||||
'/textures/guest1.png',
|
|
||||||
'/textures/guest2.png',
|
|
||||||
'/textures/guest3.png',
|
|
||||||
'/textures/guest4.png',
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- Scene dimensions for positioning ---
|
// --- Scene dimensions for positioning ---
|
||||||
const stageHeight = 1.5;
|
const stageHeight = 1.5;
|
||||||
const stageDepth = 5;
|
const stageDepth = 5;
|
||||||
const length = 20;
|
const length = 25;
|
||||||
const numGuests = 150;
|
const numGuests = 150;
|
||||||
const moveSpeed = 0.8;
|
const moveSpeed = 0.8;
|
||||||
const movementArea = { x: 15, z: length, y: 0, centerZ: -4 };
|
const movementArea = { x: 20, z: length, y: 0, centerZ: -2 };
|
||||||
const jumpChance = 0.01;
|
const jumpChance = 0.01;
|
||||||
const jumpDuration = 0.3;
|
const jumpDuration = 0.3;
|
||||||
const jumpHeight = 0.05;
|
const jumpHeight = 0.2;
|
||||||
const jumpVariance = 0.5;
|
const jumpVariance = 0.1;
|
||||||
const rushIn = false;
|
const rushIn = false;
|
||||||
const waitTimeBase = 10;
|
const waitTimeBase = 10;
|
||||||
const waitTimeVariance = 10;
|
const waitTimeVariance = 60;
|
||||||
|
|
||||||
// --- Billboard Properties ---
|
// --- Guest Properties ---
|
||||||
const guestHeight = 2.5;
|
const guestHeight = 1.8; // Approx height of the blob+head
|
||||||
const guestWidth = 2.5;
|
|
||||||
|
|
||||||
export class PartyGuests extends SceneFeature {
|
export class PartyGuests extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -35,67 +28,96 @@ export class PartyGuests extends SceneFeature {
|
|||||||
sceneFeatureManager.register(this);
|
sceneFeatureManager.register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
init() {
|
||||||
const processTexture = (texture) => {
|
// Shared Geometries
|
||||||
const image = texture.image;
|
// Body: Tall blob (Capsule)
|
||||||
const canvas = document.createElement('canvas');
|
const bodyGeo = new THREE.CapsuleGeometry(0.3, 0.8, 4, 8);
|
||||||
canvas.width = image.width;
|
// Head: Sphere
|
||||||
canvas.height = image.height;
|
const headGeo = new THREE.SphereGeometry(0.25, 16, 16);
|
||||||
const context = canvas.getContext('2d');
|
// Arm: Ellipsoid (Scaled Sphere)
|
||||||
context.drawImage(image, 0, 0);
|
const armGeo = new THREE.SphereGeometry(0.12, 16, 16);
|
||||||
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 = 20;
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const materials = await Promise.all(guestTextureUrls.map(async (url) => {
|
|
||||||
const texture = await state.loader.loadAsync(url);
|
|
||||||
const processedTexture = processTexture(texture);
|
|
||||||
return new THREE.MeshStandardMaterial({
|
|
||||||
map: processedTexture,
|
|
||||||
side: THREE.DoubleSide,
|
|
||||||
alphaTest: 0.5,
|
|
||||||
roughness: 0.7,
|
|
||||||
metalness: 0.1,
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
const createGuests = () => {
|
const createGuests = () => {
|
||||||
const geometry = new THREE.PlaneGeometry(guestWidth, guestHeight);
|
|
||||||
|
|
||||||
for (let i = 0; i < numGuests; i++) {
|
for (let i = 0; i < numGuests; i++) {
|
||||||
const material = materials[i % materials.length];
|
// Random Color
|
||||||
const guest = new THREE.Mesh(geometry, material);
|
// Dark gray-blue shades
|
||||||
|
const color = new THREE.Color().setHSL(
|
||||||
|
0.6 + (Math.random() * 0.1 - 0.05), // Hue around 0.6 (blue)
|
||||||
|
0.1 + Math.random() * 0.1, // Low saturation
|
||||||
|
0.01 + Math.random() * 0.05 // Much darker lightness
|
||||||
|
);
|
||||||
|
const material = new THREE.MeshStandardMaterial({
|
||||||
|
color: color,
|
||||||
|
roughness: 0.6,
|
||||||
|
metalness: 0.1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const group = new THREE.Group();
|
||||||
|
|
||||||
|
// Body
|
||||||
|
const body = new THREE.Mesh(bodyGeo, material);
|
||||||
|
body.position.y = 0.7; // Center of capsule (0.8 length + 0.3*2 radius = 1.4 total height. Center at 0.7)
|
||||||
|
body.castShadow = true;
|
||||||
|
body.receiveShadow = true;
|
||||||
|
group.add(body);
|
||||||
|
|
||||||
|
// Head
|
||||||
|
const head = new THREE.Mesh(headGeo, material);
|
||||||
|
head.position.y = 1.55; // Top of body
|
||||||
|
head.castShadow = true;
|
||||||
|
head.receiveShadow = true;
|
||||||
|
group.add(head);
|
||||||
|
|
||||||
|
// Arms
|
||||||
|
const createArm = (isLeft) => {
|
||||||
|
const pivot = new THREE.Group();
|
||||||
|
// Shoulder position
|
||||||
|
pivot.position.set(isLeft ? -0.35 : 0.35, 1.3, 0);
|
||||||
|
|
||||||
|
const arm = new THREE.Mesh(armGeo, material);
|
||||||
|
arm.scale.set(1, 3.5, 1); // Ellipsoid
|
||||||
|
arm.position.y = -0.4; // Hang down from pivot
|
||||||
|
arm.castShadow = true;
|
||||||
|
arm.receiveShadow = true;
|
||||||
|
|
||||||
|
pivot.add(arm);
|
||||||
|
return pivot;
|
||||||
|
};
|
||||||
|
|
||||||
|
const leftArm = createArm(true);
|
||||||
|
const rightArm = createArm(false);
|
||||||
|
|
||||||
|
group.add(leftArm);
|
||||||
|
group.add(rightArm);
|
||||||
|
|
||||||
|
// Position
|
||||||
const pos = new THREE.Vector3(
|
const pos = new THREE.Vector3(
|
||||||
(Math.random() - 0.5) * 10,
|
(Math.random() - 0.5) * 10,
|
||||||
guestHeight / 2,
|
0,
|
||||||
movementArea.centerZ + ( rushIn
|
movementArea.centerZ + ( rushIn
|
||||||
? (((Math.random()-0.5) * length * 0.6) - length * 0.3)
|
? (((Math.random()-0.5) * length * 0.6) - length * 0.3)
|
||||||
: ((Math.random()-0.5) * length))
|
: ((Math.random()-0.5) * length))
|
||||||
);
|
);
|
||||||
guest.visible = false; // Start invisible
|
|
||||||
guest.position.copy(pos);
|
group.position.copy(pos);
|
||||||
state.scene.add(guest);
|
group.visible = false;
|
||||||
|
state.scene.add(group);
|
||||||
|
|
||||||
this.guests.push({
|
this.guests.push({
|
||||||
mesh: guest,
|
mesh: group,
|
||||||
|
leftArm,
|
||||||
|
rightArm,
|
||||||
state: 'WAITING',
|
state: 'WAITING',
|
||||||
targetPosition: pos.clone(),
|
targetPosition: pos.clone(),
|
||||||
waitStartTime: 0,
|
waitStartTime: 0,
|
||||||
waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds
|
waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds
|
||||||
isMirrored: false,
|
|
||||||
canChangePose: true,
|
|
||||||
isJumping: false,
|
isJumping: false,
|
||||||
jumpStartTime: 0,
|
jumpStartTime: 0,
|
||||||
|
jumpHeight: 0,
|
||||||
|
shouldRaiseArms: false,
|
||||||
|
handsUpTimer: 0,
|
||||||
|
handsRaisedType: 'BOTH',
|
||||||
|
randomOffset: Math.random() * 100
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -106,42 +128,65 @@ export class PartyGuests extends SceneFeature {
|
|||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
if (this.guests.length === 0 || !state.partyStarted) return;
|
if (this.guests.length === 0 || !state.partyStarted) return;
|
||||||
|
|
||||||
const cameraPosition = new THREE.Vector3();
|
|
||||||
state.camera.getWorldPosition(cameraPosition);
|
|
||||||
|
|
||||||
const time = state.clock.getElapsedTime();
|
const time = state.clock.getElapsedTime();
|
||||||
|
|
||||||
this.guests.forEach(guestObj => {
|
this.guests.forEach(guestObj => {
|
||||||
const { mesh } = guestObj;
|
const { mesh, leftArm, rightArm } = guestObj;
|
||||||
mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z);
|
|
||||||
|
|
||||||
// --- Mirroring on Beat ---
|
|
||||||
if (state.music) {
|
|
||||||
if (state.music.beatIntensity > 0.8 && guestObj.canChangePose) {
|
|
||||||
guestObj.isMirrored = Math.random() < 0.5;
|
|
||||||
mesh.material.map.repeat.x = guestObj.isMirrored ? -1 : 1;
|
|
||||||
mesh.material.map.offset.x = guestObj.isMirrored ? 1 : 0;
|
|
||||||
guestObj.canChangePose = false;
|
|
||||||
} else if (state.music.beatIntensity < 0.2) {
|
|
||||||
guestObj.canChangePose = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (guestObj.state === 'WAITING') {
|
if (guestObj.state === 'WAITING') {
|
||||||
|
// Face the stage (approx z = -20)
|
||||||
|
const dx = 0 - mesh.position.x;
|
||||||
|
const dz = -20 - mesh.position.z;
|
||||||
|
const targetRotation = Math.atan2(dx, dz);
|
||||||
|
let rotDiff = targetRotation - mesh.rotation.y;
|
||||||
|
while (rotDiff > Math.PI) rotDiff -= Math.PI * 2;
|
||||||
|
while (rotDiff < -Math.PI) rotDiff += Math.PI * 2;
|
||||||
|
mesh.rotation.y += rotDiff * deltaTime * 2.0;
|
||||||
|
|
||||||
|
// Gentle Bob and Sway
|
||||||
|
if (!guestObj.isJumping) {
|
||||||
|
const bobSpeed = 6.0;
|
||||||
|
mesh.position.y = Math.abs(Math.sin(time * bobSpeed + guestObj.randomOffset)) * 0.05;
|
||||||
|
mesh.rotation.z = Math.sin(time * (bobSpeed * 0.5) + guestObj.randomOffset) * 0.05;
|
||||||
|
} else {
|
||||||
|
mesh.rotation.z = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (time > guestObj.waitStartTime + guestObj.waitTime) {
|
if (time > guestObj.waitStartTime + guestObj.waitTime) {
|
||||||
const newTarget = new THREE.Vector3(
|
const newTarget = new THREE.Vector3(
|
||||||
(Math.random() - 0.5) * movementArea.x,
|
(Math.random() - 0.5) * movementArea.x,
|
||||||
movementArea.y + guestHeight / 2,
|
0,
|
||||||
movementArea.centerZ + (Math.random() - 0.5) * movementArea.z
|
movementArea.centerZ + (Math.random() - 0.5) * movementArea.z
|
||||||
);
|
);
|
||||||
guestObj.targetPosition = newTarget;
|
guestObj.targetPosition = newTarget;
|
||||||
guestObj.state = 'MOVING';
|
guestObj.state = 'MOVING';
|
||||||
}
|
}
|
||||||
} else if (guestObj.state === 'MOVING') {
|
} else if (guestObj.state === 'MOVING') {
|
||||||
const distance = mesh.position.distanceTo(guestObj.targetPosition);
|
const currentPosFlat = new THREE.Vector3(mesh.position.x, 0, mesh.position.z);
|
||||||
|
const targetPosFlat = new THREE.Vector3(guestObj.targetPosition.x, 0, guestObj.targetPosition.z);
|
||||||
|
|
||||||
|
const distance = currentPosFlat.distanceTo(targetPosFlat);
|
||||||
if (distance > 0.1) {
|
if (distance > 0.1) {
|
||||||
const direction = guestObj.targetPosition.clone().sub(mesh.position).normalize();
|
const direction = targetPosFlat.sub(currentPosFlat).normalize();
|
||||||
mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime));
|
mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime));
|
||||||
|
|
||||||
|
// If moving away from stage (positive Z), drop hands
|
||||||
|
if (direction.z > 0.1) {
|
||||||
|
guestObj.handsUpTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Face direction
|
||||||
|
const targetRotation = Math.atan2(direction.x, direction.z);
|
||||||
|
let rotDiff = targetRotation - mesh.rotation.y;
|
||||||
|
while (rotDiff > Math.PI) rotDiff -= Math.PI * 2;
|
||||||
|
while (rotDiff < -Math.PI) rotDiff += Math.PI * 2;
|
||||||
|
mesh.rotation.y += rotDiff * deltaTime * 5;
|
||||||
|
|
||||||
|
if (!guestObj.isJumping) {
|
||||||
|
mesh.position.y = 0;
|
||||||
|
mesh.rotation.z = 0;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
guestObj.state = 'WAITING';
|
guestObj.state = 'WAITING';
|
||||||
guestObj.waitStartTime = time;
|
guestObj.waitStartTime = time;
|
||||||
@ -149,14 +194,18 @@ export class PartyGuests extends SceneFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update hands up timer
|
||||||
|
if (guestObj.handsUpTimer > 0) {
|
||||||
|
guestObj.handsUpTimer -= deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
if (guestObj.isJumping) {
|
if (guestObj.isJumping) {
|
||||||
const jumpProgress = (time - guestObj.jumpStartTime) / jumpDuration;
|
const jumpProgress = (time - guestObj.jumpStartTime) / jumpDuration;
|
||||||
if (jumpProgress < 1) {
|
if (jumpProgress < 1) {
|
||||||
const baseHeight = movementArea.y + guestHeight / 2;
|
mesh.position.y = Math.sin(jumpProgress * Math.PI) * guestObj.jumpHeight;
|
||||||
mesh.position.y = baseHeight + Math.sin(jumpProgress * Math.PI) * guestObj.jumpHeight;
|
|
||||||
} else {
|
} else {
|
||||||
guestObj.isJumping = false;
|
guestObj.isJumping = false;
|
||||||
mesh.position.y = movementArea.y + guestHeight / 2;
|
mesh.position.y = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let currentJumpChance = jumpChance * deltaTime; // Base chance over time
|
let currentJumpChance = jumpChance * deltaTime; // Base chance over time
|
||||||
@ -168,8 +217,35 @@ export class PartyGuests extends SceneFeature {
|
|||||||
guestObj.isJumping = true;
|
guestObj.isJumping = true;
|
||||||
guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
||||||
guestObj.jumpStartTime = time;
|
guestObj.jumpStartTime = time;
|
||||||
|
|
||||||
|
if (Math.random() < 0.5) {
|
||||||
|
guestObj.handsUpTimer = 2.0 + Math.random() * 3.0; // Keep hands up for 2-5 seconds
|
||||||
|
const r = Math.random();
|
||||||
|
if (r < 0.33) guestObj.handsRaisedType = 'LEFT';
|
||||||
|
else if (r < 0.66) guestObj.handsRaisedType = 'RIGHT';
|
||||||
|
else guestObj.handsRaisedType = 'BOTH';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Arm Rotation
|
||||||
|
let targetLeftAngle = 0;
|
||||||
|
let targetRightAngle = 0;
|
||||||
|
|
||||||
|
if (guestObj.handsUpTimer > 0) {
|
||||||
|
const upAngle = -Math.PI;
|
||||||
|
if (guestObj.handsRaisedType === 'LEFT' || guestObj.handsRaisedType === 'BOTH') {
|
||||||
|
targetLeftAngle = upAngle;
|
||||||
|
}
|
||||||
|
if (guestObj.handsRaisedType === 'RIGHT' || guestObj.handsRaisedType === 'BOTH') {
|
||||||
|
targetRightAngle = upAngle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
leftArm.rotation.x = THREE.MathUtils.lerp(leftArm.rotation.x, targetLeftAngle, deltaTime * 5);
|
||||||
|
rightArm.rotation.x = THREE.MathUtils.lerp(rightArm.rotation.x, targetRightAngle, deltaTime * 5);
|
||||||
|
leftArm.rotation.z = 0;
|
||||||
|
rightArm.rotation.z = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user