diff --git a/spine-ts/spine-core/src/AssetManagerBase.ts b/spine-ts/spine-core/src/AssetManagerBase.ts index aeba34ba3..20973a360 100644 --- a/spine-ts/spine-core/src/AssetManagerBase.ts +++ b/spine-ts/spine-core/src/AssetManagerBase.ts @@ -94,6 +94,7 @@ export class AssetManagerBase implements Disposable { this.assetsLoaded[path] = new Promise((resolve, reject) => { this.downloader.downloadBinary(path, (data: Uint8Array): void => { + // setTimeout(() => this.success(success, path, data), 10000); this.success(success, path, data); resolve(data); }, (status: number, responseText: string): void => { @@ -181,8 +182,7 @@ export class AssetManagerBase implements Disposable { image.onload = () => { const texture = this.textureLoader(image) this.success(success, path, texture); - setTimeout(() => resolve(texture), 1000) - // resolve(texture); + resolve(texture); }; image.onerror = () => { const errorMsg = `Couldn't load image: ${path}`; @@ -204,29 +204,72 @@ export class AssetManagerBase implements Disposable { let parent = index >= 0 ? path.substring(0, index + 1) : ""; path = this.start(path); - this.downloader.downloadText(path, (atlasText: string): void => { - try { - let atlas = new TextureAtlas(atlasText); - let toLoad = atlas.pages.length, abort = false; - for (let page of atlas.pages) { - this.loadTexture(!fileAlias ? parent + page.name : fileAlias[page.name!], - (imagePath: string, texture: Texture) => { - if (!abort) { - page.setTexture(texture); - if (--toLoad == 0) this.success(success, path, atlas); + if (this.reuseAssets(path, success, error)) return; + + this.assetsLoaded[path] = new Promise((resolve, reject) => { + this.downloader.downloadText(path, (atlasText: string): void => { + try { + let atlas = new TextureAtlas(atlasText); + let toLoad = atlas.pages.length, abort = false; + for (let page of atlas.pages) { + this.loadTexture(!fileAlias ? parent + page.name : fileAlias[page.name!], + (imagePath: string, texture: Texture) => { + if (!abort) { + page.setTexture(texture); + if (--toLoad == 0) { + this.success(success, path, atlas); + resolve(atlas); + } + } + }, + (imagePath: string, message: string) => { + if (!abort) { + const errorMsg = `Couldn't load texture atlas ${path} page image: ${imagePath}`; + this.error(error, path, errorMsg); + reject(errorMsg); + } + abort = true; } - }, - (imagePath: string, message: string) => { - if (!abort) this.error(error, path, `Couldn't load texture atlas ${path} page image: ${imagePath}`); - abort = true; - } - ); + ); + } + } catch (e) { + const errorMsg = `Couldn't parse texture atlas ${path}: ${(e as any).message}`; + this.error(error, path, errorMsg); + reject(errorMsg); } - } catch (e) { - this.error(error, path, `Couldn't parse texture atlas ${path}: ${(e as any).message}`); - } - }, (status: number, responseText: string): void => { - this.error(error, path, `Couldn't load texture atlas ${path}: status ${status}, ${responseText}`); + }, (status: number, responseText: string): void => { + const errorMsg = `Couldn't load texture atlas ${path}: status ${status}, ${responseText}`; + this.error(error, path, errorMsg); + reject(errorMsg); + }); + }); + } + + loadTextureAtlasButNoTextures (path: string, + success: (path: string, atlas: TextureAtlas) => void = () => { }, + error: (path: string, message: string) => void = () => { }, + fileAlias?: { [keyword: string]: string } + ) { + path = this.start(path); + + if (this.reuseAssets(path, success, error)) return; + + this.assetsLoaded[path] = new Promise((resolve, reject) => { + this.downloader.downloadText(path, (atlasText: string): void => { + try { + const atlas = new TextureAtlas(atlasText); + this.success(success, path, atlas); + resolve(atlas); + } catch (e) { + const errorMsg = `Couldn't parse texture atlas ${path}: ${(e as any).message}`; + this.error(error, path, errorMsg); + reject(errorMsg); + } + }, (status: number, responseText: string): void => { + const errorMsg = `Couldn't load texture atlas ${path}: status ${status}, ${responseText}`; + this.error(error, path, errorMsg); + reject(errorMsg); + }); }); } diff --git a/spine-ts/spine-webgl/example/canvas6.html b/spine-ts/spine-webgl/example/canvas6.html index c872afd8d..9ff0348eb 100644 --- a/spine-ts/spine-webgl/example/canvas6.html +++ b/spine-ts/spine-webgl/example/canvas6.html @@ -7,13 +7,13 @@ JS Library Showcase - - - - - - - - - + + + +
+ +
+
+ +
+
+ A loading spinner is shown during assets loading. Click the button below to simulate a 2 seconds loading: +
+
+ +
+
+ If you do not want to show the loading spinner, set spinner="false". +
+ Click the button below to toggle the spinner. +
+
+ +
+
+ + + +
+

+                
+            
+
+
+ + + + + +
+ +
+ +
+ It's super easy to show your different skins and animations. Just make a table and use the skin and animation attributes. +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+

+                
+            
+
+
+ + + + + + +
+ +
+ +
+ If you have many atlas pages, for example one for each skin, and you want to show only some of the skins, + pass to the pages the atlas pages you want to load as a comma concatenated list of indices. +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+

+                
+            
+
+
+ + + + +
+ +
+ +
+ Let's do the same thing above, but programmatically! + Create two arrays, one for the skin and the other for the animations, and loop over them. +
+
+ spine.createSpineWidget allows you to create a spine widget. +
+
+ By default, assets are loaded immeaditely. You can postpone that by setting manual-start="false". + Then it's your responsibility to call start() on the widget. + As usual, just wait on the loadingPromise to act on the skeleton or state. +
+ + + +
+ +
+

+                
+            
+
+
+ + + + + + +
+ +
+ +
+ When the widget (or the parent element) enters in the viewport, the callback onScreenFunction is invoked. +
+
+ By default, the callback call the widget start the first time the widget enters the viewport. + That useful in combination with manual-start="true to load assets only when they are into the viewport. +
+ The assets of the coin below are loaded only when the widget enters the viewport. +
+
+ You can overwrite that behaviour. For example, the raptor below changes animation everytime the widget enters the viewport. + +
+ +
+
+ +
+
+ + + +
+
+ + +
+ +
+

+                
+            
+
+
+ + + + + +
+ +
+
+ +
+
+ If you want to load textures programmatically, you can just pass as pages to load an empty value liek this pages="". +
+
+ In this way the skeleton and the atlas are loaded, but not the textures. +
+ Then you can loads the textures whenever you want. +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + +
+

+                
+            
+
+
+ + + + + + - + + \ No newline at end of file diff --git a/spine-ts/spine-webgl/src/SceneRenderer.ts b/spine-ts/spine-webgl/src/SceneRenderer.ts index c4e4c9bc1..b5b61e8c5 100644 --- a/spine-ts/spine-webgl/src/SceneRenderer.ts +++ b/spine-ts/spine-webgl/src/SceneRenderer.ts @@ -463,6 +463,14 @@ export class SceneRenderer implements Disposable { this.activeRenderer = null; } + resize2 () { + console.log("RESIZE COMMAND") + let canvas = this.canvas; + this.context.gl.viewport(0, 0, canvas.width, canvas.height); + this.camera.setViewport(canvas.width, canvas.height); + this.camera.update(); + } + resize (resizeMode: ResizeMode) { let canvas = this.canvas; var dpr = window.devicePixelRatio || 1; diff --git a/spine-ts/spine-webgl/src/SpineWebComponent.ts b/spine-ts/spine-webgl/src/SpineWebComponent.ts index 3f3759626..ba6b242bf 100644 --- a/spine-ts/spine-webgl/src/SpineWebComponent.ts +++ b/spine-ts/spine-webgl/src/SpineWebComponent.ts @@ -27,9 +27,7 @@ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { SpineCanvas, SpineCanvasApp, AtlasAttachmentLoader, SkeletonBinary, SkeletonJson, Skeleton, Animation, AnimationState, AnimationStateData, Physics, Vector2, Vector3, ResizeMode, Color, MixBlend, MixDirection, SceneRenderer, SkeletonData, Input } from "./index.js"; - -const loadingSpinner = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20104%2031.16%22%3E%3Cpath%20d%3D%22M104%2012.68a1.31%201.31%200%200%201-.37%201%201.28%201.28%200%200%201-.85.31H91.57a10.51%2010.51%200%200%200%20.29%202.55%204.92%204.92%200%200%200%201%202%204.27%204.27%200%200%200%201.64%201.26%206.89%206.89%200%200%200%202.6.44%2010.66%2010.66%200%200%200%202.17-.2%2012.81%2012.81%200%200%200%201.64-.44q.69-.25%201.14-.44a1.87%201.87%200%200%201%20.68-.2.44.44%200%200%201%20.27.04.43.43%200%200%201%20.16.2%201.38%201.38%200%200%201%20.09.37%204.89%204.89%200%200%201%200%20.58%204.14%204.14%200%200%201%200%20.43v.32a.83.83%200%200%201-.09.26%201.1%201.1%200%200%201-.17.22%202.77%202.77%200%200%201-.61.34%208.94%208.94%200%200%201-1.32.46%2018.54%2018.54%200%200%201-1.88.41%2013.78%2013.78%200%200%201-2.28.18%2010.55%2010.55%200%200%201-3.68-.59%206.82%206.82%200%200%201-2.66-1.74%207.44%207.44%200%200%201-1.63-2.89%2013.48%2013.48%200%200%201-.55-4%2012.76%2012.76%200%200%201%20.57-3.94%208.35%208.35%200%200%201%201.64-3%207.15%207.15%200%200%201%202.58-1.87%208.47%208.47%200%200%201%203.39-.65%208.19%208.19%200%200%201%203.41.64%206.46%206.46%200%200%201%202.32%201.73%207%207%200%200%201%201.3%202.54%2011.17%2011.17%200%200%201%20.43%203.13zm-3.14-.93a5.69%205.69%200%200%200-1.09-3.86%204.17%204.17%200%200%200-3.42-1.4%204.52%204.52%200%200%200-2%20.44%204.41%204.41%200%200%200-1.47%201.15A5.29%205.29%200%200%200%2092%209.75a7%207%200%200%200-.36%202zM80.68%2021.94a.42.42%200%200%201-.08.26.59.59%200%200%201-.25.18%201.74%201.74%200%200%201-.47.11%206.31%206.31%200%200%201-.76%200%206.5%206.5%200%200%201-.78%200%201.74%201.74%200%200%201-.47-.11.59.59%200%200%201-.25-.18.42.42%200%200%201-.08-.26V12a9.8%209.8%200%200%200-.23-2.35%204.86%204.86%200%200%200-.66-1.53%202.88%202.88%200%200%200-1.13-1%203.57%203.57%200%200%200-1.6-.34%204%204%200%200%200-2.35.83A12.71%2012.71%200%200%200%2069.11%2010v11.9a.42.42%200%200%201-.08.26.59.59%200%200%201-.25.18%201.74%201.74%200%200%201-.47.11%206.51%206.51%200%200%201-.78%200%206.31%206.31%200%200%201-.76%200%201.88%201.88%200%200%201-.48-.11.52.52%200%200%201-.25-.18.46.46%200%200%201-.07-.26v-17a.53.53%200%200%201%20.03-.21.5.5%200%200%201%20.23-.19%201.28%201.28%200%200%201%20.44-.11%208.53%208.53%200%200%201%201.39%200%201.12%201.12%200%200%201%20.43.11.6.6%200%200%201%20.22.19.47.47%200%200%201%20.07.26V7.2a10.46%2010.46%200%200%201%202.87-2.36%206.17%206.17%200%200%201%202.88-.75%206.41%206.41%200%200%201%202.87.58%205.16%205.16%200%200%201%201.88%201.54%206.15%206.15%200%200%201%201%202.26%2013.46%2013.46%200%200%201%20.31%203.11z%22%20fill%3D%22%23fff%22%2F%3E%3Cpath%20d%3D%22M43.35%202.86c.09%202.6%201.89%204%205.48%204.61%203%20.48%205.79.24%206.69-2.37%201.75-5.09-2.4-3.82-6-4.39s-6.31-2.03-6.17%202.15zm1.08%2010.69c.33%201.94%202.14%203.06%204.91%203s4.84-1.16%205.13-3.25c.53-3.88-2.53-2.38-5.3-2.3s-5.4-1.26-4.74%202.55zM48%2022.44c.55%201.45%202.06%202.06%204.1%201.63s3.45-1.11%203.33-2.76c-.21-3.06-2.22-2.1-4.26-1.66S47%2019.6%2048%2022.44zm1.78%206.78c.16%201.22%201.22%202%202.88%201.93s2.92-.67%203.13-2c.4-2.43-1.46-1.53-3.12-1.51s-3.17-.82-2.89%201.58z%22%20fill%3D%22%23ff4000%22%2F%3E%3Cpath%20d%3D%22M35.28%2013.16a15.33%2015.33%200%200%201-.48%204%208.75%208.75%200%200%201-1.42%203%206.35%206.35%200%200%201-2.32%201.91%207.14%207.14%200%200%201-3.16.67%206.1%206.1%200%200%201-1.4-.15%205.34%205.34%200%200%201-1.26-.47%207.29%207.29%200%200%201-1.24-.81q-.61-.49-1.29-1.15v8.51a.47.47%200%200%201-.08.26.56.56%200%200%201-.25.19%201.74%201.74%200%200%201-.47.11%206.47%206.47%200%200%201-.78%200%206.26%206.26%200%200%201-.76%200%201.89%201.89%200%200%201-.48-.11.49.49%200%200%201-.25-.19.51.51%200%200%201-.07-.26V4.91a.57.57%200%200%201%20.06-.27.46.46%200%200%201%20.23-.18%201.47%201.47%200%200%201%20.44-.1%207.41%207.41%200%200%201%201.3%200%201.45%201.45%200%200%201%20.43.1.52.52%200%200%201%20.24.18.51.51%200%200%201%20.07.27V7.2a18.06%2018.06%200%200%201%201.49-1.38%209%209%200%200%201%201.45-1%206.82%206.82%200%200%201%201.49-.59%207.09%207.09%200%200%201%204.78.52%206%206%200%200%201%202.13%202%208.79%208.79%200%200%201%201.2%202.9%2015.72%2015.72%200%200%201%20.4%203.51zm-3.28.36a15.64%2015.64%200%200%200-.2-2.53%207.32%207.32%200%200%200-.69-2.17%204.06%204.06%200%200%200-1.3-1.51%203.49%203.49%200%200%200-2-.57%204.1%204.1%200%200%200-1.2.18%204.92%204.92%200%200%200-1.2.57%208.54%208.54%200%200%200-1.28%201A15.77%2015.77%200%200%200%2022.76%2010v6.77a13.53%2013.53%200%200%200%202.46%202.4%204.12%204.12%200%200%200%202.44.83%203.56%203.56%200%200%200%202-.57A4.28%204.28%200%200%200%2031%2018a7.58%207.58%200%200%200%20.77-2.12%2011.43%2011.43%200%200%200%20.23-2.36zM12%2017.3a5.39%205.39%200%200%201-.48%202.33%204.73%204.73%200%200%201-1.37%201.72%206.19%206.19%200%200%201-2.12%201.06%209.62%209.62%200%200%201-2.71.36%2010.38%2010.38%200%200%201-3.21-.5A7.63%207.63%200%200%201%201%2021.82a3.25%203.25%200%200%201-.66-.43%201.09%201.09%200%200%201-.3-.53%203.59%203.59%200%200%201-.04-.93%204.06%204.06%200%200%201%200-.61%202%202%200%200%201%20.09-.4.42.42%200%200%201%20.16-.22.43.43%200%200%201%20.24-.07%201.35%201.35%200%200%201%20.61.26q.41.26%201%20.56a9.22%209.22%200%200%200%201.41.55%206.25%206.25%200%200%200%201.87.26%205.62%205.62%200%200%200%201.44-.17%203.48%203.48%200%200%200%201.12-.5%202.23%202.23%200%200%200%20.73-.84%202.68%202.68%200%200%200%20.26-1.21%202%202%200%200%200-.37-1.21%203.55%203.55%200%200%200-1-.87%208.09%208.09%200%200%200-1.36-.66l-1.56-.61a16%2016%200%200%201-1.57-.73%206%206%200%200%201-1.37-1%204.52%204.52%200%200%201-1-1.4%204.69%204.69%200%200%201-.37-2%204.88%204.88%200%200%201%20.39-1.87%204.46%204.46%200%200%201%201.16-1.61%205.83%205.83%200%200%201%201.94-1.11A8.06%208.06%200%200%201%206.53%204a8.28%208.28%200%200%201%201.36.11%209.36%209.36%200%200%201%201.23.28%205.92%205.92%200%200%201%20.94.37%204.09%204.09%200%200%201%20.59.35%201%201%200%200%201%20.26.26.83.83%200%200%201%20.09.26%201.32%201.32%200%200%200%20.06.35%203.87%203.87%200%200%201%200%20.51%204.76%204.76%200%200%201%200%20.56%201.39%201.39%200%200%201-.09.39.5.5%200%200%201-.16.22.35.35%200%200%201-.21.07%201%201%200%200%201-.49-.21%207%207%200%200%200-.83-.44%209.26%209.26%200%200%200-1.2-.44%205.49%205.49%200%200%200-1.58-.16%204.93%204.93%200%200%200-1.4.18%202.69%202.69%200%200%200-1%20.51%202.16%202.16%200%200%200-.59.83%202.43%202.43%200%200%200-.2%201%202%202%200%200%200%20.38%201.24%203.6%203.6%200%200%200%201%20.88%208.25%208.25%200%200%200%201.38.68l1.58.62q.8.32%201.59.72a6%206%200%200%201%201.39%201%204.37%204.37%200%200%201%201%201.36%204.46%204.46%200%200%201%20.37%201.8z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E"; +import { SpineCanvas, SpineCanvasApp, AtlasAttachmentLoader, SkeletonBinary, SkeletonJson, Skeleton, Animation, AnimationState, AnimationStateData, Physics, Vector2, Vector3, ResizeMode, Color, MixBlend, MixDirection, SceneRenderer, SkeletonData, Input, LoadingScreenWidget, TextureAtlas, Texture } from "./index.js"; interface Rectangle { x: number, @@ -114,6 +112,9 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions public draggable = false; public debug = false; public identifier = ""; + public loadingSpinner = true; + public manualStart = false; + public pages?: Array; // state public skeleton?: Skeleton; @@ -121,6 +122,27 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions public bounds?: Rectangle; public loadingPromise?: Promise; public loading = true; + public started = false; + public onScreenAtLeastOnce = false; + + // TODO tomorrow: la onScreenFunction di default carica le textures quando il widget viene rivelato sullo schermo. + // capire se va bene come comportamento di default + // poi spiegare i tre case + // no manual loading, no pages: tutte le pagine vengono caricate subito + // no manual loading, si pages: solo le pagine specificate vengono caricate subito (le altre se ne deve occupare manualmente il tizio) + // manual loading, no pages: tutte le pagine vengono caricate solo quando il widget è nella viewport + // manual loading, si pages: le pagine specificate vengono caricate solo quando il widget è nella viewport + // magari capire se mettere un altro parametro, tipo: loadsOnViewport + public onScreenFunction: (widget: SpineWebComponentWidget) => void = async (widget) => { + if (widget.loading && !widget.onScreenAtLeastOnce) { + widget.onScreenAtLeastOnce = true; + + console.log(widget.manualStart) + if (widget.manualStart) { + widget.start(); + } + } + } // TODO: makes the interface exposes getter, make getter and make these private // internal state @@ -130,14 +152,39 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions public dragX = 0; public dragY = 0; public dragging = false; + public intersectionObserver? : IntersectionObserver; + public onScreen = false; + public textureAtlas?: TextureAtlas; private root: ShadowRoot; private overlay: SpineWebComponentOverlay; private divLoader: HTMLDivElement; + public loadingScreen: LoadingScreenWidget | null = null; + static get observedAttributes(): string[] { - return ["atlas", "skeleton", "scale", "animation", "skin", "fit", "width", "height", "draggable", "mode", "x-axis", "y-axis", "identifier", "offset-x", "offset-y", "debug"]; + return [ + "atlas", + "skeleton", + "scale", + "animation", + "skin", + "fit", + "width", + "height", + "draggable", + "mode", + "x-axis", + "y-axis", + "identifier", + "offset-x", + "offset-y", + "debug", + "manual-start", + "spinner", + "pages" + ]; } constructor() { @@ -164,15 +211,12 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions throw new Error("Missing skeleton attribute"); } - this.overlay.skeletonList.push(this); - this.loadingPromise = this.loadSkeleton(); - this.loadingPromise.then(() => { - this.loading = false; - this.hideLoader(); - }); // async + this.overlay.addWidget(this); + if (!this.manualStart) { + this.start(); + } this.render(); - this.root.appendChild(this.divLoader); } disconnectedCallback(): void { @@ -185,7 +229,7 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions } attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void { - if (newValue) { + if (newValue !== null) { if (name === "identifier") { this.identifier = newValue; } @@ -260,7 +304,24 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions if (name === "debug") { this.debug = Boolean(newValue); } + + if (name === "spinner") { + this.loadingSpinner = Boolean(newValue); + } + + if (name === "manual-start") { + this.manualStart = Boolean(newValue); + } + + if (name === "pages") { + this.pages = newValue.split(",").reduce((acc, pageIndex) => { + const index = parseInt(pageIndex); + if (!isNaN(index)) acc.push(index); + return acc; + }, [] as Array); + } } + } // calculate bounds of the current animation on track 0, then set it @@ -294,29 +355,48 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions return overlay; } - private showLoader() { - this.divLoader.classList.remove("hide-loader"); + public start() { + // if you want to start again the widget, first reset it + if (this.started) return; + this.started = true; + + this.loadingPromise = this.loadSkeleton(); + this.loadingPromise.then(() => { + this.loading = false; + }); } - private hideLoader() { - this.divLoader.classList.add("hide-loader"); + public async loadTexturesInPagesAttribute(atlas: TextureAtlas) { + const pagesIndexToLoad = this.pages ?? atlas.pages.map((_, i) => i); // if no pages provided, loads all + + const atlasPath = this.atlasPath.includes('/') ? this.atlasPath.substring(0, this.atlasPath.lastIndexOf('/') + 1) : ''; + const promisePageList: Array> = []; + pagesIndexToLoad.forEach((index) => { + const page = atlas.pages[index]; + const promiseTextureLoad = this.loadTexture(`${atlasPath}${page.name}`).then(texture => page.setTexture(texture)); + promisePageList.push(promiseTextureLoad); + }); + + return Promise.all(promisePageList) } // add a skeleton to the overlay and set the bounds to the given animation or to the setup pose private async loadSkeleton() { - this.showLoader(); + this.loading = true; // if (this.identifier !== "TODELETE") return Promise.reject(); - const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, update, skin } = this; + const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, skin } = this; const isBinary = skeletonPath.endsWith(".skel"); - // TODO: when multiple component are loaded, they do no reuse the asset manager cache. - // TODO: we need to reuse the same texture atlas to allow batching when skeletons use the same texture + // skeleton and atlas txt are loaded immeaditely + // textures are loaeded depending on the 'pages' param: + // - [0,2]: only pages at index 0 and 2 are loaded + // - []: no page is loaded + // - undefined: all pages are loaded (default) await Promise.all([ isBinary ? this.loadBinary(skeletonPath) : this.loadJson(skeletonPath), - this.loadTextureAtlas(atlasPath), + this.loadTextureAtlasButNoTextures(atlasPath).then(atlas => this.loadTexturesInPagesAttribute(atlas)), ]); - const atlas = this.overlay.spineCanvas.assetManager.require(atlasPath); const atlasLoader = new AtlasAttachmentLoader(atlas); @@ -343,12 +423,14 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions // ideally we would know the dpi and the zoom, however they are combined // to simplify we just assume that the user wants to load the skeleton at scale 1 // at the current browser zoom level + // this might be problematic for free-scale modes (origin and inside+none) this.currentScaleDpi = window.devicePixelRatio; // skeleton.scaleX = this.currentScaleDpi; // skeleton.scaleY = this.currentScaleDpi; this.skeleton = skeleton; this.state = state; + this.textureAtlas = atlas; const bounds = this.calculateAnimationViewport(animationData); this.bounds = bounds; @@ -362,103 +444,33 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions } private render(): void { - const width = this.width === -1 ? "100%" : `${this.width}px` - const height = this.height === -1 ? "100%" : `${this.height}px` + let width; + let height; + if (this.width === -1 || this.height === -1) { + width = "0"; + height = "0"; + } else { + width = `${this.width}px` + height = `${this.height}px` + } this.root.innerHTML = ` `; - - //
- //
- //
} /* * Load assets utilities */ - private async loadBinary(path: string) { + public async loadBinary(path: string) { return new Promise((resolve, reject) => { this.overlay.spineCanvas.assetManager.loadBinary(path, (_, binary) => resolve(binary), @@ -467,7 +479,7 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions }); } - private async loadJson(path: string) { + public async loadJson(path: string) { return new Promise((resolve, reject) => { this.overlay.spineCanvas.assetManager.loadJson(path, (_, object) => resolve(object), @@ -476,7 +488,16 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions }); } - private async loadTextureAtlas(path: string) { + public async loadTexture(path: string) { + return new Promise((resolve, reject) => { + this.overlay.spineCanvas.assetManager.loadTexture(path, + (_, texture) => resolve(texture), + (_, message) => reject(message), + ); + }); + } + + public async loadTextureAtlas(path: string) { return new Promise((resolve, reject) => { this.overlay.spineCanvas.assetManager.loadTextureAtlas(path, (_, atlas) => resolve(atlas), @@ -485,6 +506,15 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions }); } + public async loadTextureAtlasButNoTextures(path: string) { + return new Promise((resolve, reject) => { + this.overlay.spineCanvas.assetManager.loadTextureAtlasButNoTextures(path, + (_, atlas) => resolve(atlas), + (_, message) => reject(message), + ); + }); + } + /* * Other utilities */ @@ -543,17 +573,21 @@ class SpineWebComponentOverlay extends HTMLElement { public skeletonList = new Array(); + private intersectionObserver? : IntersectionObserver; private resizeObserver:ResizeObserver; private input: Input; - // how many pixels to add to the - private overflowTop = .1; - private overflowBottom = .2; - private overflowLeft = .1; - private overflowRight = .1; + // how many pixels to add to the edges to prevent "edge cuttin" on fast scrolling + // be aware that the canvas is already big as the display size + private overflowTop = .0; + private overflowBottom = .0; + private overflowLeft = .0; + private overflowRight = .0; private overflowLeftSize: number private overflowTopSize: number; + private currentCanvasBaseWidth = 0; + private currentCanvasBaseHeight = 0; constructor() { super(); @@ -584,19 +618,24 @@ class SpineWebComponentOverlay extends HTMLElement { this.fps.style.left = "0"; this.root.appendChild(this.fps); + this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() }); + + this.updateCanvasSize(); + this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth; + this.overflowTopSize = this.overflowTop * document.documentElement.clientHeight; // resize and zoom // TODO: should I use the resize event? this.resizeObserver = new ResizeObserver(() => { this.updateCanvasSize(); this.zoomHandler(); - this.spineCanvas.renderer.resize(ResizeMode.Expand); }); this.resizeObserver.observe(document.body); - this.updateCanvasSize(); - this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth; - this.overflowTopSize = this.overflowTop * document.documentElement.clientHeight; + const screen = window.screen; + screen.orientation.onchange = () => { + this.updateCanvasSize(); + } this.zoomHandler(); @@ -604,11 +643,15 @@ class SpineWebComponentOverlay extends HTMLElement { window.addEventListener('scroll', this.scrollHandler); this.scrollHandler(); - this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() }); this.input = new Input(document.body, false); this.setupDragUtility(); } + addWidget(widget: SpineWebComponentWidget) { + this.skeletonList.push(widget); + this.intersectionObserver!.observe(widget.getHTMLElementReference()); + } + private setupSpineCanvasApp(): SpineCanvasApp { const red = new Color(1, 0, 0, 1); const green = new Color(0, 1, 0, 1); @@ -619,6 +662,7 @@ class SpineWebComponentOverlay extends HTMLElement { if (!skeleton || !state) return; if (update) update(canvas, delta, skeleton, state) else { + // delta = 0 state.update(delta); state.apply(skeleton); skeleton.update(delta); @@ -629,76 +673,57 @@ class SpineWebComponentOverlay extends HTMLElement { }, render: (canvas: SpineCanvas) => { - // canvas.clear(0, 0 , 0, 0); + canvas.clear(0, 0, 0, 0); let renderer = canvas.renderer; renderer.begin(); const devicePixelRatio = window.devicePixelRatio; const tempVector = new Vector3(); this.skeletonList.forEach((widget) => { - const { skeleton, bounds, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY, fit } = widget; + const { skeleton, bounds, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY, fit, loadingSpinner, onScreen, loading } = widget; - if (!skeleton) { - console.log("aaa") - // widget.style.backgroundImage = `url("${loadingSpinner}")`; - // widget.style.backgroundColor = `pink`; - // widget.classList.add("loading"); - // widget.classList.add("spine-player-button-icon-spine-logo"); - // widget.classList.add("lds-hourglass"); - // widget.shadowRoot?.host.classList.add("spine-player-button-icon-spine-logo"); - // widget.style.backgroundImage = loadingSpinner; - return; - } + if ((!onScreen && dragX === 0 && dragY === 0)) return; const divBounds = widget.getHTMLElementReference().getBoundingClientRect(); divBounds.x += this.overflowLeftSize; divBounds.y += this.overflowTopSize; - // get the desired point into the the div (center by default) in world coordinate const divX = divBounds.x + divBounds.width * (xAxis + .5); const divY = divBounds.y + divBounds.height * (-yAxis + .5); this.screenToWorld(tempVector, divX, divY); - let x = tempVector.x; - let y = tempVector.y; - if (mode === 'inside') { - let { x: ax, y: ay, width: aw, height: ah } = bounds!; + if (loading) { + if (loadingSpinner) { + if (!widget.loadingScreen) widget.loadingScreen = new LoadingScreenWidget(renderer); + widget.loadingScreen!.draw(true, tempVector.x, tempVector.y, divBounds.width * devicePixelRatio, divBounds.height * devicePixelRatio); + } + return; + } - // scale ratio - const scaleWidth = divBounds.width * devicePixelRatio / aw; - const scaleHeight = divBounds.height * devicePixelRatio / ah; + if (skeleton) { + let x = tempVector.x; + let y = tempVector.y; + if (mode === 'inside') { + let { x: ax, y: ay, width: aw, height: ah } = bounds!; - let ratioW = skeleton.scaleX; - let ratioH = skeleton.scaleY; + // scale ratio + const scaleWidth = divBounds.width * devicePixelRatio / aw; + const scaleHeight = divBounds.height * devicePixelRatio / ah; - if (fit === "fill") { // Fill the target box by distorting the source's aspect ratio. - ratioW = scaleWidth; - ratioH = scaleHeight; - } else if (fit === "fitWidth") { - ratioW = scaleWidth; - ratioH = scaleWidth; - } else if (fit === "fitHeight") { - ratioW = scaleHeight; - ratioH = scaleHeight; - } else if (fit === "contain") { - // if scaled height is bigger than div height, use height ratio instead - if (ah * scaleWidth > divBounds.height * devicePixelRatio){ - ratioW = scaleHeight; + let ratioW = skeleton.scaleX; + let ratioH = skeleton.scaleY; + + if (fit === "fill") { // Fill the target box by distorting the source's aspect ratio. + ratioW = scaleWidth; ratioH = scaleHeight; - } else { + } else if (fit === "fitWidth") { ratioW = scaleWidth; ratioH = scaleWidth; - } - } else if (fit === "cover") { - if (ah * scaleWidth < divBounds.height * devicePixelRatio){ + } else if (fit === "fitHeight") { ratioW = scaleHeight; ratioH = scaleHeight; - } else { - ratioW = scaleWidth; - ratioH = scaleWidth; - } - } else if (fit === "scaleDown") { - if (aw > divBounds.width * devicePixelRatio || ah > divBounds.height * devicePixelRatio) { + } else if (fit === "contain") { + // if scaled height is bigger than div height, use height ratio instead if (ah * scaleWidth > divBounds.height * devicePixelRatio){ ratioW = scaleHeight; ratioH = scaleHeight; @@ -706,63 +731,83 @@ class SpineWebComponentOverlay extends HTMLElement { ratioW = scaleWidth; ratioH = scaleWidth; } + } else if (fit === "cover") { + if (ah * scaleWidth < divBounds.height * devicePixelRatio){ + ratioW = scaleHeight; + ratioH = scaleHeight; + } else { + ratioW = scaleWidth; + ratioH = scaleWidth; + } + } else if (fit === "scaleDown") { + if (aw > divBounds.width * devicePixelRatio || ah > divBounds.height * devicePixelRatio) { + if (ah * scaleWidth > divBounds.height * devicePixelRatio){ + ratioW = scaleHeight; + ratioH = scaleHeight; + } else { + ratioW = scaleWidth; + ratioH = scaleWidth; + } + } + } + + // get the center of the bounds + const boundsX = (ax + aw / 2) * ratioW; + const boundsY = (ay + ah / 2) * ratioH; + + // get vertices offset: calculate the distance between div center and bounds center + x = tempVector.x - boundsX; + y = tempVector.y - boundsY; + + if (fit !== "none") { + // scale the skeleton + skeleton.scaleX = ratioW; + skeleton.scaleY = ratioH; + skeleton.updateWorldTransform(Physics.update); } } - // get the center of the bounds - const boundsX = (ax + aw / 2) * ratioW; - const boundsY = (ay + ah / 2) * ratioH; + widget.worldOffsetX = x + offsetX + dragX; + widget.worldOffsetY = y + offsetY + dragY; - // get vertices offset: calculate the distance between div center and bounds center - x = tempVector.x - boundsX; - y = tempVector.y - boundsY; + renderer.drawSkeleton(skeleton, true, -1, -1, (vertices, size, vertexSize) => { + // console.log(vertices[0]) + for (let i = 0; i < size; i+=vertexSize) { + vertices[i] = vertices[i] + widget.worldOffsetX; + vertices[i+1] = vertices[i+1] + widget.worldOffsetY; + } + }); - if (fit !== "none") { - // scale the skeleton - skeleton.scaleX = ratioW; - skeleton.scaleY = ratioH; + // drawing debug stuff + if (debug) { + // if (true) { + let { x: ax, y: ay, width: aw, height: ah } = bounds!; + + // show bounds and its center + renderer.rect(false, + ax * skeleton.scaleX + widget.worldOffsetX, + ay * skeleton.scaleY + widget.worldOffsetY, + aw * skeleton.scaleX, + ah * skeleton.scaleY, + blue); + const bbCenterX = (ax + aw / 2) * skeleton.scaleX + widget.worldOffsetX; + const bbCenterY = (ay + ah / 2) * skeleton.scaleY + widget.worldOffsetY; + renderer.circle(true, bbCenterX, bbCenterY, 10, blue); + + // show skeleton root + const root = skeleton.getRootBone()!; + renderer.circle(true, root.x + widget.worldOffsetX, root.y + widget.worldOffsetY, 10, red); + + // show shifted origin + const originX = widget.worldOffsetX - dragX - offsetX; + const originY = widget.worldOffsetY - dragY - offsetY; + renderer.circle(true, originX, originY, 10, green); + + // show line from origin to bounds center + renderer.line(originX, originY, bbCenterX, bbCenterY, green); } } - widget.worldOffsetX = x + offsetX + dragX; - widget.worldOffsetY = y + offsetY + dragY; - - renderer.drawSkeleton(skeleton, true, -1, -1, (vertices, size, vertexSize) => { - for (let i = 0; i < size; i+=vertexSize) { - vertices[i] = vertices[i] + widget.worldOffsetX; - vertices[i+1] = vertices[i+1] + widget.worldOffsetY; - } - }); - - // drawing debug stuff - if (debug) { - // if (true) { - let { x: ax, y: ay, width: aw, height: ah } = bounds!; - - // show bounds and its center - renderer.rect(false, - ax * skeleton.scaleX + widget.worldOffsetX, - ay * skeleton.scaleY + widget.worldOffsetY, - aw * skeleton.scaleX, - ah * skeleton.scaleY, - blue); - const bbCenterX = (ax + aw / 2) * skeleton.scaleX + widget.worldOffsetX; - const bbCenterY = (ay + ah / 2) * skeleton.scaleY + widget.worldOffsetY; - renderer.circle(true, bbCenterX, bbCenterY, 10, blue); - - // show skeleton root - const root = skeleton.getRootBone()!; - renderer.circle(true, root.x + widget.worldOffsetX, root.y + widget.worldOffsetY, 10, red); - - // show shifted origin - const originX = widget.worldOffsetX - dragX - offsetX; - const originY = widget.worldOffsetY - dragY - offsetY; - renderer.circle(true, originX, originY, 10, green); - - // show line from origin to bounds center - renderer.line(originX, originY, bbCenterX, bbCenterY, green); - } - }); renderer.end(); @@ -772,6 +817,19 @@ class SpineWebComponentOverlay extends HTMLElement { } connectedCallback(): void { + // TODO: move the intersectio observer to the canvas - so that we can instantiate a single one rather than one per widget + + this.intersectionObserver = new IntersectionObserver((widgets) => { + widgets.forEach(({ isIntersecting, target }) => { + + const widget = this.skeletonList.find(w => w.getHTMLElementReference() == target); + if (!widget) return; + widget.onScreen = isIntersecting; + if (isIntersecting) { + widget.onScreenFunction(widget); + } + }) + }, { rootMargin: "30px 20px 30px 20px" }); } disconnectedCallback(): void { @@ -789,7 +847,7 @@ class SpineWebComponentOverlay extends HTMLElement { tempVectorInput.set(originalEvent.pageX - window.scrollX + this.overflowLeftSize, originalEvent.pageY - window.scrollY + this.overflowTopSize, 0); this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight); this.skeletonList.forEach(widget => { - if (!widget.draggable) return; + if (!widget.draggable || (!widget.onScreen && widget.dragX === 0 && widget.dragY === 0)) return; const { worldOffsetX, worldOffsetY } = widget; const bounds = widget.bounds!; @@ -816,14 +874,13 @@ class SpineWebComponentOverlay extends HTMLElement { let dragX = tempVectorInput.x - prevX; let dragY = tempVectorInput.y - prevY; this.skeletonList.forEach(widget => { - if (widget.dragging) { - const skeleton = widget.skeleton!; - skeleton.physicsTranslate(dragX, dragY); - widget.dragX += dragX; - widget.dragY += dragY; - ev?.preventDefault(); - ev?.stopPropagation() - } + if (!widget.dragging || (!widget.onScreen && widget.dragX === 0 && widget.dragY === 0)) return; + const skeleton = widget.skeleton!; + skeleton.physicsTranslate(dragX, dragY); + widget.dragX += dragX; + widget.dragY += dragY; + ev?.preventDefault(); + ev?.stopPropagation() }); prevX = tempVectorInput.x; prevY = tempVectorInput.y; @@ -841,16 +898,12 @@ class SpineWebComponentOverlay extends HTMLElement { */ private updateCanvasSize() { - // resize canvas + // resize canvas, if necessary this.resizeCanvas(); - // recalculate overflow left and size since canvas size changed - // we could keep the initial values, avoid this and the translation below - even though we don't have a great gain - this.translateCanvas(); - // temporarely remove the div to get the page size without considering the div // this is necessary otherwise if the bigger element in the page is remove and the div - // was the second bigger element, now it would be the div to dtermine the page size + // was the second bigger element, now it would be the div to determine the page size this.div.remove(); const { width, height } = this.getPageSize(); this.root.appendChild(this.div); @@ -860,11 +913,32 @@ class SpineWebComponentOverlay extends HTMLElement { } private resizeCanvas() { - const displayWidth = document.documentElement.clientWidth; - const displayHeight = document.documentElement.clientHeight; - this.canvas.style.width = displayWidth * (1 + (this.overflowLeft + this.overflowRight)) + "px"; - this.canvas.style.height = displayHeight * (1 + (this.overflowTop + this.overflowBottom)) + "px"; - if (this.spineCanvas) this.spineCanvas.renderer.resize(ResizeMode.Expand); + console.log("START RESI:") + const screen = window.screen; + const angle = screen.orientation.angle; + const rotated = angle === 90 || angle === 270; + const width = rotated ? screen.height : screen.width; + const height = rotated ? screen.width : screen.height; + + if (this.currentCanvasBaseWidth !== width || this.currentCanvasBaseHeight !== height) { + this.currentCanvasBaseWidth = width; + this.currentCanvasBaseHeight = height; + this.overflowLeftSize = this.overflowLeft * width; + this.overflowTopSize = this.overflowTop * height; + console.log("FROM RESI: ", width, height) + + const totalWidth = width * (1 + (this.overflowLeft + this.overflowRight)); + const totalHeight = height * (1 + (this.overflowTop + this.overflowBottom)); + this.canvas.style.width = totalWidth + "px"; + this.canvas.style.height = totalHeight + "px"; + + const dpr = window.devicePixelRatio; + this.canvas.width = Math.round(totalWidth * dpr); + this.canvas.height = Math.round(totalHeight * dpr); + console.log("FROM RESI2: ", this.canvas.width, this.canvas.height) + + this.spineCanvas.renderer.resize2(); + } } private scrollHandler = () => { @@ -872,18 +946,20 @@ class SpineWebComponentOverlay extends HTMLElement { } private translateCanvas() { - const displayWidth = document.documentElement.clientWidth; - const displayHeight = document.documentElement.clientHeight; + const viewportWidth = document.documentElement.clientWidth; + const viewportHeight = document.documentElement.clientHeight; - this.overflowLeftSize = this.overflowLeft * displayWidth; - this.overflowTopSize = this.overflowTop * displayHeight; + // this.overflowLeftSize = this.overflowLeft * viewportWidth; + // this.overflowTopSize = this.overflowTop * viewportHeight; const scrollPositionX = window.scrollX - this.overflowLeftSize; const scrollPositionY = window.scrollY - this.overflowTopSize; + console.log("FROM TRAN: ", scrollPositionY, window.scrollY, this.overflowTopSize) this.canvas.style.transform =`translate(${scrollPositionX}px,${scrollPositionY}px)`; } private zoomHandler = () => { + console.log("ZOOM") this.skeletonList.forEach((widget) => { // inside mode scale automatically to fit the skeleton within its parent if (widget.mode !== 'origin' && widget.fit !== 'none') return; @@ -909,6 +985,7 @@ class SpineWebComponentOverlay extends HTMLElement { */ private screenToWorld(vec: Vector3, x: number, y: number) { vec.set(x, y, 0); + // pay attention that clientWidth/Height rounds the size - if we don't like it, we should use getBoundingClientRect as in getPagSize this.spineCanvas.renderer.camera.screenToWorld(vec, this.canvas.clientWidth, this.canvas.clientHeight); } } @@ -927,4 +1004,30 @@ customElements.define('spine-overlay', SpineWebComponentOverlay); export function getSpineWidget(identifier: string) { return document.querySelector(`spine-widget[identifier=${identifier}]`); +} + +export function createSpineWidget(parameters: { atlas: string, skeleton: string, animation: string, skin: string, manualStart?: boolean, pages: Array }): SpineWebComponentWidget { + const { + atlas, + skeleton, + animation, + skin, + manualStart = false, + pages = [], + } = parameters; + + const widget = document.createElement("spine-widget") as SpineWebComponentWidget; + + widget.setAttribute("skeleton", skeleton); + widget.setAttribute("atlas", atlas); + widget.setAttribute("skin", skin); + widget.setAttribute("animation", animation); + widget.setAttribute("manual-start", `${manualStart}`); + widget.setAttribute("pages", `${pages.join(",")}`); + + if (!manualStart) { + widget.start(); + } + + return widget; } \ No newline at end of file diff --git a/spine-ts/spine-webgl/src/index.ts b/spine-ts/spine-webgl/src/index.ts index 9f8c68d9d..e2d329a93 100644 --- a/spine-ts/spine-webgl/src/index.ts +++ b/spine-ts/spine-webgl/src/index.ts @@ -4,6 +4,7 @@ export * from "./CameraController.js"; export * from "./GLTexture.js"; export * from "./Input.js"; export * from "./LoadingScreen.js"; +export * from "./LoadingScreenWidget.js"; export * from "./Matrix4.js"; export * from "./Mesh.js"; export * from "./PolygonBatcher.js";