From de7494036c1fd1a9c43a3dd57514ff153e82dea4 Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Mon, 14 Oct 2024 18:21:56 +0200 Subject: [PATCH] Initial support of spine-widget into scrollable containers, and overlay-id for multiple spine-overlay. working --- .../example/webcomponent-tutorial.html | 125 +++++++++ .../example/webcomponent-tutorial2.html | 238 ++++++++++++++++++ .../src/SpineWebComponentWidget.ts | 230 +++++++++++------ 3 files changed, 516 insertions(+), 77 deletions(-) create mode 100644 spine-ts/spine-webgl/example/webcomponent-tutorial2.html diff --git a/spine-ts/spine-webgl/example/webcomponent-tutorial.html b/spine-ts/spine-webgl/example/webcomponent-tutorial.html index fdadca788..8ec619d72 100644 --- a/spine-ts/spine-webgl/example/webcomponent-tutorial.html +++ b/spine-ts/spine-webgl/example/webcomponent-tutorial.html @@ -113,6 +113,22 @@ width: 150px; aspect-ratio: 3 / 3; } + + .overflow-grid-container { + display: grid; + grid-template-columns: repeat(3, 100px); + grid-template-rows: repeat(4, 100px); + gap: 10px; + } + + .overflow-grid-item { + background-color: lightblue;; + width: 100px; + height: 100px; + display: flex; + align-items: center; + justify-content: center; + } + + + + + + + diff --git a/spine-ts/spine-webgl/example/webcomponent-tutorial2.html b/spine-ts/spine-webgl/example/webcomponent-tutorial2.html new file mode 100644 index 000000000..0ff41ec5a --- /dev/null +++ b/spine-ts/spine-webgl/example/webcomponent-tutorial2.html @@ -0,0 +1,238 @@ + + + + + + + + Webcomponent Tutorial + + + + + + + + + +
+ +
+ + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+

