|
@@ -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<string, SpineWebComponentOverlay>();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @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<SpineWebComponentSkeleton>();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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<string, { propertyName: keyof OverlayAttributes, type: AttributeTypes, defaultValue?: any }> = {
|
|
|
+ "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);
|