diff --git a/spine-ts/spine-widget/example/app.html b/spine-ts/spine-widget/example/app.html index 8d73283c9..50813d632 100644 --- a/spine-ts/spine-widget/example/app.html +++ b/spine-ts/spine-widget/example/app.html @@ -201,7 +201,7 @@
- + >
@@ -268,7 +268,7 @@
- + >
@@ -333,7 +333,7 @@
- + >
@@ -372,7 +372,7 @@
- + >
diff --git a/spine-ts/spine-widget/example/game.html b/spine-ts/spine-widget/example/game.html index b30dd488f..201ab3159 100644 --- a/spine-ts/spine-widget/example/game.html +++ b/spine-ts/spine-widget/example/game.html @@ -37,20 +37,20 @@
- - + + >
diff --git a/spine-ts/spine-widget/example/gui.html b/spine-ts/spine-widget/example/gui.html index b94b712f4..a8fa2e15e 100644 --- a/spine-ts/spine-widget/example/gui.html +++ b/spine-ts/spine-widget/example/gui.html @@ -41,13 +41,13 @@
- + >
diff --git a/spine-ts/spine-widget/example/login.html b/spine-ts/spine-widget/example/login.html index 3e9eaddd0..693b8d3b9 100644 --- a/spine-ts/spine-widget/example/login.html +++ b/spine-ts/spine-widget/example/login.html @@ -21,7 +21,7 @@
- + >
@@ -51,14 +51,14 @@
- + >
diff --git a/spine-ts/spine-widget/example/tutorial.html b/spine-ts/spine-widget/example/tutorial.html index 1506d297f..61318f0c8 100644 --- a/spine-ts/spine-widget/example/tutorial.html +++ b/spine-ts/spine-widget/example/tutorial.html @@ -156,14 +156,14 @@
- + >
- The <spine-widget> tag allows you to embed your Spine animations into a web page. + The <spine-skeleton> tag allows you to embed your Spine animations into a web page as a web component.

By default, the animation bounds are calculated using the specified animation, or the setup pose if no animation is provided. @@ -177,11 +177,11 @@

                 
             
@@ -205,12 +205,12 @@
- + >
You can change the fit mode of your Spine animation using the fit attribute. @@ -222,13 +222,13 @@
- + >
If you want to preserve the original scale, you can use fit="none". @@ -245,20 +245,20 @@

                 
             
@@ -280,14 +280,14 @@
- - + + >
If you want to manually size the Spine widget, specify the width and height attributes in pixels (without the "px" unit). @@ -314,15 +314,15 @@ @@ -364,14 +364,14 @@ Move the origin by a percentage of the div's width and height using the x-axis and y-axis attributes, respectively.
- + >
@@ -379,14 +379,14 @@

                 
             
@@ -408,13 +408,13 @@
- + >
Use offset-x and offset-y to move your skeleton left or right by the specified number of pixels. @@ -425,13 +425,13 @@

                 
             
@@ -453,7 +453,7 @@
- + >
You can virtually add padding to the element container using pad-left, pad-right, pad-top, and pad-bottom. @@ -476,7 +476,7 @@

                 
+>`);
             
@@ -515,7 +515,7 @@ In this example, we're zooming in on Celeste's face. You’ll probably want to use clip in this case to prevent the skeleton from overflowing.
- + >

                 
+>`);
             
@@ -569,12 +569,12 @@ If you change the animation, you can ask the widget to rescale the skeleton based on the new animation. See the code below.
- + >
@@ -595,12 +595,12 @@

                 
             
