diff --git a/.eslintrc.json b/.eslintrc.json index d0f34689c..025b5f47a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -40,6 +40,7 @@ "@typescript-eslint" ], "rules": { + "@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-empty-function": "off", diff --git a/src/assets.ts b/src/assets.ts new file mode 100644 index 000000000..eda640dae --- /dev/null +++ b/src/assets.ts @@ -0,0 +1,378 @@ +const SPRITE_ATLAS_WIDTH = 2048 +const SPRITE_ATLAS_HEIGHT = 2048 +const DEF_FONT_FILTER = "nearest" + +import type { + SpriteAnims, + NineSlice, + LoadSpriteSrc, + LoadSpriteOpt, + ImageSource, + LoadFontOpt, + TexFilter, + Outline, + BitmapFontData, + ShaderData, +} from "./types" + +import { + Event, + isDataURL, + dataURLToArrayBuffer, +} from "./utils" + +import { + GfxCtx, + Texture, +} from "./gfx" + +import { + AudioCtx, +} from "./audio" + +import { + Quad, + Color, +} from "./math" + +import TexPacker from "./texPacker" + +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 + } +} + +// get an array of frames based on configuration on how to slice the image +function slice(x = 1, y = 1, dx = 0, dy = 0, w = 1, h = 1): Quad[] { + const frames = [] + const qw = w / x + const qh = h / y + for (let j = 0; j < y; j++) { + for (let i = 0; i < x; i++) { + frames.push(new Quad( + dx + i * qw, + dy + j * qh, + qw, + qh, + )) + } + } + return frames +} + +export class SpriteData { + + tex: Texture + frames: Quad[] = [ new Quad(0, 0, 1, 1) ] + anims: SpriteAnims = {} + slice9: NineSlice | null = null + + constructor( + tex: Texture, + frames?: Quad[], + anims: SpriteAnims = {}, + slice9: NineSlice = null, + ) { + this.tex = tex + if (frames) this.frames = frames + this.anims = anims + this.slice9 = slice9 + } + + get width() { + return this.tex.width * this.frames[0].w + } + + get height() { + return this.tex.height * this.frames[0].h + } + + static from(ctx: AssetCtx, src: LoadSpriteSrc, opt: LoadSpriteOpt = {}): Promise { + return typeof src === "string" + ? SpriteData.fromURL(ctx, src, opt) + : Promise.resolve(SpriteData.fromImage(ctx, src, opt)) + } + + static fromImage(ctx: AssetCtx, data: ImageSource, opt: LoadSpriteOpt = {}): SpriteData { + const [tex, quad] = ctx.packImg(data) + const frames = opt.frames ? opt.frames.map((f) => new Quad( + quad.x + f.x * quad.w, + quad.y + f.y * quad.h, + f.w * quad.w, + f.h * quad.h, + )) : slice(opt.sliceX || 1, opt.sliceY || 1, quad.x, quad.y, quad.w, quad.h) + return new SpriteData(tex, frames, opt.anims, opt.slice9) + } + + static fromURL(ctx: AssetCtx, url: string, opt: LoadSpriteOpt = {}): Promise { + return ctx.loadImg(url).then((img) => SpriteData.fromImage(ctx, img, opt)) + } + +} + +export class FontData { + fontface: FontFace + filter: TexFilter = DEF_FONT_FILTER + outline: Outline | null = null + constructor(face: FontFace, opt: LoadFontOpt = {}) { + this.fontface = face + this.filter = opt.filter ?? DEF_FONT_FILTER + if (opt.outline) { + this.outline = { + width: 1, + color: new Color(0, 0, 0), + } + if (typeof opt.outline === "number") { + this.outline.width = opt.outline + } else if (typeof opt.outline === "object") { + if (opt.outline.width) this.outline.width = opt.outline.width + if (opt.outline.color) this.outline.color = opt.outline.color + } + } + } +} + +export class SoundData { + + buf: AudioBuffer + + constructor(buf: AudioBuffer) { + this.buf = buf + } + + static fromArrayBuffer(ctx: AssetCtx, buf: ArrayBuffer): Promise { + return new Promise((resolve, reject) => + ctx.audio.ctx.decodeAudioData(buf, resolve, reject), + ).then((buf: AudioBuffer) => new SoundData(buf)) + } + + static fromURL(ctx: AssetCtx, url: string): Promise { + if (isDataURL(url)) { + return SoundData.fromArrayBuffer(ctx, dataURLToArrayBuffer(url)) + } else { + return ctx.fetchURL(url) + .then((res) => res.arrayBuffer()) + .then((buf) => SoundData.fromArrayBuffer(ctx, buf)) + } + } + +} + +export type AssetCtx = { + gfx: GfxCtx, + audio: AudioCtx, + setURLPrefix: (prefix: string) => void, + getURLPrefix: () => string, + loadImg: (src: string) => Promise, + fetchURL: (url: string) => Promise, + packImg: TexPacker["add"], + loadSprite, +} + +export default (gfx: GfxCtx, audio: AudioCtx): AssetCtx => { + + const state = { + // prefix for when loading from a url + urlPrefix: "", + // asset holders + sprites: new AssetBucket(), + fonts: new AssetBucket(), + bitmapFonts: new AssetBucket(), + sounds: new AssetBucket(), + shaders: new AssetBucket(), + custom: new AssetBucket(), + packer: new TexPacker(gfx, SPRITE_ATLAS_WIDTH, SPRITE_ATLAS_HEIGHT), + // if we finished initially loading all assets + loaded: false, + + } + + function setURLPrefix(prefix: string) { + state.urlPrefix = prefix + } + + function getURLPrefix() { + return state.urlPrefix + } + + function fetchURL(path: string) { + const url = state.urlPrefix + path + return fetch(url) + .then((res) => { + if (!res.ok) throw new Error(`Failed to fetch "${url}"`) + return res + }) + } + + // wrapper around image loader to get a Promise + function loadImg(src: string): Promise { + const img = new Image() + img.crossOrigin = "anonymous" + img.src = isDataURL(src) ? src : state.urlPrefix + src + return new Promise((resolve, reject) => { + img.onload = () => resolve(img) + img.onerror = () => reject(new Error(`Failed to load image from "${src}"`)) + }) + } + + function createSpriteSheet( + images: ImageSource[], + opt: LoadSpriteOpt = {}, + ): SpriteData { + const canvas = document.createElement("canvas") + const width = images[0].width + const height = images[0].height + canvas.width = width * images.length + canvas.height = height + const c2d = canvas.getContext("2d") + images.forEach((img, i) => { + if (img instanceof ImageData) { + c2d.putImageData(img, i * width, 0) + } else { + c2d.drawImage(img, i * width, 0) + } + }) + const merged = c2d.getImageData(0, 0, images.length * width, height) + return SpriteData.fromImage(ctx, merged, { + ...opt, + sliceX: images.length, + sliceY: 1, + }) + } + + // load a sprite to asset manager + function loadSprite( + name: string | null, + src: LoadSpriteSrc | LoadSpriteSrc[], + opt: LoadSpriteOpt = { + sliceX: 1, + sliceY: 1, + anims: {}, + }, + ): Asset { + if (Array.isArray(src)) { + if (src.some((s) => typeof s === "string")) { + return state.sprites.add( + name, + Promise.all(src.map((s) => { + return typeof s === "string" ? loadImg(s) : Promise.resolve(s) + })).then((images) => createSpriteSheet(images, opt)), + ) + } else { + return state.sprites.addLoaded(name, createSpriteSheet(src as ImageSource[], opt)) + } + } else { + if (typeof src === "string") { + return state.sprites.add(name, SpriteData.from(ctx, src, opt)) + } else { + return state.sprites.addLoaded(name, SpriteData.fromImage(ctx, src, opt)) + } + } + } + + const ctx = { + gfx, + audio, + setURLPrefix, + getURLPrefix, + loadImg, + fetchURL, + packImg: state.packer.add, + loadSprite, + } + + return ctx + +} diff --git a/src/audio.ts b/src/audio.ts new file mode 100644 index 000000000..f2948abda --- /dev/null +++ b/src/audio.ts @@ -0,0 +1,3 @@ +export type AudioCtx = { + ctx: AudioContext, +} diff --git a/src/gfx.ts b/src/gfx.ts index 817385138..bf35570dd 100644 --- a/src/gfx.ts +++ b/src/gfx.ts @@ -1,7 +1,6 @@ import type { ImageSource, TextureOpt, - TexFilter, Uniform, } from "./types" @@ -15,7 +14,7 @@ import { deepEq, } from "./utils" -type GFXCtx = { +export type GfxCtx = { gl: WebGLRenderingContext, onDestroy: (action: () => void) => void, pushTexture: (ty: GLenum, tex: WebGLTexture) => void, @@ -32,18 +31,33 @@ type GFXCtx = { export class Texture { - ctx: GFXCtx + ctx: GfxCtx src: null | ImageSource = null glTex: WebGLTexture width: number height: number - constructor(ctx: GFXCtx, w: number, h: number, opt: TextureOpt = {}) { + constructor(ctx: GfxCtx, w: number, h: number, opt: TextureOpt = {}) { this.ctx = ctx const gl = ctx.gl this.glTex = ctx.gl.createTexture() ctx.onDestroy(() => this.free()) + + this.width = w + this.height = h + + // TODO: no default + const filter = { + "linear": gl.LINEAR, + "nearest": gl.NEAREST, + }[opt.filter] ?? gl.NEAREST + + const wrap = { + "repeat": gl.REPEAT, + "clampToEadge": gl.CLAMP_TO_EDGE, + }[opt.wrap] ?? gl.CLAMP_TO_EDGE + this.bind() if (w && h) { @@ -59,19 +73,6 @@ export class Texture { ) } - this.width = w - this.height = h - - const filter = { - "linear": gl.LINEAR, - "nearest": gl.NEAREST, - }[opt.filter] ?? gl.NEAREST - - const wrap = { - "repeat": gl.REPEAT, - "clampToEadge": gl.CLAMP_TO_EDGE, - }[opt.wrap] ?? gl.CLAMP_TO_EDGE - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap) @@ -80,14 +81,9 @@ export class Texture { } - static fromImage(ctx: GFXCtx, img: ImageSource, opt: TextureOpt = {}): Texture { - const gl = ctx.gl - const tex = new Texture(ctx, 0, 0, opt) - tex.bind() - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img) - tex.width = img.width - tex.height = img.height - tex.unbind() + static fromImage(ctx: GfxCtx, img: ImageSource, opt: TextureOpt = {}): Texture { + const tex = new Texture(ctx, img.width, img.height, opt) + tex.update(img) tex.src = img return tex } @@ -115,12 +111,12 @@ export class Texture { export class FrameBuffer { - ctx: GFXCtx + ctx: GfxCtx tex: Texture glFramebuffer: WebGLFramebuffer glRenderbuffer: WebGLRenderbuffer - constructor(ctx: GFXCtx, w: number, h: number, opt: TextureOpt = {}) { + constructor(ctx: GfxCtx, w: number, h: number, opt: TextureOpt = {}) { this.ctx = ctx const gl = ctx.gl @@ -182,6 +178,12 @@ export class FrameBuffer { return canvas.toDataURL() } + draw(action: () => void) { + this.bind() + action() + this.unbind() + } + bind() { const gl = this.ctx.gl this.ctx.pushFramebuffer(gl.FRAMEBUFFER, this.glFramebuffer) @@ -205,10 +207,10 @@ export class FrameBuffer { export class Shader { - ctx: GFXCtx + ctx: GfxCtx glProgram: WebGLProgram - constructor(ctx: GFXCtx, vert: string, frag: string, attribs: string[]) { + constructor(ctx: GfxCtx, vert: string, frag: string, attribs: string[]) { this.ctx = ctx ctx.onDestroy(() => this.free()) @@ -233,33 +235,12 @@ export class Shader { gl.linkProgram(prog) if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { - - const formatShaderError = (msg: string) => { - const FMT = /^ERROR:\s0:(?\d+):\s(?.+)/ - const match = msg.match(FMT) - return { - line: Number(match.groups.line), - // seem to be a \n\0 at the end of error messages, causing unwanted line break - msg: match.groups.msg.replace(/\n\0$/, ""), - } - } - const vertError = gl.getShaderInfoLog(vertShader) const fragError = gl.getShaderInfoLog(fragShader) let msg = "" - - if (vertError) { - const err = formatShaderError(vertError) - msg += `Vertex shader line ${err.line - 14}: ${err.msg}` - } - - if (fragError) { - const err = formatShaderError(fragError) - msg += `Fragment shader line ${err.line - 14}: ${err.msg}` - } - + if (vertError) msg += vertError + if (fragError) msg += fragError throw new Error(msg) - } gl.deleteShader(vertShader) @@ -305,10 +286,10 @@ export type VertexFormat = { export class BatchRenderer { - ctx: GFXCtx + ctx: GfxCtx - vbuf: WebGLBuffer - ibuf: WebGLBuffer + glVBuf: WebGLBuffer + glIBuf: WebGLBuffer vqueue: number[] = [] iqueue: number[] = [] stride: number @@ -323,7 +304,7 @@ export class BatchRenderer { curShader: Shader | null = null curUniform: Uniform = {} - constructor(ctx: GFXCtx, format: VertexFormat, maxVertices: number, maxIndices: number) { + constructor(ctx: GfxCtx, format: VertexFormat, maxVertices: number, maxIndices: number) { const gl = ctx.gl @@ -333,13 +314,13 @@ export class BatchRenderer { this.maxVertices = maxVertices this.maxIndices = maxIndices - this.vbuf = gl.createBuffer() - ctx.pushBuffer(gl.ARRAY_BUFFER, this.vbuf) + this.glVBuf = gl.createBuffer() + ctx.pushBuffer(gl.ARRAY_BUFFER, this.glVBuf) gl.bufferData(gl.ARRAY_BUFFER, maxVertices * 4, gl.DYNAMIC_DRAW) ctx.popBuffer(gl.ARRAY_BUFFER) - this.ibuf = gl.createBuffer() - ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibuf) + this.glIBuf = gl.createBuffer() + ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glIBuf) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, maxIndices * 4, gl.DYNAMIC_DRAW) ctx.popBuffer(gl.ELEMENT_ARRAY_BUFFER) @@ -389,9 +370,9 @@ export class BatchRenderer { const gl = this.ctx.gl - this.ctx.pushBuffer(gl.ARRAY_BUFFER, this.vbuf) + this.ctx.pushBuffer(gl.ARRAY_BUFFER, this.glVBuf) gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(this.vqueue)) - this.ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibuf) + this.ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glIBuf) gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, new Uint16Array(this.iqueue)) this.ctx.setVertexFormat(this.vertexFormat) this.curShader.bind() @@ -412,12 +393,60 @@ export class BatchRenderer { free() { const gl = this.ctx.gl - gl.deleteBuffer(this.vbuf) - gl.deleteBuffer(this.ibuf) + gl.deleteBuffer(this.glVBuf) + gl.deleteBuffer(this.glIBuf) } } +export class Mesh { + + ctx: GfxCtx + glVBuf: WebGLBuffer + glIBuf: WebGLBuffer + vertexFormat: VertexFormat + count: number + + constructor(ctx: GfxCtx, format: VertexFormat, verts: number[], indices: number[]) { + + const gl = ctx.gl + + this.vertexFormat = format + this.ctx = ctx + + this.glVBuf = gl.createBuffer() + ctx.pushBuffer(gl.ARRAY_BUFFER, this.glVBuf) + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW) + ctx.popBuffer(gl.ARRAY_BUFFER) + + this.glIBuf = gl.createBuffer() + ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glIBuf) + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW) + ctx.popBuffer(gl.ELEMENT_ARRAY_BUFFER) + + this.count = indices.length + + } + + draw(primitive?: GLenum) { + const gl = this.ctx.gl + this.ctx.pushBuffer(gl.ARRAY_BUFFER, this.glVBuf) + this.ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glIBuf) + this.ctx.setVertexFormat(this.vertexFormat) + gl.drawElements(primitive ?? gl.TRIANGLES, this.count, gl.UNSIGNED_SHORT, 0) + this.ctx.popBuffer(gl.ARRAY_BUFFER) + this.ctx.popBuffer(gl.ELEMENT_ARRAY_BUFFER) + } + + free() { + const gl = this.ctx.gl + gl.deleteBuffer(this.glVBuf) + gl.deleteBuffer(this.glIBuf) + } + + +} + // TODO: support useProgram function genBinder(func: (ty: GLenum, item: T) => void) { const bindings = {} @@ -442,9 +471,7 @@ function genBinder(func: (ty: GLenum, item: T) => void) { } } -export default (gl: WebGLRenderingContext, gopt: { - texFilter?: TexFilter, -} = {}): GFXCtx => { +export default (gl: WebGLRenderingContext): GfxCtx => { const textureBinder = genBinder(gl.bindTexture.bind(gl)) const bufferBinder = genBinder(gl.bindBuffer.bind(gl)) diff --git a/src/kaboom.ts b/src/kaboom.ts index 5626393ae..ad19d9cf4 100644 --- a/src/kaboom.ts +++ b/src/kaboom.ts @@ -8,6 +8,11 @@ import initGfx, { BatchRenderer, } from "./gfx" +import { + Asset, + AssetBucket, +} from "./assets" + import { sat, vec2, @@ -46,6 +51,7 @@ import { } from "./math" import easings from "./easings" +import TexPacker from "./texPacker" import { IDList, @@ -399,7 +405,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const fontCacheCanvas = document.createElement("canvas") fontCacheCanvas.width = MAX_TEXT_CACHE_SIZE fontCacheCanvas.height = MAX_TEXT_CACHE_SIZE - const fontCacheCtx = fontCacheCanvas.getContext("2d", { + const fontCacheC2d = fontCacheCanvas.getContext("2d", { willReadFrequently: true, }) @@ -424,63 +430,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const ggl = initGfx(gl) - class KaboomError extends Error { - constructor(msg) { - super(msg) - this.name = "KaboomError" - } - } - - class TexPacker { - private tex: Texture - private canvas: HTMLCanvasElement - private ctx: CanvasRenderingContext2D - private x: number = 0 - private y: number = 0 - private curHeight: number = 0 - constructor(w: number, h: number) { - this.canvas = document.createElement("canvas") - this.canvas.width = w - this.canvas.height = h - this.tex = Texture.fromImage(ggl, this.canvas) - this.ctx = this.canvas.getContext("2d") - } - add(img: ImageSource): [Texture, Quad] { - if (img.width > this.canvas.width || img.height > this.canvas.height) { - throw new KaboomError(`Texture size (${img.width} x ${img.height}) exceeds limit (${this.canvas.width} x ${this.canvas.height})`) - } - if (this.x + img.width > this.canvas.width) { - this.x = 0 - this.y += this.curHeight - this.curHeight = 0 - } - if (this.y + img.height > this.canvas.height) { - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) - this.tex = Texture.fromImage(ggl, this.canvas) - this.x = 0 - this.y = 0 - this.curHeight = 0 - } - const pos = new Vec2(this.x, this.y) - this.x += img.width - if (img.height > this.curHeight) { - this.curHeight = img.height - } - if (img instanceof ImageData) { - this.ctx.putImageData(img, pos.x, pos.y) - } else { - this.ctx.drawImage(img, pos.x, pos.y) - } - this.tex.update(this.canvas) - return [this.tex, new Quad( - pos.x / this.canvas.width, - pos.y / this.canvas.height, - img.width / this.canvas.width, - img.height / this.canvas.height, - )] - } - } - const gfx = (() => { const defShader = makeShader(DEF_VERT, DEF_FRAG) @@ -510,6 +459,8 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { ) } + // TODO: use this instead of change UV + // gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) gl.enable(gl.BLEND) gl.blendFuncSeparate( gl.SRC_ALPHA, @@ -536,8 +487,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { return { - // keep track of how many draw calls we're doing this frame - drawCalls: 0, // how many draw calls we're doing last frame, this is the number we give to users lastDrawCalls: 0, @@ -673,103 +622,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { })() - 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) - } - } - - 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 - } - } - const assets = { // prefix for when loading from a url urlPrefix: "", @@ -780,7 +632,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { sounds: new AssetBucket(), shaders: new AssetBucket(), custom: new AssetBucket(), - packer: new TexPacker(SPRITE_ATLAS_WIDTH, SPRITE_ATLAS_HEIGHT), + packer: new TexPacker(ggl, SPRITE_ATLAS_WIDTH, SPRITE_ATLAS_HEIGHT), // if we finished initially loading all assets loaded: false, } @@ -843,6 +695,8 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } + game.root.use(timer()) + // TODO: accept Asset? // wrap individual loaders with global loader counter, for stuff like progress bar function load(prom: Promise): Asset { @@ -875,7 +729,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const url = assets.urlPrefix + path return fetch(url) .then((res) => { - if (!res.ok) throw new KaboomError(`Failed to fetch "${url}"`) + if (!res.ok) throw new Error(`Failed to fetch "${url}"`) return res }) } @@ -899,7 +753,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { img.src = isDataURL(src) ? src : assets.urlPrefix + src return new Promise((resolve, reject) => { img.onload = () => resolve(img) - img.onerror = () => reject(new KaboomError(`Failed to load image from "${src}"`)) + img.onerror = () => reject(new Error(`Failed to load image from "${src}"`)) }) } @@ -938,7 +792,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const font = new FontFace(name, typeof src === "string" ? `url(${src})` : src) document.fonts.add(font) return assets.fonts.add(name, font.load().catch((err) => { - throw new KaboomError(`Failed to load font from "${src}": ${err}`) + throw new Error(`Failed to load font from "${src}": ${err}`) }).then((face) => new FontData(face, opt))) } @@ -1030,15 +884,15 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const height = images[0].height canvas.width = width * images.length canvas.height = height - const ctx = canvas.getContext("2d") + const c2d = canvas.getContext("2d") images.forEach((img, i) => { if (img instanceof ImageData) { - ctx.putImageData(img, i * width, 0) + c2d.putImageData(img, i * width, 0) } else { - ctx.drawImage(img, i * width, 0) + c2d.drawImage(img, i * width, 0) } }) - const merged = ctx.getImageData(0, 0, images.length * width, height) + const merged = c2d.getImageData(0, 0, images.length * width, height) return SpriteData.fromImage(merged, { ...opt, sliceX: images.length, @@ -1087,10 +941,10 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { canvas.width = data.width canvas.height = data.height * data.frames.length - const ctx = canvas.getContext("2d") + const c2d = canvas.getContext("2d") images.forEach((img: HTMLImageElement, i) => { - ctx.drawImage(img, 0, i * data.height) + c2d.drawImage(img, 0, i * data.height) }) const spr = await loadSprite(null, canvas, { @@ -1225,14 +1079,14 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { return null } else { // if all other assets are loaded and we still haven't found this sprite, throw - throw new KaboomError(`Sprite not found: ${src}`) + throw new Error(`Sprite not found: ${src}`) } } else if (src instanceof SpriteData) { return Asset.loaded(src) } else if (src instanceof Asset) { return src } else { - throw new KaboomError(`Invalid sprite: ${src}`) + throw new Error(`Invalid sprite: ${src}`) } } @@ -1246,14 +1100,14 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } else if (loadProgress() < 1) { return null } else { - throw new KaboomError(`Sound not found: ${src}`) + throw new Error(`Sound not found: ${src}`) } } else if (src instanceof SoundData) { return Asset.loaded(src) } else if (src instanceof Asset) { return src } else { - throw new KaboomError(`Invalid sound: ${src}`) + throw new Error(`Invalid sound: ${src}`) } } @@ -1270,7 +1124,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } else if (loadProgress() < 1) { return null } else { - throw new KaboomError(`Shader not found: ${src}`) + throw new Error(`Shader not found: ${src}`) } } else if (src instanceof Asset) { return src.data ? src.data : src @@ -1305,7 +1159,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } else if (loadProgress() < 1) { return null } else { - throw new KaboomError(`Font not found: ${src}`) + throw new Error(`Font not found: ${src}`) } } else if (src instanceof Asset) { return src.data ? src.data : src @@ -1614,7 +1468,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { }) } - gfx.drawCalls = 0 + gfx.renderer.numDraws = 0 gfx.fixed = false gfx.transformStack.length = 0 gfx.transform = new Mat4() @@ -1631,7 +1485,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { // TODO: don't render debug UI with framebuffer // TODO: polish framebuffer rendering / sizing issues flush() - gfx.lastDrawCalls = gfx.drawCalls + gfx.lastDrawCalls = gfx.renderer.numDraws gfx.frameBuffer.unbind() gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight) @@ -1704,7 +1558,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function drawUVQuad(opt: DrawUVQuadOpt) { if (opt.width === undefined || opt.height === undefined) { - throw new KaboomError("drawUVQuad() requires property \"width\" and \"height\".") + throw new Error("drawUVQuad() requires property \"width\" and \"height\".") } if (opt.width <= 0 || opt.height <= 0) { @@ -1768,7 +1622,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function drawTexture(opt: DrawTextureOpt) { if (!opt.tex) { - throw new KaboomError("drawTexture() requires property \"tex\".") + throw new Error("drawTexture() requires property \"tex\".") } const q = opt.quad ?? new Quad(0, 0, 1, 1) @@ -1827,7 +1681,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function drawSprite(opt: DrawSpriteOpt) { if (!opt.sprite) { - throw new KaboomError("drawSprite() requires property \"sprite\"") + throw new Error("drawSprite() requires property \"sprite\"") } // TODO: slow @@ -1840,7 +1694,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const q = spr.data.frames[opt.frame ?? 0] if (!q) { - throw new KaboomError(`Frame not found: ${opt.frame ?? 0}`) + throw new Error(`Frame not found: ${opt.frame ?? 0}`) } drawTexture(Object.assign({}, opt, { @@ -1883,7 +1737,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function drawRect(opt: DrawRectOpt) { if (opt.width === undefined || opt.height === undefined) { - throw new KaboomError("drawRect() requires property \"width\" and \"height\".") + throw new Error("drawRect() requires property \"width\" and \"height\".") } if (opt.width <= 0 || opt.height <= 0) { @@ -1951,7 +1805,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const { p1, p2 } = opt if (!p1 || !p2) { - throw new KaboomError("drawLine() requires properties \"p1\" and \"p2\".") + throw new Error("drawLine() requires properties \"p1\" and \"p2\".") } const w = opt.width || 1 @@ -1981,7 +1835,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const pts = opt.pts if (!pts) { - throw new KaboomError("drawLines() requires property \"pts\".") + throw new Error("drawLines() requires property \"pts\".") } if (pts.length < 2) { @@ -2039,7 +1893,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function drawTriangle(opt: DrawTriangleOpt) { if (!opt.p1 || !opt.p2 || !opt.p3) { - throw new KaboomError("drawPolygon() requires properties \"p1\", \"p2\" and \"p3\".") + throw new Error("drawPolygon() requires properties \"p1\", \"p2\" and \"p3\".") } return drawPolygon(Object.assign({}, opt, { pts: [opt.p1, opt.p2, opt.p3], @@ -2049,7 +1903,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function drawCircle(opt: DrawCircleOpt) { if (typeof opt.radius !== "number") { - throw new KaboomError("drawCircle() requires property \"radius\".") + throw new Error("drawCircle() requires property \"radius\".") } if (opt.radius === 0) { @@ -2067,7 +1921,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function drawEllipse(opt: DrawEllipseOpt) { if (opt.radiusX === undefined || opt.radiusY === undefined) { - throw new KaboomError("drawEllipse() requires properties \"radiusX\" and \"radiusY\".") + throw new Error("drawEllipse() requires properties \"radiusX\" and \"radiusY\".") } if (opt.radiusX === 0 || opt.radiusY === 0) { @@ -2122,7 +1976,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function drawPolygon(opt: DrawPolygonOpt) { if (!opt.pts) { - throw new KaboomError("drawPolygon() requires property \"pts\".") + throw new Error("drawPolygon() requires property \"pts\".") } const npts = opt.pts.length @@ -2294,7 +2148,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function formatText(opt: DrawTextOpt): FormattedText { if (opt.text === undefined) { - throw new KaboomError("formatText() requires property \"text\".") + throw new Error("formatText() requires property \"text\".") } let font = resolveFont(opt.font) @@ -2351,7 +2205,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { if (!atlas.font.map[ch]) { // TODO: use assets.packer to pack font texture - const c2d = fontCacheCtx + const c2d = fontCacheC2d c2d.clearRect(0, 0, fontCacheCanvas.width, fontCacheCanvas.height) c2d.font = `${font.size}px ${fontName}` c2d.textBaseline = "top" @@ -2378,7 +2232,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { atlas.cursor.y += h if (atlas.cursor.y > FONT_ATLAS_HEIGHT) { // TODO: create another atlas - throw new KaboomError("Font atlas exceeds character limit") + throw new Error("Font atlas exceeds character limit") } } @@ -2622,15 +2476,15 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } winEvents.error = (e) => { - if (e.error && e.error instanceof KaboomError) { + if (e.error && e.error instanceof Error) { handleErr(e.error) - } else if (e instanceof KaboomError) { + } else if (e instanceof Error) { handleErr(e) } } winEvents.unhandledrejection = (e) => { - if (e.reason instanceof KaboomError) { + if (e.reason instanceof Error) { handleErr(e.reason) } } @@ -2660,7 +2514,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { game.logs = game.logs.slice(0, max) } }, - error: (msg) => debug.log(new KaboomError(msg.toString ? msg.toString() : msg as string)), + error: (msg) => debug.log(new Error(msg.toString ? msg.toString() : msg as string)), curRecording: null, numObjects: () => get("*", { recursive: true }).length, get paused() { @@ -2755,7 +2609,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { add(a: CompList | GameObj = []): GameObj { const obj = Array.isArray(a) ? make(a) : a if (obj.parent) { - throw new KaboomError("Cannot add a game obj that already has a parent.") + throw new Error("Cannot add a game obj that already has a parent.") } obj.parent = this obj.transform = calcTransform(obj) @@ -2822,7 +2676,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { subtract: drawSubtracted, }[this.mask] if (!maskFunc) { - throw new KaboomError(`Invalid mask func: "${this.mask}"`) + throw new Error(`Invalid mask func: "${this.mask}"`) } maskFunc(() => { children.forEach((child) => child.draw()) @@ -2917,7 +2771,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { }) gc.push(() => delete this[k]) } else { - throw new KaboomError(`Duplicate component property: "${k}"`) + throw new Error(`Duplicate component property: "${k}"`) } } @@ -2928,7 +2782,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { if (!comp.require) return for (const dep of comp.require) { if (!this.c(dep)) { - throw new KaboomError(`Component "${comp.id}" requires component "${dep}"`) + throw new Error(`Component "${comp.id}" requires component "${dep}"`) } } } @@ -3223,7 +3077,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const events = [] forAllCurrentAndFuture(tag, (obj) => { if (!obj.area) - throw new KaboomError("onClick() requires the object to have area() component") + throw new Error("onClick() requires the object to have area() component") events.push(obj.onClick(() => action(obj))) }) return EventController.join(events) @@ -3235,7 +3089,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const events = [] forAllCurrentAndFuture(t, (obj) => { if (!obj.area) - throw new KaboomError("onHover() requires the object to have area() component") + throw new Error("onHover() requires the object to have area() component") events.push(obj.onHover(() => action(obj))) }) return EventController.join(events) @@ -3246,7 +3100,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const events = [] forAllCurrentAndFuture(t, (obj) => { if (!obj.area) - throw new KaboomError("onHoverUpdate() requires the object to have area() component") + throw new Error("onHoverUpdate() requires the object to have area() component") events.push(obj.onHoverUpdate(() => action(obj))) }) return EventController.join(events) @@ -3257,64 +3111,12 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const events = [] forAllCurrentAndFuture(t, (obj) => { if (!obj.area) - throw new KaboomError("onHoverEnd() requires the object to have area() component") + throw new Error("onHoverEnd() requires the object to have area() component") events.push(obj.onHoverEnd(() => action(obj))) }) return EventController.join(events) } - // TODO: use PromiseLike? - // TODO: use root game object and timer()? - // add an event that'd be run after t - function wait(time: number, action?: () => void): TimerController { - let t = 0 - const actions = [] - if (action) actions.push(action) - const ev = onUpdate(() => { - t += dt() - if (t >= time) { - ev.cancel() - actions.forEach((action) => action()) - } - }) - return { - paused: ev.paused, - cancel: ev.cancel, - onEnd(action) { - actions.push(action) - }, - then(action) { - this.onEnd(action) - return this - }, - } - } - - // add an event that's run every t seconds - function loop(t: number, action: () => void): EventController { - - let curTimer: null | TimerController = null - - const newAction = () => { - // TODO: should f be execute right away as loop() is called? - curTimer = wait(t, newAction) - action() - } - - curTimer = wait(0, newAction) - - return { - get paused() { - return curTimer.paused - }, - set paused(p) { - curTimer.paused = p - }, - cancel: () => curTimer.cancel(), - } - - } - function enterDebugMode() { app.onKeyPress("f1", () => { @@ -3506,7 +3308,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function anchor(o: Anchor | Vec2): AnchorComp { if (!o) { - throw new KaboomError("Please define an anchor") + throw new Error("Please define an anchor") } return { id: "anchor", @@ -3833,7 +3635,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const localArea = this.localArea() if (!(localArea instanceof Polygon || localArea instanceof Rect)) { - throw new KaboomError("Only support polygon and rect shapes for now") + throw new Error("Only support polygon and rect shapes for now") } const transform = this.transform @@ -3891,7 +3693,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const spriteLoadedEvent = new Event<[SpriteData]>() if (!src) { - throw new KaboomError("Please pass the resource name or data to sprite()") + throw new Error("Please pass the resource name or data to sprite()") } const calcTexScale = (tex: Texture, q: Quad, w?: number, h?: number): Vec2 => { @@ -3928,7 +3730,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const q = spriteData.frames[this.frame ?? 0] if (!q) { - throw new KaboomError(`Frame not found: ${this.frame ?? 0}`) + throw new Error(`Frame not found: ${this.frame ?? 0}`) } if (spriteData.slice9) { @@ -4044,7 +3846,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } if (anim.speed === 0) { - throw new KaboomError("Sprite anim speed cannot be 0") + throw new Error("Sprite anim speed cannot be 0") } curAnim.timer += dt() * this.animSpeed @@ -4085,7 +3887,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { const anim = spriteData.anims[name] if (anim === undefined) { - throw new KaboomError(`Anim not found: ${name}`) + throw new Error(`Anim not found: ${name}`) } if (curAnim) { @@ -4429,7 +4231,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { add(this: GameObj) { if (this.mass === 0) { - throw new KaboomError("Can't set body mass to 0") + throw new Error("Can't set body mass to 0") } // static vs static: don't resolve @@ -4653,7 +4455,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function health(hp: number): HealthComp { if (hp == null) { - throw new KaboomError("health() requires the initial amount of hp") + throw new Error("health() requires the initial amount of hp") } const maxHP = hp return { @@ -4695,7 +4497,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function lifespan(time: number, opt: LifespanCompOpt = {}): EmptyComp { if (time == null) { - throw new KaboomError("lifespan() requires time") + throw new Error("lifespan() requires time") } const fade = opt.fade ?? 0 return { @@ -4718,7 +4520,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { ): StateComp { if (!initState) { - throw new KaboomError("state() requires an initial state") + throw new Error("state() requires an initial state") } const events = {} @@ -4756,7 +4558,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { didFirstEnter = true if (stateList && !stateList.includes(state)) { - throw new KaboomError(`State not found: ${state}`) + throw new Error(`State not found: ${state}`) } const oldState = this.state @@ -4773,7 +4575,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { : transitions[oldState] as string[] if (!available.includes(state)) { - throw new KaboomError(`Cannot transition state from "${oldState}" to "${state}". Available transitions: ${available.map((s) => `"${s}"`).join(", ")}`) + throw new Error(`Cannot transition state from "${oldState}" to "${state}". Available transitions: ${available.map((s) => `"${s}"`).join(", ")}`) } } @@ -4868,7 +4670,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function go(name: SceneName, ...args) { if (!game.scenes[name]) { - throw new KaboomError(`Scene not found: ${name}`) + throw new Error(`Scene not found: ${name}`) } game.events.onOnce("frameEnd", () => { @@ -5069,7 +4871,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function addLevel(map: string[], opt: LevelOpt): GameObj { if (!opt.tileWidth || !opt.tileHeight) { - throw new KaboomError("Must provide tileWidth and tileHeight.") + throw new Error("Must provide tileWidth and tileHeight.") } // TODO: custom parent @@ -5302,7 +5104,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { if (typeof key === "string") { if (opt.tiles[key]) { if (typeof opt.tiles[key] !== "function") { - throw new KaboomError("Level symbol def must be a function returning a component list") + throw new Error("Level symbol def must be a function returning a component list") } return opt.tiles[key](p) } else if (opt.wildcardTile) { @@ -5311,7 +5113,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } else if (Array.isArray(key)) { return key } else { - throw new KaboomError("Expected a symbol or a component list") + throw new Error("Expected a symbol or a component list") } })() @@ -5713,10 +5515,13 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } // aliases for root game obj operations - const add: KaboomCtx["add"] = game.root.add.bind(game.root) - const readd: KaboomCtx["readd"] = game.root.readd.bind(game.root) - const destroyAll: KaboomCtx["destroyAll"] = game.root.removeAll.bind(game.root) - const get: KaboomCtx["get"] = game.root.get.bind(game.root) + const add = game.root.add.bind(game.root) + const readd = game.root.readd.bind(game.root) + const destroyAll = game.root.removeAll.bind(game.root) + const get = game.root.get.bind(game.root) + const wait = game.root.wait.bind(game.root) + const loop = game.root.loop.bind(game.root) + const tween = game.root.tween.bind(game.root) function boom(speed: number = 2, size: number = 1): Comp { let time = 0 @@ -6323,50 +6128,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } - function tween( - from: V, - to: V, - duration: number, - setValue: (value: V) => void, - easeFunc = easings.linear, - ): TweenController { - let curTime = 0 - const onEndEvents: Array<() => void> = [] - const ev = onUpdate(() => { - curTime += dt() - const t = Math.min(curTime / duration, 1) - setValue(lerp(from, to, easeFunc(t))) - if (t === 1) { - ev.cancel() - setValue(to) - onEndEvents.forEach((action) => action()) - } - }) - return { - get paused() { - return ev.paused - }, - set paused(p) { - ev.paused = p - }, - onEnd(action: () => void) { - onEndEvents.push(action) - }, - then(action: () => void) { - this.onEnd(action) - return this - }, - cancel() { - ev.cancel() - }, - finish() { - ev.cancel() - setValue(to) - onEndEvents.forEach((action) => action()) - }, - } - } - let isFirstFrame = true // main game loop @@ -6416,7 +6177,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { if (gopt.letterbox) { if (!gopt.width || !gopt.height) { - throw new KaboomError("Letterboxing requires width and height defined.") + throw new Error("Letterboxing requires width and height defined.") } const rc = cw / ch @@ -6448,7 +6209,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { if (gopt.stretch) { if (!gopt.width || !gopt.height) { - throw new KaboomError("Stretching requires width and height defined.") + throw new Error("Stretching requires width and height defined.") } } @@ -6759,7 +6520,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { Event, EventHandler, EventController, - KaboomError, } if (gopt.plugins) { diff --git a/src/texPacker.ts b/src/texPacker.ts new file mode 100644 index 000000000..e348be243 --- /dev/null +++ b/src/texPacker.ts @@ -0,0 +1,65 @@ +import type { + ImageSource, +} from "./types" + +import { + GfxCtx, + Texture, +} from "./gfx" + +import { + Vec2, + Quad, +} from "./math" + +export default class TexPacker { + private tex: Texture + private canvas: HTMLCanvasElement + private c2d: CanvasRenderingContext2D + private x: number = 0 + private y: number = 0 + private curHeight: number = 0 + private gfx: GfxCtx + constructor(gfx: GfxCtx, w: number, h: number) { + this.gfx = gfx + this.canvas = document.createElement("canvas") + this.canvas.width = w + this.canvas.height = h + this.tex = Texture.fromImage(gfx, this.canvas) + this.c2d = this.canvas.getContext("2d") + } + add(img: ImageSource): [Texture, Quad] { + if (img.width > this.canvas.width || img.height > this.canvas.height) { + throw new Error(`Texture size (${img.width} x ${img.height}) exceeds limit (${this.canvas.width} x ${this.canvas.height})`) + } + if (this.x + img.width > this.canvas.width) { + this.x = 0 + this.y += this.curHeight + this.curHeight = 0 + } + if (this.y + img.height > this.canvas.height) { + this.c2d.clearRect(0, 0, this.canvas.width, this.canvas.height) + this.tex = Texture.fromImage(this.gfx, this.canvas) + this.x = 0 + this.y = 0 + this.curHeight = 0 + } + const pos = new Vec2(this.x, this.y) + this.x += img.width + if (img.height > this.curHeight) { + this.curHeight = img.height + } + if (img instanceof ImageData) { + this.c2d.putImageData(img, pos.x, pos.y) + } else { + this.c2d.drawImage(img, pos.x, pos.y) + } + this.tex.update(this.canvas) + return [this.tex, new Quad( + pos.x / this.canvas.width, + pos.y / this.canvas.height, + img.width / this.canvas.width, + img.height / this.canvas.height, + )] + } +} diff --git a/src/types.ts b/src/types.ts index a75553b72..443ec6d8f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2346,12 +2346,6 @@ export interface KaboomCtx { * @since v3000.0 */ EventController: typeof EventController, - /** - * Error related to this kaboom instance. - * - * @since v3000.2 - */ - KaboomError: typeof KaboomError, /** * Current Kaboom library version. * @@ -2621,6 +2615,13 @@ export interface GameObjRaw { * @since v3000.0 */ draw(): void, + /** + * Draw debug info in inspect mode + * + * @since v3000.0 + */ + drawInspect: () => void, + clearEvents: () => void, /** * If there's certain tag(s) on the game obj. */ @@ -3090,9 +3091,9 @@ export interface AudioPlay { } export declare class Shader { - ctx: GFXCtx + ctx: GfxCtx glProgram: WebGLProgram - constructor(ctx: GFXCtx, vert: string, frag: string, attribs: string[]) + constructor(ctx: GfxCtx, vert: string, frag: string, attribs: string[]) bind() unbind() send(uniform: Uniform) @@ -3106,16 +3107,16 @@ export type TextureOpt = { export type ImageSource = Exclude -type GFXCtx = any +type GfxCtx = any export declare class Texture { - ctx: GFXCtx + ctx: GfxCtx src: null | ImageSource glTex: WebGLTexture width: number height: number - constructor(gfx: GFXCtx, w: number, h: number, opt?: TextureOpt) - static fromImage(ctx: GFXCtx, img: ImageSource, opt?: TextureOpt): Texture + constructor(ctx: GfxCtx, w: number, h: number, opt?: TextureOpt) + static fromImage(ctx: GfxCtx, img: ImageSource, opt?: TextureOpt): Texture update(img: ImageSource, x?: number, y?: number) bind() unbind() @@ -5235,8 +5236,6 @@ export declare class EventController { static join(events: EventController[]): EventController } -export declare class KaboomError extends Error {} - // TODO: global name conflict, renamed to KEvent? export declare class Event { add(action: (...args: Args) => void): EventController