From 5c42b6fa2dc02a4e5f476416916ec50193f4c135 Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Fri, 27 Sep 2024 16:06:23 +0200 Subject: [PATCH] Manage lifecycle for Input and SpineWebComponentOverlay. Missing SpineWebComponentWidget. --- spine-ts/spine-webgl/src/Input.ts | 74 ++++++++---- .../src/SpineWebComponentWidget.ts | 113 +++++++++++------- 2 files changed, 119 insertions(+), 68 deletions(-) diff --git a/spine-ts/spine-webgl/src/Input.ts b/spine-ts/spine-webgl/src/Input.ts index 4561f49cb..1dd79eaf6 100644 --- a/spine-ts/spine-webgl/src/Input.ts +++ b/spine-ts/spine-webgl/src/Input.ts @@ -27,7 +27,8 @@ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -export class Input { +import { Disposable } from "./index.js" +export class Input implements Disposable { element: HTMLElement; mouseX = 0; mouseY = 0; @@ -37,28 +38,34 @@ export class Input { initialPinchDistance = 0; private listeners = new Array(); private autoPreventDefault: boolean; + private callbacks: { + mouseDown: (ev: UIEvent) => void; + mouseMove: (ev: UIEvent) => void; + mouseUp: (ev: UIEvent) => void; + mouseWheel: (ev: WheelEvent) => void; + touchStart: (ev: TouchEvent) => void; + touchMove: (ev: TouchEvent) => void; + touchEnd: (ev: TouchEvent) => void; + }; constructor (element: HTMLElement, autoPreventDefault = true) { this.element = element; this.autoPreventDefault = autoPreventDefault; - this.setupCallbacks(element); + this.callbacks = this.setupCallbacks(element); } private setupCallbacks (element: HTMLElement) { - let mouseDown = (ev: UIEvent) => { + const mouseDown = (ev: UIEvent) => { if (ev instanceof MouseEvent) { let rect = element.getBoundingClientRect(); this.mouseX = ev.clientX - rect.left; this.mouseY = ev.clientY - rect.top; this.buttonDown = true; this.listeners.map((listener) => { if (listener.down) listener.down(this.mouseX, this.mouseY, ev); }); - - document.addEventListener("mousemove", mouseMove); - document.addEventListener("mouseup", mouseUp); } } - let mouseMove = (ev: UIEvent) => { + const mouseMove = (ev: UIEvent) => { if (ev instanceof MouseEvent) { let rect = element.getBoundingClientRect(); this.mouseX = ev.clientX - rect.left; @@ -74,20 +81,17 @@ export class Input { } }; - let mouseUp = (ev: UIEvent) => { + const mouseUp = (ev: UIEvent) => { if (ev instanceof MouseEvent) { let rect = element.getBoundingClientRect(); this.mouseX = ev.clientX - rect.left;; this.mouseY = ev.clientY - rect.top; this.buttonDown = false; this.listeners.map((listener) => { if (listener.up) listener.up(this.mouseX, this.mouseY, ev); }); - - document.removeEventListener("mousemove", mouseMove); - document.removeEventListener("mouseup", mouseUp); } } - let mouseWheel = (ev: WheelEvent) => { + const mouseWheel = (ev: WheelEvent) => { if (this.autoPreventDefault) ev.preventDefault(); let deltaY = ev.deltaY; if (ev.deltaMode == WheelEvent.DOM_DELTA_LINE) deltaY *= 8; @@ -95,13 +99,7 @@ export class Input { this.listeners.map((listener) => { if (listener.wheel) listener.wheel(ev.deltaY, ev); }); }; - element.addEventListener("mousedown", mouseDown, true); - element.addEventListener("mousemove", mouseMove, true); - element.addEventListener("mouseup", mouseUp, true); - element.addEventListener("wheel", mouseWheel, true); - - - element.addEventListener("touchstart", (ev: TouchEvent) => { + const touchStart = (ev: TouchEvent) => { if (!this.touch0 || !this.touch1) { var touches = ev.changedTouches; let nativeTouch = touches.item(0); @@ -126,9 +124,9 @@ export class Input { } } if (this.autoPreventDefault) ev.preventDefault(); - }, { passive: false, capture: false }); + } - element.addEventListener("touchmove", (ev: TouchEvent) => { + const touchMove = (ev: TouchEvent) => { if (this.touch0) { var touches = ev.changedTouches; let rect = element.getBoundingClientRect(); @@ -155,9 +153,9 @@ export class Input { } } if (this.autoPreventDefault) ev.preventDefault(); - }, { passive: false, capture: false }); + } - let touchEnd = (ev: TouchEvent) => { + const touchEnd = (ev: TouchEvent) => { if (this.touch0) { var touches = ev.changedTouches; let rect = element.getBoundingClientRect(); @@ -193,8 +191,38 @@ export class Input { } if (this.autoPreventDefault) ev.preventDefault(); }; + + element.addEventListener("mousedown", mouseDown, true); + element.addEventListener("mousemove", mouseMove, true); + element.addEventListener("mouseup", mouseUp, true); + element.addEventListener("wheel", mouseWheel, true); + element.addEventListener("touchstart", touchStart, { passive: false, capture: false }); + element.addEventListener("touchmove", touchMove, { passive: false, capture: false }); element.addEventListener("touchend", touchEnd, { passive: false, capture: false }); element.addEventListener("touchcancel", touchEnd); + + return { + mouseDown, + mouseMove, + mouseUp, + mouseWheel, + touchStart, + touchMove, + touchEnd, + } + } + + dispose(): void { + const element = this.element; + element.addEventListener("mousedown", this.callbacks.mouseDown, true); + element.addEventListener("mousemove", this.callbacks.mouseMove, true); + element.addEventListener("mouseup", this.callbacks.mouseUp, true); + element.addEventListener("wheel", this.callbacks.mouseWheel, true); + element.addEventListener("touchstart", this.callbacks.touchStart, { passive: false, capture: false }); + element.addEventListener("touchmove", this.callbacks.touchMove, { passive: false, capture: false }); + element.addEventListener("touchend", this.callbacks.touchEnd, { passive: false, capture: false }); + element.addEventListener("touchcancel", this.callbacks.touchEnd); + this.listeners.length = 0; } addListener (listener: InputListener) { diff --git a/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts b/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts index 0c79bf384..578a145f8 100644 --- a/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts +++ b/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts @@ -34,6 +34,7 @@ import { AtlasAttachmentLoader, AssetManager, Color, + Disposable, Input, LoadingScreen, ManagedWebGLRenderingContext, @@ -810,7 +811,12 @@ export class SpineWebComponentWidget extends HTMLElement implements WidgetAttrib } } -class SpineWebComponentOverlay extends HTMLElement { +class SpineWebComponentOverlay extends HTMLElement implements Disposable { + + public skeletonList = new Array(); + public renderer: SceneRenderer; + public assetManager: AssetManager; + private root: ShadowRoot; private div: HTMLDivElement; @@ -818,10 +824,8 @@ class SpineWebComponentOverlay extends HTMLElement { private fps: HTMLSpanElement; private fpsAppended = false; - public skeletonList = new Array(); - private intersectionObserver? : IntersectionObserver; - private input: Input; + private input?: Input; // how many pixels to add to the edges to prevent "edge cuttin" on fast scrolling // be aware that the canvas is already big as the display size @@ -836,9 +840,8 @@ class SpineWebComponentOverlay extends HTMLElement { private currentCanvasBaseWidth = 0; private currentCanvasBaseHeight = 0; - public renderer: SceneRenderer; - public assetManager: AssetManager; private disposed = false; + private detached = true; readonly time = new TimeKeeper(); constructor() { @@ -873,40 +876,40 @@ class SpineWebComponentOverlay extends HTMLElement { const context = new ManagedWebGLRenderingContext(this.canvas, { alpha: true }); this.renderer = new SceneRenderer(this.canvas, context); this.assetManager = new AssetManager(context); - this.input = new Input(this.canvas, false); - this.setupRenderingElements(); this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth; this.overflowTopSize = this.overflowTop * document.documentElement.clientHeight; + } - window.addEventListener('resize', () => { - this.updateCanvasSize(); - this.zoomHandler(); - }); - - window.screen.orientation.onchange = () => { - this.updateCanvasSize(); - // after an orientation change the scrolling changes, but the scroll event does not fire - this.scrollHandler(); - } - - window.addEventListener("scroll", this.scrollHandler); - - window.onload = () => { - this.updateCanvasSize(); - this.zoomHandler(); - - // translateCanvas starts a requestAnimationFrame loop - this.translateCanvas(); + private resizeCallback = () => { + this.updateCanvasSize(); + this.zoomHandler(); + } + private orientationChangeCallback = () => { + this.updateCanvasSize(); + // after an orientation change the scrolling changes, but the scroll event does not fire + this.scrollHandler(); + } + // right now, we scroll the canvas each frame, that makes scrolling on mobile waaay more smoother + // this is way scroll handler do nothing + private scrollHandler = () => { + // this.translateCanvas(); + } + private onLoadCallback = () => { + this.updateCanvasSize(); + this.zoomHandler(); - this.scrollHandler(); - }; + // translateCanvas starts a requestAnimationFrame loop + this.translateCanvas(); - this.input = new Input(document.body, false); - this.setupDragUtility(); + this.scrollHandler(); } - connectedCallback(): void { + window.addEventListener("resize", this.resizeCallback); + window.addEventListener("scroll", this.scrollHandler); + window.addEventListener("load", this.onLoadCallback); + window.screen.orientation.addEventListener('change', this.orientationChangeCallback); + this.intersectionObserver = new IntersectionObserver((widgets) => { widgets.forEach(({ isIntersecting, target, intersectionRatio }) => { const widget = this.skeletonList.find(w => w.getHTMLElementReference() == target); @@ -923,17 +926,40 @@ class SpineWebComponentOverlay extends HTMLElement { } }) }, { rootMargin: "30px 20px 30px 20px" }); + this.skeletonList.forEach((widget) => { + this.intersectionObserver?.observe(widget.getHTMLElementReference()); + }) + this.input = this.setupDragUtility(); + + this.detached = false; + + this.startRenderingLoop(); } disconnectedCallback(): void { + window.removeEventListener("resize", this.resizeCallback); + window.removeEventListener("scroll", this.scrollHandler); + window.removeEventListener("load", this.onLoadCallback); + window.screen.orientation.removeEventListener('change', this.orientationChangeCallback); + this.intersectionObserver?.disconnect(); + this.input?.dispose(); + this.detached = true; + } + + dispose(): void { + document.body.removeChild(this); + this.skeletonList.length = 0; + this.renderer.dispose(); + this.disposed = true; + this.detached = true; } addWidget(widget: SpineWebComponentWidget) { this.skeletonList.push(widget); - this.intersectionObserver!.observe(widget.getHTMLElementReference()); + this.intersectionObserver?.observe(widget.getHTMLElementReference()); } - private setupRenderingElements() { + private startRenderingLoop() { const updateWidgets = () => { const delta = this.time.delta; this.skeletonList.forEach(({ skeleton, state, update, onScreen, offScreenUpdateBehaviour, beforeUpdateWorldTransforms, afterUpdateWorldTransforms }) => { @@ -1195,7 +1221,7 @@ class SpineWebComponentOverlay extends HTMLElement { } const loop = () => { - if (this.disposed) return; + if (this.disposed || this.detached) return; requestAnimationFrame(loop); this.time.update(); updateWidgets(); @@ -1210,8 +1236,9 @@ class SpineWebComponentOverlay extends HTMLElement { const transparentWhite = new Color(1, 1, 1, .3); } - private setupDragUtility() { - // TODO: we should use document - body might have some margin that offset the click events - Meanwhile I take event pageX/Y + 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 point: Point = { x: 0, y: 0 }; const getInput = (ev?: MouseEvent | TouchEvent): Point => { @@ -1223,7 +1250,7 @@ class SpineWebComponentOverlay extends HTMLElement { let prevX = 0; let prevY = 0; - this.input.addListener({ + inputManager.addListener({ down: (x, y, ev) => { const input = getInput(ev); this.skeletonList.forEach(widget => { @@ -1257,7 +1284,9 @@ class SpineWebComponentOverlay extends HTMLElement { widget.dragging = false; }); } - }) + }); + + return inputManager; } /* @@ -1306,12 +1335,6 @@ class SpineWebComponentOverlay extends HTMLElement { } } - // right now, we scroll the canvas each frame, that makes scrolling on mobile waaay more smoother - // this is way scroll handler do nothing - private scrollHandler = () => { - // this.translateCanvas(); - } - private translateCanvas() { const scrollPositionX = window.scrollX - this.overflowLeftSize; const scrollPositionY = window.scrollY - this.overflowTopSize;