music-video-gen/party-cathedral/src/scene/dancers.js
2025-12-29 21:54:25 +00:00

207 lines
8.5 KiB
JavaScript

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 dancerTextureUrls = [
'/textures/dancer1.png',
];
// --- Scene dimensions for positioning ---
const stageHeight = 1.5;
const stageDepth = 5;
const length = 40;
// --- Billboard Properties ---
const dancerHeight = 2.5;
const dancerWidth = 2.5;
export class Dancers extends SceneFeature {
constructor() {
super();
this.dancers = [];
sceneFeatureManager.register(this);
}
async init() {
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 = 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(dancerTextureUrls.map(async (url) => {
const texture = await state.loader.loadAsync(url);
const processedTexture = processTexture(texture);
// Configure texture for a 2x2 sprite sheet
processedTexture.repeat.set(0.5, 0.5);
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 createDancers = () => {
const geometry = new THREE.PlaneGeometry(dancerWidth, dancerHeight);
const dancerPositions = [
new THREE.Vector3(-4, stageHeight + dancerHeight / 2, -length / 2 + stageDepth / 2 - 2),
new THREE.Vector3(0, stageHeight + dancerHeight / 2, -length / 2 + stageDepth / 2 - 1.8),
new THREE.Vector3(4, stageHeight + dancerHeight / 2, -length / 2 + stageDepth / 2 - 2.2),
];
dancerPositions.forEach((pos, index) => {
const material = materials[index % materials.length];
const dancer = new THREE.Mesh(geometry, material);
dancer.position.copy(pos);
dancer.visible = false; // Start invisible
state.scene.add(dancer);
this.dancers.push({
mesh: dancer,
baseY: pos.y,
// --- Movement State ---
state: 'WAITING',
targetPosition: pos.clone(),
waitStartTime: 0,
waitTime: 1 + Math.random() * 2, // Wait 1-3 seconds
// --- Animation State ---
currentFrame: Math.floor(Math.random() * 4), // Start on a random frame
isMirrored: false,
canChangePose: true, // Flag to ensure pose changes only once per beat
// --- Jumping State ---
isJumping: false,
jumpStartTime: 0,
});
});
};
createDancers();
}
update(deltaTime) {
if (this.dancers.length === 0 || !state.partyStarted) return;
const cameraPosition = new THREE.Vector3();
state.camera.getWorldPosition(cameraPosition);
const time = state.clock.getElapsedTime();
const jumpDuration = 0.5;
const jumpHeight = 2.0;
const moveSpeed = 2.0;
const movementArea = { x: 9, z: 3.6, centerZ: -length / 2 + stageDepth / 2 };
this.dancers.forEach(dancerObj => {
const { mesh } = dancerObj;
mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z);
// --- Point-to-Point Movement Logic ---
if (dancerObj.state === 'WAITING') {
if (time > dancerObj.waitStartTime + dancerObj.waitTime) {
// Time to find a new spot
const newTarget = new THREE.Vector3(
(Math.random() - 0.5) * movementArea.x,
dancerObj.baseY,
movementArea.centerZ + (Math.random() - 0.5) * movementArea.z
);
dancerObj.targetPosition = newTarget;
dancerObj.state = 'MOVING';
}
} else if (dancerObj.state === 'MOVING') {
const distance = mesh.position.distanceTo(dancerObj.targetPosition);
if (distance > 0.1) {
const direction = dancerObj.targetPosition.clone().sub(mesh.position).normalize();
mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime));
} else {
// Arrived at destination
dancerObj.state = 'WAITING';
dancerObj.waitStartTime = time;
dancerObj.waitTime = 1 + Math.random() * 2; // Set new wait time
}
}
// --- Spritesheet Animation ---
if (state.music) {
if (state.music.beatIntensity > 0.8 && dancerObj.canChangePose) {
// On the beat, select a new random frame and mirroring state
dancerObj.currentFrame = Math.floor(Math.random() * 4); // Select a random frame on the beat
dancerObj.isMirrored = Math.random() < 0.5;
const frameX = dancerObj.currentFrame % 2;
const frameY = Math.floor(dancerObj.currentFrame / 2);
// Adjust repeat and offset for mirroring
mesh.material.map.repeat.x = dancerObj.isMirrored ? -0.5 : 0.5;
mesh.material.map.offset.x = dancerObj.isMirrored ? (frameX * 0.5) + 0.5 : frameX * 0.5;
// The Y offset is inverted because UV coordinates start from the bottom-left
mesh.material.map.offset.y = (1 - frameY) * 0.5;
dancerObj.canChangePose = false; // Prevent changing again on this same beat
} else if (state.music.beatIntensity < 0.2) {
dancerObj.canChangePose = true; // Reset the flag when the beat is over
}
}
// --- Jumping Logic ---
if (dancerObj.isJumping) {
const jumpProgress = (time - dancerObj.jumpStartTime) / jumpDuration;
if (jumpProgress < 1.0) {
mesh.position.y = dancerObj.baseY + Math.sin(jumpProgress * Math.PI) * jumpHeight;
} else {
dancerObj.isJumping = false;
mesh.position.y = dancerObj.baseY;
}
} else {
const musicTime = state.clock.getElapsedTime();
if (state.music && state.music.isLoudEnough && state.music.beatIntensity > 0.8 && Math.random() < 0.5 && musicTime > 10) {
dancerObj.isJumping = true;
dancerObj.jumpStartTime = time;
}
}
});
}
onPartyStart() {
this.dancers.forEach(dancerObj => {
dancerObj.mesh.visible = true;
// Teleport to stage
dancerObj.state = 'WAITING';
dancerObj.mesh.position.y = dancerObj.baseY;
dancerObj.waitStartTime = state.clock.getElapsedTime();
});
}
onPartyEnd() {
this.dancers.forEach(dancerObj => {
dancerObj.isJumping = false;
//dancerObj.mesh.visible = false;
dancerObj.state = 'WAITING';
dancerObj.waitStartTime = state.clock.getElapsedTime();
});
}
}
new Dancers();