refactor all the things, add congo line of planets that bob to music
This commit is contained in:
parent
91e1761497
commit
53efb64c84
|
@ -0,0 +1,64 @@
|
||||||
|
import {
|
||||||
|
AudioAnalyser,
|
||||||
|
BufferAttribute,
|
||||||
|
BufferGeometry,
|
||||||
|
Color,
|
||||||
|
Float32BufferAttribute,
|
||||||
|
InterleavedBufferAttribute,
|
||||||
|
Mesh,
|
||||||
|
MeshToonMaterial,
|
||||||
|
} from "three";
|
||||||
|
import { RandomNumberGenerator } from "..";
|
||||||
|
|
||||||
|
export class Hand {
|
||||||
|
mesh: Mesh<BufferGeometry, MeshToonMaterial>;
|
||||||
|
originalPositions?: BufferAttribute | InterleavedBufferAttribute;
|
||||||
|
distortions?: Array<number>;
|
||||||
|
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<number>)
|
||||||
|
.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<number>).map((_position, index) => {
|
||||||
|
const distortion = this.distortions[index] * sound;
|
||||||
|
return distortion / 10 + this.originalPositions.array[index];
|
||||||
|
}),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
|
this.mesh.geometry.setAttribute("position", newPositions);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<SRT[]> {
|
||||||
|
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;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import {
|
||||||
|
AudioAnalyser,
|
||||||
|
Color,
|
||||||
|
Mesh,
|
||||||
|
MeshToonMaterial,
|
||||||
|
SphereBufferGeometry,
|
||||||
|
Vector3,
|
||||||
|
} from "three";
|
||||||
|
import { calculateSoundAverages } from "../audio";
|
||||||
|
|
||||||
|
export class Planets {
|
||||||
|
meshes: Array<Planet>;
|
||||||
|
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 = <Planet>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;
|
||||||
|
}
|
74
src/audio.ts
74
src/audio.ts
|
@ -1,22 +1,31 @@
|
||||||
import { Audio, AudioAnalyser, AudioListener, AudioLoader } from "three";
|
import {
|
||||||
import { SoundAverages } from "./types";
|
Audio,
|
||||||
import { avg } from "./utils";
|
AudioAnalyser,
|
||||||
|
AudioListener,
|
||||||
|
AudioLoader,
|
||||||
|
PositionalAudio,
|
||||||
|
} from "three";
|
||||||
|
import { avg, chunk } from "./utils";
|
||||||
|
|
||||||
const loader = new AudioLoader();
|
const loader = new AudioLoader();
|
||||||
|
|
||||||
export function LoadAudio(
|
export function LoadAudio(
|
||||||
listener: AudioListener,
|
listener: AudioListener,
|
||||||
url: string,
|
url: string,
|
||||||
volume: number
|
volume: number,
|
||||||
): Promise<Audio> {
|
type: string,
|
||||||
|
positional?: boolean
|
||||||
|
): Promise<PositionalAudio | Audio> {
|
||||||
const loadingDiv = document.getElementById("loader");
|
const loadingDiv = document.getElementById("loader");
|
||||||
loadingDiv.innerHTML = "Loading audio: 0%";
|
loadingDiv.innerHTML = `Loading ${type}: 0%`;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
loader.load(
|
loader.load(
|
||||||
url,
|
url,
|
||||||
(audio) => {
|
(audio) => {
|
||||||
const sound = new Audio(listener);
|
const sound = positional
|
||||||
|
? new PositionalAudio(listener)
|
||||||
|
: new Audio(listener);
|
||||||
sound.setBuffer(audio);
|
sound.setBuffer(audio);
|
||||||
sound.setLoop(false);
|
sound.setLoop(false);
|
||||||
sound.setVolume(volume);
|
sound.setVolume(volume);
|
||||||
|
@ -36,51 +45,12 @@ export function LoadAudio(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateSoundAverages(
|
export function calculateSoundAverages(
|
||||||
audioAnalyser: AudioAnalyser
|
audioAnalyser: AudioAnalyser,
|
||||||
): SoundAverages {
|
chunkAmount: number
|
||||||
|
): number[] {
|
||||||
const soundArray = audioAnalyser.getFrequencyData();
|
const soundArray = audioAnalyser.getFrequencyData();
|
||||||
const soundAvg = avg(soundArray) / soundArray.length;
|
const chunkedArray = chunk(soundArray, chunkAmount);
|
||||||
|
const averages = chunkedArray.map((arr) => avg(arr) / 255);
|
||||||
|
|
||||||
const lowArray = soundArray.slice(0, soundArray.length / 5 - 1);
|
return averages;
|
||||||
|
|
||||||
const lowMidArray = soundArray.slice(
|
|
||||||
soundArray.length / 5 - 1,
|
|
||||||
2 * (soundArray.length / 5) - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const midArray = soundArray.slice(
|
|
||||||
(2 * soundArray.length) / 5 - 1,
|
|
||||||
3 * (soundArray.length / 5) - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const highMidArray = soundArray.slice(
|
|
||||||
(3 * soundArray.length) / 5 - 1,
|
|
||||||
4 * (soundArray.length / 5) - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const highArray = soundArray.slice(
|
|
||||||
4 * (soundArray.length / 5) - 1,
|
|
||||||
soundArray.length - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const lowAvg = avg(lowArray) / lowArray.length;
|
|
||||||
const lowMidAvg = avg(lowMidArray) / lowMidArray.length;
|
|
||||||
const midAvg = avg(midArray) / midArray.length;
|
|
||||||
const highMidAvg = avg(highMidArray) / highMidArray.length;
|
|
||||||
const highAvg = avg(highArray) / highArray.length;
|
|
||||||
|
|
||||||
document.getElementById("debug").innerHTML = `lows: ${lowAvg.toFixed(
|
|
||||||
2
|
|
||||||
)}, low mids: ${lowMidAvg.toFixed(2)}, mids: ${midAvg.toFixed(
|
|
||||||
2
|
|
||||||
)}, high mids: ${highMidAvg.toFixed(2)}, highs: ${highAvg.toFixed(2)}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
soundAvg,
|
|
||||||
lowAvg,
|
|
||||||
lowMidAvg,
|
|
||||||
midAvg,
|
|
||||||
highMidAvg,
|
|
||||||
highAvg,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
213
src/index.ts
213
src/index.ts
|
@ -1,63 +1,42 @@
|
||||||
import {
|
import {
|
||||||
Scene,
|
Scene,
|
||||||
PerspectiveCamera,
|
|
||||||
WebGLRenderer,
|
WebGLRenderer,
|
||||||
PointLight,
|
PointLight,
|
||||||
Float32BufferAttribute,
|
Float32BufferAttribute,
|
||||||
AudioListener,
|
AudioListener,
|
||||||
AudioAnalyser,
|
AudioAnalyser,
|
||||||
Clock,
|
Clock,
|
||||||
Mesh,
|
|
||||||
MeshBasicMaterial,
|
|
||||||
MathUtils,
|
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
Points,
|
Points,
|
||||||
SphereGeometry,
|
Color,
|
||||||
|
PerspectiveCamera,
|
||||||
} from "three";
|
} from "three";
|
||||||
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
||||||
import { calculateSoundAverages, LoadAudio } from "./audio";
|
|
||||||
import { LoadModel } from "./model";
|
import { LoadModel } from "./model";
|
||||||
import { SETTINGS } from "./settings";
|
|
||||||
import { CORE, DISTORTION, DistortionMesh, SoundAverages, SRT } from "./types";
|
|
||||||
import { prng_alea } from "esm-seedrandom";
|
import { prng_alea } from "esm-seedrandom";
|
||||||
import parseSRT from "parse-srt";
|
import parseSRT from "parse-srt";
|
||||||
|
import { Planets } from "./actors/Planets";
|
||||||
|
import { OrbitCamera } from "./actors/OrbitCamera";
|
||||||
|
import { Hand } from "./actors/Hand";
|
||||||
|
import { OrbitLight } from "./actors/OrbitLight";
|
||||||
|
import { Jukebox } from "./actors/Jukebox";
|
||||||
|
|
||||||
const CORE: CORE = {
|
const CORE: CORE = {
|
||||||
renderer: null,
|
renderer: null,
|
||||||
scene: null,
|
scene: null,
|
||||||
camera: null,
|
|
||||||
clock: null,
|
clock: null,
|
||||||
rng: prng_alea(
|
rng: prng_alea(
|
||||||
"Polka-Jovial-Makeover-Wieldable-Spirited-Footprint-Recall-Handpick-Sacrifice-Jester"
|
"Polka-Jovial-Makeover-Wieldable-Spirited-Footprint-Recall-Handpick-Sacrifice-Jester"
|
||||||
),
|
),
|
||||||
avgs: null,
|
|
||||||
subtitleDiv: document.getElementById("subtitles"),
|
|
||||||
parser: parseSRT,
|
|
||||||
srt: null,
|
|
||||||
elapsedTime: 0,
|
|
||||||
playing: false,
|
playing: false,
|
||||||
};
|
|
||||||
|
|
||||||
const DISTORTION: DISTORTION = {
|
|
||||||
light: null,
|
|
||||||
hand: null,
|
|
||||||
positions: null,
|
|
||||||
audioListener: null,
|
audioListener: null,
|
||||||
audioAnalyser: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROTATION = {
|
let ORBIT_CAMERA: OrbitCamera;
|
||||||
cam: {
|
let ORBIT_LIGHT: OrbitLight;
|
||||||
angle: 0,
|
let HAND: Hand;
|
||||||
angularSpeed: MathUtils.degToRad(20),
|
let PLANETS: Planets;
|
||||||
radius: 5,
|
let JUKEBOX: Jukebox;
|
||||||
},
|
|
||||||
light: {
|
|
||||||
angle: 30,
|
|
||||||
angularSpeed: MathUtils.degToRad(40),
|
|
||||||
radius: 2,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
init().then(() => animate());
|
init().then(() => animate());
|
||||||
|
|
||||||
|
@ -65,90 +44,51 @@ async function init() {
|
||||||
const container = document.getElementById("container");
|
const container = document.getElementById("container");
|
||||||
|
|
||||||
CORE.scene = new Scene();
|
CORE.scene = new Scene();
|
||||||
CORE.camera = new PerspectiveCamera(
|
ORBIT_CAMERA = new OrbitCamera(
|
||||||
75,
|
new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000)
|
||||||
window.innerWidth / window.innerHeight,
|
|
||||||
1,
|
|
||||||
10000
|
|
||||||
);
|
);
|
||||||
CORE.scene.add(CORE.camera);
|
CORE.scene.add(ORBIT_CAMERA.camera);
|
||||||
|
CORE.audioListener = new AudioListener();
|
||||||
|
ORBIT_CAMERA.camera.add(CORE.audioListener);
|
||||||
|
|
||||||
DISTORTION.light = new PointLight(0x119911, 1);
|
ORBIT_LIGHT = new OrbitLight(new PointLight(new Color(1, 1, 1), 2, 10));
|
||||||
DISTORTION.light.counter = 0;
|
CORE.scene.add(ORBIT_LIGHT.light);
|
||||||
|
|
||||||
const geometry = new SphereGeometry(0.2);
|
|
||||||
const material = new MeshBasicMaterial({ color: 0xcccc00 });
|
|
||||||
const sphere = new Mesh(geometry, material);
|
|
||||||
DISTORTION.light.add(sphere);
|
|
||||||
CORE.scene.add(DISTORTION.light);
|
|
||||||
|
|
||||||
CORE.renderer = new WebGLRenderer();
|
CORE.renderer = new WebGLRenderer();
|
||||||
CORE.renderer.setPixelRatio(window.devicePixelRatio);
|
CORE.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
CORE.renderer.setSize(window.innerWidth, window.innerHeight);
|
CORE.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
|
||||||
container.appendChild(CORE.renderer.domElement);
|
container.appendChild(CORE.renderer.domElement);
|
||||||
window.addEventListener("resize", onWindowResize);
|
window.addEventListener("resize", onWindowResize);
|
||||||
|
|
||||||
DISTORTION.audioListener = new AudioListener();
|
|
||||||
CORE.scene.add(DISTORTION.audioListener);
|
|
||||||
|
|
||||||
CORE.clock = new Clock();
|
CORE.clock = new Clock();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
JUKEBOX = new Jukebox(parseSRT, CORE.audioListener);
|
||||||
|
const { music, musicAnalyser, poem, poemAnalyser } =
|
||||||
|
await JUKEBOX.initializeAudio(() => {
|
||||||
|
CORE.playing = true;
|
||||||
|
});
|
||||||
|
CORE.scene.add(poem);
|
||||||
|
ORBIT_LIGHT.light.add(music);
|
||||||
const model = await LoadModel();
|
const model = await LoadModel();
|
||||||
initializeModel(model);
|
initializeModel(model, poemAnalyser);
|
||||||
updateRotations(0);
|
|
||||||
initializeStars();
|
initializeStars();
|
||||||
|
ORBIT_CAMERA.update(0, HAND.mesh.position);
|
||||||
const audio = await LoadAudio(
|
PLANETS = new Planets(musicAnalyser);
|
||||||
DISTORTION.audioListener,
|
PLANETS.meshes.forEach((planet) => CORE.scene.add(planet));
|
||||||
"/static/who_am_i_with_music.mp3",
|
|
||||||
1
|
|
||||||
);
|
|
||||||
CORE.srt = await loadSRT();
|
|
||||||
DISTORTION.audioAnalyser = new AudioAnalyser(audio, 512);
|
|
||||||
|
|
||||||
const startButton = document.getElementById("startButton");
|
|
||||||
startButton.style.display = "block";
|
|
||||||
startButton.addEventListener("click", () => {
|
|
||||||
audio.play();
|
|
||||||
CORE.playing = true;
|
|
||||||
startButton.remove();
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSRT(): Promise<SRT[]> {
|
function initializeModel(model: GLTF, analyser: AudioAnalyser) {
|
||||||
const response = await fetch("static/who_am_i_with_music.srt");
|
|
||||||
const responseText = await response.text();
|
|
||||||
return CORE.parser(responseText);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeModel(model: GLTF) {
|
|
||||||
// remove second hand with text above it
|
// remove second hand with text above it
|
||||||
const objToRemove = model.scene.getObjectByName("Object_3");
|
const objToRemove = model.scene.getObjectByName("Object_3");
|
||||||
objToRemove.parent.remove(objToRemove);
|
objToRemove.parent.remove(objToRemove);
|
||||||
|
|
||||||
// turn remaining hand into wireframe
|
HAND = new Hand(model.scene.getObjectByName("Object_4"));
|
||||||
DISTORTION.hand = <DistortionMesh>model.scene.getObjectByName("Object_4");
|
HAND.initialize(CORE.rng, analyser);
|
||||||
DISTORTION.hand.material.wireframe = true;
|
|
||||||
|
|
||||||
// set up distortion for each vertex
|
|
||||||
DISTORTION.hand.originalPositions =
|
|
||||||
DISTORTION.hand.geometry.getAttribute("position");
|
|
||||||
DISTORTION.hand.distortions = (
|
|
||||||
DISTORTION.hand.originalPositions.array as Array<number>
|
|
||||||
)
|
|
||||||
.slice(0)
|
|
||||||
.map(() => CORE.rng() * 2 - 1);
|
|
||||||
DISTORTION.positions = DISTORTION.hand.geometry.getAttribute("position");
|
|
||||||
DISTORTION.hand.parent.position.x = DISTORTION.hand.parent.position.x + 2.8;
|
|
||||||
DISTORTION.hand.parent.position.y = DISTORTION.hand.parent.position.y + 0;
|
|
||||||
DISTORTION.hand.parent.position.z = DISTORTION.hand.parent.position.z + 2.2;
|
|
||||||
DISTORTION.hand.parent.rotateZ(Math.PI / 2);
|
|
||||||
DISTORTION.hand.geometry.center();
|
|
||||||
|
|
||||||
CORE.scene.add(model.scene);
|
CORE.scene.add(model.scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,76 +112,37 @@ function animate() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
const delta = CORE.clock.getDelta();
|
const delta = CORE.clock.getDelta();
|
||||||
CORE.avgs = calculateSoundAverages(DISTORTION.audioAnalyser);
|
render(delta);
|
||||||
|
|
||||||
render(delta, CORE.avgs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(delta: number, soundAvg: SoundAverages) {
|
function render(delta: number) {
|
||||||
// modulate light intensity
|
|
||||||
DISTORTION.light.counter += delta + SETTINGS.lightSpeed;
|
|
||||||
DISTORTION.light.intensity =
|
|
||||||
Math.sin(DISTORTION.light.counter) / 2 + SETTINGS.lightMin;
|
|
||||||
|
|
||||||
const sound = Math.pow(
|
|
||||||
soundAvg.soundAvg * SETTINGS.distortionMax,
|
|
||||||
SETTINGS.distortionPower
|
|
||||||
);
|
|
||||||
|
|
||||||
if (CORE.playing) {
|
if (CORE.playing) {
|
||||||
updateRotations(delta);
|
ORBIT_LIGHT.update(delta);
|
||||||
updateSubtitles(delta);
|
HAND.update();
|
||||||
|
JUKEBOX.update(delta);
|
||||||
|
PLANETS.updatePlanets(delta);
|
||||||
}
|
}
|
||||||
const newPositions = new Float32BufferAttribute(
|
|
||||||
(DISTORTION.positions.array as Array<number>).map((_position, index) => {
|
|
||||||
const distortion = DISTORTION.hand.distortions[index] * sound;
|
|
||||||
return distortion / 10 + DISTORTION.hand.originalPositions.array[index];
|
|
||||||
}),
|
|
||||||
3
|
|
||||||
);
|
|
||||||
|
|
||||||
DISTORTION.hand.geometry.setAttribute("position", newPositions);
|
CORE.renderer.render(CORE.scene, ORBIT_CAMERA.camera);
|
||||||
CORE.camera.lookAt(DISTORTION.hand.position);
|
ORBIT_CAMERA.update(delta, HAND.mesh.position);
|
||||||
CORE.renderer.render(CORE.scene, CORE.camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateRotations(delta: number) {
|
|
||||||
DISTORTION.hand.parent.rotateZ(3 / 1000);
|
|
||||||
|
|
||||||
CORE.camera.position.x = Math.sin(ROTATION.cam.angle) * ROTATION.cam.radius;
|
|
||||||
CORE.camera.position.z = Math.cos(ROTATION.cam.angle) * ROTATION.cam.radius;
|
|
||||||
CORE.camera.position.y = Math.cos(ROTATION.cam.angle) * 1.5;
|
|
||||||
ROTATION.cam.angle += ROTATION.cam.angularSpeed * delta;
|
|
||||||
|
|
||||||
DISTORTION.light.position.x =
|
|
||||||
Math.cos(ROTATION.light.angle) * ROTATION.light.radius;
|
|
||||||
DISTORTION.light.position.z =
|
|
||||||
Math.sin(ROTATION.light.angle) * ROTATION.light.radius;
|
|
||||||
DISTORTION.light.position.y = Math.cos(ROTATION.light.angle) * 5;
|
|
||||||
|
|
||||||
ROTATION.light.angle += ROTATION.light.angularSpeed * delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSubtitles(delta: number) {
|
|
||||||
CORE.elapsedTime += delta;
|
|
||||||
let found = false;
|
|
||||||
CORE.srt.forEach((srt) => {
|
|
||||||
if (CORE.elapsedTime > srt.start && CORE.elapsedTime < srt.end) {
|
|
||||||
found = true;
|
|
||||||
CORE.subtitleDiv.innerHTML = srt.text;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) CORE.subtitleDiv.innerHTML = "";
|
|
||||||
|
|
||||||
if (CORE.elapsedTime > 70) {
|
|
||||||
document.getElementById("credits").style.display = "block";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowResize() {
|
function onWindowResize() {
|
||||||
CORE.camera.aspect = window.innerWidth / window.innerHeight;
|
ORBIT_CAMERA.camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
CORE.camera.updateProjectionMatrix();
|
ORBIT_CAMERA.camera.updateProjectionMatrix();
|
||||||
|
|
||||||
CORE.renderer.setSize(window.innerWidth, window.innerHeight);
|
CORE.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CORE {
|
||||||
|
renderer: WebGLRenderer;
|
||||||
|
scene: Scene;
|
||||||
|
clock: Clock;
|
||||||
|
rng: RandomNumberGenerator;
|
||||||
|
playing: boolean;
|
||||||
|
audioListener: AudioListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RandomNumberGenerator = {
|
||||||
|
(): number;
|
||||||
|
};
|
||||||
|
|
31
src/model.ts
31
src/model.ts
|
@ -1,13 +1,18 @@
|
||||||
|
import { Texture, TextureLoader } from "three";
|
||||||
import { GLTFLoader, GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
import { GLTFLoader, GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
|
|
||||||
const loader = new GLTFLoader();
|
const gltfLoader = new GLTFLoader();
|
||||||
|
const textureLoader = new TextureLoader();
|
||||||
|
|
||||||
export function LoadModel(): Promise<GLTF> {
|
export function LoadModel(): Promise<GLTF> {
|
||||||
const loadingDiv = document.getElementById("loader");
|
const loadingDiv = document.getElementById("loader");
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
loader.load(
|
gltfLoader.load(
|
||||||
"/static/scene.gltf",
|
"/static/models/scene.gltf",
|
||||||
(gltf: GLTF) => resolve(gltf),
|
(gltf: GLTF) => {
|
||||||
|
loadingDiv.innerHTML = "";
|
||||||
|
resolve(gltf);
|
||||||
|
},
|
||||||
(progress) =>
|
(progress) =>
|
||||||
(loadingDiv.innerHTML = `Loading model: ${
|
(loadingDiv.innerHTML = `Loading model: ${
|
||||||
(progress.loaded / progress.total) * 100
|
(progress.loaded / progress.total) * 100
|
||||||
|
@ -19,3 +24,21 @@ export function LoadModel(): Promise<GLTF> {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LoadTexture(url): Promise<Texture> {
|
||||||
|
const loadingDiv = document.getElementById("loader");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
textureLoader.load(
|
||||||
|
url,
|
||||||
|
(texture: Texture) => resolve(texture),
|
||||||
|
(progress) =>
|
||||||
|
(loadingDiv.innerHTML = `Loading texture: ${
|
||||||
|
(progress.loaded / progress.total) * 100
|
||||||
|
}%`),
|
||||||
|
(error: ErrorEvent) => {
|
||||||
|
console.log(error.target);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
export const SETTINGS = {
|
|
||||||
lightSpeed: 0.02,
|
|
||||||
lightMin: 1.25,
|
|
||||||
distortionMax: 4,
|
|
||||||
distortionPower: 3,
|
|
||||||
};
|
|
69
src/types.ts
69
src/types.ts
|
@ -1,69 +0,0 @@
|
||||||
import {
|
|
||||||
AudioAnalyser,
|
|
||||||
AudioListener,
|
|
||||||
BufferAttribute,
|
|
||||||
Clock,
|
|
||||||
InterleavedBufferAttribute,
|
|
||||||
Mesh,
|
|
||||||
MeshBasicMaterial,
|
|
||||||
PerspectiveCamera,
|
|
||||||
PointLight,
|
|
||||||
Scene,
|
|
||||||
WebGLRenderer,
|
|
||||||
} from "three";
|
|
||||||
|
|
||||||
export interface CORE {
|
|
||||||
renderer: WebGLRenderer;
|
|
||||||
scene: Scene;
|
|
||||||
camera: PerspectiveCamera;
|
|
||||||
clock: Clock;
|
|
||||||
rng: RandomNumberGenerator;
|
|
||||||
avgs: SoundAverages;
|
|
||||||
subtitleDiv: HTMLElement;
|
|
||||||
parser: SRTParser;
|
|
||||||
srt: SRT[];
|
|
||||||
elapsedTime: number;
|
|
||||||
playing: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DISTORTION {
|
|
||||||
light: CountingLight;
|
|
||||||
hand: DistortionMesh;
|
|
||||||
positions: BufferAttribute | InterleavedBufferAttribute;
|
|
||||||
audioListener: AudioListener;
|
|
||||||
audioAnalyser: AudioAnalyser;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CountingLight extends PointLight {
|
|
||||||
counter?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DistortionMesh extends Mesh {
|
|
||||||
material: MeshBasicMaterial;
|
|
||||||
originalPositions: BufferAttribute | InterleavedBufferAttribute;
|
|
||||||
distortions: Array<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SoundAverages = {
|
|
||||||
soundAvg: number;
|
|
||||||
lowAvg: number;
|
|
||||||
lowMidAvg: number;
|
|
||||||
midAvg: number;
|
|
||||||
highMidAvg: number;
|
|
||||||
highAvg: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RandomNumberGenerator = {
|
|
||||||
(): number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SRTParser = {
|
|
||||||
(srt: string): SRT[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SRT = {
|
|
||||||
id: number;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
text: string;
|
|
||||||
};
|
|
11
src/utils.ts
11
src/utils.ts
|
@ -1,3 +1,14 @@
|
||||||
export function avg(list: Uint8Array) {
|
export function avg(list: Uint8Array) {
|
||||||
return list.reduce((prev, curr) => prev + curr) / list.length;
|
return list.reduce((prev, curr) => prev + curr) / list.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function chunk(arr: [] | Uint8Array, len: number) {
|
||||||
|
const chunks = [];
|
||||||
|
const n = arr.length;
|
||||||
|
let i = 0;
|
||||||
|
while (i < n) {
|
||||||
|
chunks.push(arr.slice(i, (i += len)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue