-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c66c150
commit 335f5de
Showing
9 changed files
with
869 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
/** | ||
* @file audiosystem.class.js | ||
* @version 1.0.0 | ||
*/ | ||
|
||
import config from '../resources/config.js'; | ||
import resources from '../resources/resources.js'; | ||
|
||
export default class AudioSystem { | ||
static _init() { | ||
AudioSystem._ac = null; | ||
AudioSystem._acResuming = null; | ||
|
||
AudioSystem._gain = null; | ||
AudioSystem._gainConnected = null; | ||
|
||
AudioSystem._destinationNode = null; | ||
|
||
AudioSystem._volume = config.audio.volume; | ||
|
||
AudioSystem._sounds = new Map(); | ||
AudioSystem._soundInstances = []; | ||
|
||
window.addEventListener('load', AudioSystem._handleLoad); | ||
} | ||
|
||
static _handleLoad() { | ||
const userGesture = () => { | ||
const ac = AudioSystem._ac; | ||
if (ac === null) { | ||
return; | ||
} | ||
|
||
if (ac.state !== 'running' && !AudioSystem._acResuming) { | ||
AudioSystem._acResuming = true; | ||
|
||
ac.resume().finally(() => { | ||
AudioSystem._acResuming = false; | ||
}); | ||
} | ||
}; | ||
|
||
document.addEventListener('click', userGesture); | ||
document.addEventListener('touchend', userGesture); | ||
document.addEventListener('keydown', userGesture); | ||
} | ||
|
||
static _getAudioContext() { | ||
const ac = AudioSystem._ac; | ||
if (ac === null) { | ||
throw new Error('Call .createAudioContext first'); | ||
} | ||
return ac; | ||
} | ||
|
||
static createAudioContext(audioContext = null, destinationNode = null) { | ||
const ac = audioContext === null ? new AudioContext() : audioContext; | ||
AudioSystem._ac = ac; | ||
AudioSystem._acResuming = false; | ||
|
||
AudioSystem._destinationNode = destinationNode === null ? ac.destination : destinationNode; | ||
|
||
AudioSystem._gain = ac.createGain(); | ||
AudioSystem._gainConnected = false; | ||
|
||
AudioSystem.setGlobalVolume(AudioSystem._volume); | ||
} | ||
|
||
static setGlobalVolume(volume) { | ||
AudioSystem._volume = volume; | ||
|
||
AudioSystem._getAudioContext(); | ||
|
||
AudioSystem._gain.gain.value = volume; | ||
|
||
if (volume <= 0 && AudioSystem._gainConnected) { | ||
AudioSystem._gain.disconnect(); | ||
AudioSystem._gainConnected = false; | ||
} else if (volume > 0 && !AudioSystem._gainConnected) { | ||
AudioSystem._gain.connect(AudioSystem._destinationNode); | ||
AudioSystem._gainConnected = true; | ||
} | ||
} | ||
|
||
static asyncLoadSounds() { | ||
const ac = AudioSystem._getAudioContext(); | ||
|
||
return Promise.all(Object.keys(resources.sounds).map(name => { | ||
const info = resources.sounds[name]; | ||
const url = `./sounds/${info.path}${config.debug.preventCaching ? '?time=' + new Date().getTime() : ''}`; | ||
|
||
return fetch(url, { cache: 'no-cache' }) | ||
.then(response => { | ||
if (!response.ok) { | ||
throw new Error(response.statusText); | ||
} | ||
|
||
return response.arrayBuffer(); | ||
}) | ||
.then(buffer => ac.decodeAudioData(buffer)) | ||
.then(buffer => { | ||
AudioSystem._sounds.set(name, { | ||
buffer, | ||
maxSources: info.maxSources, | ||
baseVolume: info.baseVolume, | ||
instances: [], | ||
}); | ||
}) | ||
.catch(error => { | ||
throw new Error(`Error loading sound file "${url}", ${error}`); | ||
}); | ||
})); | ||
} | ||
|
||
/** | ||
* Create, play and return a new sound instance. | ||
* @param {string} soundName - The sound name. | ||
* @param {number} [volume] - The sound volume (gain). Scaled by the sound base volume. Defaults to 1. | ||
* @param {bool} [loop] - Whether the sound should loop. Defaults to false. | ||
* @param {object} [tag] - A tag identifying the new sound instance. Disabled by default. | ||
* @param {number} [offset] - The sound start offset in seconds. Defaults to 0. | ||
* @param {number} [fadeInSeconds] - Seconds to fade in the sound. | ||
* @param {bool} [stopOldestIfMax] - Whether to stop the oldest sound instance if max instances are playing, else ignore play. | ||
* @returns {object} - The sound instance. | ||
*/ | ||
static play(soundName, volume = 1, loop = false, tag = null, offset = 0, fadeInSeconds = 0, stopOldestIfMax = true) { | ||
const ac = AudioSystem._getAudioContext(); | ||
|
||
const sound = AudioSystem._sounds.get(soundName); | ||
if (sound === undefined) { | ||
throw new Error(`Sound "${soundName}" does not exist`); | ||
} | ||
|
||
if (sound.maxSources <= 0) { | ||
return null; | ||
} | ||
|
||
if (sound.instances.length >= sound.maxSources) { | ||
if (stopOldestIfMax) { | ||
sound.instances[0].source.stop(); | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
const gain = ac.createGain(); | ||
if (fadeInSeconds <= 0) { | ||
gain.gain.value = sound.baseVolume * volume; | ||
} else { | ||
gain.gain.setValueAtTime(1e-4, ac.currentTime); | ||
gain.gain.exponentialRampToValueAtTime(sound.baseVolume * volume, ac.currentTime + fadeInSeconds); | ||
} | ||
gain.connect(AudioSystem._gain); | ||
|
||
const source = ac.createBufferSource(); | ||
source.buffer = sound.buffer; | ||
if (loop) { | ||
source.loop = true; | ||
if (offset > 0) { | ||
source.loopStart = offset; | ||
source.loopEnd = sound.buffer.duration; | ||
} | ||
} | ||
source.connect(gain); | ||
|
||
const instance = { source, gain, tag, stopping: false }; | ||
|
||
sound.instances.push(instance); | ||
AudioSystem._soundInstances.push(instance); | ||
|
||
source.onended = () => { | ||
gain.disconnect(); | ||
|
||
const si = sound.instances.indexOf(instance); | ||
if (si > -1) { | ||
sound.instances.splice(si, 1); | ||
} | ||
|
||
const i = AudioSystem._soundInstances.indexOf(instance); | ||
if (i > -1) { | ||
AudioSystem._soundInstances.splice(i, 1); | ||
} | ||
}; | ||
|
||
source.start(ac.currentTime, offset); | ||
|
||
return instance; | ||
} | ||
|
||
/** | ||
* Stop all sound instances or sound instances identified by a tag. | ||
* @param {object} [tag] - A tag identifying the sound instances to stop if not null. | ||
* @param {number} [fadeOutSeconds] - The number of seconds over which to fade out the sound instances. | ||
*/ | ||
static stop(tag = null, fadeOutSeconds = 0) { | ||
const ac = AudioSystem._getAudioContext(); | ||
|
||
for (let i = AudioSystem._soundInstances.length - 1; i > -1; i--) { | ||
const instance = AudioSystem._soundInstances[i]; | ||
|
||
if (instance.stopping || tag !== null && instance.tag !== tag) { | ||
continue; | ||
} | ||
|
||
instance.stopping = true; | ||
|
||
if (fadeOutSeconds <= 0) { | ||
instance.source.stop(); | ||
} else { | ||
instance.gain.gain.setValueAtTime(instance.gain.gain.value, ac.currentTime); | ||
instance.gain.gain.exponentialRampToValueAtTime(1e-4, ac.currentTime + fadeOutSeconds); | ||
instance.source.stop(ac.currentTime + fadeOutSeconds); | ||
} | ||
} | ||
} | ||
|
||
static getSoundNames() { | ||
return Array.from(AudioSystem._sounds.keys()); | ||
} | ||
|
||
static getSoundBuffer(soundName) { | ||
const sound = AudioSystem._sounds.get(soundName); | ||
if (sound === undefined) { | ||
throw new Error(`Sound "${soundName}" does not exist`); | ||
} | ||
return sound.buffer; | ||
} | ||
} | ||
|
||
AudioSystem._init(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/** | ||
* @file entity.class.js | ||
* @version 1.1.0 | ||
*/ | ||
|
||
export default class Entity { | ||
static _init() { | ||
if (this !== Entity) { | ||
throw new Error('Static _init called from wrong class'); | ||
} | ||
|
||
Entity._typeByName = new Map(); | ||
} | ||
|
||
constructor(context, scene, options = Object.create(null)) { | ||
this._scene = scene; | ||
|
||
this.p_updatePriority = options.updatePriority === undefined ? 0 : options.updatePriority; | ||
|
||
this._destroyed = false; | ||
|
||
scene.addEntity(this); | ||
} | ||
|
||
getScene() { | ||
return this._scene; | ||
} | ||
|
||
getDestroyed() { | ||
return this._destroyed; | ||
} | ||
|
||
destroy(context) { | ||
this._destroyed = true; | ||
} | ||
|
||
handleResize(context) { | ||
} | ||
|
||
update(context) { | ||
} | ||
|
||
static p_register() { | ||
Entity._typeByName.set(this.name, this); | ||
} | ||
|
||
static createByName(name, context, scene, options) { | ||
return new (Entity.getTypeByName(name))(context, scene, options); | ||
} | ||
|
||
static getTypeByName(name) { | ||
const type = Entity._typeByName.get(name); | ||
if (type === undefined) { | ||
throw new Error(`Entity type "${name}" has not been registered`); | ||
} | ||
return type; | ||
} | ||
|
||
static updatePrioritySortFunc(a, b) { | ||
return b.p_updatePriority - a.p_updatePriority; | ||
} | ||
} | ||
|
||
Entity._init(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/** | ||
* @file imagemanager.class.js | ||
* @version 1.1.0 | ||
*/ | ||
|
||
import config from '../resources/config.js'; | ||
import resources from '../resources/resources.js'; | ||
|
||
import * as THREE from '../lib/three.js/three.module.js'; | ||
|
||
export default class ImageManager { | ||
static _init() { | ||
ImageManager._images = new Map(); | ||
ImageManager._textures = new Map(); | ||
} | ||
|
||
static asyncLoadImages() { | ||
return Promise.all(Object.keys(resources.images).map(name => { | ||
const info = resources.images[name]; | ||
const url = `./images/${info.path}${config.debug.preventCaching ? '?time=' + new Date().getTime() : ''}`; | ||
|
||
return new Promise((resolve, reject) => { | ||
const image = new Image(); | ||
image.addEventListener('load', () => { | ||
ImageManager._images.set(name, image); | ||
|
||
resolve(); | ||
}); | ||
image.addEventListener('error', () => { | ||
reject(new Error(`Error loading image file "${url}"`)); | ||
}); | ||
image.src = url; | ||
}); | ||
})); | ||
} | ||
|
||
static getImageNames() { | ||
return Array.from(ImageManager._images.keys()); | ||
} | ||
|
||
static getImage(name) { | ||
const image = ImageManager._images.get(name); | ||
if (image === undefined) { | ||
throw new Error(`Image "${name}" does not exist`); | ||
} | ||
return image; | ||
} | ||
|
||
static getThreeTexture(name, encoding = THREE.LinearEncoding) { | ||
if (ImageManager._textures.has(name)) { | ||
return ImageManager._textures.get(name); | ||
} | ||
|
||
const image = ImageManager.getImage(name); | ||
const texture = new THREE.Texture(image); | ||
texture.encoding = encoding; | ||
texture.needsUpdate = true; | ||
ImageManager._textures.set(name, texture); | ||
return texture; | ||
} | ||
} | ||
|
||
ImageManager._init(); |
Oops, something went wrong.