Compare commits
7 Commits
383ba8baf1
...
37f0d30fee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37f0d30fee | ||
|
|
c612b38574 | ||
|
|
1c8eb8534e | ||
|
|
8abdb94182 | ||
|
|
390189e116 | ||
|
|
09a2dd03bc | ||
|
|
612a1bf501 |
@ -34,17 +34,6 @@ export function init() {
|
||||
// 6. Initialize all visual effects via the manager
|
||||
state.effectsManager = new EffectsManager(state.scene);
|
||||
|
||||
// --- 8. Debug Visualization Helpers ---
|
||||
// Visual aids for the light source positions
|
||||
if (state.debugLight && THREE.PointLightHelper) {
|
||||
const screenHelper = new THREE.PointLightHelper(state.screenLight, 0.1, 0xff0000); // Red for screen
|
||||
state.scene.add(screenHelper);
|
||||
|
||||
// Lamp Helper will now work since lampLight is added to the scene
|
||||
const lampHelperPoint = new THREE.PointLightHelper(state.lampLightPoint, 0.1, 0x00ff00); // Green for lamp
|
||||
state.scene.add(lampHelperPoint);
|
||||
}
|
||||
|
||||
// 9. Event Listeners
|
||||
window.addEventListener('resize', onWindowResize, false);
|
||||
|
||||
|
||||
181
party-cathedral/src/scene/dancers.js
Normal file
181
party-cathedral/src/scene/dancers.js
Normal file
@ -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();
|
||||
@ -61,6 +61,15 @@ export class LightBall extends SceneFeature {
|
||||
mesh.position.y = 10 + Math.cos(time * driftSpeed * 1.3 + offset) * naveHeight/2 * 0.6;
|
||||
mesh.position.z = Math.cos(time * driftSpeed * 0.7 + offset) * length/2 * 0.8;
|
||||
light.position.copy(mesh.position);
|
||||
|
||||
// --- Music Visualization ---
|
||||
if (state.music) {
|
||||
const baseIntensity = 4.0;
|
||||
light.intensity = baseIntensity + state.music.beatIntensity * 3.0;
|
||||
|
||||
const baseScale = 1.0;
|
||||
mesh.scale.setScalar(baseScale + state.music.beatIntensity * 0.5);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -224,7 +238,12 @@ export class MedievalMusicians extends SceneFeature {
|
||||
mesh.position.y = area.y + musicianHeight / 2;
|
||||
}
|
||||
} else {
|
||||
if (Math.random() < jumpChance && musicianObj.state !== 'JUMPING_PLANE' && musicianObj.state !== 'PREPARING_JUMP') {
|
||||
let currentJumpChance = jumpChance * deltaTime; // Base chance over time
|
||||
if (state.music && state.music.beatIntensity > 0.8) {
|
||||
currentJumpChance = 0.1; // High, fixed chance on the beat
|
||||
}
|
||||
|
||||
if (Math.random() < currentJumpChance && musicianObj.state !== 'JUMPING_PLANE' && musicianObj.state !== 'PREPARING_JUMP') {
|
||||
musicianObj.isJumping = true;
|
||||
musicianObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
||||
musicianObj.jumpStartTime = time;
|
||||
|
||||
39
party-cathedral/src/scene/music-visualizer.js
Normal file
39
party-cathedral/src/scene/music-visualizer.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { state } from '../state.js';
|
||||
import { SceneFeature } from './SceneFeature.js';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
|
||||
export class MusicVisualizer extends SceneFeature {
|
||||
constructor() {
|
||||
super();
|
||||
sceneFeatureManager.register(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
// Initialize music state
|
||||
state.music = {
|
||||
bpm: 120,
|
||||
beatDuration: 60 / 120,
|
||||
measureDuration: (60 / 120) * 4,
|
||||
beatIntensity: 0,
|
||||
measurePulse: 0,
|
||||
};
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
if (!state.music) return;
|
||||
|
||||
const time = state.clock.getElapsedTime();
|
||||
|
||||
// --- Calculate Beat Intensity (pulses every beat) ---
|
||||
// This creates a sharp attack and slower decay (0 -> 1 -> 0)
|
||||
const beatProgress = (time % state.music.beatDuration) / state.music.beatDuration;
|
||||
state.music.beatIntensity = Math.pow(1.0 - beatProgress, 2);
|
||||
|
||||
// --- Calculate Measure Pulse (spikes every 4 beats) ---
|
||||
// This creates a very sharp spike for the torch flame effect
|
||||
const measureProgress = (time % state.music.measureDuration) / state.music.measureDuration;
|
||||
state.music.measurePulse = measureProgress < 0.2 ? Math.sin(measureProgress * Math.PI * 5) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
new MusicVisualizer();
|
||||
@ -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,
|
||||
});
|
||||
@ -97,7 +99,7 @@ export class PartyGuests extends SceneFeature {
|
||||
|
||||
const time = state.clock.getElapsedTime();
|
||||
const moveSpeed = 1.0; // Move slower
|
||||
const movementArea = { x: 10, z: 30, y: 0, centerZ: 0 };
|
||||
const movementArea = { x: 10, z: 30, y: 0, centerZ: 5 };
|
||||
const jumpChance = 0.05; // Jump way more
|
||||
const jumpDuration = 0.5;
|
||||
const jumpHeight = 0.1;
|
||||
@ -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(
|
||||
@ -139,7 +153,12 @@ export class PartyGuests extends SceneFeature {
|
||||
mesh.position.y = movementArea.y + guestHeight / 2;
|
||||
}
|
||||
} else {
|
||||
if (Math.random() < jumpChance) {
|
||||
let currentJumpChance = jumpChance * deltaTime; // Base chance over time
|
||||
if (state.music && state.music.beatIntensity > 0.8) {
|
||||
currentJumpChance = 0.1; // High, fixed chance on the beat
|
||||
}
|
||||
|
||||
if (Math.random() < currentJumpChance) {
|
||||
guestObj.isJumping = true;
|
||||
guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
||||
guestObj.jumpStartTime = time;
|
||||
|
||||
0
party-cathedral/src/scene/pillar-candles.js
Normal file
0
party-cathedral/src/scene/pillar-candles.js
Normal file
@ -10,6 +10,11 @@ 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';
|
||||
import { RoseWindowLight } from './rose-window-light.js';
|
||||
import { RoseWindowLightshafts } from './rose-window-lightshafts.js';
|
||||
|
||||
// Scene Features ^^^
|
||||
|
||||
// --- Scene Modeling Function ---
|
||||
@ -34,11 +39,18 @@ export function createSceneObjects() {
|
||||
state.scene.add(floor);
|
||||
|
||||
// 3. Lighting (Minimal and focused)
|
||||
const ambientLight = new THREE.AmbientLight(0x606060, 0.1); // Increased ambient light for a larger space
|
||||
const ambientLight = new THREE.AmbientLight(0x606060, 0.2); // Increased ambient light for a larger space
|
||||
state.scene.add(ambientLight);
|
||||
|
||||
// Add a HemisphereLight for more natural, general illumination in a large space.
|
||||
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.2);
|
||||
const hemisphereLight = new THREE.HemisphereLight(0xffddcc, 0x444455, 0.5);
|
||||
|
||||
// Visual aids for the light source positions
|
||||
if (state.debugLight && THREE.HemisphereLightHelper) {
|
||||
// Lamp Helper will now work since lampLight is added to the scene
|
||||
const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.1, 0x00ff00); // Green for lamp
|
||||
state.scene.add(hemisphereLightHelper);
|
||||
}
|
||||
|
||||
state.scene.add(hemisphereLight);
|
||||
}
|
||||
|
||||
66
party-cathedral/src/scene/rose-window-light.js
Normal file
66
party-cathedral/src/scene/rose-window-light.js
Normal file
@ -0,0 +1,66 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { SceneFeature } from './SceneFeature.js';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
|
||||
export class RoseWindowLight extends SceneFeature {
|
||||
constructor() {
|
||||
super();
|
||||
this.spotlight = null;
|
||||
this.helper = null;
|
||||
sceneFeatureManager.register(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
// --- Dimensions for positioning ---
|
||||
const length = 40;
|
||||
const naveHeight = 15;
|
||||
const stageDepth = 5;
|
||||
|
||||
// --- Create the spotlight ---
|
||||
this.spotlight = new THREE.SpotLight(0xffffff, 100.0); // White light, high intensity
|
||||
this.spotlight.position.set(0, naveHeight, -length / 2 + 10); // Position it at the rose window
|
||||
this.spotlight.angle = Math.PI / 9; // A reasonably focused beam
|
||||
this.spotlight.penumbra = 0.3; // Soft edges
|
||||
this.spotlight.decay = 0.7;
|
||||
this.spotlight.distance = 30;
|
||||
|
||||
this.spotlight.castShadow = false;
|
||||
this.spotlight.shadow.mapSize.width = 1024;
|
||||
this.spotlight.shadow.mapSize.height = 1024;
|
||||
this.spotlight.shadow.camera.near = 1;
|
||||
this.spotlight.shadow.camera.far = 30;
|
||||
this.spotlight.shadow.focus = 1;
|
||||
|
||||
// --- Create a target for the spotlight to aim at ---
|
||||
const targetObject = new THREE.Object3D();
|
||||
targetObject.position.set(0, 0, -length / 2 + stageDepth); // Aim at the center of the stage
|
||||
state.scene.add(targetObject);
|
||||
this.spotlight.target = targetObject;
|
||||
|
||||
state.scene.add(this.spotlight);
|
||||
|
||||
// --- Add a debug helper ---
|
||||
if (state.debugLight) {
|
||||
this.helper = new THREE.SpotLightHelper(this.spotlight);
|
||||
state.scene.add(this.helper);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
if (!this.spotlight) return;
|
||||
|
||||
// Make the light pulse with the music
|
||||
if (state.music) {
|
||||
const baseIntensity = 4.0;
|
||||
this.spotlight.intensity = baseIntensity + state.music.beatIntensity * 1.0;
|
||||
}
|
||||
|
||||
// Update the helper if it exists
|
||||
if (this.helper) {
|
||||
this.helper.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new RoseWindowLight();
|
||||
149
party-cathedral/src/scene/rose-window-lightshafts.js
Normal file
149
party-cathedral/src/scene/rose-window-lightshafts.js
Normal file
@ -0,0 +1,149 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { SceneFeature } from './SceneFeature.js';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
|
||||
export class RoseWindowLightshafts extends SceneFeature {
|
||||
constructor() {
|
||||
super();
|
||||
this.shafts = [];
|
||||
sceneFeatureManager.register(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
// --- Dimensions for positioning ---
|
||||
const length = 40;
|
||||
const naveWidth = 12;
|
||||
const naveHeight = 15;
|
||||
const stageDepth = 5;
|
||||
const stageWidth = naveWidth - 1;
|
||||
|
||||
const roseWindowRadius = naveWidth / 2 - 2;
|
||||
const roseWindowCenter = new THREE.Vector3(0, naveHeight - 2, -length / 2 + 0.1);
|
||||
|
||||
// --- Procedural Noise Texture for Light Shafts ---
|
||||
const createNoiseTexture = () => {
|
||||
const width = 64;
|
||||
const height = 512;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext('2d');
|
||||
const imageData = context.createImageData(width, height);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// Create vertical streaks of noise
|
||||
const y = Math.floor((i / 4) / width);
|
||||
const noise = Math.pow(Math.random(), 2.5) * (1 - y / height) * 255;
|
||||
data[i] = noise; // R
|
||||
data[i + 1] = noise; // G
|
||||
data[i + 2] = noise; // B
|
||||
data[i + 3] = 255; // A
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
};
|
||||
|
||||
const texture = createNoiseTexture();
|
||||
texture.wrapS = THREE.RepeatWrapping;
|
||||
texture.wrapT = THREE.RepeatWrapping;
|
||||
|
||||
const baseMaterial = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
blending: THREE.AdditiveBlending,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
opacity: 0.3,
|
||||
color: 0x88aaff, // Give the light a cool blueish tint
|
||||
});
|
||||
|
||||
// --- Create multiple thin light shafts ---
|
||||
const numShafts = 12;
|
||||
for (let i = 0; i < numShafts; i++) {
|
||||
const material = baseMaterial.clone(); // Each shaft needs its own material for individual opacity
|
||||
|
||||
const startAngle = Math.random() * Math.PI * 2;
|
||||
const startRadius = Math.random() * roseWindowRadius;
|
||||
const startPoint = new THREE.Vector3(
|
||||
roseWindowCenter.x + Math.cos(startAngle) * startRadius,
|
||||
roseWindowCenter.y + Math.sin(startAngle) * startRadius,
|
||||
roseWindowCenter.z
|
||||
);
|
||||
|
||||
// Define a linear path on the floor for the beam to travel
|
||||
const floorStartPoint = new THREE.Vector3(
|
||||
(Math.random() - 0.5) * stageWidth * 1.0,
|
||||
0,
|
||||
-length / 2 + Math.random() * 10 + 0
|
||||
);
|
||||
const floorEndPoint = new THREE.Vector3(
|
||||
(Math.random() - 0.5) * stageWidth * 1.0,
|
||||
0,
|
||||
-length / 2 + Math.random() * 10 + 3
|
||||
);
|
||||
|
||||
const distance = startPoint.distanceTo(floorStartPoint);
|
||||
const geometry = new THREE.CylinderGeometry(0.1, 0.5 + Math.random() * 0.5, distance, 16, 1, true);
|
||||
const lightShaft = new THREE.Mesh(geometry, material);
|
||||
|
||||
state.scene.add(lightShaft);
|
||||
this.shafts.push({
|
||||
mesh: lightShaft,
|
||||
startPoint: startPoint, // The stationary point in the window
|
||||
endPoint: floorStartPoint.clone(), // The current position of the beam on the floor
|
||||
floorStartPoint: floorStartPoint, // The start of the sweep path
|
||||
floorEndPoint: floorEndPoint, // The end of the sweep path
|
||||
moveSpeed: 0.5 + Math.random() * 1.5, // Each shaft has a different speed
|
||||
// No 'state' needed anymore
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
const baseOpacity = 0.1;
|
||||
|
||||
this.shafts.forEach(shaft => {
|
||||
const { mesh, startPoint, endPoint, floorStartPoint, floorEndPoint, moveSpeed } = shaft;
|
||||
|
||||
// Animate texture for dust motes
|
||||
mesh.material.map.offset.y -= deltaTime * 0.001;
|
||||
|
||||
// --- Movement Logic ---
|
||||
const pathDirection = floorEndPoint.clone().sub(floorStartPoint).normalize();
|
||||
const pathLength = floorStartPoint.distanceTo(floorEndPoint);
|
||||
|
||||
// Move the endpoint along its path
|
||||
endPoint.add(pathDirection.clone().multiplyScalar(moveSpeed * deltaTime));
|
||||
|
||||
const currentDistance = floorStartPoint.distanceTo(endPoint);
|
||||
|
||||
if (currentDistance >= pathLength) {
|
||||
// Reached the end, reset to the start
|
||||
endPoint.copy(floorStartPoint);
|
||||
}
|
||||
|
||||
// --- Opacity based on Progress ---
|
||||
const progress = Math.min(currentDistance / pathLength, 1.0);
|
||||
// Use a sine curve to fade in at the start and out at the end
|
||||
const fadeOpacity = Math.sin(progress * Math.PI) * baseOpacity;
|
||||
|
||||
// --- Update Mesh Position and Orientation ---
|
||||
const distance = startPoint.distanceTo(endPoint);
|
||||
mesh.scale.y = distance;
|
||||
mesh.position.lerpVectors(startPoint, endPoint, 0.5);
|
||||
|
||||
const quaternion = new THREE.Quaternion();
|
||||
const cylinderUp = new THREE.Vector3(0, 1, 0);
|
||||
const direction = new THREE.Vector3().subVectors(endPoint, startPoint).normalize();
|
||||
quaternion.setFromUnitVectors(cylinderUp, direction);
|
||||
mesh.quaternion.copy(quaternion);
|
||||
|
||||
// --- Music Visualization ---
|
||||
const beatPulse = state.music ? state.music.beatIntensity * 0.05 : 0;
|
||||
mesh.material.opacity = fadeOpacity + beatPulse;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new RoseWindowLightshafts();
|
||||
@ -4,6 +4,8 @@ import { SceneFeature } from './SceneFeature.js';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
import sparkTextureUrl from '/textures/spark.png';
|
||||
|
||||
const lightPositionBaseY = 1.2;
|
||||
|
||||
export class StageTorches extends SceneFeature {
|
||||
constructor() {
|
||||
super();
|
||||
@ -48,7 +50,7 @@ export class StageTorches extends SceneFeature {
|
||||
|
||||
// --- Point Light ---
|
||||
const pointLight = new THREE.PointLight(0xffaa44, 2.5, 8);
|
||||
pointLight.position.y = 1.2;
|
||||
pointLight.position.y = lightPositionBaseY;
|
||||
pointLight.castShadow = true;
|
||||
pointLight.shadow.mapSize.width = 128;
|
||||
pointLight.shadow.mapSize.height = 128;
|
||||
@ -86,30 +88,46 @@ export class StageTorches extends SceneFeature {
|
||||
|
||||
update(deltaTime) {
|
||||
this.torches.forEach(torch => {
|
||||
let measurePulse = 0;
|
||||
if (state.music) {
|
||||
measurePulse = state.music.measurePulse * 4.0; // Make flames jump higher
|
||||
}
|
||||
|
||||
// --- Animate Particles ---
|
||||
const positions = torch.particles.geometry.attributes.position.array;
|
||||
let averageY = 0;
|
||||
for (let i = 0; i < torch.particleData.length; i++) {
|
||||
const data = torch.particleData[i];
|
||||
data.life -= deltaTime;
|
||||
|
||||
const yVelocity = data.velocity.y;
|
||||
if (data.life <= 0) {
|
||||
// Reset particle
|
||||
positions[i * 3] = 0;
|
||||
positions[i * 3 + 1] = 1;
|
||||
positions[i * 3 + 2] = 0;
|
||||
data.life = Math.random() * 1.0;
|
||||
data.velocity.y = Math.random() * 1.5 + measurePulse;
|
||||
} else {
|
||||
// Update position
|
||||
positions[i * 3] += data.velocity.x * deltaTime;
|
||||
positions[i * 3 + 1] += data.velocity.y * deltaTime;
|
||||
positions[i * 3 + 1] += yVelocity * deltaTime;
|
||||
positions[i * 3 + 2] += data.velocity.z * deltaTime;
|
||||
}
|
||||
averageY += positions[i * 3 + 1];
|
||||
}
|
||||
averageY = averageY / positions.length;
|
||||
torch.particles.geometry.attributes.position.needsUpdate = true;
|
||||
|
||||
// --- Flicker Light ---
|
||||
const flicker = Math.random() * 0.5;
|
||||
torch.light.intensity = 2.0 + flicker;
|
||||
const baseIntensity = 2.0;
|
||||
const flicker = Math.random() * 0.6;
|
||||
let beatPulse = 0;
|
||||
if (state.music) {
|
||||
beatPulse = state.music.beatIntensity * 1.5;
|
||||
}
|
||||
|
||||
torch.light.intensity = baseIntensity + flicker + beatPulse;
|
||||
torch.light.position.y = lightPositionBaseY + averageY;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,10 +235,13 @@ export class StainedGlass extends SceneFeature {
|
||||
|
||||
update(deltaTime) {
|
||||
// Add a subtle pulsing glow to the windows
|
||||
const pulseSpeed = 0.5;
|
||||
const minIntensity = 0.1; // Increased intensity for a stronger glow
|
||||
const maxIntensity = 0.2;
|
||||
const intensity = minIntensity + (maxIntensity - minIntensity) * (0.5 * (1 + Math.sin(state.clock.getElapsedTime() * pulseSpeed)));
|
||||
let intensity = 0.15; // Base intensity
|
||||
|
||||
// --- Music Visualization ---
|
||||
if (state.music) {
|
||||
const beatPulse = state.music.beatIntensity * 0.3;
|
||||
intensity += beatPulse;
|
||||
}
|
||||
|
||||
// To make the glow match the vertex colors, we set the emissive color to white
|
||||
// and modulate its intensity. The final glow color will be vertexColor * emissive * emissiveIntensity.
|
||||
|
||||
BIN
party-cathedral/textures/dancer1.png
Normal file
BIN
party-cathedral/textures/dancer1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Loading…
Reference in New Issue
Block a user