Manage lifecycle for Input and SpineWebComponentOverlay. Missing SpineWebComponentWidget.

This commit is contained in:
Davide Tantillo 2024-09-27 16:06:23 +02:00
parent e26034426a
commit 5c42b6fa2d
2 changed files with 121 additions and 70 deletions

View File

@ -27,7 +27,8 @@
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * 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; element: HTMLElement;
mouseX = 0; mouseX = 0;
mouseY = 0; mouseY = 0;
@ -37,28 +38,34 @@ export class Input {
initialPinchDistance = 0; initialPinchDistance = 0;
private listeners = new Array<InputListener>(); private listeners = new Array<InputListener>();
private autoPreventDefault: boolean; 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) { constructor (element: HTMLElement, autoPreventDefault = true) {
this.element = element; this.element = element;
this.autoPreventDefault = autoPreventDefault; this.autoPreventDefault = autoPreventDefault;
this.setupCallbacks(element); this.callbacks = this.setupCallbacks(element);
} }
private setupCallbacks (element: HTMLElement) { private setupCallbacks (element: HTMLElement) {
let mouseDown = (ev: UIEvent) => { const mouseDown = (ev: UIEvent) => {
if (ev instanceof MouseEvent) { if (ev instanceof MouseEvent) {
let rect = element.getBoundingClientRect(); let rect = element.getBoundingClientRect();
this.mouseX = ev.clientX - rect.left; this.mouseX = ev.clientX - rect.left;
this.mouseY = ev.clientY - rect.top; this.mouseY = ev.clientY - rect.top;
this.buttonDown = true; this.buttonDown = true;
this.listeners.map((listener) => { if (listener.down) listener.down(this.mouseX, this.mouseY, ev); }); 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) { if (ev instanceof MouseEvent) {
let rect = element.getBoundingClientRect(); let rect = element.getBoundingClientRect();
this.mouseX = ev.clientX - rect.left; 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) { if (ev instanceof MouseEvent) {
let rect = element.getBoundingClientRect(); let rect = element.getBoundingClientRect();
this.mouseX = ev.clientX - rect.left;; this.mouseX = ev.clientX - rect.left;;
this.mouseY = ev.clientY - rect.top; this.mouseY = ev.clientY - rect.top;
this.buttonDown = false; this.buttonDown = false;
this.listeners.map((listener) => { if (listener.up) listener.up(this.mouseX, this.mouseY, ev); }); 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(); if (this.autoPreventDefault) ev.preventDefault();
let deltaY = ev.deltaY; let deltaY = ev.deltaY;
if (ev.deltaMode == WheelEvent.DOM_DELTA_LINE) deltaY *= 8; 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); }); this.listeners.map((listener) => { if (listener.wheel) listener.wheel(ev.deltaY, ev); });
}; };
element.addEventListener("mousedown", mouseDown, true); const touchStart = (ev: TouchEvent) => {
element.addEventListener("mousemove", mouseMove, true);
element.addEventListener("mouseup", mouseUp, true);
element.addEventListener("wheel", mouseWheel, true);
element.addEventListener("touchstart", (ev: TouchEvent) => {
if (!this.touch0 || !this.touch1) { if (!this.touch0 || !this.touch1) {
var touches = ev.changedTouches; var touches = ev.changedTouches;
let nativeTouch = touches.item(0); let nativeTouch = touches.item(0);
@ -126,9 +124,9 @@ export class Input {
} }
} }
if (this.autoPreventDefault) ev.preventDefault(); if (this.autoPreventDefault) ev.preventDefault();
}, { passive: false, capture: false }); }
element.addEventListener("touchmove", (ev: TouchEvent) => { const touchMove = (ev: TouchEvent) => {
if (this.touch0) { if (this.touch0) {
var touches = ev.changedTouches; var touches = ev.changedTouches;
let rect = element.getBoundingClientRect(); let rect = element.getBoundingClientRect();
@ -155,9 +153,9 @@ export class Input {
} }
} }
if (this.autoPreventDefault) ev.preventDefault(); if (this.autoPreventDefault) ev.preventDefault();
}, { passive: false, capture: false }); }
let touchEnd = (ev: TouchEvent) => { const touchEnd = (ev: TouchEvent) => {
if (this.touch0) { if (this.touch0) {
var touches = ev.changedTouches; var touches = ev.changedTouches;
let rect = element.getBoundingClientRect(); let rect = element.getBoundingClientRect();
@ -193,8 +191,38 @@ export class Input {
} }
if (this.autoPreventDefault) ev.preventDefault(); 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("touchend", touchEnd, { passive: false, capture: false });
element.addEventListener("touchcancel", touchEnd); 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) { addListener (listener: InputListener) {

View File

@ -34,6 +34,7 @@ import {
AtlasAttachmentLoader, AtlasAttachmentLoader,
AssetManager, AssetManager,
Color, Color,
Disposable,
Input, Input,
LoadingScreen, LoadingScreen,
ManagedWebGLRenderingContext, 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<SpineWebComponentWidget>();
public renderer: SceneRenderer;
public assetManager: AssetManager;
private root: ShadowRoot; private root: ShadowRoot;
private div: HTMLDivElement; private div: HTMLDivElement;
@ -818,10 +824,8 @@ class SpineWebComponentOverlay extends HTMLElement {
private fps: HTMLSpanElement; private fps: HTMLSpanElement;
private fpsAppended = false; private fpsAppended = false;
public skeletonList = new Array<SpineWebComponentWidget>();
private intersectionObserver? : IntersectionObserver; private intersectionObserver? : IntersectionObserver;
private input: Input; private input?: Input;
// how many pixels to add to the edges to prevent "edge cuttin" on fast scrolling // 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 // be aware that the canvas is already big as the display size
@ -836,9 +840,8 @@ class SpineWebComponentOverlay extends HTMLElement {
private currentCanvasBaseWidth = 0; private currentCanvasBaseWidth = 0;
private currentCanvasBaseHeight = 0; private currentCanvasBaseHeight = 0;
public renderer: SceneRenderer;
public assetManager: AssetManager;
private disposed = false; private disposed = false;
private detached = true;
readonly time = new TimeKeeper(); readonly time = new TimeKeeper();
constructor() { constructor() {
@ -873,40 +876,40 @@ class SpineWebComponentOverlay extends HTMLElement {
const context = new ManagedWebGLRenderingContext(this.canvas, { alpha: true }); const context = new ManagedWebGLRenderingContext(this.canvas, { alpha: true });
this.renderer = new SceneRenderer(this.canvas, context); this.renderer = new SceneRenderer(this.canvas, context);
this.assetManager = new AssetManager(context); this.assetManager = new AssetManager(context);
this.input = new Input(this.canvas, false);
this.setupRenderingElements();
this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth; this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth;
this.overflowTopSize = this.overflowTop * document.documentElement.clientHeight; 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();
this.scrollHandler();
};
this.input = new Input(document.body, false);
this.setupDragUtility();
} }
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();
// translateCanvas starts a requestAnimationFrame loop
this.translateCanvas();
this.scrollHandler();
}
connectedCallback(): void { 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) => { this.intersectionObserver = new IntersectionObserver((widgets) => {
widgets.forEach(({ isIntersecting, target, intersectionRatio }) => { widgets.forEach(({ isIntersecting, target, intersectionRatio }) => {
const widget = this.skeletonList.find(w => w.getHTMLElementReference() == target); const widget = this.skeletonList.find(w => w.getHTMLElementReference() == target);
@ -923,17 +926,40 @@ class SpineWebComponentOverlay extends HTMLElement {
} }
}) })
}, { rootMargin: "30px 20px 30px 20px" }); }, { 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 { 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) { addWidget(widget: SpineWebComponentWidget) {
this.skeletonList.push(widget); this.skeletonList.push(widget);
this.intersectionObserver!.observe(widget.getHTMLElementReference()); this.intersectionObserver?.observe(widget.getHTMLElementReference());
} }
private setupRenderingElements() { private startRenderingLoop() {
const updateWidgets = () => { const updateWidgets = () => {
const delta = this.time.delta; const delta = this.time.delta;
this.skeletonList.forEach(({ skeleton, state, update, onScreen, offScreenUpdateBehaviour, beforeUpdateWorldTransforms, afterUpdateWorldTransforms }) => { this.skeletonList.forEach(({ skeleton, state, update, onScreen, offScreenUpdateBehaviour, beforeUpdateWorldTransforms, afterUpdateWorldTransforms }) => {
@ -1195,7 +1221,7 @@ class SpineWebComponentOverlay extends HTMLElement {
} }
const loop = () => { const loop = () => {
if (this.disposed) return; if (this.disposed || this.detached) return;
requestAnimationFrame(loop); requestAnimationFrame(loop);
this.time.update(); this.time.update();
updateWidgets(); updateWidgets();
@ -1210,8 +1236,9 @@ class SpineWebComponentOverlay extends HTMLElement {
const transparentWhite = new Color(1, 1, 1, .3); const transparentWhite = new Color(1, 1, 1, .3);
} }
private setupDragUtility() { private setupDragUtility(): Input {
// TODO: we should use document - body might have some margin that offset the click events - Meanwhile I take event pageX/Y // 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 point: Point = { x: 0, y: 0 };
const getInput = (ev?: MouseEvent | TouchEvent): Point => { const getInput = (ev?: MouseEvent | TouchEvent): Point => {
@ -1223,7 +1250,7 @@ class SpineWebComponentOverlay extends HTMLElement {
let prevX = 0; let prevX = 0;
let prevY = 0; let prevY = 0;
this.input.addListener({ inputManager.addListener({
down: (x, y, ev) => { down: (x, y, ev) => {
const input = getInput(ev); const input = getInput(ev);
this.skeletonList.forEach(widget => { this.skeletonList.forEach(widget => {
@ -1257,7 +1284,9 @@ class SpineWebComponentOverlay extends HTMLElement {
widget.dragging = false; 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() { private translateCanvas() {
const scrollPositionX = window.scrollX - this.overflowLeftSize; const scrollPositionX = window.scrollX - this.overflowLeftSize;
const scrollPositionY = window.scrollY - this.overflowTopSize; const scrollPositionY = window.scrollY - this.overflowTopSize;