From 26d1ca739da3f26ca4ecd9dfbeed0a61198d9801 Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Thu, 29 Aug 2024 09:00:55 +0200 Subject: [PATCH] Add OffScreenUpdateBehaviour --- spine-ts/spine-webgl/example/canvas6.html | 136 ++ spine-ts/spine-webgl/example/canvas7.html | 82 +- spine-ts/spine-webgl/example/canvas8.html | 1747 +++++++++++++++++ .../spine-webgl/src/LoadingScreenWidget.ts | 129 ++ spine-ts/spine-webgl/src/SceneRenderer.ts | 1 - spine-ts/spine-webgl/src/SpineWebComponent.ts | 1033 ---------- .../src/SpineWebComponentWidget.ts | 114 +- spine-ts/spine-webgl/src/index.ts | 2 +- 8 files changed, 2081 insertions(+), 1163 deletions(-) create mode 100644 spine-ts/spine-webgl/example/canvas8.html create mode 100644 spine-ts/spine-webgl/src/LoadingScreenWidget.ts delete mode 100644 spine-ts/spine-webgl/src/SpineWebComponent.ts diff --git a/spine-ts/spine-webgl/example/canvas6.html b/spine-ts/spine-webgl/example/canvas6.html index 9ff0348eb..3b8f2eb34 100644 --- a/spine-ts/spine-webgl/example/canvas6.html +++ b/spine-ts/spine-webgl/example/canvas6.html @@ -1456,7 +1456,143 @@ skins.forEach((skin, i) => { ///////////////////// --> + +
+
+ +
+ Widgets are not render while they are off screen. +
+
+ The state and skeleton update, and the skeleton apply and the skeleton updateWorldTransform functions are not invoked when the widget is off screen. +
+
+ If you want the update functions to be invoked in any case, set offscreen=update. +
+ If you want all the functions to be invoked in any case, set offscreen=pose. +
+
+ You can also overwrite the update function. Just assign a function to the update property of the widget. + In that it's your responsibility to skip the update/apply. You can use the onScreen property for convinience. +
+ +
+ +
+ +
+ +
+ + + +
+ +
+

+                
+            
+
+
+ + + + +
+ +
+
+ Widgets are not render while they are off screen. +
+ The state and skeleton update, and the skeleton apply and the skeleton updateWorldTransform functions are not invoked when the widget is off screen. +
If you want the update functions to be invoked in any case, set offscreen=update. +
If you want all the functions to be invoked in any case, set offscreen=pose. +
+
+ +
+
+
+

+                
+            
+
+ +
+ + diff --git a/spine-ts/spine-webgl/example/canvas7.html b/spine-ts/spine-webgl/example/canvas7.html index c3f5830ba..03013d25f 100644 --- a/spine-ts/spine-webgl/example/canvas7.html +++ b/spine-ts/spine-webgl/example/canvas7.html @@ -160,32 +160,6 @@ pages="0,1,4,6" > -
- -
-
- -
-
-
@@ -197,33 +171,7 @@ animation="emotes/wave" skin="erikari" pages="0,1,4,6" - > - -
- -
-
- -
-
-
@@ -236,33 +184,7 @@ animation="emotes/wave" skin="mario" pages="0,1,4,6" - > - -
- -
-
- -
-
-
diff --git a/spine-ts/spine-webgl/example/canvas8.html b/spine-ts/spine-webgl/example/canvas8.html new file mode 100644 index 000000000..c014aca4d --- /dev/null +++ b/spine-ts/spine-webgl/example/canvas8.html @@ -0,0 +1,1747 @@ + + + + + + + + JS Library Showcase + + + + + + + + +
+ +
+
+ +
+
+ The <spine-widget> tag allows you to place your Spine animations into a web page. +
+
+ By default, the animation bounds are calculated using the given animation, or the setup pose if no animation is provided. +
+ The bounds is centered and scaled to fit the parent container. +
+
+ + +
+

+                
+            
+
+
+ + + + + + +
+ +
+ +
+
+ +
+
+ You can change the fit mode of your Spine animation using the fit attribute. +
+
+ This is fit="fill". Default fit value is fit="contain". +
+
+ +
+
+ +
+
+ If you want to preserve the original scale, you can use the fit="none". + In combination with that, you can use the scale attribute to choose you desired scale. +
+
+ Other fit modes are fitWidth, fitWidth, fitHeight, cover,and scaleDown. +
+
+ +
+ + + + +
+

+                
+            
+
+
+ + + + + + + + + +
+ +
+
+ +
+
+ If you want to manually size the Spine widget, specify the attributes width and height in pixels (without the px unit). +
+
+ + +
+

+                
+            
+
+
+ + + + + + + + + + + + + +
+ +
+
+ Mode origin center the animation world origin with the center of the HTML element. +
+ You are responsible to scale the skeleton using this mode. +
+
+ Move the origin by a percentage of the div width and height by using x-axis and y-axis respectively. +
+
+ +
+
+ + +
+

+                
+            
+
+
+ + + + + +
+ +
+
+ +
+
+ Use offset-x and offset-y to move you skeleton left or right by the pixel amount you specify. +
+
+ + +
+

+                
+            
+
+
+ + + + + +
+ +
+
+ Give an identifier to your widget to get it by using the spine.getSpineWidget function. + You can easily access the Skeleton and the AnimationState of your character, and use them as if you were using spine-webgl. +
+
+ If you change animation, you can ask the widget to scale the skeleton based on the new animation. +
+
+ +
+
+ + + +
+

+                
+            
+
+
+ + + + + + +
+
+
+ Moving the div will move the skeleton origin.
+ Resizing the div will resize the skeleton in inside mode, but not in origin mode. +
+
+ +
+
+ +
+

+                
+            
+
+ +
+ + + + + + +
+ +
+
+ You can view the skeleton world origin (green), the root bone position (red), and the bounds rectangle and center (blue) by setting debug to true. +
+
+ +
+
+ +
+

+                
+            
+
+ +
+ + + + + + +
+ +
+
+ +
+
+ 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. +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + +
+

+                
+            
+
+
+ + + +
+ +
+ +
+ Widgets are not rendered while they are off screen. +
+
+ The state and skeleton update, and the skeleton apply and the skeleton updateWorldTransform functions are not invoked when the widget is off screen. +
+
+ If you want the update functions to be invoked in any case, set offscreen=update. +
+ If you want all the functions to be invoked in any case, set offscreen=pose. +
+
+ You can also overwrite the update function. Just assign a function to the update property of the widget. + In that it's your responsibility to skip the update/apply. You can use the onScreen property for convinience. +
+ +
+ +
+ +
+ +
+ + + +
+ +
+

+                
+            
+
+
+ + + + + + + +
+ +
+
+ As a bonus item, you can move you skeleton around just by setting the draggable property to true. +
+
+ +
+
+ + +
+

+                
+            
+
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/spine-ts/spine-webgl/src/LoadingScreenWidget.ts b/spine-ts/spine-webgl/src/LoadingScreenWidget.ts new file mode 100644 index 000000000..fac9a372b --- /dev/null +++ b/spine-ts/spine-webgl/src/LoadingScreenWidget.ts @@ -0,0 +1,129 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated July 28, 2023. Replaces all prior versions. + * + * Copyright (c) 2013-2023, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software or + * otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE + * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { BlendMode, Color, Disposable, TimeKeeper } from "@esotericsoftware/spine-core"; +import { GLTexture } from "./GLTexture.js"; +import { ResizeMode, SceneRenderer } from "./SceneRenderer.js"; + +let spinnerImage: HTMLImageElement; +let logoImage: HTMLImageElement; +let loaded = 0; + +const FADE_IN = 1, FADE_OUT = 1; +const logoWidth = 165, logoHeight = 108, spinnerSize = 163; + +export class LoadingScreenWidget implements Disposable { + private renderer: SceneRenderer; + private logo: GLTexture | null = null; + private spinner: GLTexture | null = null; + private angle = 0; + private fadeOut = 0; + private fadeIn = 0; + private timeKeeper = new TimeKeeper(); + backgroundColor = new Color(0.135, 0.135, 0.135, 1); + private tempColor = new Color(); + + private x = 0; + private y = 0; + + constructor (renderer: SceneRenderer) { + this.renderer = renderer; + + this.timeKeeper.maxDelta = 9; + + if (!logoImage) { + let isSafari = navigator.userAgent.indexOf("Safari") > -1; // Thank you Apple Inc. + let onload = () => loaded++; + + logoImage = new Image(); + logoImage.src = SPINE_LOGO_DATA; + if (!isSafari) logoImage.crossOrigin = "anonymous"; + logoImage.onload = onload; + + spinnerImage = new Image(); + spinnerImage.src = SPINNER_DATA; + if (!isSafari) spinnerImage.crossOrigin = "anonymous"; + spinnerImage.onload = onload; + } + } + dispose (): void { + this.logo?.dispose(); + this.spinner?.dispose(); + } + + draw (complete = false, x: number, y: number, width: number, height: number) { + if (loaded < 2 || (complete && this.fadeOut > FADE_OUT)) return; + + this.x = x; + this.y = y; + + this.timeKeeper.update(); + // let a = Math.abs(Math.sin(this.timeKeeper.totalTime + 0.25)); + let a = 1; + this.angle -= this.timeKeeper.delta * 200 * (1 + 1.5 * Math.pow(a, 5)); + + let tempColor = this.tempColor; + let renderer = this.renderer; + + renderer.batcher.setBlendMode(BlendMode.Normal, true); + + // a *= Math.min(this.fadeIn / FADE_IN, 1); + tempColor.set(a, a, a, a); + + if (!this.logo) { + this.logo = new GLTexture(renderer.context, logoImage); + this.spinner = new GLTexture(renderer.context, spinnerImage); + } + + const shiftedX = this.x - logoWidth / 2; + const shiftedY = this.y - logoHeight / 2; + renderer.drawTexture( + this.logo, + shiftedX, + shiftedY, + logoWidth, + logoHeight, + tempColor); + + if (this.spinner) renderer.drawTextureRotated( + this.spinner, + shiftedX, + shiftedY - 25, + spinnerSize, + spinnerSize, + spinnerSize / 2, + spinnerSize / 2, + this.angle, + tempColor); + } +} + +let SPINNER_DATA = "data:image/png;base64,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"; + +let SPINE_LOGO_DATA = "data:image/png;base64,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"; diff --git a/spine-ts/spine-webgl/src/SceneRenderer.ts b/spine-ts/spine-webgl/src/SceneRenderer.ts index b5b61e8c5..755ec2d7d 100644 --- a/spine-ts/spine-webgl/src/SceneRenderer.ts +++ b/spine-ts/spine-webgl/src/SceneRenderer.ts @@ -464,7 +464,6 @@ export class SceneRenderer implements Disposable { } 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); diff --git a/spine-ts/spine-webgl/src/SpineWebComponent.ts b/spine-ts/spine-webgl/src/SpineWebComponent.ts deleted file mode 100644 index ba6b242bf..000000000 --- a/spine-ts/spine-webgl/src/SpineWebComponent.ts +++ /dev/null @@ -1,1033 +0,0 @@ -/****************************************************************************** - * Spine Runtimes License Agreement - * Last updated July 28, 2023. Replaces all prior versions. - * - * Copyright (c) 2013-2023, Esoteric Software LLC - * - * Integration of the Spine Runtimes into software or otherwise creating - * derivative works of the Spine Runtimes is permitted under the terms and - * conditions of Section 2 of the Spine Editor License Agreement: - * http://esotericsoftware.com/spine-editor-license - * - * Otherwise, it is permitted to integrate the Spine Runtimes into software or - * otherwise create derivative works of the Spine Runtimes (collectively, - * "Products"), provided that each user of the Products must obtain their own - * Spine Editor license and redistribution of the Products in any form must - * include this license and copyright notice. - * - * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, - * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE - * 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, LoadingScreenWidget, TextureAtlas, Texture } from "./index.js"; - -interface Rectangle { - x: number, - y: number, - width: number, - height: number, -} - -type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void; - -type ModeType = 'inside' | 'origin'; -function isModeType(value: string): value is ModeType { - return ( - value === "inside" || - value === "origin" - ); -} - -type FitType = "fill" | "fitWidth" | "fitHeight" | "contain" | "cover" | "none" | "scaleDown"; -function isFitType(value: string): value is FitType { - return ( - value === "fill" || - value === "fitWidth" || - value === "fitHeight" || - value === "contain" || - value === "cover" || - value === "none" || - value === "scaleDown" - ); -} - -interface WidgetLayoutOptions { - mode: ModeType - debug: boolean - offsetX: number - offsetY: number - xAxis: number - yAxis: number - draggable: boolean - fit: FitType - width: number - height: number - identifier: string -} - -interface WidgetPublicState { - skeleton: Skeleton - state: AnimationState - bounds: Rectangle -} - -interface WidgetInternalState { - currentScaleDpi: number - worldOffsetX: number - worldOffsetY: number - dragging: boolean - dragX: number - dragY: number -} - - -class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions, WidgetInternalState, Partial { - - // skeleton options - public atlasPath: string; - public skeletonPath: string; - public scale = 1; - public animation?: string; - public skin?: string; - skeletonData?: SkeletonData; // TODO - update?: UpdateSpineFunction; // TODO - - // layout options - public fit: FitType = "contain"; - public mode: ModeType = "inside"; - public offsetX = 0; - public offsetY = 0; - public xAxis = 0; - public yAxis = 0; - public width = -1; - public height = -1; - public draggable = false; - public debug = false; - public identifier = ""; - public loadingSpinner = true; - public manualStart = false; - public pages?: Array; - - // state - public skeleton?: Skeleton; - public state?: AnimationState; - 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 - public currentScaleDpi = 1; - public worldOffsetX = 0; - public worldOffsetY = 0; - 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", - "manual-start", - "spinner", - "pages" - ]; - } - - constructor() { - super(); - this.root = this.attachShadow({ mode: "open" }); - this.overlay = this.initializeOverlay(); - this.atlasPath = "TODO"; - this.skeletonPath = "TODO"; - - this.divLoader = document.createElement("div"); - this.divLoader.classList.add("container-loader"); - - const loader = document.createElement("div"); - loader.classList.add("loader"); - this.divLoader.appendChild(loader); - } - - connectedCallback() { - if (!this.atlasPath) { - throw new Error("Missing atlas attribute"); - } - - if (!this.skeletonPath) { - throw new Error("Missing skeleton attribute"); - } - - this.overlay.addWidget(this); - if (!this.manualStart) { - this.start(); - } - - this.render(); - } - - disconnectedCallback(): void { - this.loadingPromise?.then(() => { - const index = this.overlay.skeletonList.indexOf(this); - if (index !== -1) { - this.overlay.skeletonList.splice(index, 1); - } - }); - } - - attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void { - if (newValue !== null) { - if (name === "identifier") { - this.identifier = newValue; - } - - if (name === "atlas") { - this.atlasPath = newValue; - } - - if (name === "skeleton") { - this.skeletonPath = newValue; - } - - if (name === "skin") { - this.skin = newValue; - } - - if (name === "fit") { - this.fit = isFitType(newValue) ? newValue : "contain"; - } - - if (name === "mode") { - this.mode = isModeType(newValue) ? newValue : "inside"; - } - - if (name === "x-axis") { - let float = this.xAxis; - float = parseFloat(newValue); - this.xAxis = float; - } - if (name === "y-axis") { - let float = this.yAxis; - float = parseFloat(newValue); - this.yAxis = float; - } - if (name === "offset-x") { - let float = 0; - float = parseInt(newValue); - this.offsetX = float; - } - if (name === "offset-y") { - let float = 0; - float = parseInt(newValue); - this.offsetY = float; - } - - if (name === "scale") { - let scaleFloat = 1; - scaleFloat = parseFloat(newValue); - this.scale = scaleFloat; - } - - if (name === "width") { - let widthFloat = 1; - widthFloat = parseFloat(newValue); - this.width = widthFloat; - } - - if (name === "height") { - let heightFloat = 1; - heightFloat = parseFloat(newValue); - this.height = heightFloat; - } - - if (name === "animation") { - this.animation = newValue; - } - - if (name === "draggable") { - this.draggable = Boolean(newValue); - } - - 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 - public recalculateBounds() { - const { skeleton, state } = this; - if (!skeleton || !state) return; - const track = state.getCurrent(0); - const animation = track?.animation as (Animation | undefined); - const bounds = this.calculateAnimationViewport(animation); - this.setBounds(bounds); - } - - // set the given bounds on the current skeleton - // bounds is used to center the skeleton in inside mode and as a input area for click events - public setBounds(bounds: Rectangle) { - const { skeleton } = this; - if (!skeleton) return; - bounds.x /= skeleton.scaleX; - bounds.y /= skeleton.scaleY; - bounds.width /= skeleton.scaleX; - bounds.height /= skeleton.scaleY; - this.bounds = bounds; - } - - private initializeOverlay(): SpineWebComponentOverlay { - let overlay = document.querySelector("spine-overlay") as SpineWebComponentOverlay; - if (!overlay) { - overlay = document.createElement("spine-overlay") as SpineWebComponentOverlay; - document.body.appendChild(overlay); - } - return overlay; - } - - 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; - }); - } - - 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.loading = true; - // if (this.identifier !== "TODELETE") return Promise.reject(); - const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, skin } = this; - const isBinary = skeletonPath.endsWith(".skel"); - - // 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.loadTextureAtlasButNoTextures(atlasPath).then(atlas => this.loadTexturesInPagesAttribute(atlas)), - ]); - - const atlas = this.overlay.spineCanvas.assetManager.require(atlasPath); - const atlasLoader = new AtlasAttachmentLoader(atlas); - - const skeletonLoader = isBinary ? new SkeletonBinary(atlasLoader) : new SkeletonJson(atlasLoader); - skeletonLoader.scale = scale; - - const skeletonFile = this.overlay.spineCanvas.assetManager.require(skeletonPath); - const skeletonData = skeletonDataInput ?? skeletonLoader.readSkeletonData(skeletonFile); - - const skeleton = new Skeleton(skeletonData); - const animationStateData = new AnimationStateData(skeletonData); - const state = new AnimationState(animationStateData); - - if (skin) { - skeleton.setSkinByName(skin); - } - - let animationData; - if (animation) { - state.setAnimation(0, animation, true); - animationData = animation ? skeleton.data.findAnimation(animation)! : undefined; - } - - // 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; - return { skeleton, state, bounds }; - } - - public getHTMLElementReference(): HTMLElement { - return this.width <= 0 || this.width <= 0 - ? this.parentElement! - : this; - } - - private render(): void { - 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 - */ - - public async loadBinary(path: string) { - return new Promise((resolve, reject) => { - this.overlay.spineCanvas.assetManager.loadBinary(path, - (_, binary) => resolve(binary), - (_, message) => reject(message), - ); - }); - } - - public async loadJson(path: string) { - return new Promise((resolve, reject) => { - this.overlay.spineCanvas.assetManager.loadJson(path, - (_, object) => resolve(object), - (_, message) => reject(message), - ); - }); - } - - 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), - (_, message) => reject(message), - ); - }); - } - - public async loadTextureAtlasButNoTextures(path: string) { - return new Promise((resolve, reject) => { - this.overlay.spineCanvas.assetManager.loadTextureAtlasButNoTextures(path, - (_, atlas) => resolve(atlas), - (_, message) => reject(message), - ); - }); - } - - /* - * Other utilities - */ - - private calculateAnimationViewport (animation?: Animation): Rectangle { - const renderer = this.overlay.spineCanvas.renderer; - const { skeleton } = this; - if (!skeleton) return { x: 0, y: 0, width: 0, height: 0 }; - skeleton.setToSetupPose(); - - let offset = new Vector2(), size = new Vector2(); - const tempArray = new Array(2); - if (!animation) { - skeleton.updateWorldTransform(Physics.update); - skeleton.getBounds(offset, size, tempArray, renderer.skeletonRenderer.getSkeletonClipping()); - return { - x: offset.x, - y: offset.y, - width: size.x, - height: size.y, - } - } - - let steps = 100, stepTime = animation.duration ? animation.duration / steps : 0, time = 0; - let minX = 100000000, maxX = -100000000, minY = 100000000, maxY = -100000000; - for (let i = 0; i < steps; i++, time += stepTime) { - animation.apply(skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn); - skeleton.updateWorldTransform(Physics.update); - skeleton.getBounds(offset, size, tempArray, renderer.skeletonRenderer.getSkeletonClipping()); - - if (!isNaN(offset.x) && !isNaN(offset.y) && !isNaN(size.x) && !isNaN(size.y)) { - minX = Math.min(offset.x, minX); - maxX = Math.max(offset.x + size.x, maxX); - minY = Math.min(offset.y, minY); - maxY = Math.max(offset.y + size.y, maxY); - } else - console.error("Animation bounds are invalid: " + animation.name); - } - - return { - x: minX, - y: minY, - width: maxX - minX, - height: maxY - minY, - } - } -} - -class SpineWebComponentOverlay extends HTMLElement { - private root: ShadowRoot; - - public spineCanvas:SpineCanvas; - private div: HTMLDivElement; - private canvas:HTMLCanvasElement; - private fps: HTMLSpanElement; - - public skeletonList = new Array(); - - private intersectionObserver? : IntersectionObserver; - private resizeObserver:ResizeObserver; - private input: Input; - - // 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(); - this.root = this.attachShadow({ mode: 'open' }); - - this.div = document.createElement('div'); - this.div.style.position = "absolute"; - this.div.style.top = "0"; - this.div.style.left = "0"; - this.div.style.setProperty("pointer-events", "none"); - this.div.style.overflow = "hidden" - // this.div.style.backgroundColor = "rgba(0, 255, 0, 0.3)"; - - this.root.appendChild(this.div); - - this.canvas = document.createElement('canvas'); - this.div.appendChild(this.canvas); - this.canvas.style.position = "absolute"; - this.canvas.style.top = "0"; - this.canvas.style.left = "0"; - this.canvas.style.setProperty("pointer-events", "none"); - this.canvas.style.transform =`translate(0px,0px)`; - // this.canvas.style.setProperty("will-change", "transform"); // performance seems to be even worse with this uncommented - - this.fps = document.createElement('span'); - this.fps.style.position = "fixed"; - this.fps.style.top = "0"; - 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.resizeObserver.observe(document.body); - - const screen = window.screen; - screen.orientation.onchange = () => { - this.updateCanvasSize(); - } - - this.zoomHandler(); - - // scroll - window.addEventListener('scroll', this.scrollHandler); - this.scrollHandler(); - - 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); - const blue = new Color(0, 0, 1, 1); - return { - update: (canvas: SpineCanvas, delta: number) => { - this.skeletonList.forEach(({ skeleton, state, update }) => { - if (!skeleton || !state) return; - if (update) update(canvas, delta, skeleton, state) - else { - // delta = 0 - state.update(delta); - state.apply(skeleton); - skeleton.update(delta); - skeleton.updateWorldTransform(Physics.update); - } - }); - this.fps.innerText = canvas.time.framesPerSecond.toFixed(2) + " fps"; - }, - - render: (canvas: SpineCanvas) => { - 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, loadingSpinner, onScreen, loading } = widget; - - 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); - - 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; - } - - if (skeleton) { - let x = tempVector.x; - let y = tempVector.y; - if (mode === 'inside') { - let { x: ax, y: ay, width: aw, height: ah } = bounds!; - - // scale ratio - const scaleWidth = divBounds.width * devicePixelRatio / aw; - const scaleHeight = divBounds.height * devicePixelRatio / ah; - - 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 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; - ratioH = scaleHeight; - } else { - 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); - } - } - - widget.worldOffsetX = x + offsetX + dragX; - widget.worldOffsetY = y + offsetY + dragY; - - 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; - } - }); - - // 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(); - }, - } - - } - - 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 { - } - - private setupDragUtility() { - // TODO: we should use document - body might have some margin that offset the click events - Meanwhile I take event pageX/Y - const tempVectorInput = new Vector3(); - - let prevX = 0; - let prevY = 0; - this.input.addListener({ - down: (x, y, ev) => { - const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0]; - 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 || (!widget.onScreen && widget.dragX === 0 && widget.dragY === 0)) return; - - const { worldOffsetX, worldOffsetY } = widget; - const bounds = widget.bounds!; - const skeleton = widget.skeleton!; - const newBounds: Rectangle = { - x: bounds.x * skeleton.scaleX + worldOffsetX, - y: bounds.y * skeleton.scaleY + worldOffsetY, - width: bounds.width * skeleton.scaleX, - height: bounds.height * skeleton.scaleY, - }; - - if (inside(tempVectorInput, newBounds)) { - widget.dragging = true; - ev?.preventDefault(); - } - }); - prevX = tempVectorInput.x; - prevY = tempVectorInput.y; - }, - dragged: (x, y, ev) => { - const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0]; - 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); - let dragX = tempVectorInput.x - prevX; - let dragY = tempVectorInput.y - prevY; - this.skeletonList.forEach(widget => { - 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; - }, - up: () => { - this.skeletonList.forEach(widget => { - widget.dragging = false; - }); - } - }) - } - - /* - * Resize/scroll utilities - */ - - private updateCanvasSize() { - // resize canvas, if necessary - this.resizeCanvas(); - - // 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 determine the page size - this.div.remove(); - const { width, height } = this.getPageSize(); - this.root.appendChild(this.div); - - this.div.style.width = width + "px"; - this.div.style.height = height + "px"; - } - - private resizeCanvas() { - 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 = () => { - this.translateCanvas(); - } - - private translateCanvas() { - const viewportWidth = document.documentElement.clientWidth; - const viewportHeight = document.documentElement.clientHeight; - - // 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; - - const skeleton = widget.skeleton; - if (!skeleton) return; - const scale = window.devicePixelRatio; - skeleton.scaleX = skeleton.scaleX / widget.currentScaleDpi * scale; - skeleton.scaleY = skeleton.scaleY / widget.currentScaleDpi * scale; - widget.currentScaleDpi = scale; - }) - } - - private getPageSize() { - // we need the bounding client rect otherwise decimals won't be returned - // this means that during zoom it might occurs that the div would be resized - // rounded 1px more making a scrollbar appear - return document.body.getBoundingClientRect(); - } - - /* - * Other utilities - */ - 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); - } -} - -const inside = (point: { x: number; y: number }, rectangle: Rectangle): boolean => { - return ( - point.x >= rectangle.x && - point.x <= rectangle.x + rectangle.width && - point.y >= rectangle.y && - point.y <= rectangle.y + rectangle.height - ); -} - -customElements.define('spine-widget', SpineWebComponentWidget); -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/SpineWebComponentWidget.ts b/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts index ba6b242bf..cab04fa58 100644 --- a/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts +++ b/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts @@ -38,6 +38,15 @@ interface Rectangle { type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void; +type OffScreenUpdateBehaviourType = "pause" | "update" | "pose"; +function isOffScreenUpdateBehaviourType(value: string): value is OffScreenUpdateBehaviourType { + return ( + value === "pause" || + value === "update" || + value === "pose" + ); +} + type ModeType = 'inside' | 'origin'; function isModeType(value: string): value is ModeType { return ( @@ -115,6 +124,7 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions public loadingSpinner = true; public manualStart = false; public pages?: Array; + public offScreenUpdateBehaviour: OffScreenUpdateBehaviourType = "pause"; // state public skeleton?: Skeleton; @@ -137,7 +147,6 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions if (widget.loading && !widget.onScreenAtLeastOnce) { widget.onScreenAtLeastOnce = true; - console.log(widget.manualStart) if (widget.manualStart) { widget.start(); } @@ -164,27 +173,28 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions 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", - "manual-start", - "spinner", - "pages" - ]; + 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", + "offscreen" + ]; } constructor() { @@ -254,6 +264,10 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions this.mode = isModeType(newValue) ? newValue : "inside"; } + if (name === "offscreen") { + this.offScreenUpdateBehaviour = isOffScreenUpdateBehaviourType(newValue) ? newValue : "pause"; + } + if (name === "x-axis") { let float = this.xAxis; float = parseFloat(newValue); @@ -579,7 +593,8 @@ class SpineWebComponentOverlay extends HTMLElement { // 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; + // making it bigger might reduce performance significantly + private overflowTop = .2; private overflowBottom = .0; private overflowLeft = .0; private overflowRight = .0; @@ -621,6 +636,9 @@ class SpineWebComponentOverlay extends HTMLElement { this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() }); this.updateCanvasSize(); + this.zoomHandler(); + this.translateCanvas(); + this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth; this.overflowTopSize = this.overflowTop * document.documentElement.clientHeight; @@ -635,11 +653,10 @@ class SpineWebComponentOverlay extends HTMLElement { const screen = window.screen; screen.orientation.onchange = () => { this.updateCanvasSize(); + // after an orientation change the scrolling changes, but the scroll event does not fire + this.scrollHandler(); } - this.zoomHandler(); - - // scroll window.addEventListener('scroll', this.scrollHandler); this.scrollHandler(); @@ -658,15 +675,19 @@ class SpineWebComponentOverlay extends HTMLElement { const blue = new Color(0, 0, 1, 1); return { update: (canvas: SpineCanvas, delta: number) => { - this.skeletonList.forEach(({ skeleton, state, update }) => { + this.skeletonList.forEach(({ skeleton, state, update, onScreen, offScreenUpdateBehaviour, skeletonPath }) => { if (!skeleton || !state) return; + if (!onScreen && offScreenUpdateBehaviour === "pause") return; if (update) update(canvas, delta, skeleton, state) else { // delta = 0 state.update(delta); - state.apply(skeleton); skeleton.update(delta); - skeleton.updateWorldTransform(Physics.update); + + if (onScreen || (!onScreen && offScreenUpdateBehaviour === "pose") ) { + state.apply(skeleton); + skeleton.updateWorldTransform(Physics.update); + } } }); this.fps.innerText = canvas.time.framesPerSecond.toFixed(2) + " fps"; @@ -913,19 +934,13 @@ class SpineWebComponentOverlay extends HTMLElement { } private resizeCanvas() { - 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; + const { width, height } = this.getScreenSize(); 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)); @@ -935,7 +950,6 @@ class SpineWebComponentOverlay extends HTMLElement { 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(); } @@ -946,20 +960,12 @@ class SpineWebComponentOverlay extends HTMLElement { } private translateCanvas() { - const viewportWidth = document.documentElement.clientWidth; - const viewportHeight = document.documentElement.clientHeight; - - // 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; @@ -973,13 +979,25 @@ class SpineWebComponentOverlay extends HTMLElement { }) } + // we need the bounding client rect otherwise decimals won't be returned + // this means that during zoom it might occurs that the div would be resized + // rounded 1px more making a scrollbar appear private getPageSize() { - // we need the bounding client rect otherwise decimals won't be returned - // this means that during zoom it might occurs that the div would be resized - // rounded 1px more making a scrollbar appear return document.body.getBoundingClientRect(); } + // screen size remain the same when it is rotated + // we need to swap them based and the orientation angle + private getScreenSize() { + const { screen } = window; + const { width, height } = window.screen; + const angle = screen.orientation.angle; + const rotated = angle === 90 || angle === 270; + return rotated + ? { width: height, height: width } + : { width, height }; + } + /* * Other utilities */ diff --git a/spine-ts/spine-webgl/src/index.ts b/spine-ts/spine-webgl/src/index.ts index e2d329a93..60c4695a0 100644 --- a/spine-ts/spine-webgl/src/index.ts +++ b/spine-ts/spine-webgl/src/index.ts @@ -14,7 +14,7 @@ export * from "./ShapeRenderer.js"; export * from "./SkeletonDebugRenderer.js"; export * from "./SkeletonRenderer.js"; export * from "./SpineCanvas.js"; -export * from "./SpineWebComponent.js"; +export * from "./SpineWebComponentWidget.js"; export * from "./Vector3.js"; export * from "./WebGL.js"; export * from "@esotericsoftware/spine-core"; \ No newline at end of file