UNPKG

@spearwolf/twopoint5d

Version:

Create 2.5D realtime graphics and pixelart with WebGL and three.js

363 lines 13.7 kB
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