Initial support of spine-widget into scrollable containers, and overlay-id for multiple spine-overlay.

working
This commit is contained in:
Davide Tantillo 2024-10-14 18:21:56 +02:00
parent e9a07bd6b9
commit de7494036c
3 changed files with 518 additions and 79 deletions

View File

@ -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;
}
</style>
<script>
@ -1987,6 +2003,115 @@ stretchyman.update = (canvas, delta, skeleton, state) => {
/////////////////////
-->
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-left">
If you use a spine widget in a scrollable element, the effect might not be the desired one. There will be two problems:
<br>
<br>
1) The widget will be slightly slower to scroll than the html behind <br>
2) The widget will overflow the scrollable container until the html element container is visible <br>
<br>
In order to fix this behaviour, it is necessary to insert a dedicated <code>spine-overlay</code> webcomponent as a direct child of the scrollable container.
Moreover, it is necessary to perform the following actions: <br>
<br>
1) The scrollable container must have a <code>transform</code> css attribute. If it hasn't this attribute the <code>spine-overlay</code> will add it for you.
If your scrollable container has already this css attribute, or if you prefer to add it by yourself (example: <code>transform: translateZ(0);</code>), set the <code>scrollable-tweak-off</code> to the <code>spine-overlay</code>.
<br>
2) The <code>spine-overlay</code> must have the <code>scrollable</code> attribute
<br>
3) The <code>spine-overlay</code> must have an <code>overlay-id</code> attribute. Choose the value you prefer.
<br>
4) Each <code>spine-widget</code> must have an <code>overlay-id</code> attribute. The same as the hosting <code>spine-overlay</code>.
<br>
<br>
Additionally, you can set <code>overflow-top</code>, <code>overflow-bottom</code>, <code>overflow-left</code>, <code>overflow-right</code> attributes to the <code>spine-overlay</code> in order to make the canvas bigger and prevent scrolling artifacts.
</div>
<div class="split-top split" style="justify-content: space-between">
<div class="split-left" style="overflow-y: auto; width: 100px; height: 200px;">
<div class="overflow-grid-container">
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
</div>
</div>
<div class="split-left" style="overflow-y: auto; width: 100px; height: 200px;">
<spine-overlay
overlay-id="scroll"
scrollable
scrollable-tweak-off
overflow-top=".2"
overflow-bottom=".2"
overflow-left=".2"
overflow-right=".2"
></spine-overlay>
<div class="overflow-grid-container">
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
</div>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/celestial-circus-pma.atlas"
skeleton="assets/celestial-circus-pro.skel"
animation="wings-and-feet"
isdraggable="true"
></spine-widget>`
);</script>
</code></pre>
</div>
</div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<script>
spine.SpineWebComponentWidget.SHOW_FPS = true;
</script>

View File

@ -0,0 +1,238 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="../dist/iife/spine-webgl.js"></script>
<!-- <script src="./spine-webgl.min.js"></script> -->
<title>Webcomponent Tutorial</title>
<style>
body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
font-size: 16px;
}
.section {
display: flex;
justify-content: center;
align-items: center;
color: white;
background-color: #3498db;
}
.split {
display: flex;
justify-content: center;
align-items: center;
}
.full-width {
width: 100%;
}
.split-left, .split-right {
width: 50%;
min-height: 50%;
padding: 1rem;
margin: 1rem;
border: 1px solid salmon;
}
.split-nosize {
border: 1px solid salmon;
}
.split-size {
padding: 1rem;
margin: 1rem;
}
.navigation {
display: flex;
position: fixed;
left: 20px;
bottom: 20px;
transform: translateY(-50%);
}
.nav-btn {
display: block;
margin: 0px 5px;
padding: 10px;
background-color: rgba(255, 255, 255, 0.7);
border: none;
cursor: pointer;
}
.vertical-split {
display: flex;
flex-direction: column;
}
.high-page {
height: 600px;
}
.split-top {
width: 100%;
height: 600px;
}
.split-bottom {
width: 100%;
/* height: 600px; */
}
.split-bottom {
background-color: #1e1e1e;
color: #d4d4d4;
overflow: auto;
}
.split-bottom pre {
height: 100%;
margin: 0;
}
.split-bottom code {
font-family: 'Consolas', 'Courier New', monospace;
font-size: 12px;
line-height: 1.5;
display: block;
padding: 1rem;
}
.skin-grid {
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: space-evenly;
padding: 20px;
box-sizing: border-box;
}
.skin-grid-element {
border: 1px solid #ccc;
width: 150px;
aspect-ratio: 3 / 3;
}
.overflow-grid-container {
display: grid;
grid-template-columns: repeat(4, 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;
}
</style>
<script>
function escapeHTMLandInject(text) {
const escaped = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
document.currentScript.parentElement.innerHTML = escaped;
}
</script>
</head>
<body>
<!--
/////////////////////
// start section //
/////////////////////
-->
<div class="section vertical-split">
<div class="split-top split" style="justify-content: space-between">
<!-- <div class="split-left" style="overflow-y: auto; width: 200px; height: 200px;">
<div class="overflow-grid-container">
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel"></spine-widget></div>
</div>
</div> -->
<div class="split-left" style="overflow-y: auto; width: 200px; height: 200px;">
<spine-overlay
overlay-id="scroll"
scrollable
overflow-top=".2"
overflow-bottom=".2"
overflow-left=".2"
overflow-right=".2"
></spine-overlay>
<div class="overflow-grid-container">
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
<div class="overflow-grid-item"><spine-widget atlas="assets/spineboy-pma.atlas" skeleton="assets/spineboy-pro.skel" overlay-id="scroll"></spine-widget></div>
</div>
</div>
</div>
<div class="split-bottom">
<pre><code id="code-display">
<script>escapeHTMLandInject(`
<spine-widget
atlas="assets/celestial-circus-pma.atlas"
skeleton="assets/celestial-circus-pro.skel"
animation="wings-and-feet"
isdraggable="true"
></spine-widget>`
);</script>
</code></pre>
</div>
</div>
<div style="height: 1000px;"> SPACE </div>
<!--
/////////////////////
// end section //
/////////////////////
-->
<script>
spine.SpineWebComponentWidget.SHOW_FPS = true;
</script>
</body>
</html>

View File

@ -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<string, { propertyName: keyof WidgetAttributes, type: AttributeTypes, defaultValue?: any }> = {
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<number>);
}
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;
}
}
}
class SpineWebComponentOverlay extends HTMLElement implements Disposable {
interface OverlayAttributes {
overlayId?: string
scrollable: boolean
overflowTop: number
overflowBottom: number
overflowLeft: number
overflowRight: number
}
class SpineWebComponentOverlay extends HTMLElement implements OverlayAttributes, Disposable {
public skeletonList = new Array<SpineWebComponentWidget>();
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;
this.assetManager = new AssetManager(context);
}
static attributesDescription: Record<string, { propertyName: keyof OverlayAttributes, type: AttributeTypes, defaultValue?: any }> = {
"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<number>);
}
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;
}
}