From c612b38574ea012160f597f260126432e1206180 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sat, 22 Nov 2025 10:30:08 +0100 Subject: [PATCH] Feature: rose window lightshafts --- party-cathedral/src/scene/root.js | 2 + .../src/scene/rose-window-lightshafts.js | 149 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 party-cathedral/src/scene/rose-window-lightshafts.js diff --git a/party-cathedral/src/scene/root.js b/party-cathedral/src/scene/root.js index 7bfa700..5e3a8ef 100644 --- a/party-cathedral/src/scene/root.js +++ b/party-cathedral/src/scene/root.js @@ -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 --- diff --git a/party-cathedral/src/scene/rose-window-lightshafts.js b/party-cathedral/src/scene/rose-window-lightshafts.js new file mode 100644 index 0000000..208a499 --- /dev/null +++ b/party-cathedral/src/scene/rose-window-lightshafts.js @@ -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(); \ No newline at end of file