@spearwolf/twopoint5d
Version:
Create 2.5D realtime graphics and pixelart with WebGL and three.js
363 lines • 13.7 kB
JavaScript
import { emit, eventize, off, once, retain } from '@spearwolf/eventize';
import { batch, createEffect, createSignal, SignalGroup, touch } from '@spearwolf/signalize';
import { ImageLoader } from 'three/webgpu';
import { FrameBasedAnimations } from './FrameBasedAnimations.js';
import { TextureCoords } from './TextureCoords.js';
import { TextureFactory } from './TextureFactory.js';
import { TexturePackerJson } from './TexturePackerJson.js';
import { TileSet } from './TileSet.js';
const getTimingOptions = (data) => {
if ('frameRate' in data && data.frameRate !== undefined) {
return { frameRate: data.frameRate };
}
if ('duration' in data && data.duration !== undefined) {
return { duration: data.duration };
}
throw new Error('Either duration or frameRate must be provided in animation data');
};
export const TextureResourceSubtypes = {
ImageCoords: 'imageCoords',
Atlas: 'atlas',
TileSet: 'tileSet',
Texture: 'texture',
FrameBasedAnimations: 'frameBasedAnimations',
};
export const TextureResourceEvents = {
ImageCoords: TextureResourceSubtypes.ImageCoords,
Atlas: TextureResourceSubtypes.Atlas,
TileSet: TextureResourceSubtypes.TileSet,
Texture: TextureResourceSubtypes.Texture,
FrameBasedAnimations: TextureResourceSubtypes.FrameBasedAnimations,
Dispose: 'dispose',
Error: 'error',
};
const cmpTexClasses = (a, b) => {
if (a === b) {
return true;
}
return `${a?.join() ?? ''}` === `${b?.join() ?? ''}`;
};
const cmpTexCoords = (a, b) => {
if (a === b) {
return true;
}
if (a && b) {
return (a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height && a.flip === b.flip && a.parent === b.parent);
}
return false;
};
const cmpTileSetOptions = (a, b) => {
if (a === b) {
return true;
}
if (a && b) {
return (a.tileWidth === b.tileWidth &&
a.tileHeight === b.tileHeight &&
a.margin === b.margin &&
a.spacing === b.spacing &&
a.padding === b.padding &&
a.tileCount === b.tileCount &&
a.firstId === b.firstId);
}
return false;
};
const OnDispose = TextureResourceEvents.Dispose;
const OnError = TextureResourceEvents.Error;
export class TextureResource {
static fromImage(id, imageUrl, textureClasses) {
const resource = new TextureResource(id, 'image');
batch(() => {
resource.imageUrl = imageUrl;
resource.textureClasses = textureClasses?.slice();
});
return resource;
}
static fromTileSet(id, imageUrl, tileSetOptions, textureClasses, frameBasedAnimations) {
const resource = new TextureResource(id, 'tileset');
batch(() => {
resource.imageUrl = imageUrl;
resource.#tileSetOptions = createSignal(tileSetOptions, { compare: cmpTileSetOptions, attach: resource });
resource.#tileSet = createSignal(undefined, { attach: resource });
resource.#atlas = createSignal(undefined, { attach: resource });
resource.textureClasses = textureClasses?.slice();
resource.frameBasedAnimationsData = frameBasedAnimations;
});
return resource;
}
static fromAtlas(id, atlasUrl, overrideImageUrl, textureClasses, frameBasedAnimations) {
const resource = new TextureResource(id, 'atlas');
batch(() => {
resource.#atlasUrl = createSignal(atlasUrl, { attach: resource });
resource.#atlasJson = createSignal(undefined, { attach: resource });
resource.#atlas = createSignal(undefined, { attach: resource });
resource.#overrideImageUrl = createSignal(overrideImageUrl, { attach: resource });
resource.textureClasses = textureClasses?.slice();
resource.frameBasedAnimationsData = frameBasedAnimations;
});
return resource;
}
#atlasUrl;
#atlasJson;
#overrideImageUrl;
#atlas;
#tileSetOptions;
#tileSet;
#frameBasedAnimations;
#frameBasedAnimationsData;
#textureClasses;
#imageUrl;
#imageCoords;
#textureFactory;
#texture;
#renderer;
get imageUrl() {
return this.#imageUrl.value;
}
set imageUrl(val) {
this.#imageUrl.set(val);
}
get imageCoords() {
return this.#imageCoords.value;
}
set imageCoords(val) {
this.#imageCoords.set(val);
}
get atlasUrl() {
return this.#atlasUrl?.value;
}
set atlasUrl(value) {
this.#atlasUrl?.set(value);
}
get atlasJson() {
return this.#atlasJson?.value;
}
set atlasJson(value) {
this.#atlasJson?.set(value);
}
get overrideImageUrl() {
return this.#overrideImageUrl?.value;
}
set overrideImageUrl(value) {
this.#overrideImageUrl?.set(value);
}
get atlas() {
return this.#atlas?.value;
}
set atlas(value) {
this.#atlas?.set(value);
}
get tileSetOptions() {
return this.#tileSetOptions?.value;
}
set tileSetOptions(value) {
this.#tileSetOptions?.set(value);
}
get tileSet() {
return this.#tileSet?.value;
}
set tileSet(value) {
this.#tileSet?.set(value);
}
get frameBasedAnimations() {
return this.#frameBasedAnimations.value;
}
set frameBasedAnimations(value) {
this.#frameBasedAnimations.set(value);
}
get frameBasedAnimationsData() {
return this.#frameBasedAnimationsData.value;
}
set frameBasedAnimationsData(value) {
this.#frameBasedAnimationsData.set(value);
}
get textureClasses() {
return this.#textureClasses.value;
}
set textureClasses(value) {
if (Array.isArray(value) && value.length === 0) {
value = undefined;
}
this.#textureClasses.set(value);
}
get textureFactory() {
return this.#textureFactory.value;
}
set textureFactory(value) {
this.#textureFactory.set(value);
}
get texture() {
return this.#texture.value;
}
set texture(value) {
this.#texture.set(value);
}
get renderer() {
return this.#renderer.value;
}
set renderer(value) {
this.#renderer.set(value);
}
#load;
#disposed;
constructor(id, type) {
this.#frameBasedAnimations = createSignal(undefined, { attach: this });
this.#frameBasedAnimationsData = createSignal(undefined, { attach: this });
this.#textureClasses = createSignal(undefined, { compare: cmpTexClasses, attach: this });
this.#imageUrl = createSignal(undefined, { attach: this });
this.#imageCoords = createSignal(undefined, { compare: cmpTexCoords, attach: this });
this.#textureFactory = createSignal(undefined, { attach: this });
this.#texture = createSignal(undefined, { attach: this });
this.#renderer = createSignal(undefined, { attach: this });
this.refCount = 0;
this.#load = false;
this.#disposed = false;
eventize(this);
this.id = id;
this.type = type;
retain(this, ['imageCoords', 'atlas', 'tileSet', 'texture', 'frameBasedAnimations']);
}
dispose() {
if (this.#disposed)
return;
this.#disposed = true;
emit(this, OnDispose);
SignalGroup.delete(this);
off(this);
}
load() {
if (!this.#load) {
this.#load = true;
this.#imageCoords.onChange((value) => {
emit(this, 'imageCoords', value);
});
this.#atlas?.onChange((value) => {
emit(this, 'atlas', value);
});
this.#tileSet?.onChange((value) => {
emit(this, 'tileSet', value);
});
this.#frameBasedAnimations.onChange((value) => {
emit(this, 'frameBasedAnimations', value);
});
this.#texture.onChange((value) => {
emit(this, 'texture', value);
});
const unsubscribeOnDispose = (effect) => {
once(this, OnDispose, () => {
effect.destroy();
});
};
unsubscribeOnDispose(createEffect(() => {
const factory = this.#textureFactory.get();
const url = this.#imageUrl.get();
const classes = this.#textureClasses.get();
if (!factory || !url)
return;
let aborted = false;
let texture;
new ImageLoader()
.loadAsync(url)
.then((image) => {
if (aborted)
return;
texture = factory.create(image, ...(classes ?? []));
texture.name = this.id;
batch(() => {
this.imageCoords = new TextureCoords(0, 0, image.width, image.height);
this.texture = texture;
});
})
.catch((error) => {
if (aborted)
return;
emit(this, OnError, { source: 'image', url, error });
});
return () => {
aborted = true;
texture?.dispose();
};
}));
if (this.tileSetOptions) {
unsubscribeOnDispose(createEffect(() => {
if (this.imageCoords && this.tileSetOptions) {
this.tileSet = new TileSet(this.imageCoords, this.tileSetOptions);
this.atlas = this.tileSet.atlas;
}
}, [this.#imageCoords, this.#tileSetOptions]));
unsubscribeOnDispose(createEffect(() => {
if (this.tileSet && this.frameBasedAnimationsData) {
this.frameBasedAnimations = new FrameBasedAnimations();
for (const [name, data] of Object.entries(this.frameBasedAnimationsData)) {
const timing = getTimingOptions(data);
if ('tileIds' in data) {
this.frameBasedAnimations.add(name, timing, this.tileSet, data.tileIds);
}
else {
const _data = data;
this.frameBasedAnimations.add(name, timing, this.tileSet, _data.firstTileId, _data.tileCount);
}
}
}
}, [this.#tileSet, this.#frameBasedAnimationsData]));
}
if (this.atlasUrl) {
unsubscribeOnDispose(createEffect(() => {
const atlasUrl = this.atlasUrl;
if (!atlasUrl)
return;
const ac = new AbortController();
let aborted = false;
(async () => {
try {
const response = await fetch(atlasUrl, { signal: ac.signal });
const atlasJson = await response.json();
if (aborted)
return;
this.atlasJson = atlasJson;
}
catch (error) {
if (aborted)
return;
emit(this, OnError, { source: 'atlas', url: atlasUrl, error });
}
})();
return () => {
aborted = true;
ac.abort();
};
}, [this.#atlasUrl]));
unsubscribeOnDispose(createEffect(() => {
if (this.atlasJson) {
this.imageUrl = this.overrideImageUrl ?? this.atlasJson.meta.image;
}
}, [this.#atlasJson, this.#overrideImageUrl]));
unsubscribeOnDispose(createEffect(() => {
if (this.atlasJson && this.imageCoords) {
const [atlas] = TexturePackerJson.parse(this.atlasJson, this.imageCoords);
this.atlas = atlas;
}
}, [this.#atlasJson, this.#imageCoords]));
unsubscribeOnDispose(createEffect(() => {
if (this.atlas && this.frameBasedAnimationsData) {
this.frameBasedAnimations = new FrameBasedAnimations();
for (const [name, data] of Object.entries(this.frameBasedAnimationsData)) {
if ('frameNameQuery' in data) {
const timing = getTimingOptions(data);
this.frameBasedAnimations.add(name, timing, this.atlas, data.frameNameQuery);
}
}
}
}, [this.#atlas, this.#frameBasedAnimationsData]));
touch(this.#atlasUrl);
}
unsubscribeOnDispose(createEffect(() => {
const renderer = this.#renderer.get();
if (renderer && !this.#textureFactory.value) {
this.textureFactory = new TextureFactory(renderer);
}
}));
}
return this;
}
}
//# sourceMappingURL=TextureResource.js.map