diff --git a/src/actors/Hand.ts b/src/actors/Hand.ts new file mode 100644 index 0000000..d0cd483 --- /dev/null +++ b/src/actors/Hand.ts @@ -0,0 +1,64 @@ +import { + AudioAnalyser, + BufferAttribute, + BufferGeometry, + Color, + Float32BufferAttribute, + InterleavedBufferAttribute, + Mesh, + MeshToonMaterial, +} from "three"; +import { RandomNumberGenerator } from ".."; + +export class Hand { + mesh: Mesh; + originalPositions?: BufferAttribute | InterleavedBufferAttribute; + distortions?: Array; + analyser?: AudioAnalyser; + positions?: BufferAttribute | InterleavedBufferAttribute; + distortionMax: number; + distortionPower: number; + + constructor(mesh) { + this.mesh = mesh; + this.distortionMax = 4; + this.distortionPower = 3; + } + + initialize(rng: RandomNumberGenerator, analyser: AudioAnalyser) { + this.analyser = analyser; + // set up distortion for each vertex + this.originalPositions = this.mesh.geometry.getAttribute("position"); + this.distortions = (this.originalPositions.array as Array) + .slice(0) + .map(() => rng() * 2 - 1); + this.positions = this.mesh.geometry.getAttribute("position"); + this.mesh.parent.position.x = this.mesh.parent.position.x + 2.8; + this.mesh.parent.position.y = this.mesh.parent.position.y + 0; + this.mesh.parent.position.z = this.mesh.parent.position.z + 2.2; + this.mesh.parent.rotateZ(Math.PI / 2); + this.mesh.geometry.center(); + + this.mesh.material = new MeshToonMaterial({ + color: new Color(0, 0.3, 0), + }); + this.mesh.material.wireframe = true; + } + + update() { + const sound = Math.pow( + (this.analyser.getAverageFrequency() / 255) * this.distortionMax, + this.distortionPower + ); + + const newPositions = new Float32BufferAttribute( + (this.positions.array as Array).map((_position, index) => { + const distortion = this.distortions[index] * sound; + return distortion / 10 + this.originalPositions.array[index]; + }), + 3 + ); + + this.mesh.geometry.setAttribute("position", newPositions); + } +} diff --git a/src/actors/Jukebox.ts b/src/actors/Jukebox.ts new file mode 100644 index 0000000..29521ee --- /dev/null +++ b/src/actors/Jukebox.ts @@ -0,0 +1,94 @@ +import { AudioAnalyser, AudioListener } from "three"; +import { LoadAudio } from "../audio"; + +export class Jukebox { + parser: SRTParser; + listener: AudioListener; + srts?: SRT[]; + elapsedTime: number; + subtitleDiv: HTMLElement; + + constructor(parser: SRTParser, listener: AudioListener) { + this.parser = parser; + this.listener = listener; + this.elapsedTime = 0; + this.subtitleDiv = document.getElementById("subtitles"); + } + + async loadSRT(): Promise { + const response = await fetch("static/music/who_am_i_with_music.srt"); + const responseText = await response.text(); + return this.parser(responseText); + } + + async initializeAudio(clickCallback: ClickCallback) { + const silence = new Audio("/static/music/silence.mp3"); + const music = await LoadAudio( + this.listener, + "/static/music/space_chillout.mp3", + 1, + "music", + true + ); + const poem = await LoadAudio( + this.listener, + "/static/music/who_am_i.mp3", + 1, + "poem", + false + ); + this.srts = await this.loadSRT(); + + const musicAnalyser = new AudioAnalyser(music, 512); + const poemAnalyser = new AudioAnalyser(poem, 512); + + const startButton = document.getElementById("startButton"); + startButton.style.display = "block"; + startButton.addEventListener("click", () => { + silence.play(); + + music.play(); + poem.play(); + // CORE.playing = true; + startButton.remove(); + clickCallback(); + }); + + return { + music, + musicAnalyser, + poem, + poemAnalyser, + }; + } + + update(delta: number) { + this.elapsedTime += delta; + let found = false; + this.srts.forEach((srt) => { + if (this.elapsedTime > srt.start && this.elapsedTime < srt.end) { + found = true; + this.subtitleDiv.innerHTML = srt.text; + } + }); + + if (!found) this.subtitleDiv.innerHTML = ""; + + if (this.elapsedTime > 70) { + document.getElementById("credits").style.display = "block"; + } + } +} + +type SRTParser = { + (srt: string): SRT[]; +}; + +type SRT = { + id: number; + start: number; + end: number; + text: string; +}; + +type ClickCallback = () => void; diff --git a/src/actors/OrbitCamera.ts b/src/actors/OrbitCamera.ts new file mode 100644 index 0000000..e6dda01 --- /dev/null +++ b/src/actors/OrbitCamera.ts @@ -0,0 +1,24 @@ +import { MathUtils, PerspectiveCamera, Vector3 } from "three"; + +export class OrbitCamera { + angle: number; + angularSpeed: number; + radius: number; + camera: PerspectiveCamera; + + constructor(camera: PerspectiveCamera) { + this.camera = camera; + this.angularSpeed = MathUtils.degToRad(20); + this.angle = 0; + this.radius = 5; + console.log(this.angularSpeed); + } + + update(delta: number, lookAt: Vector3) { + this.camera.position.x = Math.sin(this.angle) * this.radius; + this.camera.position.z = Math.cos(this.angle) * this.radius; + this.camera.position.y = Math.cos(this.angle) * 1.5 + 1; + this.angle += this.angularSpeed * delta; + this.camera.lookAt(lookAt); + } +} diff --git a/src/actors/OrbitLight.ts b/src/actors/OrbitLight.ts new file mode 100644 index 0000000..2cf7943 --- /dev/null +++ b/src/actors/OrbitLight.ts @@ -0,0 +1,38 @@ +import { + MathUtils, + Mesh, + MeshToonMaterial, + PointLight, + SphereBufferGeometry, +} from "three"; + +export class OrbitLight { + angle: number; + angularSpeed: number; + radius: number; + light: PointLight; + + constructor(light: PointLight) { + this.light = light; + this.angle = 30; + this.angularSpeed = MathUtils.degToRad(35); + this.radius = 1.5; + this.initialize(); + } + + initialize() { + const geometry = new SphereBufferGeometry(0.1); + const material = new MeshToonMaterial({ color: 0xffff00 }); + const sphere = new Mesh(geometry, material); + this.light.add(sphere); + this.update(0); + } + + update(delta: number) { + this.light.position.x = Math.cos(this.angle) * this.radius * 2; + this.light.position.z = Math.sin(this.angle) * this.radius; + this.light.position.y = Math.cos(this.angle) * 5; + + this.angle += this.angularSpeed * delta; + } +} diff --git a/src/actors/Planets.ts b/src/actors/Planets.ts new file mode 100644 index 0000000..8c6b3d9 --- /dev/null +++ b/src/actors/Planets.ts @@ -0,0 +1,67 @@ +import { + AudioAnalyser, + Color, + Mesh, + MeshToonMaterial, + SphereBufferGeometry, + Vector3, +} from "three"; +import { calculateSoundAverages } from "../audio"; + +export class Planets { + meshes: Array; + analyser: AudioAnalyser; + + constructor(analyser: AudioAnalyser) { + this.meshes = this.initializePlanets(); + this.analyser = analyser; + } + + initializePlanets() { + const radius = 2; + const geometry = new SphereBufferGeometry(0.2); + const material = new MeshToonMaterial({ + color: new Color(0, 0.3, 0), + wireframe: true, + }); + + return [...Array(8)].map((_, idx) => { + const sphere = new Mesh(geometry, material); + const x = Math.sin((idx / 4) * Math.PI) * radius; + const z = Math.cos((idx / 4) * Math.PI) * radius; + sphere.position.set(x + 0.25, -1, z + 0.1); + sphere.originalPosition = sphere.position.clone(); + sphere.offsetY = 0; + sphere.angleSpeed = 2; + sphere.angle = 0; + return sphere; + }); + } + + updatePlanets(delta: number) { + const sounds = calculateSoundAverages(this.analyser, 12); + const radius = 2; + this.meshes.forEach((planet, idx) => { + planet.offsetY = (sounds[idx] / 255) * 500 * (idx + 1); + planet.position.setX( + Math.cos(planet.angle + (idx / 4) * Math.PI) * radius + 0.25 + ); + planet.position.setY( + planet.originalPosition.y + Math.max(planet.offsetY, -0.05) + ); + planet.position.setZ( + Math.sin(planet.angle + (idx / 4) * Math.PI) * radius + 0.1 + ); + planet.angle += delta * planet.angleSpeed; + }); + } +} + +export interface Planet extends Mesh { + geometry: SphereBufferGeometry; + material: MeshToonMaterial; + originalPosition?: Vector3; + offsetY?: number; + angleSpeed?: number; + angle?: number; +} diff --git a/src/audio.ts b/src/audio.ts index 0862e7a..01abdec 100644 --- a/src/audio.ts +++ b/src/audio.ts @@ -1,22 +1,31 @@ -import { Audio, AudioAnalyser, AudioListener, AudioLoader } from "three"; -import { SoundAverages } from "./types"; -import { avg } from "./utils"; +import { + Audio, + AudioAnalyser, + AudioListener, + AudioLoader, + PositionalAudio, +} from "three"; +import { avg, chunk } from "./utils"; const loader = new AudioLoader(); export function LoadAudio( listener: AudioListener, url: string, - volume: number -): Promise