mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-07 03:06:55 +08:00
Managed lifecycle for SpineWebComponentWidget.
This commit is contained in:
parent
5c42b6fa2d
commit
2db1873fd5
@ -142,7 +142,8 @@ export class LoadingScreen implements Disposable {
|
|||||||
const shiftedY = y - logoHeight / 2;
|
const shiftedY = y - logoHeight / 2;
|
||||||
renderer.drawTexture(this.logo, shiftedX, shiftedY, logoWidth, logoHeight);
|
renderer.drawTexture(this.logo, shiftedX, shiftedY, logoWidth, logoHeight);
|
||||||
|
|
||||||
if (this.spinner) renderer.drawTextureRotated(this.spinner, shiftedX, shiftedY - 25, spinnerSize, spinnerSize, spinnerSize / 2, spinnerSize / 2, this.timeKeeper.delta * 500);
|
this.angle -= this.timeKeeper.delta * 500;
|
||||||
|
if (this.spinner) renderer.drawTextureRotated(this.spinner, shiftedX, shiftedY - 25, spinnerSize, spinnerSize, spinnerSize / 2, spinnerSize / 2, this.angle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -141,6 +141,7 @@ interface WidgetPublicProperties {
|
|||||||
loading: boolean
|
loading: boolean
|
||||||
started: boolean
|
started: boolean
|
||||||
textureAtlas: TextureAtlas
|
textureAtlas: TextureAtlas
|
||||||
|
disposed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage of this properties is discouraged because they can be made private in the future
|
// Usage of this properties is discouraged because they can be made private in the future
|
||||||
@ -153,7 +154,7 @@ interface WidgetInternalProperties {
|
|||||||
debugDragDiv: HTMLDivElement
|
debugDragDiv: HTMLDivElement
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SpineWebComponentWidget extends HTMLElement implements WidgetAttributes, WidgetOverridableMethods, WidgetInternalProperties, Partial<WidgetPublicProperties> {
|
export class SpineWebComponentWidget extends HTMLElement implements Disposable, WidgetAttributes, WidgetOverridableMethods, WidgetInternalProperties, Partial<WidgetPublicProperties> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, enables a top-left span showing FPS (it has black text)
|
* If true, enables a top-left span showing FPS (it has black text)
|
||||||
@ -469,6 +470,10 @@ export class SpineWebComponentWidget extends HTMLElement implements WidgetAttrib
|
|||||||
*/
|
*/
|
||||||
public debugDragDiv: HTMLDivElement;
|
public debugDragDiv: HTMLDivElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, indicate {@link dispose} has been called and the widget cannot be used anymore
|
||||||
|
*/
|
||||||
|
public disposed = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional: Pass a `SkeletonData`, if you want to avoid creating a new one
|
* Optional: Pass a `SkeletonData`, if you want to avoid creating a new one
|
||||||
@ -521,8 +526,12 @@ export class SpineWebComponentWidget extends HTMLElement implements WidgetAttrib
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
if (this.disposed) {
|
||||||
|
throw new Error("You cannot attach a disposed widget");
|
||||||
|
};
|
||||||
|
|
||||||
this.overlay.addWidget(this);
|
this.overlay.addWidget(this);
|
||||||
if (!this.manualStart) {
|
if (!this.manualStart && !this.started) {
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
this.render();
|
this.render();
|
||||||
@ -535,52 +544,16 @@ export class SpineWebComponentWidget extends HTMLElement implements WidgetAttrib
|
|||||||
this.overlay.skeletonList.splice(index, 1);
|
this.overlay.skeletonList.splice(index, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.debugDragDiv?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static castBoolean(value: string | null, defaultValue = "") {
|
dispose() {
|
||||||
return value === "true" || value === "" ? true : false;
|
this.remove();
|
||||||
}
|
this.loadingScreen?.dispose();
|
||||||
|
this.skeletonData = undefined;
|
||||||
private static castString(value: string | null, defaultValue = "") {
|
this.skeleton = undefined;
|
||||||
return value === null ? defaultValue : value;
|
this.state = undefined;
|
||||||
}
|
this.disposed = true;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
|
||||||
@ -590,38 +563,6 @@ export class SpineWebComponentWidget extends HTMLElement implements WidgetAttrib
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Recalculates and sets the bounds of the current animation on track 0.
|
|
||||||
* Useful when animations or skins are set programmatically.
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
public recalculateBounds(): void {
|
|
||||||
const { skeleton, state } = this;
|
|
||||||
if (!skeleton || !state) return;
|
|
||||||
const track = state.getCurrent(0);
|
|
||||||
const animation = track?.animation as (Animation | undefined);
|
|
||||||
const bounds = this.calculateAnimationViewport(animation);
|
|
||||||
this.setBounds(bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the given bounds on the current skeleton.
|
|
||||||
* Useful when you want you skeleton to have a fixed size, or you want to
|
|
||||||
* focus a certain detail of the skeleton. If the skeleton overflow the element container
|
|
||||||
* consider setting {@link clip} to `true`.
|
|
||||||
* @param bounds
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
public setBounds(bounds: Rectangle): void {
|
|
||||||
const { skeleton } = this;
|
|
||||||
if (!skeleton) return;
|
|
||||||
bounds.x /= skeleton.scaleX;
|
|
||||||
bounds.y /= skeleton.scaleY;
|
|
||||||
bounds.width /= skeleton.scaleX;
|
|
||||||
bounds.height /= skeleton.scaleY;
|
|
||||||
this.bounds = bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the widget. Starting the widget means to load the assets currently set into
|
* Starts the widget. Starting the widget means to load the assets currently set into
|
||||||
* {@link atlasPath} and {@link skeletonPath}.
|
* {@link atlasPath} and {@link skeletonPath}.
|
||||||
@ -669,10 +610,42 @@ export class SpineWebComponentWidget extends HTMLElement implements WidgetAttrib
|
|||||||
: this;
|
: this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculates and sets the bounds of the current animation on track 0.
|
||||||
|
* Useful when animations or skins are set programmatically.
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
public recalculateBounds(): void {
|
||||||
|
const { skeleton, state } = this;
|
||||||
|
if (!skeleton || !state) return;
|
||||||
|
const track = state.getCurrent(0);
|
||||||
|
const animation = track?.animation as (Animation | undefined);
|
||||||
|
const bounds = this.calculateAnimationViewport(animation);
|
||||||
|
this.setBounds(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the given bounds on the current skeleton.
|
||||||
|
* Useful when you want you skeleton to have a fixed size, or you want to
|
||||||
|
* focus a certain detail of the skeleton. If the skeleton overflow the element container
|
||||||
|
* consider setting {@link clip} to `true`.
|
||||||
|
* @param bounds
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public setBounds(bounds: Rectangle): void {
|
||||||
|
const { skeleton } = this;
|
||||||
|
if (!skeleton) return;
|
||||||
|
bounds.x /= skeleton.scaleX;
|
||||||
|
bounds.y /= skeleton.scaleY;
|
||||||
|
bounds.width /= skeleton.scaleX;
|
||||||
|
bounds.height /= skeleton.scaleY;
|
||||||
|
this.bounds = bounds;
|
||||||
|
}
|
||||||
|
|
||||||
// add a skeleton to the overlay and set the bounds to the given animation or to the setup pose
|
// add a skeleton to the overlay and set the bounds to the given animation or to the setup pose
|
||||||
private async loadSkeleton() {
|
private async loadSkeleton() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
// if (this.identifier !== "TODELETE") return Promise.reject();
|
|
||||||
const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, skin } = this;
|
const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, skin } = this;
|
||||||
if (!atlasPath || !skeletonPath) {
|
if (!atlasPath || !skeletonPath) {
|
||||||
throw new Error(`Missing atlas path or skeleton path. Assets cannot be loaded: atlas: ${atlasPath}, skeleton: ${skeletonPath}`);
|
throw new Error(`Missing atlas path or skeleton path. Assets cannot be loaded: atlas: ${atlasPath}, skeleton: ${skeletonPath}`);
|
||||||
@ -755,7 +728,7 @@ export class SpineWebComponentWidget extends HTMLElement implements WidgetAttrib
|
|||||||
// TODO: allow the possibility to instantiate multiple overlay (eg: background, foreground),
|
// 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
|
// to give them an identifier, and to specify which overlay is assigned to a widget
|
||||||
private initializeOverlay(): SpineWebComponentOverlay {
|
private initializeOverlay(): SpineWebComponentOverlay {
|
||||||
let overlay = document.querySelector("spine-overlay") as SpineWebComponentOverlay;
|
let overlay = this.overlay || document.querySelector("spine-overlay") as SpineWebComponentOverlay;
|
||||||
if (!overlay) {
|
if (!overlay) {
|
||||||
overlay = document.createElement("spine-overlay") as SpineWebComponentOverlay;
|
overlay = document.createElement("spine-overlay") as SpineWebComponentOverlay;
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
@ -809,6 +782,52 @@ export class SpineWebComponentWidget extends HTMLElement implements WidgetAttrib
|
|||||||
height: maxY - minY,
|
height: maxY - minY,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
class SpineWebComponentOverlay extends HTMLElement implements Disposable {
|
||||||
@ -841,7 +860,7 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable {
|
|||||||
private currentCanvasBaseHeight = 0;
|
private currentCanvasBaseHeight = 0;
|
||||||
|
|
||||||
private disposed = false;
|
private disposed = false;
|
||||||
private detached = true;
|
private loaded = false;
|
||||||
readonly time = new TimeKeeper();
|
readonly time = new TimeKeeper();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -885,29 +904,31 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable {
|
|||||||
this.updateCanvasSize();
|
this.updateCanvasSize();
|
||||||
this.zoomHandler();
|
this.zoomHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
private orientationChangeCallback = () => {
|
private orientationChangeCallback = () => {
|
||||||
this.updateCanvasSize();
|
this.updateCanvasSize();
|
||||||
// after an orientation change the scrolling changes, but the scroll event does not fire
|
// after an orientation change the scrolling changes, but the scroll event does not fire
|
||||||
this.scrollHandler();
|
this.scrollHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
// right now, we scroll the canvas each frame, that makes scrolling on mobile waaay more smoother
|
// right now, we scroll the canvas each frame, that makes scrolling on mobile waaay more smoother
|
||||||
// this is way scroll handler do nothing
|
// this is way scroll handler do nothing
|
||||||
private scrollHandler = () => {
|
private scrollHandler = () => {
|
||||||
// this.translateCanvas();
|
// this.translateCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onLoadCallback = () => {
|
private onLoadCallback = () => {
|
||||||
this.updateCanvasSize();
|
this.updateCanvasSize();
|
||||||
this.zoomHandler();
|
this.zoomHandler();
|
||||||
|
|
||||||
// translateCanvas starts a requestAnimationFrame loop
|
|
||||||
this.translateCanvas();
|
|
||||||
|
|
||||||
this.scrollHandler();
|
this.scrollHandler();
|
||||||
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
window.addEventListener("resize", this.resizeCallback);
|
window.addEventListener("resize", this.resizeCallback);
|
||||||
window.addEventListener("scroll", this.scrollHandler);
|
window.addEventListener("scroll", this.scrollHandler);
|
||||||
window.addEventListener("load", this.onLoadCallback);
|
window.addEventListener("load", this.onLoadCallback);
|
||||||
|
if (this.loaded) this.onLoadCallback();
|
||||||
window.screen.orientation.addEventListener('change', this.orientationChangeCallback);
|
window.screen.orientation.addEventListener('change', this.orientationChangeCallback);
|
||||||
|
|
||||||
this.intersectionObserver = new IntersectionObserver((widgets) => {
|
this.intersectionObserver = new IntersectionObserver((widgets) => {
|
||||||
@ -931,8 +952,6 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable {
|
|||||||
})
|
})
|
||||||
this.input = this.setupDragUtility();
|
this.input = this.setupDragUtility();
|
||||||
|
|
||||||
this.detached = false;
|
|
||||||
|
|
||||||
this.startRenderingLoop();
|
this.startRenderingLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -943,15 +962,13 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable {
|
|||||||
window.screen.orientation.removeEventListener('change', this.orientationChangeCallback);
|
window.screen.orientation.removeEventListener('change', this.orientationChangeCallback);
|
||||||
this.intersectionObserver?.disconnect();
|
this.intersectionObserver?.disconnect();
|
||||||
this.input?.dispose();
|
this.input?.dispose();
|
||||||
this.detached = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
document.body.removeChild(this);
|
this.remove();
|
||||||
this.skeletonList.length = 0;
|
this.skeletonList.length = 0;
|
||||||
this.renderer.dispose();
|
this.renderer.dispose();
|
||||||
this.disposed = true;
|
this.disposed = true;
|
||||||
this.detached = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addWidget(widget: SpineWebComponentWidget) {
|
addWidget(widget: SpineWebComponentWidget) {
|
||||||
@ -1221,9 +1238,11 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loop = () => {
|
const loop = () => {
|
||||||
if (this.disposed || this.detached) return;
|
if (this.disposed || !this.isConnected) return;
|
||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
|
if (!this.loaded) return;
|
||||||
this.time.update();
|
this.time.update();
|
||||||
|
this.translateCanvas();
|
||||||
updateWidgets();
|
updateWidgets();
|
||||||
renderWidgets();
|
renderWidgets();
|
||||||
}
|
}
|
||||||
@ -1339,7 +1358,9 @@ class SpineWebComponentOverlay extends HTMLElement implements Disposable {
|
|||||||
const scrollPositionX = window.scrollX - this.overflowLeftSize;
|
const scrollPositionX = window.scrollX - this.overflowLeftSize;
|
||||||
const scrollPositionY = window.scrollY - this.overflowTopSize;
|
const scrollPositionY = window.scrollY - this.overflowTopSize;
|
||||||
this.canvas.style.transform =`translate(${scrollPositionX}px,${scrollPositionY}px)`;
|
this.canvas.style.transform =`translate(${scrollPositionX}px,${scrollPositionY}px)`;
|
||||||
requestAnimationFrame(() => this.translateCanvas());
|
requestAnimationFrame(() => {
|
||||||
|
if (this.isConnected) this.translateCanvas();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private zoomHandler = () => {
|
private zoomHandler = () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user