import { Event, } from "./utils" export class Asset { loaded: boolean = false data: D | null = null error: Error | null = null private onLoadEvents: Event<[D]> = new Event() private onErrorEvents: Event<[Error]> = new Event() private onFinishEvents: Event<[]> = new Event() constructor(loader: Promise) { loader.then((data) => { this.data = data this.onLoadEvents.trigger(data) }).catch((err) => { this.error = err if (this.onErrorEvents.numListeners() > 0) { this.onErrorEvents.trigger(err) } else { throw err } }).finally(() => { this.onFinishEvents.trigger() this.loaded = true }) } static loaded(data: D): Asset { const asset = new Asset(Promise.resolve(data)) as Asset asset.data = data asset.loaded = true return asset } onLoad(action: (data: D) => void) { if (this.loaded && this.data) { action(this.data) } else { this.onLoadEvents.add(action) } return this } onError(action: (err: Error) => void) { if (this.loaded && this.error) { action(this.error) } else { this.onErrorEvents.add(action) } return this } onFinish(action: () => void) { if (this.loaded) { action() } else { this.onFinishEvents.add(action) } return this } then(action: (data: D) => void): Asset { return this.onLoad(action) } catch(action: (err: Error) => void): Asset { return this.onError(action) } finally(action: () => void): Asset { return this.onFinish(action) } } export class AssetBucket { assets: Map> = new Map() lastUID: number = 0 add(name: string | null, loader: Promise): Asset { // if user don't provide a name we use a generated one const id = name ?? (this.lastUID++ + "") const asset = new Asset(loader) this.assets.set(id, asset) return asset } addLoaded(name: string | null, data: D): Asset { const id = name ?? (this.lastUID++ + "") const asset = Asset.loaded(data) this.assets.set(id, asset) return asset } get(handle: string): Asset | void { return this.assets.get(handle) } progress(): number { if (this.assets.size === 0) { return 1 } let loaded = 0 this.assets.forEach((asset) => { if (asset.loaded) { loaded++ } }) return loaded / this.assets.size } } export function fetchURL(url: string) { return fetch(url) .then((res) => { if (!res.ok) throw new Error(`Failed to fetch "${url}"`) return res }) } export function fetchJSON(path: string) { return fetchURL(path).then((res) => res.json()) } export function fetchText(path: string) { return fetchURL(path).then((res) => res.text()) } export function fetchArrayBuffer(path: string) { return fetchURL(path).then((res) => res.arrayBuffer()) } // wrapper around image loader to get a Promise export function loadImg(src: string): Promise { const img = new Image() img.crossOrigin = "anonymous" img.src = src return new Promise((resolve, reject) => { img.onload = () => resolve(img) img.onerror = () => reject(new Error(`Failed to load image from "${src}"`)) }) }