Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TexPacker.remove #418

Merged
merged 2 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions examples/largeTexture.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
]);
Expand Down
23 changes: 16 additions & 7 deletions src/assets/asset.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<D> {
loaded: boolean = false;
Expand All @@ -19,13 +21,15 @@ export class Asset<D> {
private onLoadEvents: KEvent<[D]> = new KEvent();
private onErrorEvents: KEvent<[Error]> = new KEvent();
private onFinishEvents: KEvent<[]> = new KEvent();

constructor(loader: Promise<D>) {
loader.then((data) => {
this.loaded = true;
this.data = data;
this.onLoadEvents.trigger(data);
}).catch((err) => {
this.error = err;

if (this.onErrorEvents.numListeners() > 0) {
this.onErrorEvents.trigger(err);
}
Expand Down Expand Up @@ -84,17 +88,20 @@ export class Asset<D> {
export class AssetBucket<D> {
assets: Map<string, Asset<D>> = new Map();
lastUID: number = 0;

add(name: string | null, loader: Promise<D>): Asset<D> {
// 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<D> {
const id = name ?? (this.lastUID++ + "");
const asset = Asset.loaded(data);
this.assets.set(id, asset);

return asset;
}
// if not found return undefined
Expand All @@ -106,21 +113,22 @@ export class AssetBucket<D> {
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) {
Expand Down Expand Up @@ -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<HTMLImageElement> {
export function loadImage(src: string): Promise<HTMLImageElement> {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = src;

return new Promise<HTMLImageElement>((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = () =>
Expand Down
4 changes: 2 additions & 2 deletions src/assets/bitmapFont.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -35,7 +35,7 @@ export function loadBitmapFont(
): Asset<BitmapFontData> {
return assets.bitmapFonts.add(
name,
loadImg(src)
loadImage(src)
.then((img) => {
return makeFont(
Texture.fromImage(gfx.ggl, img, opt),
Expand Down
4 changes: 2 additions & 2 deletions src/assets/pedit.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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;
Expand Down
27 changes: 18 additions & 9 deletions src/assets/sprite.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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(
Expand All @@ -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<SpriteData> {
return loadImg(url).then((img) => SpriteData.fromImage(img, opt));
return loadImage(url).then((img) => SpriteData.fromImage(img, opt));
}
}

Expand All @@ -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;
}
Expand All @@ -205,13 +209,14 @@ export function loadSprite(
},
): Asset<SpriteData> {
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)),
);
Expand All @@ -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));
}
Expand Down Expand Up @@ -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 = {},
Expand All @@ -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);
Expand All @@ -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,
Expand Down
128 changes: 128 additions & 0 deletions src/gfx/classes/FrameBuffer.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading