duckengine
Version:
A 2D Game Engine for the web.
519 lines (445 loc) • 11.5 kB
text/typescript
// utils
import { Duck } from '../..';
import Game from '../game';
import Texture from '../texture/texture';
import Scene from '../scene';
import TextureSheet from '../texture/textureSheet';
import Debug from '../debug/debug';
import getImageData from '../../utils/getImageData';
import TextureAtlas from '../texture/textureAtlas';
import TextureBase from '../texture/textureBase';
// loads images by URL or file path
// static class
/**
* @class Loader
* @classdesc A class that loads images and other assets
* @description The Loader Class. Preloading and loading is handled here
* @since 1.0.0-beta
*/
export default class Loader {
/**
* @memberof Loader
* @description Game instance
* @type Game
* @since 1.0.0-beta
*/
public game: Game;
/**
* @memberof Loader
* @description Scene instance
* @type Scene
* @since 2.0.0
*/
public scene: Scene;
/**
* @memberof Loader
* @description An array of loaded Textures
* @type Duck.Types.Loader.TextureStackItem<TextureBase<'image'>>[]
* @since 2.0.0
*/
public textureStack: Duck.Types.Loader.TextureStackItem<
TextureBase<'image'>
>[];
/**
* @memberof Loader
* @description An array of loaded JSON files
* @type Duck.Types.Loader.StackItem<Record<string, unknown>>[]
* @since 2.0.0
*/
public jsonStack: Duck.Types.Loader.StackItem<Record<string, unknown>>[];
/**
* @memberof Loader
* @description An array of loaded HTML Documents
* @type Duck.Types.Loader.StackItem<Document>[]
* @since 2.0.0
*/
public htmlStack: Duck.Types.Loader.StackItem<Document>[];
/**
* @memberof Loader
* @description An array of loaded XML Documents
* @type Duck.Types.Loader.StackItem<Document>[]
* @since 2.0.0
*/
public xmlStack: Duck.Types.Loader.StackItem<Document>[];
/**
* @memberof Loader
* @description An array of loaded FontFaces
* @type Duck.Types.Loader.StackItem<FontFace>[]
* @since 2.0.0
*/
public fontStack: Duck.Types.Loader.StackItem<FontFace>[];
/**
* @memberof Loader
* @description An array of loaded Audio elements
* @type Duck.Types.Loader.StackItem<HTMLAudioElement>[]
* @since 2.0.0
*/
public audioStack: Duck.Types.Loader.StackItem<HTMLAudioElement>[];
/**
* @constructor Loader
* @description Creates a Loader instance
* @param {Game} game Game instance
* @param {Scene} scene Scene instance
* @since 1.0.0-beta
*/
constructor(game: Game, scene: Scene) {
this.game = game;
this.scene = scene;
this.textureStack = [];
this.jsonStack = [];
this.htmlStack = [];
this.xmlStack = [];
this.fontStack = [];
this.audioStack = [];
}
protected async tryCache(prefix: string, key: string) {
return this.game.cacheManager.get(`${prefix}_${key}`);
}
protected async saveCache(prefix: string, key: string, value: string) {
return this.game.cacheManager.set(`${prefix}_${key}`, value);
}
/**
* @memberof Loader
* @description Loads an image and creates a texture, caches it if it does not already exist in the cache (clear cache if texture does not update
* if you edit the image src)
* @param {string} pathOrURL Path to the file or the URL
* @param {string} key Key of the texture, used to load the texture in sprites
* @param {number} w Width of image
* @param {number} h Height of image
* @since 2.0.0
*/
public async loadTexture(
pathOrURL: string,
key: string,
w: number,
h: number
) {
const image = new Image();
image.width = w;
image.height = h;
// try cache
const cacheData = await this.tryCache('texture', key);
if (cacheData) {
image.setAttribute('src', cacheData); // DATA URL object
if (this.game.config.debug) {
new Debug.Log('Loaded texture from cache.');
}
const texture = new Texture<'image'>('image', image, w, h);
this.textureStack.push({
type: 'texture',
value: texture,
key,
dataType: 'base',
});
return image;
} else {
const res = await fetch(pathOrURL);
const blob = await res.blob();
const url = URL.createObjectURL(blob.slice(0, 4000));
image.src = url;
const texture = new Texture<'image'>('image', image, w, h);
this.textureStack.push({
type: 'texture',
value: texture,
key,
dataType: 'base',
});
// save image in cache
getImageData(
image,
w,
h,
(data) => {
this.saveCache('texture', key, data);
},
() => {
new Debug.Error(
'Failed to save texture in cache, report this bug.'
);
}
);
return image;
}
}
/**
* @memberof Loader
* @description Loads an image and creates a texture sheet, caches it if it does not already exist in the cache (clear cache if texture does not update
* if you edit the image src)
* @param {string} pathOrURL Path to the file or the URL
* @param {string} key Key of the texture, used to load the texture in sprites and spritesheet
* @param {number} w Width of image
* @param {number} h Height of image
* @since 2.1.0
*/
public async loadTextureSheet(
pathOrURL: string,
key: string,
frameWidth: number,
frameHeight: number,
rows: number,
cols: number
) {
const image = new Image();
image.width = frameWidth * cols;
image.height = frameHeight * rows;
// try cache
const cacheData = await this.tryCache('texture_sheet', key);
if (cacheData) {
image.setAttribute('src', cacheData); // DATA URL object
if (this.game.config.debug) {
new Debug.Log('Loaded texture sheet from cache.');
}
const texture = new TextureSheet(
image,
frameWidth,
frameHeight,
rows,
cols
);
this.textureStack.push({
type: 'texture',
value: texture,
key,
dataType: 'sheet',
});
return image;
} else {
const res = await fetch(pathOrURL);
const blob = await res.blob();
const url = URL.createObjectURL(blob.slice(0, 4000));
image.src = url;
const texture = new TextureSheet(
image,
frameWidth,
frameHeight,
rows,
cols
);
this.textureStack.push({
type: 'texture',
value: texture,
key,
dataType: 'sheet',
});
// save image in cache
getImageData(
image,
frameWidth * cols,
frameHeight * rows,
(data) => {
this.saveCache('texture_sheet', key, data);
},
() => {
new Debug.Error(
'Failed to save texture sheet in cache, report this bug.'
);
}
);
return image;
}
}
public async loadTextureAtlas(
atlasKey: string,
texturePathOrURL: string,
jsonPathOrURL: string,
textureKey: string,
jsonKey: string,
imageW: number,
imageH: number
) {
// load texture and json
// both try for cache
await this.loadTexture(texturePathOrURL, textureKey, imageW, imageH);
await this.loadJSON(jsonPathOrURL, jsonKey);
// create TextureAtlas
const textureAtlas = new TextureAtlas(
textureKey,
jsonKey,
imageW,
imageH,
this.game,
this.scene
);
this.textureStack.push({
type: 'texture',
value: textureAtlas,
key: atlasKey,
dataType: 'atlas',
});
}
/**
* @memberof Loader
* @description Loads a JSON file and adds it to the jsonStack, caches it if it does not already exist
* @param {string} pathOrURL Path to the file or the URL
* @param {string} key Key of the file to use to save it as
* @since 2.0.0
*/
public async loadJSON(
pathOrURL: string,
key: string
): Promise<Record<string, unknown>> {
// try cache
const cacheData = await this.tryCache('json', key);
if (cacheData) {
const json = JSON.parse(cacheData);
if (this.game.config.debug) {
new Debug.Log('Loaded JSON from cache');
}
this.jsonStack.push({
type: 'json',
value: json,
key,
});
return json;
} else {
const res = await fetch(pathOrURL);
const json: Record<string, unknown> = await res.json();
this.jsonStack.push({
type: 'json',
value: json,
key,
});
// save in cache
this.saveCache('json', key, JSON.stringify(json));
return json;
}
}
/**
* @memberof Loader
* @description Loads a HTML file and adds it to the htmlStack
* @param {string} pathOrURL Path to the file or the URL
* @param {string} key Key of the file to use to save it as
* @since 2.0.0
*/
public async loadHTML(pathOrURL: string, key: string) {
// try cache
const cacheData = await this.tryCache('html', key);
if (cacheData) {
const text = cacheData;
if (this.game.config.debug) {
new Debug.Log('Loaded HTML from cache');
}
const html = new window.DOMParser().parseFromString(
text,
'text/html'
);
this.htmlStack.push({
type: 'html',
value: html,
key,
});
return html;
} else {
const res = await fetch(pathOrURL);
const text = await res.text();
const html = new window.DOMParser().parseFromString(
text,
'text/html'
);
this.htmlStack.push({
type: 'html',
value: html,
key,
});
// save in cache
this.saveCache('html', key, text);
return html;
}
}
/**
* @memberof Loader
* @description Loads a XML file and adds it to the xmlStack
* @param {string} pathOrURL Path to the file or the URL
* @param {string} key Key of the file to use to save it as
* @since 2.0.0
*/
public async loadXML(pathOrURL: string, key: string) {
// try cache
const cacheData = await this.tryCache('xml', key);
if (cacheData) {
const text = cacheData;
if (this.game.config.debug) {
new Debug.Log('Loaded XML from cache');
}
const xml = new window.DOMParser().parseFromString(
text,
'text/xml'
);
this.xmlStack.push({
type: 'xml',
value: xml,
key,
});
return xml;
} else {
const res = await fetch(pathOrURL);
const text = await res.text();
const xml = new window.DOMParser().parseFromString(
text,
'text/xml'
);
this.xmlStack.push({
type: 'xml',
value: xml,
key,
});
// save in cache
this.saveCache('xml', key, text);
return xml;
}
}
/**
* @memberof Loader
* @description Loads a font and adds it to the fontStack
* @param {string} fontFamily Font Family
* @param {string} pathOrURL Path to the file or the URL
* @param {string} key Key of the file to use to save it as
* @param {FontFaceDescriptors} [descriptors] Font Face Descriptors
* @since 2.0.0
*/
public async loadFont(
fontFamily: string,
pathOrURL: string,
key: string,
descriptors?: FontFaceDescriptors
) {
const font = new FontFace(fontFamily, `url(${pathOrURL})`, descriptors);
const res = await font.load();
document.fonts.add(font);
this.fontStack.push({
type: 'font',
value: res,
key,
});
return res;
}
/**
* @memberof Loader
* @description Loads an Audio file and adds it to the audioStack
* @param {string} pathOrURL Path to the file or the URL
* @param {string} key Key of the file to use to save it as
* @param {string} [mimeType] The mime type of the file, optional -> default: 'audio/mp3'
* @since 2.0.0
*/
public async loadAudio(
pathOrURL: string,
key: string,
mimeType = 'audio/mp3'
) {
const res = await fetch(pathOrURL);
const reader = res.body?.getReader();
const result = await reader?.read();
const blob = new Blob([result?.value] as unknown as BlobPart[], {
type: mimeType,
});
const url = window.URL.createObjectURL(blob);
const audio = new Audio();
audio.src = url;
this.audioStack.push({
type: 'audio',
value: audio,
key,
});
return audio;
}
}