+                
+            
+
+ +
+ +
SPACE
+ + + + + + + \ No newline at end of file diff --git a/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts b/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts index ff4405bd3..605f616b3 100644 --- a/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts +++ b/spine-ts/spine-webgl/src/SpineWebComponentWidget.ts @@ -573,7 +573,7 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable, private root: ShadowRoot; // Reference to the overlay webcomponent - private overlay: SpineWebComponentOverlay; + private overlay!: SpineWebComponentOverlay; static attributesDescription: Record = { atlas: { propertyName: "atlasPath", type: "string" }, @@ -615,7 +615,6 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable, constructor () { super(); this.root = this.attachShadow({ mode: "closed" }); - this.overlay = this.initializeOverlay(); this.debugDragDiv = document.createElement("div"); this.debugDragDiv.style.position = "absolute"; @@ -628,7 +627,10 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable, throw new Error("You cannot attach a disposed widget"); }; - customElements.whenDefined("spine-overlay").then(() => this.overlay.addWidget(this)); + customElements.whenDefined("spine-overlay").then(() => { + if (!this.overlay) this.overlay = this.initializeOverlay(this.getAttribute("overlay-id")); + this.overlay.addWidget(this); + }); if (!this.manualStart && !this.started) { this.start(); } @@ -636,9 +638,9 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable, } disconnectedCallback (): void { - const index = this.overlay.skeletonList.indexOf(this); + const index = this.overlay!.skeletonList.indexOf(this); if (index !== -1) { - this.overlay.skeletonList.splice(index, 1); + this.overlay!.skeletonList.splice(index, 1); } this.debugDragDiv?.remove(); } @@ -654,7 +656,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 val = SpineWebComponentWidget.castValue(type, newValue, defaultValue); + const val = castValue(type, newValue, defaultValue); (this as any)[propertyName] = val; return; } @@ -824,8 +826,9 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable, // Create a new overlay webcomponent, if no one exists yet. // TODO: allow the possibility to instantiate multiple overlay (eg: background, foreground), // to give them an identifier, and to specify which overlay is assigned to a widget - private initializeOverlay (): SpineWebComponentOverlay { - let overlay = this.overlay || document.querySelector("spine-overlay") as SpineWebComponentOverlay; + private initializeOverlay (overlayId: string | null): SpineWebComponentOverlay { + const queryString = overlayId === null ? "spine-overlay:not([overlay-id])" : `spine-overlay[overlay-id=${overlayId}]`; + let overlay = this.overlay || document.querySelector(queryString) as SpineWebComponentOverlay; if (!overlay) { overlay = document.createElement("spine-overlay") as SpineWebComponentOverlay; document.body.appendChild(overlay); @@ -882,60 +885,26 @@ export class SpineWebComponentWidget extends HTMLElement implements Disposable, } } - private static castBoolean (value: string | null, defaultValue = "") { - return value === "true" || value === "" ? true : false; - } - - private static castString (value: string | null, defaultValue = "") { - return value === null ? defaultValue : value; - } - - private static castNumber (value: string | null, defaultValue = 0) { - if (value === null) return defaultValue; - - const parsed = parseFloat(value); - if (Number.isNaN(parsed)) return defaultValue; - return parsed; - } - - private static 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); - } +} - private static castValue (type: AttributeTypes, value: string | null, defaultValue?: any) { - switch (type) { - case "string": - return SpineWebComponentWidget.castString(value, defaultValue); - case "number": - return SpineWebComponentWidget.castNumber(value, defaultValue); - case "boolean": - return SpineWebComponentWidget.castBoolean(value, defaultValue); - case "string-number": - return SpineWebComponentWidget.castArrayNumber(value, defaultValue); - case "fitType": - return isFitType(value) ? value : defaultValue; - case "modeType": - return isModeType(value) ? value : defaultValue; - case "offScreenUpdateBehaviourType": - return isOffScreenUpdateBehaviourType(value) ? value : defaultValue; - default: - break; - } - } +interface OverlayAttributes { + overlayId?: string + scrollable: boolean + overflowTop: number + overflowBottom: number + overflowLeft: number + overflowRight: number } -class SpineWebComponentOverlay extends HTMLElement implements Disposable { +class SpineWebComponentOverlay extends HTMLElement implements OverlayAttributes, Disposable { public skeletonList = new Array(); public renderer: SceneRenderer; public assetManager: AssetManager; private root: ShadowRoot; + public overlayId?: string; + public scrollable = false; private div: HTMLDivElement; private canvas: HTMLCanvasElement; @@ -946,15 +915,15 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { private resizeObserver?:ResizeObserver; 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 cutting" on fast scrolling // be aware that the canvas is already big as the display size // making it bigger might reduce performance significantly - private overflowTop = .2; - private overflowBottom = .0; - private overflowLeft = .0; - private overflowRight = .0; - private overflowLeftSize: number - private overflowTopSize: number; + public overflowTop = .2; + public overflowBottom = .0; + public overflowLeft = .0; + public overflowRight = .0; + private overflowLeftSize = 0; + private overflowTopSize = 0; private currentCanvasBaseWidth = 0; private currentCanvasBaseHeight = 0; @@ -965,7 +934,7 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { constructor () { super(); - this.root = this.attachShadow({ mode: "closed" }); + this.root = this.attachShadow({ mode: "open" }); this.div = document.createElement("div"); this.div.style.position = "absolute"; @@ -973,11 +942,12 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { this.div.style.left = "0"; this.div.style.setProperty("pointer-events", "none"); this.div.style.overflow = "hidden" - // this.div.style.backgroundColor = "rgba(0, 255, 0, 0.3)"; + // this.div.style.backgroundColor = "rgba(0, 255, 0, 0.1)"; this.root.appendChild(this.div); this.canvas = document.createElement("canvas"); + this.div.appendChild(this.canvas); this.canvas.style.position = "absolute"; this.canvas.style.top = "0"; @@ -994,10 +964,28 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { const context = new ManagedWebGLRenderingContext(this.canvas, { alpha: true }); this.renderer = new SceneRenderer(this.canvas, context); + this.assetManager = new AssetManager(context); + } - this.overflowLeftSize = this.overflowLeft * document.documentElement.clientWidth; - this.overflowTopSize = this.overflowTop * document.documentElement.clientHeight; + static attributesDescription: Record = { + "overlay-id": { propertyName: "overlayId", type: "string" }, + "scrollable": { propertyName: "scrollable", 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 resizeCallback = () => { @@ -1025,7 +1013,7 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { } connectedCallback (): void { - window.addEventListener("scroll", this.scrollHandler); + // window.addEventListener("scroll", this.scrollHandler); window.addEventListener("load", this.onLoadCallback); if (this.loaded) this.onLoadCallback(); window.screen.orientation.addEventListener('change', this.orientationChangeCallback); @@ -1052,7 +1040,15 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { // 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. this.resizeObserver = new ResizeObserver(this.resizeCallback); - this.resizeObserver.observe(document.body); + if (this.scrollable) { + const style = getComputedStyle(this.parentElement!); + if (style.transform === "none") { + this.parentElement!.style.transform = `translateZ(0)`; + } + this.resizeObserver.observe(this.parentElement!); + } else { + this.resizeObserver.observe(document.body); + } this.skeletonList.forEach((widget) => { this.intersectionObserver?.observe(widget.getHTMLElementReference()); @@ -1063,7 +1059,7 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { } disconnectedCallback (): void { - window.removeEventListener("scroll", this.scrollHandler); + // window.removeEventListener("scroll", this.scrollHandler); window.removeEventListener("load", this.onLoadCallback); window.screen.orientation.removeEventListener('change', this.orientationChangeCallback); this.intersectionObserver?.disconnect(); @@ -1173,6 +1169,7 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { let renderer = this.renderer; renderer.begin(); + const ref = this.parentElement!.getBoundingClientRect(); const tempVector = new Vector3(); this.skeletonList.forEach((widget) => { const { skeleton, bounds, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY, fit, loadingSpinner, onScreen, loading, clip, isDraggable } = widget; @@ -1184,6 +1181,10 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { divBounds.x = divBounds.left + this.overflowLeftSize; divBounds.y = divBounds.top + this.overflowTopSize; + if (this.scrollable) { + divBounds.x -= ref.left; + divBounds.y -= ref.top; + } const { padLeft, padRight, padTop, padBottom } = widget const paddingShiftHorizontal = (padLeft - padRight) / 2; @@ -1441,16 +1442,36 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { // temporarely remove the div to get the page size without considering the div // this is necessary otherwise if the bigger element in the page is remove and the div // was the second bigger element, now it would be the div to determine the page size - this.div.remove(); - const { width, height } = this.getPageSize(); - this.root.appendChild(this.div); - this.div.style.width = width + "px"; - this.div.style.height = height + "px"; + + if (!this.scrollable) { + this.div?.remove(); + const { width, height } = this.getPageSize(); + this.div!.style.width = width + "px"; + this.div!.style.height = height + "px"; + this.root.appendChild(this.div!); + } else { + this.div?.remove(); + this.div!.style.width = this.parentElement!.scrollWidth + "px"; + this.div!.style.height = this.parentElement!.scrollHeight + "px"; + // this.canvas.style.transform = `translate(${-this.overflowLeftSize}px,${-this.overflowTopSize}px)`; + this.root.appendChild(this.div!); + } + + } private resizeCanvas () { - let { width, height } = this.getScreenSize(); + let width, height; + if (!this.overlayId) { + const screenSize = this.getScreenSize(); + width = screenSize.width; + height = screenSize.height; + } else { + width = this.parentElement!.clientWidth; + height = this.parentElement!.clientHeight; + } + // this is needed because screen size is wrong when zoom levels occurs // zooming out will make the canvas smaller and its known that zoom level @@ -1458,8 +1479,8 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { // ideally, window.innerWidth/innerHeight would be preferrable. However // on mobile browsers the dynamic search bar makes the innerHeight smaller // at the beginning (changing the canvas size at each scroll is not ideal) - width = Math.max(width, window.innerWidth); - height = Math.max(height, window.innerHeight); + // width = Math.max(width, window.innerWidth); + // height = Math.max(height, window.innerHeight); if (this.currentCanvasBaseWidth !== width || this.currentCanvasBaseHeight !== height) { this.currentCanvasBaseWidth = width; @@ -1477,8 +1498,17 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable { } private translateCanvas () { - const scrollPositionX = window.scrollX - this.overflowLeftSize; - const scrollPositionY = window.scrollY - this.overflowTopSize; + let scrollPositionX = -this.overflowLeftSize; + let scrollPositionY = -this.overflowTopSize; + + if (!this.scrollable) { + scrollPositionX += window.scrollX; + scrollPositionY += window.scrollY; + } else { + scrollPositionX += this.parentElement!.scrollLeft; + scrollPositionY += this.parentElement!.scrollTop; + } + this.canvas.style.transform = `translate(${scrollPositionX}px,${scrollPositionY}px)`; } @@ -1578,4 +1608,50 @@ 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 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 "string-number": + return castArrayNumber(value, defaultValue); + case "fitType": + return isFitType(value) ? value : defaultValue; + case "modeType": + return isModeType(value) ? value : defaultValue; + case "offScreenUpdateBehaviourType": + return isOffScreenUpdateBehaviourType(value) ? value : defaultValue; + default: + break; + } } \ No newline at end of file