add poem with background song, style things, parse and display subtitles, add stars and orbiting light, add camera movement
This commit is contained in:
parent
c182456a21
commit
b8d15577f3
78
index.html
78
index.html
|
@ -5,57 +5,51 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<title>Audio Visualizer</title>
|
<title>Audio Visualizer</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; background: black">
|
<body style="margin: 0; background: black">
|
||||||
<div id="container">
|
<div id="container">
|
||||||
|
<div id="loader">Loading model: 0%</div>
|
||||||
|
<div id="startButton" style="display: none">BEGIN</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="loader"
|
id="debug"
|
||||||
style="
|
style="
|
||||||
color: #efefef;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 0px;
|
||||||
left: 50%;
|
right: 0px;
|
||||||
transform: translate(-50%, -50%);
|
color: #efefef;
|
||||||
"
|
|
||||||
>
|
|
||||||
Loading model: 0%
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="startButton"
|
|
||||||
style="
|
|
||||||
display: none;
|
display: none;
|
||||||
color: #efefef;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 80px;
|
|
||||||
height: 25px;
|
|
||||||
border: 2px solid #efefef;
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px 40px;
|
|
||||||
cursor: pointer;
|
|
||||||
"
|
"
|
||||||
>
|
></div>
|
||||||
BEGIN
|
<div id="subtitles"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div id="credits" style="display: none">
|
||||||
style="
|
<div style="padding: 20px; background-color: rgba(0, 0, 0, 0.5)">
|
||||||
position: absolute;
|
CREDITS:
|
||||||
left: 50%;
|
<p>
|
||||||
bottom: 0px;
|
Song: Space Chillout by
|
||||||
color: #efefef;
|
<a
|
||||||
font-size: 10px;
|
href="/users/penguinmusic-24940186/?tab=audio&utm_source=link-attribution&utm_medium=referral&utm_campaign=audio&utm_content=14194"
|
||||||
transform: translateX(-50%);
|
>penguinmusic</a
|
||||||
max-width: 800px;
|
>
|
||||||
width: 100%;
|
from
|
||||||
"
|
<a
|
||||||
>
|
href="https://pixabay.com/?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=14194"
|
||||||
<div style="padding: 8px">
|
>Pixabay</a
|
||||||
"Low Poly Human Hands (All Quads)" (https://skfb.ly/WuXz) by Jeremy E.
|
>.
|
||||||
Grayson is licensed under Creative Commons Attribution
|
</p>
|
||||||
(http://creativecommons.org/licenses/by/4.0/).
|
<p>
|
||||||
|
3D Hand Model: "Low Poly Human Hands (All Quads)" by
|
||||||
|
<a href="https://skfb.ly/WuXz">Jeremy E. Grayson</a>.
|
||||||
|
</p>
|
||||||
|
<p>Poem & coding by yours truly.</p>
|
||||||
|
<p>
|
||||||
|
View the
|
||||||
|
<a href="https://git.silentsilas.com/silentsilas/audio-visualizer"
|
||||||
|
>source code</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="dist/index.js"></script>
|
<script src="dist/index.js"></script>
|
||||||
|
|
|
@ -9,10 +9,13 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/three": "^0.138.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
||||||
"@typescript-eslint/parser": "^5.14.0",
|
"@typescript-eslint/parser": "^5.14.0",
|
||||||
"esbuild": "^0.14.25",
|
"esbuild": "^0.14.25",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
|
"esm-seedrandom": "^3.0.5",
|
||||||
|
"parse-srt": "^1.0.0-alpha",
|
||||||
"three": "^0.138.3"
|
"three": "^0.138.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -106,6 +109,12 @@
|
||||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
|
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/three": {
|
||||||
|
"version": "0.138.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.138.0.tgz",
|
||||||
|
"integrity": "sha512-D8AoV7h2kbCfrv/DcebHOFh1WDwyus3HdooBkAwcBikXArdqnsQ38PQ85JCunnvun160oA9jz53GszF3zch3tg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.14.0",
|
"version": "5.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.14.0.tgz",
|
||||||
|
@ -994,6 +1003,12 @@
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esm-seedrandom": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/esm-seedrandom/-/esm-seedrandom-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-pMAq0mFIr5JQ3Ihbng7EBLMJ+llMbaDKkiG44pqbSXS0NIZWtEANpOpxb5s6Q8Q2R562P26qMHPv8YtP/NHh9g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "9.3.1",
|
"version": "9.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz",
|
||||||
|
@ -1479,6 +1494,12 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-srt": {
|
||||||
|
"version": "1.0.0-alpha",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srt/-/parse-srt-1.0.0-alpha.tgz",
|
||||||
|
"integrity": "sha1-hePGDTHk3Qkk+vVB6oWhiPw0T2E=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/path-is-absolute": {
|
"node_modules/path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
@ -1916,6 +1937,12 @@
|
||||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
|
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/three": {
|
||||||
|
"version": "0.138.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.138.0.tgz",
|
||||||
|
"integrity": "sha512-D8AoV7h2kbCfrv/DcebHOFh1WDwyus3HdooBkAwcBikXArdqnsQ38PQ85JCunnvun160oA9jz53GszF3zch3tg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@typescript-eslint/eslint-plugin": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.14.0",
|
"version": "5.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.14.0.tgz",
|
||||||
|
@ -2437,6 +2464,12 @@
|
||||||
"integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
|
"integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"esm-seedrandom": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/esm-seedrandom/-/esm-seedrandom-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-pMAq0mFIr5JQ3Ihbng7EBLMJ+llMbaDKkiG44pqbSXS0NIZWtEANpOpxb5s6Q8Q2R562P26qMHPv8YtP/NHh9g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"espree": {
|
"espree": {
|
||||||
"version": "9.3.1",
|
"version": "9.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz",
|
||||||
|
@ -2823,6 +2856,12 @@
|
||||||
"callsites": "^3.0.0"
|
"callsites": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parse-srt": {
|
||||||
|
"version": "1.0.0-alpha",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srt/-/parse-srt-1.0.0-alpha.tgz",
|
||||||
|
"integrity": "sha1-hePGDTHk3Qkk+vVB6oWhiPw0T2E=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"path-is-absolute": {
|
"path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
|
|
@ -12,10 +12,13 @@
|
||||||
"author": "Silas",
|
"author": "Silas",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/three": "^0.138.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
||||||
"@typescript-eslint/parser": "^5.14.0",
|
"@typescript-eslint/parser": "^5.14.0",
|
||||||
"esbuild": "^0.14.25",
|
"esbuild": "^0.14.25",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
|
"esm-seedrandom": "^3.0.5",
|
||||||
|
"parse-srt": "^1.0.0-alpha",
|
||||||
"three": "^0.138.3"
|
"three": "^0.138.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
64
src/audio.ts
64
src/audio.ts
|
@ -1,19 +1,25 @@
|
||||||
import { Audio, AudioLoader } from "three";
|
import { Audio, AudioAnalyser, AudioListener, AudioLoader } from "three";
|
||||||
|
import { SoundAverages } from "./types";
|
||||||
|
import { avg } from "./utils";
|
||||||
|
|
||||||
const loader = new AudioLoader();
|
const loader = new AudioLoader();
|
||||||
|
|
||||||
export function LoadAudio(listener): Promise<Audio> {
|
export function LoadAudio(
|
||||||
|
listener: AudioListener,
|
||||||
|
url: string,
|
||||||
|
volume: number
|
||||||
|
): Promise<Audio> {
|
||||||
const loadingDiv = document.getElementById("loader");
|
const loadingDiv = document.getElementById("loader");
|
||||||
loadingDiv.innerHTML = "Loading audio: 0%";
|
loadingDiv.innerHTML = "Loading audio: 0%";
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
loader.load(
|
loader.load(
|
||||||
"/static/audio.mp3",
|
url,
|
||||||
(audio) => {
|
(audio) => {
|
||||||
const sound = new Audio(listener);
|
const sound = new Audio(listener);
|
||||||
sound.setBuffer(audio);
|
sound.setBuffer(audio);
|
||||||
sound.setLoop(false);
|
sound.setLoop(false);
|
||||||
sound.setVolume(0.1);
|
sound.setVolume(volume);
|
||||||
loadingDiv.innerHTML = "";
|
loadingDiv.innerHTML = "";
|
||||||
return resolve(sound);
|
return resolve(sound);
|
||||||
},
|
},
|
||||||
|
@ -28,3 +34,53 @@ export function LoadAudio(listener): Promise<Audio> {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calculateSoundAverages(
|
||||||
|
audioAnalyser: AudioAnalyser
|
||||||
|
): SoundAverages {
|
||||||
|
const soundArray = audioAnalyser.getFrequencyData();
|
||||||
|
const soundAvg = avg(soundArray) / soundArray.length;
|
||||||
|
|
||||||
|
const lowArray = soundArray.slice(0, soundArray.length / 5 - 1);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
211
src/index.ts
211
src/index.ts
|
@ -7,65 +7,114 @@ import {
|
||||||
AudioListener,
|
AudioListener,
|
||||||
AudioAnalyser,
|
AudioAnalyser,
|
||||||
Clock,
|
Clock,
|
||||||
|
Mesh,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
MathUtils,
|
||||||
|
BufferGeometry,
|
||||||
|
Points,
|
||||||
|
SphereGeometry,
|
||||||
} from "three";
|
} from "three";
|
||||||
import { LoadAudio } from "./audio";
|
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 parseSRT from "parse-srt";
|
||||||
|
|
||||||
const GLOBAL = {
|
const CORE: CORE = {
|
||||||
renderer: null,
|
renderer: null,
|
||||||
scene: null,
|
scene: null,
|
||||||
camera: null,
|
camera: null,
|
||||||
|
clock: null,
|
||||||
|
rng: prng_alea(
|
||||||
|
"Polka-Jovial-Makeover-Wieldable-Spirited-Footprint-Recall-Handpick-Sacrifice-Jester"
|
||||||
|
),
|
||||||
|
avgs: null,
|
||||||
|
subtitleDiv: document.getElementById("subtitles"),
|
||||||
|
parser: parseSRT,
|
||||||
|
srt: null,
|
||||||
|
elapsedTime: 0,
|
||||||
|
playing: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DISTORTION: DISTORTION = {
|
||||||
light: null,
|
light: null,
|
||||||
hand: null,
|
hand: null,
|
||||||
positions: null,
|
positions: null,
|
||||||
distortionLevel: null,
|
|
||||||
audioListener: null,
|
audioListener: null,
|
||||||
audioAnalyser: null,
|
audioAnalyser: null,
|
||||||
clock: null,
|
|
||||||
};
|
};
|
||||||
const avg = (list) => list.reduce((prev, curr) => prev + curr) / list.length;
|
|
||||||
|
const ROTATION = {
|
||||||
|
cam: {
|
||||||
|
angle: 0,
|
||||||
|
angularSpeed: MathUtils.degToRad(20),
|
||||||
|
radius: 5,
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
angle: 30,
|
||||||
|
angularSpeed: MathUtils.degToRad(40),
|
||||||
|
radius: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
init().then(() => animate());
|
init().then(() => animate());
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const container = document.getElementById("container");
|
const container = document.getElementById("container");
|
||||||
|
|
||||||
GLOBAL.scene = new Scene();
|
CORE.scene = new Scene();
|
||||||
GLOBAL.camera = new PerspectiveCamera(
|
CORE.camera = new PerspectiveCamera(
|
||||||
75,
|
75,
|
||||||
window.innerWidth / window.innerHeight,
|
window.innerWidth / window.innerHeight,
|
||||||
1,
|
1,
|
||||||
10000
|
10000
|
||||||
);
|
);
|
||||||
GLOBAL.camera.position.z = 3;
|
CORE.scene.add(CORE.camera);
|
||||||
|
|
||||||
GLOBAL.light = new PointLight(0x119911, 1);
|
DISTORTION.light = new PointLight(0x119911, 1);
|
||||||
GLOBAL.light.counter = 0;
|
DISTORTION.light.counter = 0;
|
||||||
GLOBAL.light.position.set(0, 0.15, 0.15);
|
|
||||||
GLOBAL.scene.add(GLOBAL.light);
|
|
||||||
|
|
||||||
GLOBAL.renderer = new WebGLRenderer();
|
const geometry = new SphereGeometry(0.2);
|
||||||
GLOBAL.renderer.setPixelRatio(window.devicePixelRatio);
|
const material = new MeshBasicMaterial({ color: 0xcccc00 });
|
||||||
GLOBAL.renderer.setSize(window.innerWidth, window.innerHeight);
|
const sphere = new Mesh(geometry, material);
|
||||||
container.appendChild(GLOBAL.renderer.domElement);
|
DISTORTION.light.add(sphere);
|
||||||
|
CORE.scene.add(DISTORTION.light);
|
||||||
|
|
||||||
|
CORE.renderer = new WebGLRenderer();
|
||||||
|
CORE.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
CORE.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
container.appendChild(CORE.renderer.domElement);
|
||||||
window.addEventListener("resize", onWindowResize);
|
window.addEventListener("resize", onWindowResize);
|
||||||
|
|
||||||
GLOBAL.audioListener = new AudioListener();
|
DISTORTION.audioListener = new AudioListener();
|
||||||
GLOBAL.scene.add(GLOBAL.audioListener);
|
CORE.scene.add(DISTORTION.audioListener);
|
||||||
|
|
||||||
GLOBAL.clock = new Clock();
|
CORE.clock = new Clock();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const model = await LoadModel();
|
const model = await LoadModel();
|
||||||
initializeModel(model);
|
initializeModel(model);
|
||||||
|
updateRotations(0);
|
||||||
|
initializeStars();
|
||||||
|
|
||||||
const audio = await LoadAudio(GLOBAL.audioListener);
|
const audio = await LoadAudio(
|
||||||
initializeAudio(audio);
|
DISTORTION.audioListener,
|
||||||
|
"/static/who_am_i_with_music.mp3",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
audio.onEnded = () => {
|
||||||
|
document.getElementById("credits").style.display = "block";
|
||||||
|
};
|
||||||
|
CORE.srt = await loadSRT();
|
||||||
|
DISTORTION.audioAnalyser = new AudioAnalyser(audio, 512);
|
||||||
|
|
||||||
const startButton = document.getElementById("startButton");
|
const startButton = document.getElementById("startButton");
|
||||||
startButton.style.display = "block";
|
startButton.style.display = "block";
|
||||||
startButton.addEventListener("click", () => {
|
startButton.addEventListener("click", () => {
|
||||||
audio.play();
|
audio.play();
|
||||||
|
CORE.playing = true;
|
||||||
startButton.remove();
|
startButton.remove();
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -73,63 +122,125 @@ async function init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeAudio(audio) {
|
async function loadSRT(): Promise<SRT[]> {
|
||||||
GLOBAL.audioAnalyser = new AudioAnalyser(audio, 512);
|
const response = await fetch("static/who_am_i_with_music.srt");
|
||||||
|
const responseText = await response.text();
|
||||||
|
return CORE.parser(responseText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeModel(model) {
|
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
|
// turn remaining hand into wireframe
|
||||||
GLOBAL.hand = model.scene.getObjectByName("Object_4");
|
DISTORTION.hand = <DistortionMesh>model.scene.getObjectByName("Object_4");
|
||||||
GLOBAL.hand.material.wireframe = true;
|
DISTORTION.hand.material.wireframe = true;
|
||||||
|
|
||||||
// set up distortion for each vertex
|
// set up distortion for each vertex
|
||||||
GLOBAL.hand.originalPositions = GLOBAL.hand.geometry.getAttribute("position");
|
DISTORTION.hand.originalPositions =
|
||||||
GLOBAL.hand.distortions = GLOBAL.hand.originalPositions.array
|
DISTORTION.hand.geometry.getAttribute("position");
|
||||||
|
DISTORTION.hand.distortions = (
|
||||||
|
DISTORTION.hand.originalPositions.array as Array<number>
|
||||||
|
)
|
||||||
.slice(0)
|
.slice(0)
|
||||||
.map(() => Math.random() * 2 - 1);
|
.map(() => CORE.rng() * 2 - 1);
|
||||||
GLOBAL.positions = GLOBAL.hand.geometry.getAttribute("position");
|
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();
|
||||||
|
|
||||||
// center hand in scene
|
CORE.scene.add(model.scene);
|
||||||
GLOBAL.hand.position.x = GLOBAL.hand.position.x + 1.5;
|
}
|
||||||
|
|
||||||
GLOBAL.scene.add(model.scene);
|
function initializeStars() {
|
||||||
|
const geometry = new BufferGeometry();
|
||||||
|
const vertices = [];
|
||||||
|
for (let i = 0; i < 10000; i++) {
|
||||||
|
const x = CORE.rng() * 2000 - 1000;
|
||||||
|
const y = CORE.rng() * 2000 - 1000;
|
||||||
|
const z = CORE.rng() * 2000 - 1000;
|
||||||
|
|
||||||
|
vertices.push(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.setAttribute("position", new Float32BufferAttribute(vertices, 3));
|
||||||
|
const points = new Points(geometry);
|
||||||
|
CORE.scene.add(points);
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
const delta = GLOBAL.clock.getDelta();
|
const delta = CORE.clock.getDelta();
|
||||||
|
CORE.avgs = calculateSoundAverages(DISTORTION.audioAnalyser);
|
||||||
|
|
||||||
const soundArray = GLOBAL.audioAnalyser.getFrequencyData();
|
render(delta, CORE.avgs);
|
||||||
const soundAvg = avg(soundArray) / soundArray.length;
|
|
||||||
|
|
||||||
render(delta, Math.pow(soundAvg * 5, 5));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(delta, soundAvg) {
|
function render(delta: number, soundAvg: SoundAverages) {
|
||||||
// modulate light intensity between 0.5 and 1.5
|
// modulate light intensity
|
||||||
GLOBAL.light.counter += delta + 0.02;
|
DISTORTION.light.counter += delta + SETTINGS.lightSpeed;
|
||||||
GLOBAL.light.intensity = Math.sin(GLOBAL.light.counter) / 2 + 1;
|
DISTORTION.light.intensity =
|
||||||
|
Math.sin(DISTORTION.light.counter) / 2 + SETTINGS.lightMin;
|
||||||
|
|
||||||
|
const sound = Math.pow(
|
||||||
|
soundAvg.soundAvg * SETTINGS.distortionMax,
|
||||||
|
SETTINGS.distortionPower
|
||||||
|
);
|
||||||
|
|
||||||
|
if (CORE.playing) {
|
||||||
|
updateRotations(delta);
|
||||||
|
updateSubtitles(delta);
|
||||||
|
}
|
||||||
const newPositions = new Float32BufferAttribute(
|
const newPositions = new Float32BufferAttribute(
|
||||||
GLOBAL.positions.array.map((_position, index) => {
|
(DISTORTION.positions.array as Array<number>).map((_position, index) => {
|
||||||
const distortion = GLOBAL.hand.distortions[index] * soundAvg;
|
const distortion = DISTORTION.hand.distortions[index] * sound;
|
||||||
return distortion / 10 + GLOBAL.hand.originalPositions.array[index];
|
return distortion / 10 + DISTORTION.hand.originalPositions.array[index];
|
||||||
}),
|
}),
|
||||||
3
|
3
|
||||||
);
|
);
|
||||||
|
|
||||||
GLOBAL.hand.geometry.setAttribute("position", newPositions);
|
DISTORTION.hand.geometry.setAttribute("position", newPositions);
|
||||||
GLOBAL.renderer.render(GLOBAL.scene, GLOBAL.camera);
|
CORE.camera.lookAt(DISTORTION.hand.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 = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowResize() {
|
function onWindowResize() {
|
||||||
GLOBAL.camera.aspect = window.innerWidth / window.innerHeight;
|
CORE.camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
GLOBAL.camera.updateProjectionMatrix();
|
CORE.camera.updateProjectionMatrix();
|
||||||
|
|
||||||
GLOBAL.renderer.setSize(window.innerWidth, window.innerHeight);
|
CORE.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
}
|
}
|
||||||
|
|
13
src/model.ts
13
src/model.ts
|
@ -1,22 +1,13 @@
|
||||||
import { AnimationClip, Scene, Camera } from "three";
|
import { GLTFLoader, GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
||||||
|
|
||||||
const loader = new GLTFLoader();
|
const loader = new GLTFLoader();
|
||||||
|
|
||||||
export interface GLTF {
|
|
||||||
animations: AnimationClip[];
|
|
||||||
scene: Scene;
|
|
||||||
scenes: Scene[];
|
|
||||||
cameras: Camera[];
|
|
||||||
asset: object;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
loader.load(
|
||||||
"/static/scene.gltf",
|
"/static/scene.gltf",
|
||||||
(object: GLTF) => resolve(object),
|
(gltf: GLTF) => resolve(gltf),
|
||||||
(progress) =>
|
(progress) =>
|
||||||
(loadingDiv.innerHTML = `Loading model: ${
|
(loadingDiv.innerHTML = `Loading model: ${
|
||||||
(progress.loaded / progress.total) * 100
|
(progress.loaded / progress.total) * 100
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const SETTINGS = {
|
||||||
|
lightSpeed: 0.02,
|
||||||
|
lightMin: 1.25,
|
||||||
|
distortionMax: 4,
|
||||||
|
distortionPower: 3,
|
||||||
|
};
|
|
@ -0,0 +1,69 @@
|
||||||
|
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;
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function avg(list: Uint8Array) {
|
||||||
|
return list.reduce((prev, curr) => prev + curr) / list.length;
|
||||||
|
}
|
BIN
static/audio.mp3
BIN
static/audio.mp3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,65 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "Victor Mono";
|
||||||
|
src: url("/static/font/VictorMono-Medium.woff2") format("woff2"),
|
||||||
|
/* Super Modern Browsers */ url("/static/font/VictorMono-Medium.woff")
|
||||||
|
format("woff"),
|
||||||
|
/* Pretty Modern Browsers */ url("/static/font/VictorMono-Medium.ttf")
|
||||||
|
format("truetype"); /* Safari, Android, iOS */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Victor Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startButton {
|
||||||
|
color: #efefef;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 80px;
|
||||||
|
height: 25px;
|
||||||
|
border: 2px solid #efefef;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitles {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 200px;
|
||||||
|
left: 50%;
|
||||||
|
color: #efefef;
|
||||||
|
font-size: 20px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#credits {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
color: #efefef;
|
||||||
|
font-size: 12px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader {
|
||||||
|
color: #efefef;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link {
|
||||||
|
color: #33aa33;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: #33aa33;
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,44 @@
|
||||||
|
1
|
||||||
|
00:00:17,134 --> 00:00:19,470
|
||||||
|
Who am I?
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:20,570 --> 00:00:23,870
|
||||||
|
Am I the clothes on my back and the hair on my head?
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:25,110 --> 00:00:28,210
|
||||||
|
Am I the people I’m with and the words I’ve said?
|
||||||
|
|
||||||
|
4
|
||||||
|
00:00:29,510 --> 00:00:32,810
|
||||||
|
Am I the field I study and the school I attend?
|
||||||
|
|
||||||
|
5
|
||||||
|
00:00:33,510 --> 00:00:37,010
|
||||||
|
Am I the programs I write and the poems I’ve penned?
|
||||||
|
|
||||||
|
6
|
||||||
|
00:00:38,110 --> 00:00:41,610
|
||||||
|
Am I the music I enjoy and the guitar I play?
|
||||||
|
|
||||||
|
7
|
||||||
|
00:00:42,200 --> 00:00:45,710
|
||||||
|
Am I the grade on my test and my ten page essay?
|
||||||
|
|
||||||
|
8
|
||||||
|
00:00:46,500 --> 00:00:50,490
|
||||||
|
Am I the habits I’ve formed and the phrases I repeat?
|
||||||
|
|
||||||
|
9
|
||||||
|
00:00:51,500 --> 00:00:55,200
|
||||||
|
Am I the fake smile given to every person I meet?
|
||||||
|
|
||||||
|
10
|
||||||
|
00:00:56,000 --> 00:01:00,500
|
||||||
|
Am I the religion I follow and politician I elect?
|
||||||
|
|
||||||
|
11
|
||||||
|
00:01:01,000 --> 00:01:06,500
|
||||||
|
Am I every action I carry out and every thought I reflect?
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue