diff --git a/party-cathedral/src/scene/dancers.js b/party-cathedral/src/scene/dancers.js new file mode 100644 index 0000000..16f5cd0 --- /dev/null +++ b/party-cathedral/src/scene/dancers.js @@ -0,0 +1,181 @@ +import * as THREE from 'three'; +import { state } from '../state.js'; +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.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); + + return new THREE.MeshStandardMaterial({ + map: processedTexture, + side: THREE.DoubleSide, + alphaTest: 0.5, + roughness: 0.7, + metalness: 0.1, + }); + })); + + 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(4.5, stageHeight + dancerHeight / 2, -length / 2 + stageDepth / 2 - 1.8), + ]; + + dancerPositions.forEach((pos, index) => { + const material = materials[index % materials.length]; + const dancer = new THREE.Mesh(geometry, material); + dancer.position.copy(pos); + 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) 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: 10, z: 4, 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 { + if (state.music && state.music.beatIntensity > 0.8 && Math.random() < 0.2) { + dancerObj.isJumping = true; + dancerObj.jumpStartTime = time; + } + } + }); + } +} + +new Dancers(); \ No newline at end of file diff --git a/party-cathedral/src/scene/medieval-musicians.js b/party-cathedral/src/scene/medieval-musicians.js index d740717..5bca15d 100644 --- a/party-cathedral/src/scene/medieval-musicians.js +++ b/party-cathedral/src/scene/medieval-musicians.js @@ -103,6 +103,8 @@ export class MedievalMusicians extends SceneFeature { jumpStartPos: null, jumpEndPos: null, jumpProgress: 0, + isMirrored: false, + canChangePose: true, // --- State for jumping in place --- isJumping: false, @@ -138,6 +140,18 @@ export class MedievalMusicians extends SceneFeature { // We only want to rotate on the Y axis to keep them upright mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z); + // --- Mirroring on Beat --- + if (state.music) { + if (state.music.beatIntensity > 0.8 && musicianObj.canChangePose) { + musicianObj.isMirrored = Math.random() < 0.5; + mesh.material.map.repeat.x = musicianObj.isMirrored ? -1 : 1; + mesh.material.map.offset.x = musicianObj.isMirrored ? 1 : 0; + musicianObj.canChangePose = false; + } else if (state.music.beatIntensity < 0.2) { + musicianObj.canChangePose = true; + } + } + // --- Main State Machine --- const area = musicianObj.currentPlane === 'stage' ? stageArea : floorArea; const otherArea = musicianObj.currentPlane === 'stage' ? floorArea : stageArea; diff --git a/party-cathedral/src/scene/party-guests.js b/party-cathedral/src/scene/party-guests.js index 7e23c50..e992098 100644 --- a/party-cathedral/src/scene/party-guests.js +++ b/party-cathedral/src/scene/party-guests.js @@ -80,6 +80,8 @@ export class PartyGuests extends SceneFeature { targetPosition: pos.clone(), waitStartTime: 0, waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds + isMirrored: false, + canChangePose: true, isJumping: false, jumpStartTime: 0, }); @@ -107,6 +109,18 @@ export class PartyGuests extends SceneFeature { const { mesh } = 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 (time > guestObj.waitStartTime + guestObj.waitTime) { const newTarget = new THREE.Vector3( diff --git a/party-cathedral/src/scene/root.js b/party-cathedral/src/scene/root.js index 4f7718c..feeaead 100644 --- a/party-cathedral/src/scene/root.js +++ b/party-cathedral/src/scene/root.js @@ -10,6 +10,7 @@ import { Stage } from './stage.js'; import { MedievalMusicians } from './medieval-musicians.js'; import { PartyGuests } from './party-guests.js'; import { StageTorches } from './stage-torches.js'; +import { Dancers } from './dancers.js'; import { MusicVisualizer } from './music-visualizer.js'; // Scene Features ^^^ diff --git a/party-cathedral/textures/dancer1.png b/party-cathedral/textures/dancer1.png new file mode 100644 index 0000000..25be565 Binary files /dev/null and b/party-cathedral/textures/dancer1.png differ