Feature: rose window lightshafts

This commit is contained in:
Dejvino 2025-11-22 10:30:08 +01:00
parent 1c8eb8534e
commit c612b38574
2 changed files with 151 additions and 0 deletions

View File

@ -13,6 +13,8 @@ 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 ---

View 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 = 7;
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.1;
// --- 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.25 : 0;
mesh.material.opacity = fadeOpacity + beatPulse;
});
}
}
new RoseWindowLightshafts();