From 9b9493758b8fc6dc5a4f9239aeb2242e7b3d9645 Mon Sep 17 00:00:00 2001 From: lajbel Date: Tue, 24 Sep 2024 17:48:44 -0300 Subject: [PATCH 1/2] feat: TexPacker.remove --- examples/largeTexture.js | 6 +- src/assets/asset.ts | 23 +++- src/assets/bitmapFont.ts | 4 +- src/assets/pedit.ts | 4 +- src/assets/sprite.ts | 27 ++-- src/gfx/classes/FrameBuffer.ts | 128 ++++++++++++++++++ .../{texPacker.ts => classes/TexPacker.ts} | 63 ++++++++- src/gfx/gfx.ts | 126 +---------------- src/gfx/index.ts | 3 +- src/kaplay.ts | 16 ++- 10 files changed, 243 insertions(+), 157 deletions(-) create mode 100644 src/gfx/classes/FrameBuffer.ts rename src/gfx/{texPacker.ts => classes/TexPacker.ts} (62%) diff --git a/examples/largeTexture.js b/examples/largeTexture.js index 7f5f9c59..cd3f4100 100644 --- a/examples/largeTexture.js +++ b/examples/largeTexture.js @@ -2,12 +2,12 @@ kaplay(); -let cameraPosition = camPos(); -let cameraScale = 1; - // Loads a random 2500px image loadSprite("bigyoshi", "/examples/sprites/YOSHI.png"); +let cameraPosition = camPos(); +let cameraScale = 1; + add([ sprite("bigyoshi"), ]); diff --git a/src/assets/asset.ts b/src/assets/asset.ts index 42bdf1d4..ae34d7bb 100644 --- a/src/assets/asset.ts +++ b/src/assets/asset.ts @@ -1,6 +1,6 @@ import { SPRITE_ATLAS_HEIGHT, SPRITE_ATLAS_WIDTH } from "../constants"; +import TexPacker from "../gfx/classes/TexPacker"; import type { GfxCtx } from "../gfx/gfx"; -import TexPacker from "../gfx/texPacker"; import { assets } from "../kaplay"; import { KEvent } from "../utils"; import type { BitmapFontData } from "./bitmapFont"; @@ -11,6 +11,8 @@ import type { SpriteData } from "./sprite"; /** * An asset is a resource that is loaded asynchronously. + * + * It can be a sprite, a sound, a font, a shader, etc. */ export class Asset { loaded: boolean = false; @@ -19,6 +21,7 @@ export class Asset { private onLoadEvents: KEvent<[D]> = new KEvent(); private onErrorEvents: KEvent<[Error]> = new KEvent(); private onFinishEvents: KEvent<[]> = new KEvent(); + constructor(loader: Promise) { loader.then((data) => { this.loaded = true; @@ -26,6 +29,7 @@ export class Asset { this.onLoadEvents.trigger(data); }).catch((err) => { this.error = err; + if (this.onErrorEvents.numListeners() > 0) { this.onErrorEvents.trigger(err); } @@ -84,17 +88,20 @@ export class Asset { 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; } // if not found return undefined @@ -106,21 +113,22 @@ export class AssetBucket { 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; - }); + return fetch(url).then((res) => { + if (!res.ok) throw new Error(`Failed to fetch "${url}"`); + return res; + }); } export function fetchJSON(path: string) { @@ -148,10 +156,11 @@ export function loadJSON(name: string, url: string) { } // wrapper around image loader to get a Promise -export function loadImg(src: string): Promise { +export function loadImage(src: string): Promise { const img = new Image(); img.crossOrigin = "anonymous"; img.src = src; + return new Promise((resolve, reject) => { img.onload = () => resolve(img); img.onerror = () => diff --git a/src/assets/bitmapFont.ts b/src/assets/bitmapFont.ts index a1a9ddf6..13cdbf20 100644 --- a/src/assets/bitmapFont.ts +++ b/src/assets/bitmapFont.ts @@ -3,7 +3,7 @@ import { Texture } from "../gfx"; import { assets, gfx } from "../kaplay"; import type { Quad } from "../math/math"; import type { TexFilter } from "../types"; -import { type Asset, loadImg } from "./asset"; +import { type Asset, loadImage } from "./asset"; import { makeFont } from "./font"; export interface GfxFont { @@ -35,7 +35,7 @@ export function loadBitmapFont( ): Asset { return assets.bitmapFonts.add( name, - loadImg(src) + loadImage(src) .then((img) => { return makeFont( Texture.fromImage(gfx.ggl, img, opt), diff --git a/src/assets/pedit.ts b/src/assets/pedit.ts index d0b653b7..adc5888d 100644 --- a/src/assets/pedit.ts +++ b/src/assets/pedit.ts @@ -1,5 +1,5 @@ import { assets } from "../kaplay"; -import { type Asset, fetchJSON, loadImg } from "./asset"; +import { type Asset, fetchJSON, loadImage } from "./asset"; import { loadSprite, type SpriteAnims, type SpriteData } from "./sprite"; import { fixURL } from "./utils"; @@ -22,7 +22,7 @@ export function loadPedit( const data = typeof src === "string" ? await fetchJSON(src) : src; - const images = await Promise.all(data.frames.map(loadImg)); + const images = await Promise.all(data.frames.map(loadImage)); const canvas = document.createElement("canvas"); canvas.width = data.width; canvas.height = data.height * data.frames.length; diff --git a/src/assets/sprite.ts b/src/assets/sprite.ts index d69f8bd6..9eda2d92 100644 --- a/src/assets/sprite.ts +++ b/src/assets/sprite.ts @@ -1,4 +1,4 @@ -import { Asset, loadImg, loadProgress } from "../assets"; +import { Asset, loadImage, loadProgress } from "../assets"; import type { DrawSpriteOpt } from "../gfx"; import type { Texture } from "../gfx/gfx"; import { assets } from "../kaplay"; @@ -95,17 +95,20 @@ export class SpriteData { frames: Quad[] = [new Quad(0, 0, 1, 1)]; anims: SpriteAnims = {}; slice9: NineSlice | null = null; + packerId: number | null; constructor( tex: Texture, frames?: Quad[], anims: SpriteAnims = {}, slice9: NineSlice | null = null, + packerId: number | null = null, ) { this.tex = tex; if (frames) this.frames = frames; this.anims = anims; this.slice9 = slice9; + this.packerId = packerId; } /** @@ -132,7 +135,7 @@ export class SpriteData { data: ImageSource, opt: LoadSpriteOpt = {}, ): SpriteData { - const [tex, quad] = assets.packer.add(data); + const [tex, quad, packerId] = assets.packer.add(data); const frames = opt.frames ? opt.frames.map((f) => new Quad( @@ -150,14 +153,15 @@ export class SpriteData { quad.w, quad.h, ); - return new SpriteData(tex, frames, opt.anims, opt.slice9); + + return new SpriteData(tex, frames, opt.anims, opt.slice9, packerId); } static fromURL( url: string, opt: LoadSpriteOpt = {}, ): Promise { - return loadImg(url).then((img) => SpriteData.fromImage(img, opt)); + return loadImage(url).then((img) => SpriteData.fromImage(img, opt)); } } @@ -179,9 +183,9 @@ export function resolveSprite( throw new Error(`Sprite not found: ${src}`); } } - else if (src instanceof SpriteData) { - return Asset.loaded(src); - } + // else if (src instanceof SpriteData) { + // return Asset.loaded(src); + // } else if (src instanceof Asset) { return src; } @@ -205,13 +209,14 @@ export function loadSprite( }, ): Asset { src = fixURL(src); + if (Array.isArray(src)) { if (src.some((s) => typeof s === "string")) { return assets.sprites.add( name, Promise.all(src.map((s) => { return typeof s === "string" - ? loadImg(s) + ? loadImage(s) : Promise.resolve(s); })).then((images) => createSpriteSheet(images, opt)), ); @@ -224,6 +229,7 @@ export function loadSprite( } } else { + console.log(assets.sprites); if (typeof src === "string") { return assets.sprites.add(name, SpriteData.from(src, opt)); } @@ -256,7 +262,6 @@ export function slice(x = 1, y = 1, dx = 0, dy = 0, w = 1, h = 1): Quad[] { } // TODO: load synchronously if passed ImageSource - export function createSpriteSheet( images: ImageSource[], opt: LoadSpriteOpt = {}, @@ -266,8 +271,10 @@ export function createSpriteSheet( const height = images[0].height; canvas.width = width * images.length; canvas.height = height; + const c2d = canvas.getContext("2d"); if (!c2d) throw new Error("Failed to create canvas context"); + images.forEach((img, i) => { if (img instanceof ImageData) { c2d.putImageData(img, i * width, 0); @@ -276,7 +283,9 @@ export function createSpriteSheet( c2d.drawImage(img, i * width, 0); } }); + const merged = c2d.getImageData(0, 0, images.length * width, height); + return SpriteData.fromImage(merged, { ...opt, sliceX: images.length, diff --git a/src/gfx/classes/FrameBuffer.ts b/src/gfx/classes/FrameBuffer.ts new file mode 100644 index 00000000..4c43b8fe --- /dev/null +++ b/src/gfx/classes/FrameBuffer.ts @@ -0,0 +1,128 @@ +import type { TextureOpt } from "../../types"; +import { type GfxCtx, Texture } from "../gfx"; + +/** + * @group GFX + */ + +export class FrameBuffer { + ctx: GfxCtx; + tex: Texture; + glFramebuffer: WebGLFramebuffer; + glRenderbuffer: WebGLRenderbuffer; + + constructor(ctx: GfxCtx, w: number, h: number, opt: TextureOpt = {}) { + this.ctx = ctx; + const gl = ctx.gl; + ctx.onDestroy(() => this.free()); + this.tex = new Texture(ctx, w, h, opt); + + const frameBuffer = gl.createFramebuffer(); + const renderBuffer = gl.createRenderbuffer(); + + if (!frameBuffer || !renderBuffer) { + throw new Error("Failed to create framebuffer"); + } + + this.glFramebuffer = frameBuffer; + this.glRenderbuffer = renderBuffer; + + this.bind(); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, w, h); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + this.tex.glTex, + 0, + ); + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, + gl.DEPTH_STENCIL_ATTACHMENT, + gl.RENDERBUFFER, + this.glRenderbuffer, + ); + this.unbind(); + } + + get width() { + return this.tex.width; + } + + get height() { + return this.tex.height; + } + + toImageData() { + const gl = this.ctx.gl; + const data = new Uint8ClampedArray(this.width * this.height * 4); + this.bind(); + gl.readPixels( + 0, + 0, + this.width, + this.height, + gl.RGBA, + gl.UNSIGNED_BYTE, + data, + ); + this.unbind(); + // flip vertically + const bytesPerRow = this.width * 4; + const temp = new Uint8Array(bytesPerRow); + for (let y = 0; y < (this.height / 2 | 0); y++) { + const topOffset = y * bytesPerRow; + const bottomOffset = (this.height - y - 1) * bytesPerRow; + temp.set(data.subarray(topOffset, topOffset + bytesPerRow)); + data.copyWithin( + topOffset, + bottomOffset, + bottomOffset + bytesPerRow, + ); + data.set(temp, bottomOffset); + } + return new ImageData(data, this.width, this.height); + } + + toDataURL() { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = this.width; + canvas.height = this.height; + + if (!ctx) throw new Error("Failed to get 2d context"); + + ctx.putImageData(this.toImageData(), 0, 0); + return canvas.toDataURL(); + } + + clear() { + const gl = this.ctx.gl; + gl.clear(gl.COLOR_BUFFER_BIT); + } + + draw(action: () => void) { + this.bind(); + action(); + this.unbind(); + } + + bind() { + this.ctx.pushFramebuffer(this.glFramebuffer); + this.ctx.pushRenderbuffer(this.glRenderbuffer); + this.ctx.pushViewport({ x: 0, y: 0, w: this.width, h: this.height }); + } + + unbind() { + this.ctx.popFramebuffer(); + this.ctx.popRenderbuffer(); + this.ctx.popViewport(); + } + + free() { + const gl = this.ctx.gl; + gl.deleteFramebuffer(this.glFramebuffer); + gl.deleteRenderbuffer(this.glRenderbuffer); + this.tex.free(); + } +} diff --git a/src/gfx/texPacker.ts b/src/gfx/classes/TexPacker.ts similarity index 62% rename from src/gfx/texPacker.ts rename to src/gfx/classes/TexPacker.ts index 1ac1d98a..34938a23 100644 --- a/src/gfx/texPacker.ts +++ b/src/gfx/classes/TexPacker.ts @@ -1,18 +1,23 @@ -import type { ImageSource } from "../types"; - -import { type GfxCtx, Texture } from "../gfx"; - -import { Quad, Vec2 } from "../math/math"; +import { Quad, Vec2 } from "../../math/math"; +import type { ImageSource } from "../../types"; +import { type GfxCtx, Texture } from ".."; export default class TexPacker { + private lastTextureId: number = 0; private textures: Texture[] = []; private bigTextures: Texture[] = []; + private texturesPosition: Map = new Map(); 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"); @@ -21,23 +26,29 @@ export default class TexPacker { this.textures = [Texture.fromImage(gfx, this.canvas)]; this.bigTextures = []; + // render canvas on the dom + document.body.appendChild(this.canvas); + const context2D = this.canvas.getContext("2d"); if (!context2D) throw new Error("Failed to get 2d context"); this.c2d = context2D; } - add(img: ImageSource): [Texture, Quad] { + + add(img: ImageSource): [Texture, Quad, number] { if (img.width > this.canvas.width || img.height > this.canvas.height) { const tex = Texture.fromImage(this.gfx, img); this.bigTextures.push(tex); - return [tex, new Quad(0, 0, 1, 1)]; + return [tex, new Quad(0, 0, 1, 1), 0]; } + // next row if (this.x + img.width > this.canvas.width) { this.x = 0; this.y += this.curHeight; this.curHeight = 0; } + // next texture if (this.y + img.height > this.canvas.height) { this.c2d.clearRect(0, 0, this.canvas.width, this.canvas.height); @@ -46,19 +57,38 @@ export default class TexPacker { this.y = 0; this.curHeight = 0; } + const curTex = this.textures[this.textures.length - 1]; const pos = new Vec2(this.x, this.y); + const differenceWidth = this.canvas.width - this.x; + const differenceHeight = this.canvas.height - this.y; + + console.log(differenceHeight); + console.log(differenceWidth); + 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); } + curTex.update(this.canvas); + + this.texturesPosition.set(this.lastTextureId, { + position: pos, + size: new Vec2(img.width, img.height), + texture: curTex, + }); + + this.lastTextureId++; + return [ curTex, new Quad( @@ -67,6 +97,7 @@ export default class TexPacker { img.width / this.canvas.width, img.height / this.canvas.height, ), + this.lastTextureId - 1, ]; } free() { @@ -77,4 +108,22 @@ export default class TexPacker { tex.free(); } } + remove(packerId: number) { + const tex = this.texturesPosition.get(packerId); + + if (!tex) { + throw new Error("Texture with packer id not found"); + } + + this.c2d.clearRect( + tex.position.x, + tex.position.y, + tex.size.x, + tex.size.y, + ); + + tex.texture.update(this.canvas); + this.texturesPosition.delete(packerId); + this.x -= tex.size.x; + } } diff --git a/src/gfx/gfx.ts b/src/gfx/gfx.ts index 08946254..3be38bd3 100644 --- a/src/gfx/gfx.ts +++ b/src/gfx/gfx.ts @@ -13,6 +13,7 @@ export class Texture { constructor(ctx: GfxCtx, w: number, h: number, opt: TextureOpt = {}) { this.ctx = ctx; + const gl = ctx.gl; const glText = ctx.gl.createTexture(); @@ -99,131 +100,6 @@ export class Texture { } } -/** - * @group GFX - */ -export class FrameBuffer { - ctx: GfxCtx; - tex: Texture; - glFramebuffer: WebGLFramebuffer; - glRenderbuffer: WebGLRenderbuffer; - - constructor(ctx: GfxCtx, w: number, h: number, opt: TextureOpt = {}) { - this.ctx = ctx; - const gl = ctx.gl; - ctx.onDestroy(() => this.free()); - this.tex = new Texture(ctx, w, h, opt); - - const frameBuffer = gl.createFramebuffer(); - const renderBuffer = gl.createRenderbuffer(); - - if (!frameBuffer || !renderBuffer) { - throw new Error("Failed to create framebuffer"); - } - - this.glFramebuffer = frameBuffer; - this.glRenderbuffer = renderBuffer; - - this.bind(); - gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, w, h); - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - this.tex.glTex, - 0, - ); - gl.framebufferRenderbuffer( - gl.FRAMEBUFFER, - gl.DEPTH_STENCIL_ATTACHMENT, - gl.RENDERBUFFER, - this.glRenderbuffer, - ); - this.unbind(); - } - - get width() { - return this.tex.width; - } - - get height() { - return this.tex.height; - } - - toImageData() { - const gl = this.ctx.gl; - const data = new Uint8ClampedArray(this.width * this.height * 4); - this.bind(); - gl.readPixels( - 0, - 0, - this.width, - this.height, - gl.RGBA, - gl.UNSIGNED_BYTE, - data, - ); - this.unbind(); - // flip vertically - const bytesPerRow = this.width * 4; - const temp = new Uint8Array(bytesPerRow); - for (let y = 0; y < (this.height / 2 | 0); y++) { - const topOffset = y * bytesPerRow; - const bottomOffset = (this.height - y - 1) * bytesPerRow; - temp.set(data.subarray(topOffset, topOffset + bytesPerRow)); - data.copyWithin( - topOffset, - bottomOffset, - bottomOffset + bytesPerRow, - ); - data.set(temp, bottomOffset); - } - return new ImageData(data, this.width, this.height); - } - - toDataURL() { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.width = this.width; - canvas.height = this.height; - - if (!ctx) throw new Error("Failed to get 2d context"); - - ctx.putImageData(this.toImageData(), 0, 0); - return canvas.toDataURL(); - } - - clear() { - const gl = this.ctx.gl; - gl.clear(gl.COLOR_BUFFER_BIT); - } - - draw(action: () => void) { - this.bind(); - action(); - this.unbind(); - } - - bind() { - this.ctx.pushFramebuffer(this.glFramebuffer); - this.ctx.pushRenderbuffer(this.glRenderbuffer); - this.ctx.pushViewport({ x: 0, y: 0, w: this.width, h: this.height }); - } - - unbind() { - this.ctx.popFramebuffer(); - this.ctx.popRenderbuffer(); - this.ctx.popViewport(); - } - - free() { - const gl = this.ctx.gl; - gl.deleteFramebuffer(this.glFramebuffer); - gl.deleteRenderbuffer(this.glRenderbuffer); - this.tex.free(); - } -} - export type VertexFormat = { name: string; size: number; diff --git a/src/gfx/index.ts b/src/gfx/index.ts index 1595f8f5..c6305621 100644 --- a/src/gfx/index.ts +++ b/src/gfx/index.ts @@ -1,9 +1,10 @@ export * from "./anchor"; export * from "./bg"; +export * from "./classes/FrameBuffer"; +export * from "./classes/TexPacker"; export * from "./draw"; export * from "./formatText"; export * from "./gfx"; export * from "./gfxApp"; export * from "./stack"; -export * from "./texPacker"; export * from "./viewport"; diff --git a/src/kaplay.ts b/src/kaplay.ts index aab80a6b..87d2ce9e 100644 --- a/src/kaplay.ts +++ b/src/kaplay.ts @@ -440,6 +440,7 @@ const kaplay = < function makeCanvas(w: number, h: number) { const fb = new FrameBuffer(ggl, w, h); + return { clear: () => fb.clear(), free: () => fb.free(), @@ -685,8 +686,21 @@ const kaplay = < const query = game.root.query.bind(game.root); const tween = game.root.tween.bind(game.root); - kaSprite = loadSprite(null, kaSpriteSrc); boomSprite = loadSprite(null, boomSpriteSrc); + kaSprite = loadSprite(null, kaSpriteSrc); + let f: Asset; + + setTimeout(() => { + f = loadSprite("ghosty", "/sprites/ghosty.png"); + }, 5000); + + setTimeout(() => { + assets.packer.remove(f.data!.packerId!); + }, 6000); + + setInterval(() => { + loadSprite(null, "/sprites/ghosty.png"); + }, 1000); function fixedUpdateFrame() { // update every obj From a74206c01eb7cecc3d4cf0c59b344a075735411c Mon Sep 17 00:00:00 2001 From: lajbel Date: Sat, 19 Oct 2024 13:01:45 -0300 Subject: [PATCH 2/2] chore: remove test code --- src/kaplay.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/kaplay.ts b/src/kaplay.ts index 87d2ce9e..052263bb 100644 --- a/src/kaplay.ts +++ b/src/kaplay.ts @@ -688,19 +688,6 @@ const kaplay = < boomSprite = loadSprite(null, boomSpriteSrc); kaSprite = loadSprite(null, kaSpriteSrc); - let f: Asset; - - setTimeout(() => { - f = loadSprite("ghosty", "/sprites/ghosty.png"); - }, 5000); - - setTimeout(() => { - assets.packer.remove(f.data!.packerId!); - }, 6000); - - setInterval(() => { - loadSprite(null, "/sprites/ghosty.png"); - }, 1000); function fixedUpdateFrame() { // update every obj