@@ -993,13 +993,13 @@ async function updateCelesteAnimations() { Here we slightly shift the root to prevent it from overlapping with the origin.
- + >
@@ -1326,13 +1326,13 @@ function removeDiv() {
- + >
A loading spinner is shown while assets are loading. Click the button below to simulate a 2-second loading delay: @@ -1372,13 +1372,13 @@ function removeDiv() {

                 
@@ -1663,118 +1663,118 @@ function toggleSpinner(element) {
 
             
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
@@ -1786,118 +1786,118 @@ function toggleSpinner(element) { escapeHTMLandInject(`
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
- + >
`) @@ -2059,20 +2059,20 @@ skins.forEach((skin, i) => {
- + >
- + >
@@ -2895,7 +2895,7 @@ tank.beforeUpdateWorldTransforms = (delta, skeleton, state) => {
If you need to determine the cursor position in the overlay world, you might find useful the following properties.
- For spine-widget: + For spine-skeleton:
  • cursorWorldX and cursorWorldY are the x and y of the cursor relative to the skeleton root (spine world).
  • worldX and worldY are the x and y of the root relative to the canvas/webgl context origin (spine world).
  • @@ -2960,14 +2960,14 @@ function createCircleOfDivs(numDivs = 8) { div.style.fontWeight = 'bold'; div.style.transform = \`translate(\${x}px, \${y}px)\`; div.innerHTML = \` - + > \`; container.appendChild(div); @@ -3053,14 +3053,14 @@ function createCircleOfDivs(numDivs = 8) { div.style.fontWeight = 'bold'; div.style.transform = `translate(${x}px, ${y}px)`; div.innerHTML = ` - + > `; container.appendChild(div); @@ -3150,7 +3150,7 @@ const updateControl = (widget, controlBone, mouseX, mouseY, tempVector) => {

- { animation="emotes/wave" isinteractive style="width: 150px; height: 150px;" - > + > - { animation="emotes/wave" isinteractive style="width: 150px; height: 150px;" - > + >
@@ -3200,7 +3200,7 @@ const darkPicker = document.getElementById("dark-picker");

                 
 
 
diff --git a/spine-ts/spine-widget/src/SpineWebComponentOverlay.ts b/spine-ts/spine-widget/src/SpineWebComponentOverlay.ts
new file mode 100644
index 000000000..00cf58c8f
--- /dev/null
+++ b/spine-ts/spine-widget/src/SpineWebComponentOverlay.ts
@@ -0,0 +1,1023 @@
+/******************************************************************************
+ * 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 { AssetManager, Color, Input, LoadingScreen, ManagedWebGLRenderingContext, Physics, SceneRenderer, TimeKeeper, Vector2, Vector3 } from "@esotericsoftware/spine-webgl"
+import { SpineWebComponentSkeleton } from "./SpineWebComponentSkeleton"
+import { AttributeTypes, castValue, Point, Rectangle } from "./wcUtils"
+
+interface OverlayAttributes {
+	overlayId?: string
+	noAutoParentTransform: boolean
+	overflowTop: number
+	overflowBottom: number
+	overflowLeft: number
+	overflowRight: number
+}
+
+export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttributes, Disposable {
+	private static OVERLAY_ID = "spine-overlay-default-identifier";
+	private static OVERLAY_LIST = new Map();
+
+	/**
+	 * @internal
+	 */
+	static getOrCreateOverlay (overlayId: string | null): SpineWebComponentOverlay {
+		let overlay = SpineWebComponentOverlay.OVERLAY_LIST.get(overlayId || SpineWebComponentOverlay.OVERLAY_ID);
+		if (!overlay) {
+			overlay = document.createElement('spine-overlay') as SpineWebComponentOverlay;
+			overlay.setAttribute('overlay-id', SpineWebComponentOverlay.OVERLAY_ID);
+			document.body.appendChild(overlay);
+		}
+		return overlay;
+	}
+
+	/**
+	 * A list holding the widgets added to this overlay.
+	 */
+	public widgets = new Array();
+
+	/**
+	 * The {@link SceneRenderer} used by this overlay.
+	 */
+	public renderer: SceneRenderer;
+
+	/**
+	 * The {@link AssetManager} used by this overlay.
+	 */
+	public assetManager: AssetManager;
+
+	/**
+	 * The identifier of this overlay. This is necessary when multiply overlay are created.
+	   * Connected to `overlay-id` attribute.
+	 */
+	public overlayId?: string;
+
+	/**
+	 * If `false` (default value), the overlay container style will be affected adding `transform: translateZ(0);` to it.
+	 * The `transform` is not affected if it already exists on the container.
+	 * This is necessary to make the scrolling works with containers that scroll in a different way with respect to the page, as explained in {@link appendedToBody}.
+	 * Connected to `no-auto-parent-transform` attribute.
+	 */
+	public noAutoParentTransform = false;
+
+	/**
+	 * The canvas is continuously translated so that it covers the viewport. This translation might be slightly slower during fast scrolling.
+	 * If the canvas has the same size as the viewport, while scrolling it might be slighlty misaligned with the viewport.
+	 * This parameter defines, as percentage of the viewport height, the pixels to add to the top of the canvas to prevent this effect.
+	 * Making the canvas too big might reduce performance.
+	 * Default value: 0.2.
+	 * Connected to `overflow-top` attribute.
+	 */
+	public overflowTop = .2;
+
+	/**
+	 * The canvas is continuously translated so that it covers the viewport. This translation might be slightly slower during fast scrolling.
+	 * If the canvas has the same size as the viewport, while scrolling it might be slighlty misaligned with the viewport.
+	 * This parameter defines, as percentage of the viewport height, the pixels to add to the bottom of the canvas to prevent this effect.
+	 * Making the canvas too big might reduce performance.
+	 * Default value: 0.
+	 * Connected to `overflow-bottom` attribute.
+	 */
+	public overflowBottom = .0;
+
+	/**
+	 * The canvas is continuously translated so that it covers the viewport. This translation might be slightly slower during fast scrolling.
+	 * If the canvas has the same size as the viewport, while scrolling it might be slighlty misaligned with the viewport.
+	 * This parameter defines, as percentage of the viewport width, the pixels to add to the left of the canvas to prevent this effect.
+	 * Making the canvas too big might reduce performance.
+	 * Default value: 0.
+	 * Connected to `overflow-left` attribute.
+	 */
+	public overflowLeft = .0;
+
+	/**
+	 * The canvas is continuously translated so that it covers the viewport. This translation might be slightly slower during fast scrolling.
+	 * If the canvas has the same size as the viewport, while scrolling it might be slighlty misaligned with the viewport.
+	 * This parameter defines, as percentage of the viewport width, the pixels to add to the right of the canvas to prevent this effect.
+	 * Making the canvas too big might reduce performance.
+	 * Default value: 0.
+	 * Connected to `overflow-right` attribute.
+	 */
+	public overflowRight = .0;
+
+	private root: ShadowRoot;
+
+	private div: HTMLDivElement;
+	private boneFollowersParent: HTMLDivElement;
+	private canvas: HTMLCanvasElement;
+	private fps: HTMLSpanElement;
+	private fpsAppended = false;
+
+	private intersectionObserver?: IntersectionObserver;
+	private resizeObserver?: ResizeObserver;
+	private input?: Input;
+
+	private overflowLeftSize = 0;
+	private overflowTopSize = 0;
+
+	private lastCanvasBaseWidth = 0;
+	private lastCanvasBaseHeight = 0;
+
+	private disposed = false;
+	private loaded = false;
+
+	/**
+	 * appendedToBody is assegned in the connectedCallback.
+	 * When true, the overlay will have the size of the element container in contrast to the default behaviour where the
+	 * overlay has always the size of the screen.
+	 * This is necessary when the overlay is inserted into a container that scroll in a different way with respect to the page.
+	 * Otherwise the following problems might occur:
+	 * 1) For containers appendedToBody, the widget will be slightly slower to scroll than the html behind. The effect is more evident for lower refresh rate display.
+	 * 2) For containers appendedToBody, the widget will overflow the container bounds until the widget html element container is visible
+	 * 3) For fixed containers, the widget will scroll in a jerky way
+	 *
+	 * In order to fix this behaviour, it is necessary to insert a dedicated `spine-overlay` webcomponent as a direct child of the container.
+	 * Moreover, it is necessary to perform the following actions:
+	 * 1) The scrollable container must have a `transform` css attribute. If it hasn't this attribute the `spine-overlay` will add it for you.
+	 * If your scrollable container has already this css attribute, or if you prefer to add it by yourself (example: `transform: translateZ(0);`), set the `no-auto-parent-transform` to the `spine-overlay`.
+	 * 2) The `spine-overlay` must have an `overlay-id` attribute. Choose the value you prefer.
+	 * 3) Each `spine-skeleton` must have an `overlay-id` attribute. The same as the hosting `spine-overlay`.
+	   * Connected to `scrollable` attribute.
+	 */
+	private appendedToBody = true;
+
+	readonly time = new TimeKeeper();
+
+	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.1)";
+
+		this.root.appendChild(this.div);
+
+		this.canvas = document.createElement("canvas");
+		this.boneFollowersParent = document.createElement("div");
+
+		this.div.appendChild(this.canvas);
+		this.canvas.style.position = "absolute";
+		this.canvas.style.top = "0";
+		this.canvas.style.left = "0";
+
+		this.div.appendChild(this.boneFollowersParent);
+		this.boneFollowersParent.style.position = "absolute";
+		this.boneFollowersParent.style.top = "0";
+		this.boneFollowersParent.style.left = "0";
+		this.boneFollowersParent.style.whiteSpace = "nowrap";
+		this.boneFollowersParent.style.setProperty("pointer-events", "none");
+		this.boneFollowersParent.style.transform = `translate(0px,0px)`;
+
+		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";
+
+		const context = new ManagedWebGLRenderingContext(this.canvas, { alpha: true });
+		this.renderer = new SceneRenderer(this.canvas, context);
+
+		this.assetManager = new AssetManager(context);
+	}
+	[Symbol.dispose](): void {
+		throw new Error("Method not implemented.")
+	}
+
+	connectedCallback (): void {
+		this.appendedToBody = this.parentElement !== document.body;
+
+		let overlayId = this.getAttribute('overlay-id');
+		if (!overlayId) {
+			overlayId = SpineWebComponentOverlay.OVERLAY_ID;
+			this.setAttribute('overlay-id', overlayId);
+		}
+		const existingOverlay = SpineWebComponentOverlay.OVERLAY_LIST.get(overlayId);
+		if (existingOverlay && existingOverlay !== this) {
+			throw new Error(`"SpineWebComponentOverlay - You cannot have two spine-overlay with the same overlay-id: ${overlayId}"`);
+		}
+		SpineWebComponentOverlay.OVERLAY_LIST.set(overlayId, this);
+		// window.addEventListener("scroll", this.scrolledCallback);
+
+		if (document.readyState !== "complete") {
+			window.addEventListener("load", this.loadedCallback);
+		} else {
+			this.loadedCallback();
+		}
+
+		window.screen.orientation.addEventListener('change', this.orientationChangedCallback);
+
+		this.intersectionObserver = new IntersectionObserver((widgets) => {
+			for (const elem of widgets) {
+				const { target, intersectionRatio } = elem;
+				let { isIntersecting } = elem;
+				for (const widget of this.widgets) {
+					if (widget.getHostElement() != target) continue;
+
+					// old browsers do not have isIntersecting
+					if (isIntersecting === undefined) {
+						isIntersecting = intersectionRatio > 0;
+					}
+
+					widget.onScreen = isIntersecting;
+					if (isIntersecting) {
+						widget.onScreenFunction(widget);
+					}
+				}
+			}
+		}, { rootMargin: "30px 20px 30px 20px" });
+
+		// resize observer is supported by all major browsers today chrome started to support it in version 64 (early 2018)
+		// we cannot use window.resize event since it does not fire when body resizes, but not the window
+		// Alternatively, we can store the body size, check the current body size in the loop (like the translateCanvas), and
+		// if they differs call the resizeCallback. I already tested it, and it works. ResizeObserver should be more efficient.
+		if (this.appendedToBody) {
+			// if the element is scrollable, the user does not disable translate tweak, and the parent did not have already a transform, add the tweak
+			if (this.appendedToBody && !this.noAutoParentTransform && getComputedStyle(this.parentElement!).transform === "none") {
+				this.parentElement!.style.transform = `translateZ(0)`;
+			}
+			this.resizeObserver = new ResizeObserver(this.resizedCallback);
+			this.resizeObserver.observe(this.parentElement!);
+		} else {
+			window.addEventListener("resize", this.resizedCallback)
+		}
+
+		for (const widget of this.widgets) {
+			this.intersectionObserver?.observe(widget.getHostElement());
+		}
+		this.input = this.setupDragUtility();
+
+		this.startRenderingLoop();
+	}
+
+	private hasCssTweakOff () {
+		return this.noAutoParentTransform && getComputedStyle(this.parentElement!).transform === "none";
+	}
+
+	private running = false;
+	disconnectedCallback (): void {
+		const id = this.getAttribute('overlay-id');
+		if (id) SpineWebComponentOverlay.OVERLAY_LIST.delete(id);
+		// window.removeEventListener("scroll", this.scrolledCallback);
+		window.removeEventListener("load", this.loadedCallback);
+		window.removeEventListener("resize", this.resizedCallback);
+		window.screen.orientation.removeEventListener('change', this.orientationChangedCallback);
+		this.intersectionObserver?.disconnect();
+		this.resizeObserver?.disconnect();
+		this.input?.dispose();
+	}
+
+
+	static attributesDescription: Record = {
+		"overlay-id": { propertyName: "overlayId", type: "string" },
+		"no-auto-parent-transform": { propertyName: "noAutoParentTransform", type: "boolean" },
+		"overflow-top": { propertyName: "overflowTop", type: "number" },
+		"overflow-bottom": { propertyName: "overflowBottom", type: "number" },
+		"overflow-left": { propertyName: "overflowLeft", type: "number" },
+		"overflow-right": { propertyName: "overflowRight", type: "number" },
+	}
+
+	static get observedAttributes (): string[] {
+		return Object.keys(SpineWebComponentOverlay.attributesDescription);
+	}
+
+	attributeChangedCallback (name: string, oldValue: string | null, newValue: string | null): void {
+		const { type, propertyName, defaultValue } = SpineWebComponentOverlay.attributesDescription[name];
+		const val = castValue(type, newValue, defaultValue);
+		(this as any)[propertyName] = val;
+		return;
+	}
+
+	private resizedCallback = () => {
+		this.updateCanvasSize();
+	}
+
+	private orientationChangedCallback = () => {
+		this.updateCanvasSize();
+		// after an orientation change the scrolling changes, but the scroll event does not fire
+		this.scrolledCallback();
+	}
+
+	// right now, we scroll the canvas each frame before rendering loop, that makes scrolling on mobile waaay more smoother
+	// this is way scroll handler do nothing
+	private scrolledCallback = () => {
+		// this.translateCanvas();
+	}
+
+	private loadedCallback = () => {
+		this.updateCanvasSize();
+		this.scrolledCallback();
+		if (!this.loaded) {
+			this.loaded = true;
+			this.parentElement!.appendChild(this);
+		}
+	}
+
+	/**
+	 * Remove the overlay from the DOM, dispose all the contained widgets, and dispose the renderer.
+	 */
+	dispose (): void {
+		for (const widget of [...this.widgets]) widget.dispose();
+
+		this.remove();
+		this.widgets.length = 0;
+		this.renderer.dispose();
+		this.disposed = true;
+		this.assetManager.dispose();
+	}
+
+	/**
+	 * Add the widget to the overlay.
+	 * If the widget is after the overlay in the DOM, the overlay is appended after the widget.
+	 * @param widget The widget to add to the overlay
+	 */
+	addWidget (widget: SpineWebComponentSkeleton) {
+		this.widgets.push(widget);
+		this.intersectionObserver?.observe(widget.getHostElement());
+		if (this.loaded) {
+			const comparison = this.compareDocumentPosition(widget);
+			// DOCUMENT_POSITION_DISCONNECTED is needed when a widget is inside the overlay (due to followBone)
+			if ((comparison & Node.DOCUMENT_POSITION_FOLLOWING) && !(comparison & Node.DOCUMENT_POSITION_DISCONNECTED)) {
+				this.parentElement!.appendChild(this);
+			}
+		}
+	}
+
+	/**
+	 * Remove the widget from the overlay.
+	 * @param widget The widget to remove from the overlay
+	 */
+	removeWidget (widget: SpineWebComponentSkeleton) {
+		const index = this.widgets.findIndex(w => w === widget);
+		if (index === -1) return false;
+
+		this.widgets.splice(index);
+		this.intersectionObserver?.unobserve(widget.getHostElement());
+		return true;
+	}
+
+	addSlotFollowerElement (element: HTMLElement) {
+		this.boneFollowersParent.appendChild(element);
+		this.resizedCallback();
+	}
+
+	private tempFollowBoneVector = new Vector3();
+	private startRenderingLoop () {
+		if (this.running) return;
+
+		const updateWidgets = () => {
+			const delta = this.time.delta;
+			for (const { skeleton, state, update, onScreen, offScreenUpdateBehaviour, beforeUpdateWorldTransforms, afterUpdateWorldTransforms } of this.widgets) {
+				if (!skeleton || !state) continue;
+				if (!onScreen && offScreenUpdateBehaviour === "pause") continue;
+				if (update) update(delta, skeleton, state)
+				else {
+					// delta = 0
+					state.update(delta);
+					skeleton.update(delta);
+
+					if (onScreen || (!onScreen && offScreenUpdateBehaviour === "pose")) {
+						state.apply(skeleton);
+						beforeUpdateWorldTransforms(delta, skeleton, state);
+						skeleton.updateWorldTransform(Physics.update);
+						afterUpdateWorldTransforms(delta, skeleton, state);
+					}
+				}
+			}
+
+			// fps top-left span
+			if (SpineWebComponentSkeleton.SHOW_FPS) {
+				if (!this.fpsAppended) {
+					this.div.appendChild(this.fps);
+					this.fpsAppended = true;
+				}
+				this.fps.innerText = this.time.framesPerSecond.toFixed(2) + " fps";
+			} else {
+				if (this.fpsAppended) {
+					this.div.removeChild(this.fps);
+					this.fpsAppended = false;
+				}
+			}
+		};
+
+		const clear = (r: number, g: number, b: number, a: number) => {
+			this.renderer.context.gl.clearColor(r, g, b, a);
+			this.renderer.context.gl.clear(this.renderer.context.gl.COLOR_BUFFER_BIT);
+		}
+
+		const startScissor = (divBounds: Rectangle) => {
+			this.renderer.end();
+			this.renderer.begin();
+			this.renderer.context.gl.enable(this.renderer.context.gl.SCISSOR_TEST);
+			this.renderer.context.gl.scissor(
+				this.screenToWorldLength(divBounds.x),
+				this.canvas.height - this.screenToWorldLength(divBounds.y + divBounds.height),
+				this.screenToWorldLength(divBounds.width),
+				this.screenToWorldLength(divBounds.height)
+			);
+		}
+
+		const endScissor = () => {
+			this.renderer.end();
+			this.renderer.context.gl.disable(this.renderer.context.gl.SCISSOR_TEST);
+			this.renderer.begin();
+		}
+
+		const renderWidgets = () => {
+			clear(0, 0, 0, 0);
+			let renderer = this.renderer;
+			renderer.begin();
+
+			let ref: DOMRect;
+			let offsetLeftForOevrlay = 0;
+			let offsetTopForOverlay = 0;
+			if (this.appendedToBody) {
+				ref = this.parentElement!.getBoundingClientRect();
+				const computedStyle = getComputedStyle(this.parentElement!);
+				offsetLeftForOevrlay = ref.left + parseFloat(computedStyle.borderLeftWidth);
+				offsetTopForOverlay = ref.top + parseFloat(computedStyle.borderTopWidth);
+			}
+
+			const tempVector = new Vector3();
+			for (const widget of this.widgets) {
+				const { skeleton, pma, bounds, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY, fit, noSpinner, onScreen, loading, clip, isDraggable } = widget;
+
+				if (widget.isOffScreenAndWasMoved()) continue;
+				const elementRef = widget.getHostElement();
+				const divBounds = elementRef.getBoundingClientRect();
+				// need to use left and top, because x and y are not available on older browser
+				divBounds.x = divBounds.left + this.overflowLeftSize;
+				divBounds.y = divBounds.top + this.overflowTopSize;
+
+				if (this.appendedToBody) {
+					divBounds.x -= offsetLeftForOevrlay;
+					divBounds.y -= offsetTopForOverlay;
+				}
+
+				const { padLeft, padRight, padTop, padBottom } = widget
+				const paddingShiftHorizontal = (padLeft - padRight) / 2;
+				const paddingShiftVertical = (padTop - padBottom) / 2;
+
+				// get the desired point into the the div (center by default) in world coordinate
+				const divX = divBounds.x + divBounds.width * ((xAxis + .5) + paddingShiftHorizontal);
+				const divY = divBounds.y + divBounds.height * ((-yAxis + .5) + paddingShiftVertical) - 1;
+				this.screenToWorld(tempVector, divX, divY);
+				let divOriginX = tempVector.x;
+				let divOriginY = tempVector.y;
+
+				const paddingShrinkWidth = 1 - (padLeft + padRight);
+				const paddingShrinkHeight = 1 - (padTop + padBottom);
+				const divWidthWorld = this.screenToWorldLength(divBounds.width * paddingShrinkWidth);
+				const divHeightWorld = this.screenToWorldLength(divBounds.height * paddingShrinkHeight);
+
+				if (clip) startScissor(divBounds);
+
+				if (loading) {
+					if (noSpinner) {
+						if (!widget.loadingScreen) widget.loadingScreen = new LoadingScreen(renderer);
+						widget.loadingScreen!.drawInCoordinates(divOriginX, divOriginY);
+					}
+					if (clip) endScissor();
+					continue;
+				}
+
+				if (skeleton) {
+					if (mode === "inside") {
+						let { x: ax, y: ay, width: aw, height: ah } = bounds;
+						if (aw <= 0 || ah <= 0) continue;
+
+						// scale ratio
+						const scaleWidth = divWidthWorld / aw;
+						const scaleHeight = divHeightWorld / ah;
+
+						// default value is used for fit = none
+						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 === "width") {
+							ratioW = scaleWidth;
+							ratioH = scaleWidth;
+						} else if (fit === "height") {
+							ratioW = scaleHeight;
+							ratioH = scaleHeight;
+						} else if (fit === "contain") {
+							// if scaled height is bigger than div height, use height ratio instead
+							if (ah * scaleWidth > divHeightWorld) {
+								ratioW = scaleHeight;
+								ratioH = scaleHeight;
+							} else {
+								ratioW = scaleWidth;
+								ratioH = scaleWidth;
+							}
+						} else if (fit === "cover") {
+							if (ah * scaleWidth < divHeightWorld) {
+								ratioW = scaleHeight;
+								ratioH = scaleHeight;
+							} else {
+								ratioW = scaleWidth;
+								ratioH = scaleWidth;
+							}
+						} else if (fit === "scaleDown") {
+							if (aw > divWidthWorld || ah > divHeightWorld) {
+								if (ah * scaleWidth > divHeightWorld) {
+									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
+						divOriginX = divOriginX - boundsX;
+						divOriginY = divOriginY - boundsY;
+
+						if (fit !== "none") {
+							// scale the skeleton
+							skeleton.scaleX = ratioW;
+							skeleton.scaleY = ratioH;
+							skeleton.updateWorldTransform(Physics.update);
+						}
+					}
+
+					const worldOffsetX = divOriginX + offsetX + dragX;
+					const worldOffsetY = divOriginY + offsetY + dragY;
+
+					widget.worldX = worldOffsetX;
+					widget.worldY = worldOffsetY;
+
+					renderer.drawSkeleton(skeleton, pma, -1, -1, (vertices, size, vertexSize) => {
+						for (let i = 0; i < size; i += vertexSize) {
+							vertices[i] = vertices[i] + worldOffsetX;
+							vertices[i + 1] = vertices[i + 1] + worldOffsetY;
+						}
+					});
+
+					// drawing debug stuff
+					if (debug) {
+						// if (true) {
+						let { x: ax, y: ay, width: aw, height: ah } = bounds;
+
+						// show bounds and its center
+						if (isDraggable) {
+							renderer.rect(true,
+								ax * skeleton.scaleX + worldOffsetX,
+								ay * skeleton.scaleY + worldOffsetY,
+								aw * skeleton.scaleX,
+								ah * skeleton.scaleY,
+								transparentRed);
+						}
+
+						renderer.rect(false,
+							ax * skeleton.scaleX + worldOffsetX,
+							ay * skeleton.scaleY + worldOffsetY,
+							aw * skeleton.scaleX,
+							ah * skeleton.scaleY,
+							blue);
+						const bbCenterX = (ax + aw / 2) * skeleton.scaleX + worldOffsetX;
+						const bbCenterY = (ay + ah / 2) * skeleton.scaleY + worldOffsetY;
+						renderer.circle(true, bbCenterX, bbCenterY, 10, blue);
+
+						// show skeleton root
+						const root = skeleton.getRootBone()!;
+						renderer.circle(true, root.x + worldOffsetX, root.y + worldOffsetY, 10, red);
+
+						// show shifted origin
+						const originX = worldOffsetX - dragX - offsetX;
+						const originY = worldOffsetY - dragY - offsetY;
+						renderer.circle(true, originX, originY, 10, green);
+
+						// show line from origin to bounds center
+						renderer.line(originX, originY, bbCenterX, bbCenterY, green);
+					}
+
+					if (clip) endScissor();
+				}
+			}
+
+			renderer.end();
+		}
+
+		const updateBoneFollowers = () => {
+			for (const widget of this.widgets) {
+				if (widget.isOffScreenAndWasMoved() || !widget.skeleton) continue;
+
+				for (const boneFollower of widget.boneFollowerList) {
+					const { slot, bone, element, followAttachmentAttach, followRotation, followOpacity, followScale } = boneFollower;
+					const { worldX, worldY } = widget;
+					this.worldToScreen(this.tempFollowBoneVector, bone.worldX + worldX, bone.worldY + worldY);
+
+					if (Number.isNaN(this.tempFollowBoneVector.x)) continue;
+
+					let x = this.tempFollowBoneVector.x - this.overflowLeftSize;
+					let y = this.tempFollowBoneVector.y - this.overflowTopSize;
+
+					if (!this.appendedToBody) {
+						x += window.scrollX;
+						y += window.scrollY;
+					}
+
+					element.style.transform = `translate(calc(-50% + ${x.toFixed(2)}px),calc(-50% + ${y.toFixed(2)}px))`
+						+ (followRotation ? ` rotate(${-bone.getWorldRotationX()}deg)` : "")
+						+ (followScale ? ` scale(${bone.getWorldScaleX()}, ${bone.getWorldScaleY()})` : "")
+						;
+
+					element.style.display = ""
+
+					if (followAttachmentAttach && !slot.attachment) {
+						element.style.opacity = "0";
+					} else if (followOpacity) {
+						element.style.opacity = `${slot.color.a}`;
+					}
+
+				}
+			}
+		}
+
+		const loop = () => {
+			if (this.disposed || !this.isConnected) {
+				this.running = false;
+				return;
+			};
+			requestAnimationFrame(loop);
+			if (!this.loaded) return;
+			this.time.update();
+			this.translateCanvas();
+			updateWidgets();
+			renderWidgets();
+			updateBoneFollowers();
+		}
+
+		requestAnimationFrame(loop);
+		this.running = true;
+
+		const red = new Color(1, 0, 0, 1);
+		const green = new Color(0, 1, 0, 1);
+		const blue = new Color(0, 0, 1, 1);
+		const transparentWhite = new Color(1, 1, 1, .3);
+		const transparentRed = new Color(1, 0, 0, .3);
+	}
+
+	public cursorCanvasX = 1;
+	public cursorCanvasY = 1;
+	public cursorWorldX = 1;
+	public cursorWorldY = 1;
+
+	private tempVector = new Vector3();
+	private updateCursor (input: Point) {
+		this.cursorCanvasX = input.x - window.scrollX;
+		this.cursorCanvasY = input.y - window.scrollY;
+
+		if (this.appendedToBody) {
+			const ref = this.parentElement!.getBoundingClientRect();
+			this.cursorCanvasX -= ref.left;
+			this.cursorCanvasY -= ref.top;
+		}
+
+		let tempVector = this.tempVector;
+		tempVector.set(this.cursorCanvasX, this.cursorCanvasY, 0);
+		this.renderer.camera.screenToWorld(tempVector, this.canvas.clientWidth, this.canvas.clientHeight);
+
+		if (Number.isNaN(tempVector.x) || Number.isNaN(tempVector.y)) return;
+		this.cursorWorldX = tempVector.x;
+		this.cursorWorldY = tempVector.y;
+	}
+
+	private updateWidgetCursor (widget: SpineWebComponentSkeleton): boolean {
+		if (widget.worldX === Infinity) return false;
+
+		widget.cursorWorldX = this.cursorWorldX - widget.worldX;
+		widget.cursorWorldY = this.cursorWorldY - widget.worldY;
+
+		return true;
+	}
+
+	private setupDragUtility (): Input {
+		// TODO: we should use document - body might have some margin that offset the click events - Meanwhile I take event pageX/Y
+		const inputManager = new Input(document.body, false)
+		const inputPointTemp: Point = new Vector2();
+
+		const getInput = (ev?: MouseEvent | TouchEvent): Point => {
+			const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
+			inputPointTemp.x = originalEvent.pageX + this.overflowLeftSize;
+			inputPointTemp.y = originalEvent.pageY + this.overflowTopSize;
+			return inputPointTemp;
+		}
+
+		let lastX = 0;
+		let lastY = 0;
+		inputManager.addListener({
+			// moved is used to pass cursor position wrt to canvas and widget position and currently is EXPERIMENTAL
+			moved: (x, y, ev) => {
+				const input = getInput(ev);
+				this.updateCursor(input);
+
+				for (const widget of this.widgets) {
+					if (!this.updateWidgetCursor(widget) || !widget.onScreen) continue;
+
+					widget.cursorEventUpdate("move", ev);
+				}
+			},
+			down: (x, y, ev) => {
+				const input = getInput(ev);
+
+				this.updateCursor(input);
+
+				for (const widget of this.widgets) {
+					if (!this.updateWidgetCursor(widget) || widget.isOffScreenAndWasMoved()) continue;
+
+					widget.cursorEventUpdate("down", ev);
+
+					if ((widget.isInteractive && widget.cursorInsideBounds) || (!widget.isInteractive && widget.isCursorInsideBounds())) {
+						if (!widget.isDraggable) continue;
+
+						widget.dragging = true;
+						ev?.preventDefault();
+					}
+
+				}
+				lastX = input.x;
+				lastY = input.y;
+			},
+			dragged: (x, y, ev) => {
+				const input = getInput(ev);
+
+				let dragX = input.x - lastX;
+				let dragY = input.y - lastY;
+
+				this.updateCursor(input);
+
+				for (const widget of this.widgets) {
+					if (!this.updateWidgetCursor(widget) || widget.isOffScreenAndWasMoved()) continue;
+
+					widget.cursorEventUpdate("drag", ev);
+
+					if (!widget.dragging) continue;
+
+					const skeleton = widget.skeleton!;
+					widget.dragX += this.screenToWorldLength(dragX);
+					widget.dragY -= this.screenToWorldLength(dragY);
+					skeleton.physicsTranslate(dragX, -dragY);
+					ev?.preventDefault();
+					ev?.stopPropagation();
+				}
+				lastX = input.x;
+				lastY = input.y;
+			},
+			up: (x, y, ev) => {
+				for (const widget of this.widgets) {
+					widget.dragging = false;
+
+					if (widget.cursorInsideBounds) {
+						widget.cursorEventUpdate("up", ev);
+					}
+				}
+			}
+		});
+
+		return inputManager;
+	}
+
+	/*
+	* Resize/scroll utilities
+	*/
+
+	private updateCanvasSize () {
+		const { width, height } = this.getViewportSize();
+
+		// if the target width/height changes, resize the canvas.
+		if (this.lastCanvasBaseWidth !== width || this.lastCanvasBaseHeight !== height) {
+			this.lastCanvasBaseWidth = width;
+			this.lastCanvasBaseHeight = height;
+			this.overflowLeftSize = this.overflowLeft * width;
+			this.overflowTopSize = this.overflowTop * 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";
+			this.resize(totalWidth, totalHeight);
+		}
+
+		// 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(); is it better width/height to zero?
+		// this.div!.style.width = 0 + "px";
+		// this.div!.style.height = 0 + "px";
+		this.div!.style.display = "none";
+		if (!this.appendedToBody) {
+			const { width, height } = this.getPageSize();
+			this.div!.style.width = width + "px";
+			this.div!.style.height = height + "px";
+		} else {
+			if (this.hasCssTweakOff()) {
+				// this case lags if scrolls or position fixed
+				// users should never use tweak off, unless the parent container has already a transform
+				this.div!.style.width = this.parentElement!.clientWidth + "px";
+				this.div!.style.height = this.parentElement!.clientHeight + "px";
+				this.canvas.style.transform = `translate(${-this.overflowLeftSize}px,${-this.overflowTopSize}px)`;
+			} else {
+				this.div!.style.width = this.parentElement!.scrollWidth + "px";
+				this.div!.style.height = this.parentElement!.scrollHeight + "px";
+			}
+		}
+		this.div!.style.display = "";
+		// this.root.appendChild(this.div!);
+	}
+
+	private resize (width: number, height: number) {
+		let canvas = this.canvas;
+		canvas.width = Math.round(this.screenToWorldLength(width));
+		canvas.height = Math.round(this.screenToWorldLength(height));
+		this.renderer.context.gl.viewport(0, 0, canvas.width, canvas.height);
+		this.renderer.camera.setViewport(canvas.width, canvas.height);
+		this.renderer.camera.update();
+	}
+
+	// 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 () {
+		return document.body.getBoundingClientRect();
+	}
+
+	private lastViewportWidth = 0;
+	private lastViewportHeight = 0;
+	private lastDPR = 0;
+	private static readonly WIDTH_INCREMENT = 1.15;
+	private static readonly HEIGHT_INCREMENT = 1.2;
+	private static readonly MAX_CANVAS_WIDTH = 7000;
+	private static readonly MAX_CANVAS_HEIGHT = 7000;
+
+	// determine the target viewport width and height.
+	// The target width/height won't change if the viewport shrink to avoid useless re render (especially re render bursts on mobile)
+	private getViewportSize (): { width: number, height: number } {
+		if (this.appendedToBody) {
+			return {
+				width: this.parentElement!.clientWidth,
+				height: this.parentElement!.clientHeight,
+			}
+		}
+
+		let width = window.innerWidth;
+		let height = window.innerHeight;
+
+		const dpr = this.getDevicePixelRatio();
+		if (dpr !== this.lastDPR) {
+			this.lastDPR = dpr;
+			this.lastViewportWidth = this.lastViewportWidth === 0 ? width : width * SpineWebComponentOverlay.WIDTH_INCREMENT;
+			this.lastViewportHeight = height * SpineWebComponentOverlay.HEIGHT_INCREMENT;
+
+			this.updateWidgetScales();
+		} else {
+			if (width > this.lastViewportWidth) this.lastViewportWidth = width * SpineWebComponentOverlay.WIDTH_INCREMENT;
+			if (height > this.lastViewportHeight) this.lastViewportHeight = height * SpineWebComponentOverlay.HEIGHT_INCREMENT;
+		}
+
+		// if the resulting canvas width/height is too high, scale the DPI
+		if (this.lastViewportHeight * (1 + this.overflowTop + this.overflowBottom) * dpr > SpineWebComponentOverlay.MAX_CANVAS_HEIGHT ||
+			this.lastViewportWidth * (1 + this.overflowLeft + this.overflowRight) * dpr > SpineWebComponentOverlay.MAX_CANVAS_WIDTH) {
+			this.dprScale += .5;
+			return this.getViewportSize();
+		}
+
+		return {
+			width: this.lastViewportWidth,
+			height: this.lastViewportHeight,
+		}
+	}
+
+	/**
+	 * @internal
+	 */
+	public getDevicePixelRatio () {
+		return window.devicePixelRatio / this.dprScale;
+	}
+	private dprScale = 1;
+
+	private updateWidgetScales () {
+		for (const widget of this.widgets) {
+			// inside mode scale automatically to fit the skeleton within its parent
+			if (widget.mode !== "origin" && widget.fit !== "none") continue;
+
+			const skeleton = widget.skeleton;
+			if (!skeleton) continue;
+
+			// I'm not sure about this. With mode origin and fit none:
+			// case 1) If I comment this scale code, the skeleton is never scaled and will be always at the same size and won't change size while zooming
+			// case 2) Otherwise, the skeleton is loaded always at the same size, but changes size while zooming
+			const scale = this.getDevicePixelRatio();
+			skeleton.scaleX = skeleton.scaleX / widget.dprScale * scale;
+			skeleton.scaleY = skeleton.scaleY / widget.dprScale * scale;
+			widget.dprScale = scale;
+		}
+	}
+
+	private translateCanvas () {
+		let scrollPositionX = -this.overflowLeftSize;
+		let scrollPositionY = -this.overflowTopSize;
+
+		if (!this.appendedToBody) {
+			scrollPositionX += window.scrollX;
+			scrollPositionY += window.scrollY;
+		} else {
+
+			// Ideally this should be the only scrollable case (no-auto-parent-transform not enabled or at least an ancestor has transform)
+			// I'd like to get rid of the code below
+			if (!this.hasCssTweakOff()) {
+				scrollPositionX += this.parentElement!.scrollLeft;
+				scrollPositionY += this.parentElement!.scrollTop;
+			} else {
+				const { left, top } = this.parentElement!.getBoundingClientRect();
+				scrollPositionX += left + window.scrollX;
+				scrollPositionY += top + window.scrollY;
+
+				let offsetParent = this.offsetParent;
+				do {
+					if (offsetParent === document.body) break;
+
+					const htmlOffsetParentElement = offsetParent as HTMLElement;
+					if (htmlOffsetParentElement.style.position === "fixed" || htmlOffsetParentElement.style.position === "sticky" || htmlOffsetParentElement.style.position === "absolute") {
+						const parentRect = htmlOffsetParentElement.getBoundingClientRect();
+						this.div.style.transform = `translate(${left - parentRect.left}px,${top - parentRect.top}px)`;
+						return;
+					}
+
+					offsetParent = htmlOffsetParentElement.offsetParent;
+				} while (offsetParent);
+
+				this.div.style.transform = `translate(${scrollPositionX + this.overflowLeftSize}px,${scrollPositionY + this.overflowTopSize}px)`;
+				return;
+			}
+
+		}
+
+		this.canvas.style.transform = `translate(${scrollPositionX}px,${scrollPositionY}px)`;
+	}
+
+	/*
+	* Other utilities
+	*/
+	public 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.renderer.camera.screenToWorld(vec, this.canvas.clientWidth, this.canvas.clientHeight);
+	}
+	public worldToScreen (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.renderer.camera.worldToScreen(vec, this.canvas.clientWidth, this.canvas.clientHeight);
+		this.renderer.camera.worldToScreen(vec, this.worldToScreenLength(this.renderer.camera.viewportWidth), this.worldToScreenLength(this.renderer.camera.viewportHeight));
+	}
+	public screenToWorldLength (length: number) {
+		return length * this.getDevicePixelRatio();
+	}
+	public worldToScreenLength (length: number) {
+		return length / this.getDevicePixelRatio();
+	}
+}
+
+customElements.define("spine-overlay", SpineWebComponentOverlay);
diff --git a/spine-ts/spine-widget/src/SpineWebComponentWidget.ts b/spine-ts/spine-widget/src/SpineWebComponentSkeleton.ts
similarity index 51%
rename from spine-ts/spine-widget/src/SpineWebComponentWidget.ts
rename to spine-ts/spine-widget/src/SpineWebComponentSkeleton.ts
index 6940a88a6..5e8f1abb4 100644
--- a/spine-ts/spine-widget/src/SpineWebComponentWidget.ts
+++ b/spine-ts/spine-widget/src/SpineWebComponentSkeleton.ts
@@ -32,24 +32,17 @@ import {
 	AnimationState,
 	AnimationStateData,
 	AtlasAttachmentLoader,
-	AssetManager,
-	Color,
 	Disposable,
-	Input,
 	LoadingScreen,
-	ManagedWebGLRenderingContext,
 	MixBlend,
 	MixDirection,
 	Physics,
-	SceneRenderer,
 	SkeletonBinary,
 	SkeletonData,
 	SkeletonJson,
 	Skeleton,
 	TextureAtlas,
-	TimeKeeper,
 	Vector2,
-	Vector3,
 	Utils,
 	NumberArrayLike,
 	Slot,
@@ -57,108 +50,16 @@ import {
 	MeshAttachment,
 	Bone,
 } from "@esotericsoftware/spine-webgl";
-
-interface Point {
-	x: number,
-	y: number,
-}
-
-interface Rectangle extends Point {
-	width: number,
-	height: number,
-}
+import { AttributeTypes, castValue, isBase64, Rectangle } from "./wcUtils";
+import { SpineWebComponentOverlay } from "./SpineWebComponentOverlay";
 
 type UpdateSpineWidgetFunction = (delta: number, skeleton: Skeleton, state: AnimationState) => void;
 
 export type OffScreenUpdateBehaviourType = "pause" | "update" | "pose";
-function isOffScreenUpdateBehaviourType (value: string | null): value is OffScreenUpdateBehaviourType {
-	return (
-		value === "pause" ||
-		value === "update" ||
-		value === "pose"
-	);
-}
-
 export type ModeType = "inside" | "origin";
-function isModeType (value: string | null): value is ModeType {
-	return (
-		value === "inside" ||
-		value === "origin"
-	);
-}
-
 export type FitType = "fill" | "width" | "height" | "contain" | "cover" | "none" | "scaleDown";
-function isFitType (value: string | null): value is FitType {
-	return (
-		value === "fill" ||
-		value === "width" ||
-		value === "height" ||
-		value === "contain" ||
-		value === "cover" ||
-		value === "none" ||
-		value === "scaleDown"
-	);
-}
-
-const animatonTypeRegExp = /\[([^\]]+)\]/g;
-
 export type AnimationsInfo = Record }>;
 export type AnimationsType = { animationName: string | "#EMPTY#", loop?: boolean, delay?: number, mixDuration?: number };
-
-function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined {
-	if (value === null) {
-		return undefined;
-	}
-
-	const matches = value.match(animatonTypeRegExp);
-	if (!matches) return undefined;
-
-	return matches.reduce((obj, group) => {
-		const [trackIndexStringOrLoopDefinition, animationNameOrTrackIndexStringCycle, loop, delayString, mixDurationString] = group.slice(1, -1).split(',').map(v => v.trim());
-
-		if (trackIndexStringOrLoopDefinition === "loop") {
-			if (!Number.isInteger(Number(animationNameOrTrackIndexStringCycle))) {
-				throw new Error(`Track index of cycle in ${group} must be a positive integer number, instead it is ${animationNameOrTrackIndexStringCycle}. Original value: ${value}`);
-			}
-			const animationInfoObject = obj[animationNameOrTrackIndexStringCycle] ||= { animations: [] };
-			animationInfoObject.cycle = true;
-			return obj;
-		}
-
-		const trackIndex = Number(trackIndexStringOrLoopDefinition);
-		if (!Number.isInteger(trackIndex)) {
-			throw new Error(`Track index in ${group} must be a positive integer number, instead it is ${trackIndexStringOrLoopDefinition}. Original value: ${value}`);
-		}
-
-		let delay;
-		if (delayString !== undefined) {
-			delay = parseFloat(delayString);
-			if (isNaN(delay)) {
-				throw new Error(`Delay in ${group} must be a positive number, instead it is ${delayString}. Original value: ${value}`);
-			}
-		}
-
-		let mixDuration;
-		if (mixDurationString !== undefined) {
-			mixDuration = parseFloat(mixDurationString);
-			if (isNaN(mixDuration)) {
-				throw new Error(`mixDuration in ${group} must be a positive number, instead it is ${mixDurationString}. Original value: ${value}`);
-			}
-		}
-
-		const animationInfoObject = obj[trackIndexStringOrLoopDefinition] ||= { animations: [] };
-		animationInfoObject.animations.push({
-			animationName: animationNameOrTrackIndexStringCycle,
-			loop: loop.trim().toLowerCase() === "true",
-			delay,
-			mixDuration,
-		});
-		return obj;
-	}, {} as AnimationsInfo);
-}
-
-export type AttributeTypes = "string" | "number" | "boolean" | "array-number" | "array-string" | "object" | "fitType" | "modeType" | "offScreenUpdateBehaviourType" | "animationsInfo";
-
 export type CursorEventType = "down" | "up" | "enter" | "leave" | "move" | "drag";
 export type CursorEventTypesInput = Exclude;
 
@@ -208,7 +109,7 @@ interface WidgetOverridableMethods {
 	update?: UpdateSpineWidgetFunction;
 	beforeUpdateWorldTransforms: UpdateSpineWidgetFunction;
 	afterUpdateWorldTransforms: UpdateSpineWidgetFunction;
-	onScreenFunction: (widget: SpineWebComponentWidget) => void
+	onScreenFunction: (widget: SpineWebComponentSkeleton) => void
 }
 
 // Properties that does not map to any widget attribute, but that might be useful
@@ -218,7 +119,7 @@ interface WidgetPublicProperties {
 	bounds: Rectangle
 	onScreen: boolean
 	onScreenAtLeastOnce: boolean
-	whenReady: Promise
+	whenReady: Promise
 	loading: boolean
 	started: boolean
 	textureAtlas: TextureAtlas
@@ -234,7 +135,7 @@ interface WidgetInternalProperties {
 	dragY: number
 }
 
-export class SpineWebComponentWidget extends HTMLElement implements Disposable, WidgetAttributes, WidgetOverridableMethods, WidgetInternalProperties, Partial {
+export class SpineWebComponentSkeleton extends HTMLElement implements Disposable, WidgetAttributes, WidgetOverridableMethods, WidgetInternalProperties, Partial {
 
 	/**
 	 * If true, enables a top-left span showing FPS (it has black text)
@@ -668,7 +569,7 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
 	 * By default, the callback call the {@link start} method the first time the widget
 	 * enters the screen viewport.
 	 */
-	public onScreenFunction: (widget: SpineWebComponentWidget) => void = async (widget) => {
+	public onScreenFunction: (widget: SpineWebComponentSkeleton) => void = async (widget) => {
 		if (widget.loading && !widget.onScreenAtLeastOnce) {
 			widget.onScreenAtLeastOnce = true;
 
@@ -841,7 +742,7 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
 	}
 
 	static get observedAttributes (): string[] {
-		return Object.keys(SpineWebComponentWidget.attributesDescription);
+		return Object.keys(SpineWebComponentSkeleton.attributesDescription);
 	}
 
 	constructor () {
@@ -910,7 +811,7 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
 	}
 
 	attributeChangedCallback (name: string, oldValue: string | null, newValue: string | null): void {
-		const { type, propertyName, defaultValue } = SpineWebComponentWidget.attributesDescription[name];
+		const { type, propertyName, defaultValue } = SpineWebComponentSkeleton.attributesDescription[name];
 		const val = castValue(type, newValue, defaultValue);
 		(this as any)[propertyName] = val;
 		return;
@@ -1202,7 +1103,6 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
 	 */
 	public cursorInsideBounds = false;
 
-	private pointTemp = new Vector2();
 	private verticesTemp = Utils.newFloatArray(2 * 1024);
 
 	/**
@@ -1249,12 +1149,15 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
 	public isCursorInsideBounds (): boolean {
 		if (this.isOffScreenAndWasMoved() || !this.skeleton) return false;
 
-		this.pointTemp.set(
-			this.cursorWorldX / this.skeleton.scaleX,
-			this.cursorWorldY / this.skeleton.scaleY,
-		);
+		const x = this.cursorWorldX / this.skeleton.scaleX;
+		const y = this.cursorWorldY / this.skeleton.scaleY;
 
-		return inside(this.pointTemp, this.bounds);
+		return (
+			x >= this.bounds.x &&
+			x <= this.bounds.x + this.bounds.width &&
+			y >= this.bounds.y &&
+			y <= this.bounds.y + this.bounds.height
+		);
 	}
 
 	private checkSlotInteraction (type: CursorEventTypesInput, originalEvent?: UIEvent) {
@@ -1427,1012 +1330,16 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable,
 
 }
 
-interface OverlayAttributes {
-	overlayId?: string
-	noAutoParentTransform: boolean
-	overflowTop: number
-	overflowBottom: number
-	overflowLeft: number
-	overflowRight: number
+customElements.define("spine-skeleton", SpineWebComponentSkeleton);
+
+export function getSpineWidget (identifier: string): SpineWebComponentSkeleton {
+    return document.querySelector(`spine-skeleton[identifier=${identifier}]`) as SpineWebComponentSkeleton;
 }
 
-class SpineWebComponentOverlay extends HTMLElement implements OverlayAttributes, Disposable {
-	private static OVERLAY_ID = "spine-overlay-default-identifier";
-	private static OVERLAY_LIST = new Map();
+export function createSpineWidget (parameters: WidgetAttributes): SpineWebComponentSkeleton {
+	const widget = document.createElement("spine-skeleton") as SpineWebComponentSkeleton;
 
-	/**
-	 * @internal
-	 */
-	static getOrCreateOverlay (overlayId: string | null): SpineWebComponentOverlay {
-		let overlay = SpineWebComponentOverlay.OVERLAY_LIST.get(overlayId || SpineWebComponentOverlay.OVERLAY_ID);
-		if (!overlay) {
-			overlay = document.createElement('spine-overlay') as SpineWebComponentOverlay;
-			overlay.setAttribute('overlay-id', SpineWebComponentOverlay.OVERLAY_ID);
-			document.body.appendChild(overlay);
-		}
-		return overlay;
-	}
-
-	/**
-	 * A list holding the widgets added to this overlay.
-	 */
-	public widgets = new Array();
-
-	/**
-	 * The {@link SceneRenderer} used by this overlay.
-	 */
-	public renderer: SceneRenderer;
-
-	/**
-	 * The {@link AssetManager} used by this overlay.
-	 */
-	public assetManager: AssetManager;
-
-	/**
-	 * The identifier of this overlay. This is necessary when multiply overlay are created.
-	   * Connected to `overlay-id` attribute.
-	 */
-	public overlayId?: string;
-
-	/**
-	 * If `false` (default value), the overlay container style will be affected adding `transform: translateZ(0);` to it.
-	 * The `transform` is not affected if it already exists on the container.
-	 * This is necessary to make the scrolling works with containers that scroll in a different way with respect to the page, as explained in {@link appendedToBody}.
-	 * Connected to `no-auto-parent-transform` attribute.
-	 */
-	public noAutoParentTransform = false;
-
-	/**
-	 * The canvas is continuously translated so that it covers the viewport. This translation might be slightly slower during fast scrolling.
-	 * If the canvas has the same size as the viewport, while scrolling it might be slighlty misaligned with the viewport.
-	 * This parameter defines, as percentage of the viewport height, the pixels to add to the top of the canvas to prevent this effect.
-	 * Making the canvas too big might reduce performance.
-	 * Default value: 0.2.
-	 * Connected to `overflow-top` attribute.
-	 */
-	public overflowTop = .2;
-
-	/**
-	 * The canvas is continuously translated so that it covers the viewport. This translation might be slightly slower during fast scrolling.
-	 * If the canvas has the same size as the viewport, while scrolling it might be slighlty misaligned with the viewport.
-	 * This parameter defines, as percentage of the viewport height, the pixels to add to the bottom of the canvas to prevent this effect.
-	 * Making the canvas too big might reduce performance.
-	 * Default value: 0.
-	 * Connected to `overflow-bottom` attribute.
-	 */
-	public overflowBottom = .0;
-
-	/**
-	 * The canvas is continuously translated so that it covers the viewport. This translation might be slightly slower during fast scrolling.
-	 * If the canvas has the same size as the viewport, while scrolling it might be slighlty misaligned with the viewport.
-	 * This parameter defines, as percentage of the viewport width, the pixels to add to the left of the canvas to prevent this effect.
-	 * Making the canvas too big might reduce performance.
-	 * Default value: 0.
-	 * Connected to `overflow-left` attribute.
-	 */
-	public overflowLeft = .0;
-
-	/**
-	 * The canvas is continuously translated so that it covers the viewport. This translation might be slightly slower during fast scrolling.
-	 * If the canvas has the same size as the viewport, while scrolling it might be slighlty misaligned with the viewport.
-	 * This parameter defines, as percentage of the viewport width, the pixels to add to the right of the canvas to prevent this effect.
-	 * Making the canvas too big might reduce performance.
-	 * Default value: 0.
-	 * Connected to `overflow-right` attribute.
-	 */
-	public overflowRight = .0;
-
-	private root: ShadowRoot;
-
-	private div: HTMLDivElement;
-	private boneFollowersParent: HTMLDivElement;
-	private canvas: HTMLCanvasElement;
-	private fps: HTMLSpanElement;
-	private fpsAppended = false;
-
-	private intersectionObserver?: IntersectionObserver;
-	private resizeObserver?: ResizeObserver;
-	private input?: Input;
-
-	private overflowLeftSize = 0;
-	private overflowTopSize = 0;
-
-	private lastCanvasBaseWidth = 0;
-	private lastCanvasBaseHeight = 0;
-
-	private disposed = false;
-	private loaded = false;
-
-	/**
-	 * appendedToBody is assegned in the connectedCallback.
-	 * When true, the overlay will have the size of the element container in contrast to the default behaviour where the
-	 * overlay has always the size of the screen.
-	 * This is necessary when the overlay is inserted into a container that scroll in a different way with respect to the page.
-	 * Otherwise the following problems might occur:
-	 * 1) For containers appendedToBody, the widget will be slightly slower to scroll than the html behind. The effect is more evident for lower refresh rate display.
-	 * 2) For containers appendedToBody, the widget will overflow the container bounds until the widget html element container is visible
-	 * 3) For fixed containers, the widget will scroll in a jerky way
-	 *
-	 * In order to fix this behaviour, it is necessary to insert a dedicated `spine-overlay` webcomponent as a direct child of the container.
-	 * Moreover, it is necessary to perform the following actions:
-	 * 1) The scrollable container must have a `transform` css attribute. If it hasn't this attribute the `spine-overlay` will add it for you.
-	 * If your scrollable container has already this css attribute, or if you prefer to add it by yourself (example: `transform: translateZ(0);`), set the `no-auto-parent-transform` to the `spine-overlay`.
-	 * 2) The `spine-overlay` must have an `overlay-id` attribute. Choose the value you prefer.
-	 * 3) Each `spine-widget` must have an `overlay-id` attribute. The same as the hosting `spine-overlay`.
-	   * Connected to `scrollable` attribute.
-	 */
-	private appendedToBody = true;
-
-	readonly time = new TimeKeeper();
-
-	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.1)";
-
-		this.root.appendChild(this.div);
-
-		this.canvas = document.createElement("canvas");
-		this.boneFollowersParent = document.createElement("div");
-
-		this.div.appendChild(this.canvas);
-		this.canvas.style.position = "absolute";
-		this.canvas.style.top = "0";
-		this.canvas.style.left = "0";
-
-		this.div.appendChild(this.boneFollowersParent);
-		this.boneFollowersParent.style.position = "absolute";
-		this.boneFollowersParent.style.top = "0";
-		this.boneFollowersParent.style.left = "0";
-		this.boneFollowersParent.style.whiteSpace = "nowrap";
-		this.boneFollowersParent.style.setProperty("pointer-events", "none");
-		this.boneFollowersParent.style.transform = `translate(0px,0px)`;
-
-		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";
-
-		const context = new ManagedWebGLRenderingContext(this.canvas, { alpha: true });
-		this.renderer = new SceneRenderer(this.canvas, context);
-
-		this.assetManager = new AssetManager(context);
-	}
-
-	connectedCallback (): void {
-		this.appendedToBody = this.parentElement !== document.body;
-
-		let overlayId = this.getAttribute('overlay-id');
-		if (!overlayId) {
-			overlayId = SpineWebComponentOverlay.OVERLAY_ID;
-			this.setAttribute('overlay-id', overlayId);
-		}
-		const existingOverlay = SpineWebComponentOverlay.OVERLAY_LIST.get(overlayId);
-		if (existingOverlay && existingOverlay !== this) {
-			throw new Error(`"SpineWebComponentOverlay - You cannot have two spine-overlay with the same overlay-id: ${overlayId}"`);
-		}
-		SpineWebComponentOverlay.OVERLAY_LIST.set(overlayId, this);
-		// window.addEventListener("scroll", this.scrolledCallback);
-
-		if (document.readyState !== "complete") {
-			window.addEventListener("load", this.loadedCallback);
-		} else {
-			this.loadedCallback();
-		}
-
-		window.screen.orientation.addEventListener('change', this.orientationChangedCallback);
-
-		this.intersectionObserver = new IntersectionObserver((widgets) => {
-			for (const elem of widgets) {
-				const { target, intersectionRatio } = elem;
-				let { isIntersecting } = elem;
-				for (const widget of this.widgets) {
-					if (widget.getHostElement() != target) continue;
-
-					// old browsers do not have isIntersecting
-					if (isIntersecting === undefined) {
-						isIntersecting = intersectionRatio > 0;
-					}
-
-					widget.onScreen = isIntersecting;
-					if (isIntersecting) {
-						widget.onScreenFunction(widget);
-					}
-				}
-			}
-		}, { rootMargin: "30px 20px 30px 20px" });
-
-		// resize observer is supported by all major browsers today chrome started to support it in version 64 (early 2018)
-		// we cannot use window.resize event since it does not fire when body resizes, but not the window
-		// Alternatively, we can store the body size, check the current body size in the loop (like the translateCanvas), and
-		// if they differs call the resizeCallback. I already tested it, and it works. ResizeObserver should be more efficient.
-		if (this.appendedToBody) {
-			// if the element is scrollable, the user does not disable translate tweak, and the parent did not have already a transform, add the tweak
-			if (this.appendedToBody && !this.noAutoParentTransform && getComputedStyle(this.parentElement!).transform === "none") {
-				this.parentElement!.style.transform = `translateZ(0)`;
-			}
-			this.resizeObserver = new ResizeObserver(this.resizedCallback);
-			this.resizeObserver.observe(this.parentElement!);
-		} else {
-			window.addEventListener("resize", this.resizedCallback)
-		}
-
-		for (const widget of this.widgets) {
-			this.intersectionObserver?.observe(widget.getHostElement());
-		}
-		this.input = this.setupDragUtility();
-
-		this.startRenderingLoop();
-	}
-
-	private hasCssTweakOff () {
-		return this.noAutoParentTransform && getComputedStyle(this.parentElement!).transform === "none";
-	}
-
-	private running = false;
-	disconnectedCallback (): void {
-		const id = this.getAttribute('overlay-id');
-		if (id) SpineWebComponentOverlay.OVERLAY_LIST.delete(id);
-		// window.removeEventListener("scroll", this.scrolledCallback);
-		window.removeEventListener("load", this.loadedCallback);
-		window.removeEventListener("resize", this.resizedCallback);
-		window.screen.orientation.removeEventListener('change', this.orientationChangedCallback);
-		this.intersectionObserver?.disconnect();
-		this.resizeObserver?.disconnect();
-		this.input?.dispose();
-	}
-
-
-	static attributesDescription: Record = {
-		"overlay-id": { propertyName: "overlayId", type: "string" },
-		"no-auto-parent-transform": { propertyName: "noAutoParentTransform", type: "boolean" },
-		"overflow-top": { propertyName: "overflowTop", type: "number" },
-		"overflow-bottom": { propertyName: "overflowBottom", type: "number" },
-		"overflow-left": { propertyName: "overflowLeft", type: "number" },
-		"overflow-right": { propertyName: "overflowRight", type: "number" },
-	}
-
-	static get observedAttributes (): string[] {
-		return Object.keys(SpineWebComponentOverlay.attributesDescription);
-	}
-
-	attributeChangedCallback (name: string, oldValue: string | null, newValue: string | null): void {
-		const { type, propertyName, defaultValue } = SpineWebComponentOverlay.attributesDescription[name];
-		const val = castValue(type, newValue, defaultValue);
-		(this as any)[propertyName] = val;
-		return;
-	}
-
-	private resizedCallback = () => {
-		this.updateCanvasSize();
-	}
-
-	private orientationChangedCallback = () => {
-		this.updateCanvasSize();
-		// after an orientation change the scrolling changes, but the scroll event does not fire
-		this.scrolledCallback();
-	}
-
-	// right now, we scroll the canvas each frame before rendering loop, that makes scrolling on mobile waaay more smoother
-	// this is way scroll handler do nothing
-	private scrolledCallback = () => {
-		// this.translateCanvas();
-	}
-
-	private loadedCallback = () => {
-		this.updateCanvasSize();
-		this.scrolledCallback();
-		if (!this.loaded) {
-			this.loaded = true;
-			this.parentElement!.appendChild(this);
-		}
-	}
-
-	/**
-	 * Remove the overlay from the DOM, dispose all the contained widgets, and dispose the renderer.
-	 */
-	dispose (): void {
-		for (const widget of [...this.widgets]) widget.dispose();
-
-		this.remove();
-		this.widgets.length = 0;
-		this.renderer.dispose();
-		this.disposed = true;
-		this.assetManager.dispose();
-	}
-
-	/**
-	 * Add the widget to the overlay.
-	 * If the widget is after the overlay in the DOM, the overlay is appended after the widget.
-	 * @param widget The widget to add to the overlay
-	 */
-	addWidget (widget: SpineWebComponentWidget) {
-		this.widgets.push(widget);
-		this.intersectionObserver?.observe(widget.getHostElement());
-		if (this.loaded) {
-			const comparison = this.compareDocumentPosition(widget);
-			// DOCUMENT_POSITION_DISCONNECTED is needed when a widget is inside the overlay (due to followBone)
-			if ((comparison & Node.DOCUMENT_POSITION_FOLLOWING) && !(comparison & Node.DOCUMENT_POSITION_DISCONNECTED)) {
-				this.parentElement!.appendChild(this);
-			}
-		}
-	}
-
-	/**
-	 * Remove the widget from the overlay.
-	 * @param widget The widget to remove from the overlay
-	 */
-	removeWidget (widget: SpineWebComponentWidget) {
-		const index = this.widgets.findIndex(w => w === widget);
-		if (index === -1) return false;
-
-		this.widgets.splice(index);
-		this.intersectionObserver?.unobserve(widget.getHostElement());
-		return true;
-	}
-
-	addSlotFollowerElement (element: HTMLElement) {
-		this.boneFollowersParent.appendChild(element);
-		this.resizedCallback();
-	}
-
-	private tempFollowBoneVector = new Vector3();
-	private startRenderingLoop () {
-		if (this.running) return;
-
-		const updateWidgets = () => {
-			const delta = this.time.delta;
-			for (const { skeleton, state, update, onScreen, offScreenUpdateBehaviour, beforeUpdateWorldTransforms, afterUpdateWorldTransforms } of this.widgets) {
-				if (!skeleton || !state) continue;
-				if (!onScreen && offScreenUpdateBehaviour === "pause") continue;
-				if (update) update(delta, skeleton, state)
-				else {
-					// delta = 0
-					state.update(delta);
-					skeleton.update(delta);
-
-					if (onScreen || (!onScreen && offScreenUpdateBehaviour === "pose")) {
-						state.apply(skeleton);
-						beforeUpdateWorldTransforms(delta, skeleton, state);
-						skeleton.updateWorldTransform(Physics.update);
-						afterUpdateWorldTransforms(delta, skeleton, state);
-					}
-				}
-			}
-
-			// fps top-left span
-			if (SpineWebComponentWidget.SHOW_FPS) {
-				if (!this.fpsAppended) {
-					this.div.appendChild(this.fps);
-					this.fpsAppended = true;
-				}
-				this.fps.innerText = this.time.framesPerSecond.toFixed(2) + " fps";
-			} else {
-				if (this.fpsAppended) {
-					this.div.removeChild(this.fps);
-					this.fpsAppended = false;
-				}
-			}
-		};
-
-		const clear = (r: number, g: number, b: number, a: number) => {
-			this.renderer.context.gl.clearColor(r, g, b, a);
-			this.renderer.context.gl.clear(this.renderer.context.gl.COLOR_BUFFER_BIT);
-		}
-
-		const startScissor = (divBounds: Rectangle) => {
-			this.renderer.end();
-			this.renderer.begin();
-			this.renderer.context.gl.enable(this.renderer.context.gl.SCISSOR_TEST);
-			this.renderer.context.gl.scissor(
-				this.screenToWorldLength(divBounds.x),
-				this.canvas.height - this.screenToWorldLength(divBounds.y + divBounds.height),
-				this.screenToWorldLength(divBounds.width),
-				this.screenToWorldLength(divBounds.height)
-			);
-		}
-
-		const endScissor = () => {
-			this.renderer.end();
-			this.renderer.context.gl.disable(this.renderer.context.gl.SCISSOR_TEST);
-			this.renderer.begin();
-		}
-
-		const renderWidgets = () => {
-			clear(0, 0, 0, 0);
-			let renderer = this.renderer;
-			renderer.begin();
-
-			let ref: DOMRect;
-			let offsetLeftForOevrlay = 0;
-			let offsetTopForOverlay = 0;
-			if (this.appendedToBody) {
-				ref = this.parentElement!.getBoundingClientRect();
-				const computedStyle = getComputedStyle(this.parentElement!);
-				offsetLeftForOevrlay = ref.left + parseFloat(computedStyle.borderLeftWidth);
-				offsetTopForOverlay = ref.top + parseFloat(computedStyle.borderTopWidth);
-			}
-
-			const tempVector = new Vector3();
-			for (const widget of this.widgets) {
-				const { skeleton, pma, bounds, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY, fit, noSpinner, onScreen, loading, clip, isDraggable } = widget;
-
-				if (widget.isOffScreenAndWasMoved()) continue;
-				const elementRef = widget.getHostElement();
-				const divBounds = elementRef.getBoundingClientRect();
-				// need to use left and top, because x and y are not available on older browser
-				divBounds.x = divBounds.left + this.overflowLeftSize;
-				divBounds.y = divBounds.top + this.overflowTopSize;
-
-				if (this.appendedToBody) {
-					divBounds.x -= offsetLeftForOevrlay;
-					divBounds.y -= offsetTopForOverlay;
-				}
-
-				const { padLeft, padRight, padTop, padBottom } = widget
-				const paddingShiftHorizontal = (padLeft - padRight) / 2;
-				const paddingShiftVertical = (padTop - padBottom) / 2;
-
-				// get the desired point into the the div (center by default) in world coordinate
-				const divX = divBounds.x + divBounds.width * ((xAxis + .5) + paddingShiftHorizontal);
-				const divY = divBounds.y + divBounds.height * ((-yAxis + .5) + paddingShiftVertical) - 1;
-				this.screenToWorld(tempVector, divX, divY);
-				let divOriginX = tempVector.x;
-				let divOriginY = tempVector.y;
-
-				const paddingShrinkWidth = 1 - (padLeft + padRight);
-				const paddingShrinkHeight = 1 - (padTop + padBottom);
-				const divWidthWorld = this.screenToWorldLength(divBounds.width * paddingShrinkWidth);
-				const divHeightWorld = this.screenToWorldLength(divBounds.height * paddingShrinkHeight);
-
-				if (clip) startScissor(divBounds);
-
-				if (loading) {
-					if (noSpinner) {
-						if (!widget.loadingScreen) widget.loadingScreen = new LoadingScreen(renderer);
-						widget.loadingScreen!.drawInCoordinates(divOriginX, divOriginY);
-					}
-					if (clip) endScissor();
-					continue;
-				}
-
-				if (skeleton) {
-					if (mode === "inside") {
-						let { x: ax, y: ay, width: aw, height: ah } = bounds;
-						if (aw <= 0 || ah <= 0) continue;
-
-						// scale ratio
-						const scaleWidth = divWidthWorld / aw;
-						const scaleHeight = divHeightWorld / ah;
-
-						// default value is used for fit = none
-						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 === "width") {
-							ratioW = scaleWidth;
-							ratioH = scaleWidth;
-						} else if (fit === "height") {
-							ratioW = scaleHeight;
-							ratioH = scaleHeight;
-						} else if (fit === "contain") {
-							// if scaled height is bigger than div height, use height ratio instead
-							if (ah * scaleWidth > divHeightWorld) {
-								ratioW = scaleHeight;
-								ratioH = scaleHeight;
-							} else {
-								ratioW = scaleWidth;
-								ratioH = scaleWidth;
-							}
-						} else if (fit === "cover") {
-							if (ah * scaleWidth < divHeightWorld) {
-								ratioW = scaleHeight;
-								ratioH = scaleHeight;
-							} else {
-								ratioW = scaleWidth;
-								ratioH = scaleWidth;
-							}
-						} else if (fit === "scaleDown") {
-							if (aw > divWidthWorld || ah > divHeightWorld) {
-								if (ah * scaleWidth > divHeightWorld) {
-									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
-						divOriginX = divOriginX - boundsX;
-						divOriginY = divOriginY - boundsY;
-
-						if (fit !== "none") {
-							// scale the skeleton
-							skeleton.scaleX = ratioW;
-							skeleton.scaleY = ratioH;
-							skeleton.updateWorldTransform(Physics.update);
-						}
-					}
-
-					const worldOffsetX = divOriginX + offsetX + dragX;
-					const worldOffsetY = divOriginY + offsetY + dragY;
-
-					widget.worldX = worldOffsetX;
-					widget.worldY = worldOffsetY;
-
-					renderer.drawSkeleton(skeleton, pma, -1, -1, (vertices, size, vertexSize) => {
-						for (let i = 0; i < size; i += vertexSize) {
-							vertices[i] = vertices[i] + worldOffsetX;
-							vertices[i + 1] = vertices[i + 1] + worldOffsetY;
-						}
-					});
-
-					// drawing debug stuff
-					if (debug) {
-						// if (true) {
-						let { x: ax, y: ay, width: aw, height: ah } = bounds;
-
-						// show bounds and its center
-						if (isDraggable) {
-							renderer.rect(true,
-								ax * skeleton.scaleX + worldOffsetX,
-								ay * skeleton.scaleY + worldOffsetY,
-								aw * skeleton.scaleX,
-								ah * skeleton.scaleY,
-								transparentRed);
-						}
-
-						renderer.rect(false,
-							ax * skeleton.scaleX + worldOffsetX,
-							ay * skeleton.scaleY + worldOffsetY,
-							aw * skeleton.scaleX,
-							ah * skeleton.scaleY,
-							blue);
-						const bbCenterX = (ax + aw / 2) * skeleton.scaleX + worldOffsetX;
-						const bbCenterY = (ay + ah / 2) * skeleton.scaleY + worldOffsetY;
-						renderer.circle(true, bbCenterX, bbCenterY, 10, blue);
-
-						// show skeleton root
-						const root = skeleton.getRootBone()!;
-						renderer.circle(true, root.x + worldOffsetX, root.y + worldOffsetY, 10, red);
-
-						// show shifted origin
-						const originX = worldOffsetX - dragX - offsetX;
-						const originY = worldOffsetY - dragY - offsetY;
-						renderer.circle(true, originX, originY, 10, green);
-
-						// show line from origin to bounds center
-						renderer.line(originX, originY, bbCenterX, bbCenterY, green);
-					}
-
-					if (clip) endScissor();
-				}
-			}
-
-			renderer.end();
-		}
-
-		const updateBoneFollowers = () => {
-			for (const widget of this.widgets) {
-				if (widget.isOffScreenAndWasMoved() || !widget.skeleton) continue;
-
-				for (const boneFollower of widget.boneFollowerList) {
-					const { slot, bone, element, followAttachmentAttach, followRotation, followOpacity, followScale } = boneFollower;
-					const { worldX, worldY } = widget;
-					this.worldToScreen(this.tempFollowBoneVector, bone.worldX + worldX, bone.worldY + worldY);
-
-					if (Number.isNaN(this.tempFollowBoneVector.x)) continue;
-
-					let x = this.tempFollowBoneVector.x - this.overflowLeftSize;
-					let y = this.tempFollowBoneVector.y - this.overflowTopSize;
-
-					if (!this.appendedToBody) {
-						x += window.scrollX;
-						y += window.scrollY;
-					}
-
-					element.style.transform = `translate(calc(-50% + ${x.toFixed(2)}px),calc(-50% + ${y.toFixed(2)}px))`
-						+ (followRotation ? ` rotate(${-bone.getWorldRotationX()}deg)` : "")
-						+ (followScale ? ` scale(${bone.getWorldScaleX()}, ${bone.getWorldScaleY()})` : "")
-						;
-
-					element.style.display = ""
-
-					if (followAttachmentAttach && !slot.attachment) {
-						element.style.opacity = "0";
-					} else if (followOpacity) {
-						element.style.opacity = `${slot.color.a}`;
-					}
-
-				}
-			}
-		}
-
-		const loop = () => {
-			if (this.disposed || !this.isConnected) {
-				this.running = false;
-				return;
-			};
-			requestAnimationFrame(loop);
-			if (!this.loaded) return;
-			this.time.update();
-			this.translateCanvas();
-			updateWidgets();
-			renderWidgets();
-			updateBoneFollowers();
-		}
-
-		requestAnimationFrame(loop);
-		this.running = true;
-
-		const red = new Color(1, 0, 0, 1);
-		const green = new Color(0, 1, 0, 1);
-		const blue = new Color(0, 0, 1, 1);
-		const transparentWhite = new Color(1, 1, 1, .3);
-		const transparentRed = new Color(1, 0, 0, .3);
-	}
-
-	public cursorCanvasX = 1;
-	public cursorCanvasY = 1;
-	public cursorWorldX = 1;
-	public cursorWorldY = 1;
-
-	private tempVector = new Vector3();
-	private updateCursor (input: Point) {
-		this.cursorCanvasX = input.x - window.scrollX;
-		this.cursorCanvasY = input.y - window.scrollY;
-
-		if (this.appendedToBody) {
-			const ref = this.parentElement!.getBoundingClientRect();
-			this.cursorCanvasX -= ref.left;
-			this.cursorCanvasY -= ref.top;
-		}
-
-		let tempVector = this.tempVector;
-		tempVector.set(this.cursorCanvasX, this.cursorCanvasY, 0);
-		this.renderer.camera.screenToWorld(tempVector, this.canvas.clientWidth, this.canvas.clientHeight);
-
-		if (Number.isNaN(tempVector.x) || Number.isNaN(tempVector.y)) return;
-		this.cursorWorldX = tempVector.x;
-		this.cursorWorldY = tempVector.y;
-	}
-
-	private updateWidgetCursor (widget: SpineWebComponentWidget): boolean {
-		if (widget.worldX === Infinity) return false;
-
-		widget.cursorWorldX = this.cursorWorldX - widget.worldX;
-		widget.cursorWorldY = this.cursorWorldY - widget.worldY;
-
-		return true;
-	}
-
-	private setupDragUtility (): Input {
-		// TODO: we should use document - body might have some margin that offset the click events - Meanwhile I take event pageX/Y
-		const inputManager = new Input(document.body, false)
-		const inputPointTemp: Point = new Vector2();
-
-		const getInput = (ev?: MouseEvent | TouchEvent): Point => {
-			const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
-			inputPointTemp.x = originalEvent.pageX + this.overflowLeftSize;
-			inputPointTemp.y = originalEvent.pageY + this.overflowTopSize;
-			return inputPointTemp;
-		}
-
-		let lastX = 0;
-		let lastY = 0;
-		inputManager.addListener({
-			// moved is used to pass cursor position wrt to canvas and widget position and currently is EXPERIMENTAL
-			moved: (x, y, ev) => {
-				const input = getInput(ev);
-				this.updateCursor(input);
-
-				for (const widget of this.widgets) {
-					if (!this.updateWidgetCursor(widget) || !widget.onScreen) continue;
-
-					widget.cursorEventUpdate("move", ev);
-				}
-			},
-			down: (x, y, ev) => {
-				const input = getInput(ev);
-
-				this.updateCursor(input);
-
-				for (const widget of this.widgets) {
-					if (!this.updateWidgetCursor(widget) || widget.isOffScreenAndWasMoved()) continue;
-
-					widget.cursorEventUpdate("down", ev);
-
-					if ((widget.isInteractive && widget.cursorInsideBounds) || (!widget.isInteractive && widget.isCursorInsideBounds())) {
-						if (!widget.isDraggable) continue;
-
-						widget.dragging = true;
-						ev?.preventDefault();
-					}
-
-				}
-				lastX = input.x;
-				lastY = input.y;
-			},
-			dragged: (x, y, ev) => {
-				const input = getInput(ev);
-
-				let dragX = input.x - lastX;
-				let dragY = input.y - lastY;
-
-				this.updateCursor(input);
-
-				for (const widget of this.widgets) {
-					if (!this.updateWidgetCursor(widget) || widget.isOffScreenAndWasMoved()) continue;
-
-					widget.cursorEventUpdate("drag", ev);
-
-					if (!widget.dragging) continue;
-
-					const skeleton = widget.skeleton!;
-					widget.dragX += this.screenToWorldLength(dragX);
-					widget.dragY -= this.screenToWorldLength(dragY);
-					skeleton.physicsTranslate(dragX, -dragY);
-					ev?.preventDefault();
-					ev?.stopPropagation();
-				}
-				lastX = input.x;
-				lastY = input.y;
-			},
-			up: (x, y, ev) => {
-				for (const widget of this.widgets) {
-					widget.dragging = false;
-
-					if (widget.cursorInsideBounds) {
-						widget.cursorEventUpdate("up", ev);
-					}
-				}
-			}
-		});
-
-		return inputManager;
-	}
-
-	/*
-	* Resize/scroll utilities
-	*/
-
-	private updateCanvasSize () {
-		const { width, height } = this.getViewportSize();
-
-		// if the target width/height changes, resize the canvas.
-		if (this.lastCanvasBaseWidth !== width || this.lastCanvasBaseHeight !== height) {
-			this.lastCanvasBaseWidth = width;
-			this.lastCanvasBaseHeight = height;
-			this.overflowLeftSize = this.overflowLeft * width;
-			this.overflowTopSize = this.overflowTop * 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";
-			this.resize(totalWidth, totalHeight);
-		}
-
-		// 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(); is it better width/height to zero?
-		// this.div!.style.width = 0 + "px";
-		// this.div!.style.height = 0 + "px";
-		this.div!.style.display = "none";
-		if (!this.appendedToBody) {
-			const { width, height } = this.getPageSize();
-			this.div!.style.width = width + "px";
-			this.div!.style.height = height + "px";
-		} else {
-			if (this.hasCssTweakOff()) {
-				// this case lags if scrolls or position fixed
-				// users should never use tweak off, unless the parent container has already a transform
-				this.div!.style.width = this.parentElement!.clientWidth + "px";
-				this.div!.style.height = this.parentElement!.clientHeight + "px";
-				this.canvas.style.transform = `translate(${-this.overflowLeftSize}px,${-this.overflowTopSize}px)`;
-			} else {
-				this.div!.style.width = this.parentElement!.scrollWidth + "px";
-				this.div!.style.height = this.parentElement!.scrollHeight + "px";
-			}
-		}
-		this.div!.style.display = "";
-		// this.root.appendChild(this.div!);
-	}
-
-	private resize (width: number, height: number) {
-		let canvas = this.canvas;
-		canvas.width = Math.round(this.screenToWorldLength(width));
-		canvas.height = Math.round(this.screenToWorldLength(height));
-		this.renderer.context.gl.viewport(0, 0, canvas.width, canvas.height);
-		this.renderer.camera.setViewport(canvas.width, canvas.height);
-		this.renderer.camera.update();
-	}
-
-	// 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 () {
-		return document.body.getBoundingClientRect();
-	}
-
-	private lastViewportWidth = 0;
-	private lastViewportHeight = 0;
-	private lastDPR = 0;
-	private static readonly WIDTH_INCREMENT = 1.15;
-	private static readonly HEIGHT_INCREMENT = 1.2;
-	private static readonly MAX_CANVAS_WIDTH = 7000;
-	private static readonly MAX_CANVAS_HEIGHT = 7000;
-
-	// determine the target viewport width and height.
-	// The target width/height won't change if the viewport shrink to avoid useless re render (especially re render bursts on mobile)
-	private getViewportSize (): { width: number, height: number } {
-		if (this.appendedToBody) {
-			return {
-				width: this.parentElement!.clientWidth,
-				height: this.parentElement!.clientHeight,
-			}
-		}
-
-		let width = window.innerWidth;
-		let height = window.innerHeight;
-
-		const dpr = this.getDevicePixelRatio();
-		if (dpr !== this.lastDPR) {
-			this.lastDPR = dpr;
-			this.lastViewportWidth = this.lastViewportWidth === 0 ? width : width * SpineWebComponentOverlay.WIDTH_INCREMENT;
-			this.lastViewportHeight = height * SpineWebComponentOverlay.HEIGHT_INCREMENT;
-
-			this.updateWidgetScales();
-		} else {
-			if (width > this.lastViewportWidth) this.lastViewportWidth = width * SpineWebComponentOverlay.WIDTH_INCREMENT;
-			if (height > this.lastViewportHeight) this.lastViewportHeight = height * SpineWebComponentOverlay.HEIGHT_INCREMENT;
-		}
-
-		// if the resulting canvas width/height is too high, scale the DPI
-		if (this.lastViewportHeight * (1 + this.overflowTop + this.overflowBottom) * dpr > SpineWebComponentOverlay.MAX_CANVAS_HEIGHT ||
-			this.lastViewportWidth * (1 + this.overflowLeft + this.overflowRight) * dpr > SpineWebComponentOverlay.MAX_CANVAS_WIDTH) {
-			this.dprScale += .5;
-			return this.getViewportSize();
-		}
-
-		return {
-			width: this.lastViewportWidth,
-			height: this.lastViewportHeight,
-		}
-	}
-
-	/**
-	 * @internal
-	 */
-	public getDevicePixelRatio () {
-		return window.devicePixelRatio / this.dprScale;
-	}
-	private dprScale = 1;
-
-	private updateWidgetScales () {
-		for (const widget of this.widgets) {
-			// inside mode scale automatically to fit the skeleton within its parent
-			if (widget.mode !== "origin" && widget.fit !== "none") continue;
-
-			const skeleton = widget.skeleton;
-			if (!skeleton) continue;
-
-			// I'm not sure about this. With mode origin and fit none:
-			// case 1) If I comment this scale code, the skeleton is never scaled and will be always at the same size and won't change size while zooming
-			// case 2) Otherwise, the skeleton is loaded always at the same size, but changes size while zooming
-			const scale = this.getDevicePixelRatio();
-			skeleton.scaleX = skeleton.scaleX / widget.dprScale * scale;
-			skeleton.scaleY = skeleton.scaleY / widget.dprScale * scale;
-			widget.dprScale = scale;
-		}
-	}
-
-	private translateCanvas () {
-		let scrollPositionX = -this.overflowLeftSize;
-		let scrollPositionY = -this.overflowTopSize;
-
-		if (!this.appendedToBody) {
-			scrollPositionX += window.scrollX;
-			scrollPositionY += window.scrollY;
-		} else {
-
-			// Ideally this should be the only scrollable case (no-auto-parent-transform not enabled or at least an ancestor has transform)
-			// I'd like to get rid of the code below
-			if (!this.hasCssTweakOff()) {
-				scrollPositionX += this.parentElement!.scrollLeft;
-				scrollPositionY += this.parentElement!.scrollTop;
-			} else {
-				const { left, top } = this.parentElement!.getBoundingClientRect();
-				scrollPositionX += left + window.scrollX;
-				scrollPositionY += top + window.scrollY;
-
-				let offsetParent = this.offsetParent;
-				do {
-					if (offsetParent === document.body) break;
-
-					const htmlOffsetParentElement = offsetParent as HTMLElement;
-					if (htmlOffsetParentElement.style.position === "fixed" || htmlOffsetParentElement.style.position === "sticky" || htmlOffsetParentElement.style.position === "absolute") {
-						const parentRect = htmlOffsetParentElement.getBoundingClientRect();
-						this.div.style.transform = `translate(${left - parentRect.left}px,${top - parentRect.top}px)`;
-						return;
-					}
-
-					offsetParent = htmlOffsetParentElement.offsetParent;
-				} while (offsetParent);
-
-				this.div.style.transform = `translate(${scrollPositionX + this.overflowLeftSize}px,${scrollPositionY + this.overflowTopSize}px)`;
-				return;
-			}
-
-		}
-
-		this.canvas.style.transform = `translate(${scrollPositionX}px,${scrollPositionY}px)`;
-	}
-
-	/*
-	* Other utilities
-	*/
-	public 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.renderer.camera.screenToWorld(vec, this.canvas.clientWidth, this.canvas.clientHeight);
-	}
-	public worldToScreen (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.renderer.camera.worldToScreen(vec, this.canvas.clientWidth, this.canvas.clientHeight);
-		this.renderer.camera.worldToScreen(vec, this.worldToScreenLength(this.renderer.camera.viewportWidth), this.worldToScreenLength(this.renderer.camera.viewportHeight));
-	}
-	public screenToWorldLength (length: number) {
-		return length * this.getDevicePixelRatio();
-	}
-	public worldToScreenLength (length: number) {
-		return length / this.getDevicePixelRatio();
-	}
-}
-
-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): SpineWebComponentWidget {
-	return document.querySelector(`spine-widget[identifier=${identifier}]`) as SpineWebComponentWidget;
-}
-
-export function createSpineWidget (parameters: WidgetAttributes): SpineWebComponentWidget {
-	const widget = document.createElement("spine-widget") as SpineWebComponentWidget;
-
-	Object.entries(SpineWebComponentWidget.attributesDescription).forEach(entry => {
+	Object.entries(SpineWebComponentSkeleton.attributesDescription).forEach(entry => {
 		const [key, { propertyName }] = entry;
 		const value = parameters[propertyName];
 		if (value) widget.setAttribute(key, value as any);
@@ -2440,71 +1347,3 @@ export function createSpineWidget (parameters: WidgetAttributes): SpineWebCompon
 
 	return widget;
 }
-
-function castBoolean (value: string | null, defaultValue = "") {
-	return value === "true" || value === "" ? true : false;
-}
-
-function castString (value: string | null, defaultValue = "") {
-	return value === null ? defaultValue : value;
-}
-
-function castNumber (value: string | null, defaultValue = 0) {
-	if (value === null) return defaultValue;
-
-	const parsed = parseFloat(value);
-	if (Number.isNaN(parsed)) return defaultValue;
-	return parsed;
-}
-
-function castArrayNumber (value: string | null, defaultValue = undefined) {
-	if (value === null) return defaultValue;
-	return value.split(",").reduce((acc, pageIndex) => {
-		const index = parseInt(pageIndex);
-		if (!isNaN(index)) acc.push(index);
-		return acc;
-	}, [] as Array);
-}
-
-function castArrayString (value: string | null, defaultValue = undefined) {
-	if (value === null) return defaultValue;
-	return value.split(",");
-}
-
-function castObject (value: string | null, defaultValue = undefined) {
-	if (value === null) return null;
-	return JSON.parse(value);
-}
-
-const base64RegExp = /^(([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$/;
-function isBase64 (str: string) {
-	return base64RegExp.test(str);
-}
-
-function castValue (type: AttributeTypes, value: string | null, defaultValue?: any) {
-	switch (type) {
-		case "string":
-			return castString(value, defaultValue);
-		case "number":
-			return castNumber(value, defaultValue);
-		case "boolean":
-			return castBoolean(value, defaultValue);
-		case "array-number":
-			return castArrayNumber(value, defaultValue);
-		case "array-string":
-			return castArrayString(value, defaultValue);
-		case "object":
-			return castObject(value, defaultValue);
-		case "fitType":
-			return isFitType(value) ? value : defaultValue;
-		case "modeType":
-			return isModeType(value) ? value : defaultValue;
-		case "offScreenUpdateBehaviourType":
-			return isOffScreenUpdateBehaviourType(value) ? value : defaultValue;
-		case "animationsInfo":
-			return castToAnimationsInfo(value) || defaultValue;
-		default:
-			break;
-	}
-
-}
\ No newline at end of file
diff --git a/spine-ts/spine-widget/src/index.ts b/spine-ts/spine-widget/src/index.ts
index 2003bf32c..4c4406161 100644
--- a/spine-ts/spine-widget/src/index.ts
+++ b/spine-ts/spine-widget/src/index.ts
@@ -1,3 +1,4 @@
-export * from './SpineWebComponentWidget.js';
+export * from './SpineWebComponentSkeleton.js';
+export * from './SpineWebComponentOverlay.js';
 export * from "@esotericsoftware/spine-core";
-export * from "@esotericsoftware/spine-webgl";
\ No newline at end of file
+export * from "@esotericsoftware/spine-webgl";
diff --git a/spine-ts/spine-widget/src/wcUtils.ts b/spine-ts/spine-widget/src/wcUtils.ts
new file mode 100644
index 000000000..59b52f72d
--- /dev/null
+++ b/spine-ts/spine-widget/src/wcUtils.ts
@@ -0,0 +1,189 @@
+/******************************************************************************
+ * 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 { AnimationsInfo, FitType, ModeType, OffScreenUpdateBehaviourType } from "./SpineWebComponentSkeleton";
+
+const animatonTypeRegExp = /\[([^\]]+)\]/g;
+export type AttributeTypes = "string" | "number" | "boolean" | "array-number" | "array-string" | "object" | "fitType" | "modeType" | "offScreenUpdateBehaviourType" | "animationsInfo";
+
+export function castValue (type: AttributeTypes, value: string | null, defaultValue?: any) {
+	switch (type) {
+		case "string":
+			return castString(value, defaultValue);
+		case "number":
+			return castNumber(value, defaultValue);
+		case "boolean":
+			return castBoolean(value, defaultValue);
+		case "array-number":
+			return castArrayNumber(value, defaultValue);
+		case "array-string":
+			return castArrayString(value, defaultValue);
+		case "object":
+			return castObject(value, defaultValue);
+		case "fitType":
+			return isFitType(value) ? value : defaultValue;
+		case "modeType":
+			return isModeType(value) ? value : defaultValue;
+		case "offScreenUpdateBehaviourType":
+			return isOffScreenUpdateBehaviourType(value) ? value : defaultValue;
+		case "animationsInfo":
+			return castToAnimationsInfo(value) || defaultValue;
+		default:
+			break;
+	}
+}
+
+function castBoolean (value: string | null, defaultValue = "") {
+	return value === "true" || value === "" ? true : false;
+}
+
+function castString (value: string | null, defaultValue = "") {
+	return value === null ? defaultValue : value;
+}
+
+function castNumber (value: string | null, defaultValue = 0) {
+	if (value === null) return defaultValue;
+
+	const parsed = parseFloat(value);
+	if (Number.isNaN(parsed)) return defaultValue;
+	return parsed;
+}
+
+function castArrayNumber (value: string | null, defaultValue = undefined) {
+	if (value === null) return defaultValue;
+	return value.split(",").reduce((acc, pageIndex) => {
+		const index = parseInt(pageIndex);
+		if (!isNaN(index)) acc.push(index);
+		return acc;
+	}, [] as Array);
+}
+
+function castArrayString (value: string | null, defaultValue = undefined) {
+	if (value === null) return defaultValue;
+	return value.split(",");
+}
+
+function castObject (value: string | null, defaultValue = undefined) {
+	if (value === null) return null;
+	return JSON.parse(value);
+}
+
+
+function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined {
+	if (value === null) {
+		return undefined;
+	}
+
+	const matches = value.match(animatonTypeRegExp);
+	if (!matches) return undefined;
+
+	return matches.reduce((obj, group) => {
+		const [trackIndexStringOrLoopDefinition, animationNameOrTrackIndexStringCycle, loop, delayString, mixDurationString] = group.slice(1, -1).split(',').map(v => v.trim());
+
+		if (trackIndexStringOrLoopDefinition === "loop") {
+			if (!Number.isInteger(Number(animationNameOrTrackIndexStringCycle))) {
+				throw new Error(`Track index of cycle in ${group} must be a positive integer number, instead it is ${animationNameOrTrackIndexStringCycle}. Original value: ${value}`);
+			}
+			const animationInfoObject = obj[animationNameOrTrackIndexStringCycle] ||= { animations: [] };
+			animationInfoObject.cycle = true;
+			return obj;
+		}
+
+		const trackIndex = Number(trackIndexStringOrLoopDefinition);
+		if (!Number.isInteger(trackIndex)) {
+			throw new Error(`Track index in ${group} must be a positive integer number, instead it is ${trackIndexStringOrLoopDefinition}. Original value: ${value}`);
+		}
+
+		let delay;
+		if (delayString !== undefined) {
+			delay = parseFloat(delayString);
+			if (isNaN(delay)) {
+				throw new Error(`Delay in ${group} must be a positive number, instead it is ${delayString}. Original value: ${value}`);
+			}
+		}
+
+		let mixDuration;
+		if (mixDurationString !== undefined) {
+			mixDuration = parseFloat(mixDurationString);
+			if (isNaN(mixDuration)) {
+				throw new Error(`mixDuration in ${group} must be a positive number, instead it is ${mixDurationString}. Original value: ${value}`);
+			}
+		}
+
+		const animationInfoObject = obj[trackIndexStringOrLoopDefinition] ||= { animations: [] };
+		animationInfoObject.animations.push({
+			animationName: animationNameOrTrackIndexStringCycle,
+			loop: loop.trim().toLowerCase() === "true",
+			delay,
+			mixDuration,
+		});
+		return obj;
+	}, {} as AnimationsInfo);
+}
+
+function isFitType (value: string | null): value is FitType {
+	return (
+		value === "fill" ||
+		value === "width" ||
+		value === "height" ||
+		value === "contain" ||
+		value === "cover" ||
+		value === "none" ||
+		value === "scaleDown"
+	);
+}
+
+function isOffScreenUpdateBehaviourType (value: string | null): value is OffScreenUpdateBehaviourType {
+	return (
+		value === "pause" ||
+		value === "update" ||
+		value === "pose"
+	);
+}
+
+function isModeType (value: string | null): value is ModeType {
+	return (
+		value === "inside" ||
+		value === "origin"
+	);
+}
+const base64RegExp = /^(([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$/;
+export function isBase64 (str: string) {
+	return base64RegExp.test(str);
+}
+
+export interface Point {
+	x: number,
+	y: number,
+}
+
+export interface Rectangle extends Point {
+	width: number,
+	height: number,
+}
\ No newline at end of file