mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
WIP - Refactor + animation and skin attribute change will reinit the widget
This commit is contained in:
parent
d178f5de7c
commit
4536789e39
@ -273,6 +273,52 @@ export class AssetManagerBase implements Disposable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Promisified versions of load function
|
||||||
|
async loadBinaryAsync(path: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.loadBinary(path,
|
||||||
|
(_, binary) => resolve(binary),
|
||||||
|
(_, message) => reject(message),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadJsonAsync(path: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.loadJson(path,
|
||||||
|
(_, object) => resolve(object),
|
||||||
|
(_, message) => reject(message),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTextureAsync(path: string) {
|
||||||
|
return new Promise<Texture>((resolve, reject) => {
|
||||||
|
this.loadTexture(path,
|
||||||
|
(_, texture) => resolve(texture),
|
||||||
|
(_, message) => reject(message),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTextureAtlasAsync(path: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.loadTextureAtlas(path,
|
||||||
|
(_, atlas) => resolve(atlas),
|
||||||
|
(_, message) => reject(message),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTextureAtlasButNoTexturesAsync(path: string) {
|
||||||
|
return new Promise<TextureAtlas>((resolve, reject) => {
|
||||||
|
this.loadTextureAtlasButNoTextures(path,
|
||||||
|
(_, atlas) => resolve(atlas),
|
||||||
|
(_, message) => reject(message),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get (path: string) {
|
get (path: string) {
|
||||||
return this.assets[this.pathPrefix + path];
|
return this.assets[this.pathPrefix + path];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1198,8 +1198,8 @@
|
|||||||
|
|
||||||
// create the widget
|
// create the widget
|
||||||
const widgetSection = spine.createSpineWidget({
|
const widgetSection = spine.createSpineWidget({
|
||||||
atlas: "assets/chibi-stickers-pma.atlas",
|
atlasPath: "assets/chibi-stickers-pma.atlas",
|
||||||
skeleton: "assets/chibi-stickers.json",
|
skeletonPath: "assets/chibi-stickers.json",
|
||||||
animation,
|
animation,
|
||||||
skin,
|
skin,
|
||||||
pages,
|
pages,
|
||||||
@ -1246,8 +1246,8 @@ skins.forEach((skin, i) => {
|
|||||||
|
|
||||||
// create the widget
|
// create the widget
|
||||||
const widgetSection = spine.createSpineWidget({
|
const widgetSection = spine.createSpineWidget({
|
||||||
atlas: "assets/chibi-stickers-pma.atlas",
|
atlasPath: "assets/chibi-stickers-pma.atlas",
|
||||||
skeleton: "assets/chibi-stickers.json",
|
skeletonPath: "assets/chibi-stickers.json",
|
||||||
animation,
|
animation,
|
||||||
skin,
|
skin,
|
||||||
pages,
|
pages,
|
||||||
@ -1826,7 +1826,7 @@ stretchyman.update = (canvas, delta, skeleton, state) => {
|
|||||||
atlas="assets/celestial-circus-pma.atlas"
|
atlas="assets/celestial-circus-pma.atlas"
|
||||||
skeleton="assets/celestial-circus-pro.skel"
|
skeleton="assets/celestial-circus-pro.skel"
|
||||||
animation="wings-and-feet"
|
animation="wings-and-feet"
|
||||||
draggable="true"
|
draggable
|
||||||
></spine-widget>
|
></spine-widget>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,627 +0,0 @@
|
|||||||
/******************************************************************************
|
|
||||||
* Spine Runtimes License Agreement
|
|
||||||
* Last updated July 28, 2023. Replaces all prior versions.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2013-2023, Esoteric Software LLC
|
|
||||||
*
|
|
||||||
* Integration of the Spine Runtimes into software or otherwise creating
|
|
||||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
|
||||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
|
||||||
* http://esotericsoftware.com/spine-editor-license
|
|
||||||
*
|
|
||||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software or
|
|
||||||
* otherwise create derivative works of the Spine Runtimes (collectively,
|
|
||||||
* "Products"), provided that each user of the Products must obtain their own
|
|
||||||
* Spine Editor license and redistribution of the Products in any form must
|
|
||||||
* include this license and copyright notice.
|
|
||||||
*
|
|
||||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
|
||||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
|
||||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
|
||||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
|
|
||||||
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
import { SpineCanvas, SpineCanvasApp, AtlasAttachmentLoader, SkeletonBinary, SkeletonJson, Skeleton, Animation, AnimationState, AnimationStateData, Physics, Vector2, Vector3, ResizeMode, Color, MixBlend, MixDirection, SceneRenderer, SkeletonData, Input } from "./index.js";
|
|
||||||
|
|
||||||
interface Rectangle {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OverlaySkeletonOptions {
|
|
||||||
atlasPath: string,
|
|
||||||
skeletonPath: string,
|
|
||||||
scale: number,
|
|
||||||
animation?: string,
|
|
||||||
skeletonData?: SkeletonData,
|
|
||||||
update?: UpdateSpineFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void;
|
|
||||||
|
|
||||||
interface OverlayHTMLOptions {
|
|
||||||
element: HTMLElement,
|
|
||||||
mode?: OverlayElementMode,
|
|
||||||
debug?: boolean,
|
|
||||||
offsetX?: number,
|
|
||||||
offsetY?: number,
|
|
||||||
xAxis?: number,
|
|
||||||
yAxis?: number,
|
|
||||||
draggable?: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
type OverlayHTMLElement = Required<OverlayHTMLOptions> & { element: HTMLElement, worldOffsetX: number, worldOffsetY: number, dragging: boolean, dragX: number, dragY: number };
|
|
||||||
|
|
||||||
type OverlayElementMode = 'inside' | 'origin';
|
|
||||||
|
|
||||||
/** Manages the life-cycle and WebGL context of a {@link SpineCanvasOverlay}. */
|
|
||||||
export class SpineCanvasOverlay {
|
|
||||||
|
|
||||||
private spineCanvas:SpineCanvas;
|
|
||||||
private canvas:HTMLCanvasElement;
|
|
||||||
private input:Input;
|
|
||||||
|
|
||||||
private skeletonList = new Array<{
|
|
||||||
skeleton: Skeleton,
|
|
||||||
state: AnimationState,
|
|
||||||
bounds: Rectangle,
|
|
||||||
htmlOptionsList: Array<OverlayHTMLElement>,
|
|
||||||
update?: UpdateSpineFunction,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
private resizeObserver:ResizeObserver;
|
|
||||||
private disposed = false;
|
|
||||||
|
|
||||||
private currentTranslateX = 0;
|
|
||||||
private currentTranslateY = 0;
|
|
||||||
private additionalPixelsBottom = 200;
|
|
||||||
private offsetHeight = 50;
|
|
||||||
private offsetHeightDraw: number;
|
|
||||||
/** Constructs a new spine canvas, rendering to the provided HTML canvas. */
|
|
||||||
constructor () {
|
|
||||||
this.canvas = document.createElement('canvas');
|
|
||||||
document.body.appendChild(this.canvas);
|
|
||||||
this.canvas.style.position = "absolute";
|
|
||||||
this.canvas.style.top = "0";
|
|
||||||
this.canvas.style.left = "0";
|
|
||||||
this.canvas.style.setProperty("pointer-events", "none");
|
|
||||||
this.offsetHeightDraw = this.offsetHeight;
|
|
||||||
this.canvas.style.transform =`translate(${this.currentTranslateX}px,${this.currentTranslateY}px)`;
|
|
||||||
// this.canvas.style.display = "inline";
|
|
||||||
// this.canvas.style.overflow = "hidden"; // useless
|
|
||||||
// this.canvas.style.setProperty("will-change", "transform"); // performance seems to be even worse with this uncommented
|
|
||||||
this.updateCanvasSize();
|
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateCanvasSize();
|
|
||||||
this.spineCanvas.renderer.resize(ResizeMode.Expand);
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(document.body);
|
|
||||||
|
|
||||||
window.addEventListener('scroll', this.scrollHandler);
|
|
||||||
|
|
||||||
|
|
||||||
this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() });
|
|
||||||
|
|
||||||
this.input = new Input(document.body, false);
|
|
||||||
this.setupDragUtility();
|
|
||||||
}
|
|
||||||
|
|
||||||
// add a skeleton to the overlay and set the bounds to the given animation or to the setup pose
|
|
||||||
public async addSkeleton(
|
|
||||||
skeletonOptions: OverlaySkeletonOptions,
|
|
||||||
htmlOptionsList: Array<OverlayHTMLOptions> | OverlayHTMLOptions | Array<HTMLElement> | HTMLElement | NodeList = [],
|
|
||||||
) {
|
|
||||||
const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, update } = skeletonOptions;
|
|
||||||
const isBinary = skeletonPath.endsWith(".skel");
|
|
||||||
await Promise.all([
|
|
||||||
isBinary ? this.loadBinary(skeletonPath) : this.loadJson(skeletonPath),
|
|
||||||
this.loadTextureAtlas(atlasPath),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const atlas = this.spineCanvas.assetManager.require(atlasPath);
|
|
||||||
const atlasLoader = new AtlasAttachmentLoader(atlas);
|
|
||||||
|
|
||||||
const skeletonLoader = isBinary ? new SkeletonBinary(atlasLoader) : new SkeletonJson(atlasLoader);
|
|
||||||
skeletonLoader.scale = scale;
|
|
||||||
|
|
||||||
const skeletonFile = this.spineCanvas.assetManager.require(skeletonPath);
|
|
||||||
const skeletonData = skeletonDataInput ?? skeletonLoader.readSkeletonData(skeletonFile);
|
|
||||||
|
|
||||||
const skeleton = new Skeleton(skeletonData);
|
|
||||||
const animationStateData = new AnimationStateData(skeletonData);
|
|
||||||
const state = new AnimationState(animationStateData);
|
|
||||||
|
|
||||||
let animationData;
|
|
||||||
if (animation) {
|
|
||||||
state.setAnimation(0, animation, true);
|
|
||||||
animationData = animation ? skeleton.data.findAnimation(animation)! : undefined;
|
|
||||||
}
|
|
||||||
const bounds = this.calculateAnimationViewport(skeleton, animationData);
|
|
||||||
|
|
||||||
let list: Array<OverlayHTMLOptions>;
|
|
||||||
if (htmlOptionsList instanceof HTMLElement) htmlOptionsList = [htmlOptionsList] as Array<HTMLElement>;
|
|
||||||
if (htmlOptionsList instanceof NodeList) htmlOptionsList = Array.from(htmlOptionsList) as Array<HTMLElement>;
|
|
||||||
if ('element' in htmlOptionsList) htmlOptionsList = [htmlOptionsList] as Array<OverlayHTMLOptions>;
|
|
||||||
|
|
||||||
if (htmlOptionsList.length > 0 && htmlOptionsList[0] instanceof HTMLElement) {
|
|
||||||
list = htmlOptionsList.map(element => ({ element } as OverlayHTMLOptions));
|
|
||||||
} else {
|
|
||||||
list = htmlOptionsList as Array<OverlayHTMLOptions>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapList = list.map(({ element, mode: givenMode, debug = false, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0, draggable = false, }, i) => {
|
|
||||||
const mode = givenMode ?? 'inside';
|
|
||||||
if (mode == 'inside' && i > 0) {
|
|
||||||
console.warn("inside option works with multiple html elements only if the elements have the same dimension"
|
|
||||||
+ "This is because the skeleton is scaled to stay into the div."
|
|
||||||
+ "You can call addSkeleton several time (skeleton data can be reuse, if given).");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
element: element as HTMLElement,
|
|
||||||
mode,
|
|
||||||
debug,
|
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
xAxis,
|
|
||||||
yAxis,
|
|
||||||
draggable,
|
|
||||||
dragX: 0,
|
|
||||||
dragY: 0,
|
|
||||||
worldOffsetX: 0,
|
|
||||||
worldOffsetY: 0,
|
|
||||||
dragging: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.skeletonList.push({ skeleton, state, update, bounds, htmlOptionsList: mapList });
|
|
||||||
|
|
||||||
return { skeleton, state };
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate bounds of the current animation on track 0, then set it
|
|
||||||
public recalculateBounds(skeleton: Skeleton) {
|
|
||||||
const element = this.skeletonList.find(element => element.skeleton === skeleton);
|
|
||||||
if (!element) return;
|
|
||||||
const track = element.state.getCurrent(0);
|
|
||||||
const animation = track?.animation as (Animation | undefined);
|
|
||||||
const bounds = this.calculateAnimationViewport(skeleton, animation);
|
|
||||||
this.setBounds(skeleton, bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the given bounds on the current skeleton
|
|
||||||
// bounds is used to center the skeleton in inside mode and as a input area for click events
|
|
||||||
public setBounds(skeleton: Skeleton, bounds: Rectangle) {
|
|
||||||
bounds.x /= skeleton.scaleX;
|
|
||||||
bounds.y /= skeleton.scaleY;
|
|
||||||
bounds.width /= skeleton.scaleX;
|
|
||||||
bounds.height /= skeleton.scaleY;
|
|
||||||
const element = this.skeletonList.find(element => element.skeleton === skeleton);
|
|
||||||
if (element) {
|
|
||||||
element.bounds = bounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Load assets utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
public async loadBinary(path: string) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.spineCanvas.assetManager.loadBinary(path,
|
|
||||||
(_, binary) => resolve(binary),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadJson(path: string) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.spineCanvas.assetManager.loadJson(path,
|
|
||||||
(_, object) => resolve(object),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadTextureAtlas(path: string) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.spineCanvas.assetManager.loadTextureAtlas(path,
|
|
||||||
(_, atlas) => resolve(atlas),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Init utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
private setupSpineCanvasApp(): SpineCanvasApp {
|
|
||||||
const red = new Color(1, 0, 0, 1);
|
|
||||||
const green = new Color(0, 1, 0, 1);
|
|
||||||
const blue = new Color(0, 0, 1, 1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
update: (canvas: SpineCanvas, delta: number) => {
|
|
||||||
this.skeletonList.forEach(({ skeleton, state, update, htmlOptionsList }) => {
|
|
||||||
if (htmlOptionsList.length === 0) return;
|
|
||||||
if (update) update(canvas, delta, skeleton, state)
|
|
||||||
else {
|
|
||||||
state.update(delta);
|
|
||||||
state.apply(skeleton);
|
|
||||||
skeleton.update(delta);
|
|
||||||
skeleton.updateWorldTransform(Physics.update);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
(document.body.querySelector("#fps")! as HTMLElement).innerText = canvas.time.framesPerSecond.toFixed(2) + " fps";
|
|
||||||
},
|
|
||||||
|
|
||||||
render: (canvas: SpineCanvas) => {
|
|
||||||
// canvas.clear(1, 0, 0, .1);
|
|
||||||
let renderer = canvas.renderer;
|
|
||||||
renderer.begin();
|
|
||||||
|
|
||||||
const devicePixelRatio = window.devicePixelRatio;
|
|
||||||
const tempVector = new Vector3();
|
|
||||||
this.skeletonList.forEach(({ skeleton, htmlOptionsList, bounds }) => {
|
|
||||||
if (htmlOptionsList.length === 0) return;
|
|
||||||
|
|
||||||
let { x: ax, y: ay, width: aw, height: ah } = bounds;
|
|
||||||
|
|
||||||
htmlOptionsList.forEach((list) => {
|
|
||||||
const { element, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY } = list;
|
|
||||||
const divBounds = element.getBoundingClientRect();
|
|
||||||
// divBounds.y += this.offsetHeightDraw;
|
|
||||||
|
|
||||||
let x = 0, y = 0;
|
|
||||||
if (mode === 'inside') {
|
|
||||||
// scale ratio
|
|
||||||
const scaleWidth = divBounds.width * devicePixelRatio / aw;
|
|
||||||
const scaleHeight = divBounds.height * devicePixelRatio / ah;
|
|
||||||
|
|
||||||
// attempt to use width ratio
|
|
||||||
let ratio = scaleWidth;
|
|
||||||
let scaledW = aw * ratio;
|
|
||||||
let scaledH = ah * ratio;
|
|
||||||
|
|
||||||
// if scaled height is bigger than div height, use height ratio instead
|
|
||||||
if (scaledH > divBounds.height * devicePixelRatio) ratio = scaleHeight;
|
|
||||||
|
|
||||||
// get the center of the bounds
|
|
||||||
const boundsX = (ax + aw / 2) * ratio;
|
|
||||||
const boundsY = (ay + ah / 2) * ratio;
|
|
||||||
|
|
||||||
// get the center of the div in world coordinate
|
|
||||||
// const divX = divBounds.x + divBounds.width / 2 + window.scrollX;
|
|
||||||
// const divY = divBounds.y - 1 + divBounds.height / 2 + window.scrollY;
|
|
||||||
const divX = divBounds.x + divBounds.width / 2;
|
|
||||||
const divY = divBounds.y - 1 + divBounds.height / 2;
|
|
||||||
this.screenToWorld(tempVector, divX, divY);
|
|
||||||
|
|
||||||
// get vertices offset: calculate the distance between div center and bounds center
|
|
||||||
x = tempVector.x - boundsX;
|
|
||||||
y = tempVector.y - boundsY;
|
|
||||||
|
|
||||||
// scale the skeleton
|
|
||||||
skeleton.scaleX = ratio;
|
|
||||||
skeleton.scaleY = ratio;
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// TODO: window.devicePixelRatio to manage browser zoom
|
|
||||||
|
|
||||||
// get the center of the div in world coordinate
|
|
||||||
// const divX = divBounds.x + divBounds.width * xAxis + window.scrollX;
|
|
||||||
// const divY = divBounds.y + divBounds.height * yAxis + window.scrollY;
|
|
||||||
const divX = divBounds.x + divBounds.width * xAxis;
|
|
||||||
const divY = divBounds.y + divBounds.height * yAxis;
|
|
||||||
this.screenToWorld(tempVector, divX, divY);
|
|
||||||
// console.log(tempVector.x, tempVector.y)
|
|
||||||
// console.log(window.devicePixelRatio)
|
|
||||||
|
|
||||||
// get vertices offset
|
|
||||||
x = tempVector.x;
|
|
||||||
y = tempVector.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
list.worldOffsetX = x + offsetX + dragX;
|
|
||||||
list.worldOffsetY = y + offsetY + dragY;
|
|
||||||
|
|
||||||
renderer.drawSkeleton(skeleton, true, -1, -1, (vertices, size, vertexSize) => {
|
|
||||||
for (let i = 0; i < size; i+=vertexSize) {
|
|
||||||
vertices[i] = vertices[i] + list.worldOffsetX;
|
|
||||||
vertices[i+1] = vertices[i+1] + list.worldOffsetY;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// drawing debug stuff
|
|
||||||
if (debug) {
|
|
||||||
// if (true) {
|
|
||||||
// show bounds and its center
|
|
||||||
renderer.rect(false,
|
|
||||||
ax * skeleton.scaleX + list.worldOffsetX,
|
|
||||||
ay * skeleton.scaleY + list.worldOffsetY,
|
|
||||||
aw * skeleton.scaleX,
|
|
||||||
ah * skeleton.scaleY,
|
|
||||||
blue);
|
|
||||||
const bbCenterX = (ax + aw / 2) * skeleton.scaleX + list.worldOffsetX;
|
|
||||||
const bbCenterY = (ay + ah / 2) * skeleton.scaleY + list.worldOffsetY;
|
|
||||||
renderer.circle(true, bbCenterX, bbCenterY, 10, blue);
|
|
||||||
|
|
||||||
// show skeleton root
|
|
||||||
const root = skeleton.getRootBone()!;
|
|
||||||
renderer.circle(true, root.x + list.worldOffsetX, root.y + list.worldOffsetY, 10, red);
|
|
||||||
|
|
||||||
// show shifted origin
|
|
||||||
const originX = list.worldOffsetX - dragX - offsetX;
|
|
||||||
const originY = list.worldOffsetY - dragY - offsetY;
|
|
||||||
renderer.circle(true, originX, originY, 10, green);
|
|
||||||
|
|
||||||
// show line from origin to bounds center
|
|
||||||
renderer.line(originX, originY, bbCenterX, bbCenterY, green);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
renderer.end();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupDragUtility() {
|
|
||||||
// TODO: we should use document - body might have some margin that offset the click events - Meanwhile I take event pageX/Y
|
|
||||||
const tempVectorInput = new Vector3();
|
|
||||||
|
|
||||||
let prevX = 0;
|
|
||||||
let prevY = 0;
|
|
||||||
this.input.addListener({
|
|
||||||
down: (x, y, ev) => {
|
|
||||||
const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
|
|
||||||
tempVectorInput.set(originalEvent.pageX - window.scrollX, originalEvent.pageY - window.scrollY, 0);
|
|
||||||
this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight);
|
|
||||||
this.skeletonList.forEach(({ htmlOptionsList, bounds, skeleton }) => {
|
|
||||||
htmlOptionsList.forEach((element) => {
|
|
||||||
if (!element.draggable) return;
|
|
||||||
|
|
||||||
const { worldOffsetX, worldOffsetY } = element;
|
|
||||||
const newBounds: Rectangle = {
|
|
||||||
x: bounds.x * skeleton.scaleX + worldOffsetX,
|
|
||||||
y: bounds.y * skeleton.scaleY + worldOffsetY,
|
|
||||||
width: bounds.width * skeleton.scaleX,
|
|
||||||
height: bounds.height * skeleton.scaleY,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.inside(tempVectorInput, newBounds)) {
|
|
||||||
element.dragging = true;
|
|
||||||
ev?.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
prevX = tempVectorInput.x;
|
|
||||||
prevY = tempVectorInput.y;
|
|
||||||
},
|
|
||||||
dragged: (x, y, ev) => {
|
|
||||||
const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
|
|
||||||
tempVectorInput.set(originalEvent.pageX - window.scrollX, originalEvent.pageY - window.scrollY, 0);
|
|
||||||
this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight);
|
|
||||||
let dragX = tempVectorInput.x - prevX;
|
|
||||||
let dragY = tempVectorInput.y - prevY;
|
|
||||||
this.skeletonList.forEach(({ htmlOptionsList, bounds, skeleton }) => {
|
|
||||||
htmlOptionsList.forEach((element) => {
|
|
||||||
const { dragging } = element;
|
|
||||||
|
|
||||||
if (dragging) {
|
|
||||||
skeleton.physicsTranslate(dragX, dragY);
|
|
||||||
element.dragX += dragX;
|
|
||||||
element.dragY += dragY;
|
|
||||||
ev?.preventDefault();
|
|
||||||
ev?.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
prevX = tempVectorInput.x;
|
|
||||||
prevY = tempVectorInput.y;
|
|
||||||
},
|
|
||||||
up: () => {
|
|
||||||
this.skeletonList.forEach(({ htmlOptionsList }) => {
|
|
||||||
htmlOptionsList.forEach((element) => {
|
|
||||||
element.dragging = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Resize/scroll utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
private updateCanvasSize() {
|
|
||||||
// const pageSize = this.getPageSize();
|
|
||||||
// this.canvas.style.width = pageSize.width + "px";
|
|
||||||
// this.canvas.style.height = pageSize.height + "px";
|
|
||||||
|
|
||||||
// const displayWidth = window.innerWidth;
|
|
||||||
// const displayHeight = window.innerHeight;
|
|
||||||
|
|
||||||
|
|
||||||
const displayWidth = document.documentElement.clientWidth;
|
|
||||||
const displayHeight = document.documentElement.clientHeight;
|
|
||||||
this.canvas.style.width = displayWidth + "px";
|
|
||||||
this.canvas.style.height = displayHeight + "px";
|
|
||||||
// this.canvas.style.height = displayHeight + this.additionalPixelsBottom + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
// private scrollHandler = () => {
|
|
||||||
// const { width, height } = this.getPageSize()
|
|
||||||
|
|
||||||
// // const viewportHeightWithScrollbar = window.innerHeight;
|
|
||||||
// // const viewportHeightNoScrollbar = document.documentElement.clientHeight;
|
|
||||||
// // const scrollbarHeight = viewportHeightWithScrollbar - viewportHeightNoScrollbar;
|
|
||||||
// // const bottomY = viewportHeightNoScrollbar + window.scrollY;
|
|
||||||
|
|
||||||
// // const viewportWidthWithScrollbar = window.innerWidth;
|
|
||||||
// // const viewportWidthNoScrollbar = document.documentElement.clientWidth;
|
|
||||||
// // const scrollbarWidth = viewportWidthWithScrollbar - viewportWidthNoScrollbar;
|
|
||||||
// // const bottomX = viewportWidthNoScrollbar + window.scrollY;
|
|
||||||
|
|
||||||
// // if (bottomX + scrollbarWidth <= width) {
|
|
||||||
// // this.currentTranslateX = window.scrollX;
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // if (bottomY + scrollbarHeight <= height) {
|
|
||||||
// // this.currentTranslateY = window.scrollY;
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// const viewportHeightNoScrollbar = document.documentElement.clientHeight;
|
|
||||||
// const bottomY = viewportHeightNoScrollbar + window.scrollY;
|
|
||||||
|
|
||||||
// const viewportWidthNoScrollbar = document.documentElement.clientWidth;
|
|
||||||
// const bottomX = viewportWidthNoScrollbar + window.scrollX;
|
|
||||||
|
|
||||||
// if (bottomX <= width) {
|
|
||||||
// this.currentTranslateX = window.scrollX;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (window.scrollY <= this.offsetHeight) {
|
|
||||||
// console.log(window.scrollY);
|
|
||||||
// console.log("aaa")
|
|
||||||
// this.currentTranslateY = 0;
|
|
||||||
// this.offsetHeightDraw = this.offsetHeight;
|
|
||||||
// } else if (bottomY + this.additionalPixelsBottom - this.offsetHeight <= height) {
|
|
||||||
// console.log("bbb")
|
|
||||||
// this.currentTranslateY = window.scrollY - this.offsetHeight;
|
|
||||||
// this.offsetHeightDraw = this.offsetHeight;
|
|
||||||
// } else {
|
|
||||||
// console.log("ccc")
|
|
||||||
// this.currentTranslateY = window.scrollY - this.additionalPixelsBottom;
|
|
||||||
// this.offsetHeightDraw = this.additionalPixelsBottom;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // translate should be faster
|
|
||||||
// this.canvas.style.transform =`translate(${this.currentTranslateX}px,${this.currentTranslateY}px)`;
|
|
||||||
// console.log(`translate(${this.currentTranslateX}px,${this.currentTranslateY}px)`)
|
|
||||||
// // this.canvas.style.top = `${this.currentTranslateY}px`;
|
|
||||||
// // this.canvas.style.left = `${this.currentTranslateX}px`;
|
|
||||||
// }
|
|
||||||
|
|
||||||
private scrollHandler = () => {
|
|
||||||
const { width, height } = this.getPageSize()
|
|
||||||
|
|
||||||
const viewportHeightNoScrollbar = document.documentElement.clientHeight;
|
|
||||||
const bottomY = viewportHeightNoScrollbar + window.scrollY;
|
|
||||||
|
|
||||||
const viewportWidthNoScrollbar = document.documentElement.clientWidth;
|
|
||||||
const bottomX = viewportWidthNoScrollbar + window.scrollY;
|
|
||||||
|
|
||||||
if (bottomX <= width) {
|
|
||||||
this.currentTranslateX = window.scrollX;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bottomY <= height) {
|
|
||||||
this.currentTranslateY = window.scrollY;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.canvas.style.transform =`translate(${this.currentTranslateX}px,${this.currentTranslateY}px)`;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPageSize() {
|
|
||||||
const width = Math.max(
|
|
||||||
document.body.scrollWidth,
|
|
||||||
document.documentElement.scrollWidth,
|
|
||||||
document.body.offsetWidth,
|
|
||||||
document.documentElement.offsetWidth,
|
|
||||||
document.documentElement.clientWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
const height = Math.max(
|
|
||||||
document.body.scrollHeight,
|
|
||||||
document.documentElement.scrollHeight,
|
|
||||||
document.body.offsetHeight,
|
|
||||||
document.documentElement.offsetHeight,
|
|
||||||
document.documentElement.clientHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Other utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
private calculateAnimationViewport (skeleton: Skeleton, animation?: Animation): Rectangle {
|
|
||||||
skeleton.setToSetupPose();
|
|
||||||
|
|
||||||
let offset = new Vector2(), size = new Vector2();
|
|
||||||
const tempArray = new Array<number>(2);
|
|
||||||
if (!animation) {
|
|
||||||
skeleton.updateWorldTransform(Physics.update);
|
|
||||||
skeleton.getBounds(offset, size, tempArray, this.spineCanvas.renderer.skeletonRenderer.getSkeletonClipping());
|
|
||||||
return {
|
|
||||||
x: offset.x,
|
|
||||||
y: offset.y,
|
|
||||||
width: size.x,
|
|
||||||
height: size.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let steps = 100, stepTime = animation.duration ? animation.duration / steps : 0, time = 0;
|
|
||||||
let minX = 100000000, maxX = -100000000, minY = 100000000, maxY = -100000000;
|
|
||||||
for (let i = 0; i < steps; i++, time += stepTime) {
|
|
||||||
animation.apply(skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn);
|
|
||||||
skeleton.updateWorldTransform(Physics.update);
|
|
||||||
skeleton.getBounds(offset, size, tempArray, this.spineCanvas.renderer.skeletonRenderer.getSkeletonClipping());
|
|
||||||
|
|
||||||
if (!isNaN(offset.x) && !isNaN(offset.y) && !isNaN(size.x) && !isNaN(size.y)) {
|
|
||||||
minX = Math.min(offset.x, minX);
|
|
||||||
maxX = Math.max(offset.x + size.x, maxX);
|
|
||||||
minY = Math.min(offset.y, minY);
|
|
||||||
maxY = Math.max(offset.y + size.y, maxY);
|
|
||||||
} else
|
|
||||||
console.error("Animation bounds are invalid: " + animation.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: minX,
|
|
||||||
y: minY,
|
|
||||||
width: maxX - minX,
|
|
||||||
height: maxY - minY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private screenToWorld(vec: Vector3, x: number, y: number) {
|
|
||||||
vec.set(x, y, 0);
|
|
||||||
this.spineCanvas.renderer.camera.screenToWorld(vec, this.canvas.clientWidth, this.canvas.clientHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private inside(point: { x: number; y: number }, rectangle: Rectangle): boolean {
|
|
||||||
return (
|
|
||||||
point.x >= rectangle.x &&
|
|
||||||
point.x <= rectangle.x + rectangle.width &&
|
|
||||||
point.y >= rectangle.y &&
|
|
||||||
point.y <= rectangle.y + rectangle.height
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
dispose () {
|
|
||||||
this.spineCanvas.dispose();
|
|
||||||
this.canvas.remove();
|
|
||||||
this.disposed = true;
|
|
||||||
this.resizeObserver.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,580 +0,0 @@
|
|||||||
/******************************************************************************
|
|
||||||
* Spine Runtimes License Agreement
|
|
||||||
* Last updated July 28, 2023. Replaces all prior versions.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2013-2023, Esoteric Software LLC
|
|
||||||
*
|
|
||||||
* Integration of the Spine Runtimes into software or otherwise creating
|
|
||||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
|
||||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
|
||||||
* http://esotericsoftware.com/spine-editor-license
|
|
||||||
*
|
|
||||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software or
|
|
||||||
* otherwise create derivative works of the Spine Runtimes (collectively,
|
|
||||||
* "Products"), provided that each user of the Products must obtain their own
|
|
||||||
* Spine Editor license and redistribution of the Products in any form must
|
|
||||||
* include this license and copyright notice.
|
|
||||||
*
|
|
||||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
|
||||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
|
||||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
|
||||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
|
|
||||||
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
import { SpineCanvas, SpineCanvasApp, AtlasAttachmentLoader, SkeletonBinary, SkeletonJson, Skeleton, Animation, AnimationState, AnimationStateData, Physics, Vector2, Vector3, ResizeMode, Color, MixBlend, MixDirection, SceneRenderer, SkeletonData, Input } from "./index.js";
|
|
||||||
|
|
||||||
interface Rectangle {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OverlaySkeletonOptions {
|
|
||||||
atlasPath: string,
|
|
||||||
skeletonPath: string,
|
|
||||||
scale: number,
|
|
||||||
animation?: string,
|
|
||||||
skeletonData?: SkeletonData,
|
|
||||||
update?: UpdateSpineFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateSpineFunction = (canvas: SpineCanvas, delta: number, skeleton: Skeleton, state: AnimationState) => void;
|
|
||||||
|
|
||||||
interface OverlayHTMLOptions {
|
|
||||||
element: HTMLElement,
|
|
||||||
mode?: OverlayElementMode,
|
|
||||||
debug?: boolean,
|
|
||||||
offsetX?: number,
|
|
||||||
offsetY?: number,
|
|
||||||
xAxis?: number,
|
|
||||||
yAxis?: number,
|
|
||||||
draggable?: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
type OverlayHTMLElement = Required<OverlayHTMLOptions> & { element: HTMLElement, worldOffsetX: number, worldOffsetY: number, dragging: boolean, dragX: number, dragY: number };
|
|
||||||
|
|
||||||
type OverlayElementMode = 'inside' | 'origin';
|
|
||||||
|
|
||||||
/** Manages the life-cycle and WebGL context of a {@link SpineCanvasOverlay}. */
|
|
||||||
export class SpineCanvasOverlay {
|
|
||||||
|
|
||||||
private spineCanvas:SpineCanvas;
|
|
||||||
private canvas:HTMLCanvasElement;
|
|
||||||
private input:Input;
|
|
||||||
|
|
||||||
private skeletonList = new Array<{
|
|
||||||
skeleton: Skeleton,
|
|
||||||
state: AnimationState,
|
|
||||||
bounds: Rectangle,
|
|
||||||
htmlOptionsList: Array<OverlayHTMLElement>,
|
|
||||||
update?: UpdateSpineFunction,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
private resizeObserver:ResizeObserver;
|
|
||||||
private disposed = false;
|
|
||||||
|
|
||||||
private currentTranslateX = 0;
|
|
||||||
private currentTranslateY = 0;
|
|
||||||
/** Constructs a new spine canvas, rendering to the provided HTML canvas. */
|
|
||||||
constructor () {
|
|
||||||
this.canvas = document.createElement('canvas');
|
|
||||||
document.body.appendChild(this.canvas);
|
|
||||||
this.canvas.style.position = "absolute";
|
|
||||||
this.canvas.style.top = "0";
|
|
||||||
this.canvas.style.left = "0";
|
|
||||||
this.canvas.style.display = "inline";
|
|
||||||
this.canvas.style.setProperty("pointer-events", "none");
|
|
||||||
// this.canvas.style.setProperty("will-change", "transform");
|
|
||||||
this.updateCanvasSize();
|
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateCanvasSize();
|
|
||||||
this.spineCanvas.renderer.resize(ResizeMode.Expand);
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(document.body);
|
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
const { width, height } = this.getPageSize()
|
|
||||||
|
|
||||||
// const viewportHeightWithScrollbar = window.innerHeight;
|
|
||||||
// const viewportHeightNoScrollbar = document.documentElement.clientHeight;
|
|
||||||
// const scrollbarHeight = viewportHeightWithScrollbar - viewportHeightNoScrollbar;
|
|
||||||
// const bottomY = viewportHeightNoScrollbar + window.scrollY;
|
|
||||||
|
|
||||||
// const viewportWidthWithScrollbar = window.innerWidth;
|
|
||||||
// const viewportWidthNoScrollbar = document.documentElement.clientWidth;
|
|
||||||
// const scrollbarWidth = viewportWidthWithScrollbar - viewportWidthNoScrollbar;
|
|
||||||
// const bottomX = viewportWidthNoScrollbar + window.scrollY;
|
|
||||||
|
|
||||||
// if (bottomX + scrollbarWidth <= width) {
|
|
||||||
// this.currentTranslateX = window.scrollX;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (bottomY + scrollbarHeight <= height) {
|
|
||||||
// this.currentTranslateY = window.scrollY;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const viewportHeightNoScrollbar = document.documentElement.clientHeight;
|
|
||||||
const bottomY = viewportHeightNoScrollbar + window.scrollY;
|
|
||||||
|
|
||||||
const viewportWidthNoScrollbar = document.documentElement.clientWidth;
|
|
||||||
const bottomX = viewportWidthNoScrollbar + window.scrollY;
|
|
||||||
|
|
||||||
if (bottomX <= width) {
|
|
||||||
this.currentTranslateX = window.scrollX;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bottomY <= height) {
|
|
||||||
this.currentTranslateY = window.scrollY;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.canvas.style.transform =`translate(${this.currentTranslateX}px,${this.currentTranslateY}px)`;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
this.spineCanvas = new SpineCanvas(this.canvas, { app: this.setupSpineCanvasApp() });
|
|
||||||
|
|
||||||
this.input = new Input(document.body, false);
|
|
||||||
this.setupDragUtility();
|
|
||||||
}
|
|
||||||
|
|
||||||
// add a skeleton to the overlay and set the bounds to the given animation or to the setup pose
|
|
||||||
public async addSkeleton(
|
|
||||||
skeletonOptions: OverlaySkeletonOptions,
|
|
||||||
htmlOptionsList: Array<OverlayHTMLOptions> | OverlayHTMLOptions | Array<HTMLElement> | HTMLElement | NodeList = [],
|
|
||||||
) {
|
|
||||||
const { atlasPath, skeletonPath, scale = 1, animation, skeletonData: skeletonDataInput, update } = skeletonOptions;
|
|
||||||
const isBinary = skeletonPath.endsWith(".skel");
|
|
||||||
await Promise.all([
|
|
||||||
isBinary ? this.loadBinary(skeletonPath) : this.loadJson(skeletonPath),
|
|
||||||
this.loadTextureAtlas(atlasPath),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const atlas = this.spineCanvas.assetManager.require(atlasPath);
|
|
||||||
const atlasLoader = new AtlasAttachmentLoader(atlas);
|
|
||||||
|
|
||||||
const skeletonLoader = isBinary ? new SkeletonBinary(atlasLoader) : new SkeletonJson(atlasLoader);
|
|
||||||
skeletonLoader.scale = scale;
|
|
||||||
|
|
||||||
const skeletonFile = this.spineCanvas.assetManager.require(skeletonPath);
|
|
||||||
const skeletonData = skeletonDataInput ?? skeletonLoader.readSkeletonData(skeletonFile);
|
|
||||||
|
|
||||||
const skeleton = new Skeleton(skeletonData);
|
|
||||||
const animationStateData = new AnimationStateData(skeletonData);
|
|
||||||
const state = new AnimationState(animationStateData);
|
|
||||||
|
|
||||||
let animationData;
|
|
||||||
if (animation) {
|
|
||||||
state.setAnimation(0, animation, true);
|
|
||||||
animationData = animation ? skeleton.data.findAnimation(animation)! : undefined;
|
|
||||||
}
|
|
||||||
const bounds = this.calculateAnimationViewport(skeleton, animationData);
|
|
||||||
|
|
||||||
let list: Array<OverlayHTMLOptions>;
|
|
||||||
if (htmlOptionsList instanceof HTMLElement) htmlOptionsList = [htmlOptionsList] as Array<HTMLElement>;
|
|
||||||
if (htmlOptionsList instanceof NodeList) htmlOptionsList = Array.from(htmlOptionsList) as Array<HTMLElement>;
|
|
||||||
if ('element' in htmlOptionsList) htmlOptionsList = [htmlOptionsList] as Array<OverlayHTMLOptions>;
|
|
||||||
|
|
||||||
if (htmlOptionsList.length > 0 && htmlOptionsList[0] instanceof HTMLElement) {
|
|
||||||
list = htmlOptionsList.map(element => ({ element } as OverlayHTMLOptions));
|
|
||||||
} else {
|
|
||||||
list = htmlOptionsList as Array<OverlayHTMLOptions>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapList = list.map(({ element, mode: givenMode, debug = false, offsetX = 0, offsetY = 0, xAxis = 0, yAxis = 0, draggable = false, }, i) => {
|
|
||||||
const mode = givenMode ?? 'inside';
|
|
||||||
if (mode == 'inside' && i > 0) {
|
|
||||||
console.warn("inside option works with multiple html elements only if the elements have the same dimension"
|
|
||||||
+ "This is because the skeleton is scaled to stay into the div."
|
|
||||||
+ "You can call addSkeleton several time (skeleton data can be reuse, if given).");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
element: element as HTMLElement,
|
|
||||||
mode,
|
|
||||||
debug,
|
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
xAxis,
|
|
||||||
yAxis,
|
|
||||||
draggable,
|
|
||||||
dragX: 0,
|
|
||||||
dragY: 0,
|
|
||||||
worldOffsetX: 0,
|
|
||||||
worldOffsetY: 0,
|
|
||||||
dragging: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.skeletonList.push({ skeleton, state, update, bounds, htmlOptionsList: mapList });
|
|
||||||
|
|
||||||
return { skeleton, state };
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate bounds of the current animation on track 0, then set it
|
|
||||||
public recalculateBounds(skeleton: Skeleton) {
|
|
||||||
const element = this.skeletonList.find(element => element.skeleton === skeleton);
|
|
||||||
if (!element) return;
|
|
||||||
const track = element.state.getCurrent(0);
|
|
||||||
const animation = track?.animation as (Animation | undefined);
|
|
||||||
const bounds = this.calculateAnimationViewport(skeleton, animation);
|
|
||||||
this.setBounds(skeleton, bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the given bounds on the current skeleton
|
|
||||||
// bounds is used to center the skeleton in inside mode and as a input area for click events
|
|
||||||
public setBounds(skeleton: Skeleton, bounds: Rectangle) {
|
|
||||||
bounds.x /= skeleton.scaleX;
|
|
||||||
bounds.y /= skeleton.scaleY;
|
|
||||||
bounds.width /= skeleton.scaleX;
|
|
||||||
bounds.height /= skeleton.scaleY;
|
|
||||||
const element = this.skeletonList.find(element => element.skeleton === skeleton);
|
|
||||||
if (element) {
|
|
||||||
element.bounds = bounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Load assets utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
public async loadBinary(path: string) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.spineCanvas.assetManager.loadBinary(path,
|
|
||||||
(_, binary) => resolve(binary),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadJson(path: string) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.spineCanvas.assetManager.loadJson(path,
|
|
||||||
(_, object) => resolve(object),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadTextureAtlas(path: string) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.spineCanvas.assetManager.loadTextureAtlas(path,
|
|
||||||
(_, atlas) => resolve(atlas),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Init utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
private setupSpineCanvasApp(): SpineCanvasApp {
|
|
||||||
const red = new Color(1, 0, 0, 1);
|
|
||||||
const green = new Color(0, 1, 0, 1);
|
|
||||||
const blue = new Color(0, 0, 1, 1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
update: (canvas: SpineCanvas, delta: number) => {
|
|
||||||
this.skeletonList.forEach(({ skeleton, state, update, htmlOptionsList }) => {
|
|
||||||
if (htmlOptionsList.length === 0) return;
|
|
||||||
if (update) update(canvas, delta, skeleton, state)
|
|
||||||
else {
|
|
||||||
state.update(delta);
|
|
||||||
state.apply(skeleton);
|
|
||||||
skeleton.update(delta);
|
|
||||||
skeleton.updateWorldTransform(Physics.update);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
(document.body.querySelector("#fps")! as HTMLElement).innerText = canvas.time.framesPerSecond.toFixed(2) + " fps";
|
|
||||||
},
|
|
||||||
|
|
||||||
render: (canvas: SpineCanvas) => {
|
|
||||||
let renderer = canvas.renderer;
|
|
||||||
renderer.begin();
|
|
||||||
|
|
||||||
const devicePixelRatio = window.devicePixelRatio;
|
|
||||||
const tempVector = new Vector3();
|
|
||||||
this.skeletonList.forEach(({ skeleton, htmlOptionsList, bounds }) => {
|
|
||||||
if (htmlOptionsList.length === 0) return;
|
|
||||||
|
|
||||||
let { x: ax, y: ay, width: aw, height: ah } = bounds;
|
|
||||||
|
|
||||||
htmlOptionsList.forEach((list) => {
|
|
||||||
const { element, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY } = list;
|
|
||||||
const divBounds = element.getBoundingClientRect();
|
|
||||||
|
|
||||||
let x = 0, y = 0;
|
|
||||||
if (mode === 'inside') {
|
|
||||||
// scale ratio
|
|
||||||
const scaleWidth = divBounds.width * devicePixelRatio / aw;
|
|
||||||
const scaleHeight = divBounds.height * devicePixelRatio / ah;
|
|
||||||
|
|
||||||
// attempt to use width ratio
|
|
||||||
let ratio = scaleWidth;
|
|
||||||
let scaledW = aw * ratio;
|
|
||||||
let scaledH = ah * ratio;
|
|
||||||
|
|
||||||
// if scaled height is bigger than div height, use height ratio instead
|
|
||||||
if (scaledH > divBounds.height * devicePixelRatio) ratio = scaleHeight;
|
|
||||||
|
|
||||||
// get the center of the bounds
|
|
||||||
const boundsX = (ax + aw / 2) * ratio;
|
|
||||||
const boundsY = (ay + ah / 2) * ratio;
|
|
||||||
|
|
||||||
// get the center of the div in world coordinate
|
|
||||||
// const divX = divBounds.x + divBounds.width / 2 + window.scrollX;
|
|
||||||
// const divY = divBounds.y - 1 + divBounds.height / 2 + window.scrollY;
|
|
||||||
const divX = divBounds.x + divBounds.width / 2;
|
|
||||||
const divY = divBounds.y - 1 + divBounds.height / 2;
|
|
||||||
this.screenToWorld(tempVector, divX, divY);
|
|
||||||
|
|
||||||
// get vertices offset: calculate the distance between div center and bounds center
|
|
||||||
x = tempVector.x - boundsX;
|
|
||||||
y = tempVector.y - boundsY;
|
|
||||||
|
|
||||||
// scale the skeleton
|
|
||||||
skeleton.scaleX = ratio;
|
|
||||||
skeleton.scaleY = ratio;
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// TODO: window.devicePixelRatio to manage browser zoom
|
|
||||||
|
|
||||||
// get the center of the div in world coordinate
|
|
||||||
// const divX = divBounds.x + divBounds.width * xAxis + window.scrollX;
|
|
||||||
// const divY = divBounds.y + divBounds.height * yAxis + window.scrollY;
|
|
||||||
const divX = divBounds.x + divBounds.width * xAxis;
|
|
||||||
const divY = divBounds.y + divBounds.height * yAxis;
|
|
||||||
this.screenToWorld(tempVector, divX, divY);
|
|
||||||
// console.log(tempVector.x, tempVector.y)
|
|
||||||
// console.log(window.devicePixelRatio)
|
|
||||||
|
|
||||||
// get vertices offset
|
|
||||||
x = tempVector.x;
|
|
||||||
y = tempVector.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
list.worldOffsetX = x + offsetX + dragX;
|
|
||||||
list.worldOffsetY = y + offsetY + dragY;
|
|
||||||
|
|
||||||
renderer.drawSkeleton(skeleton, true, -1, -1, (vertices, size, vertexSize) => {
|
|
||||||
for (let i = 0; i < size; i+=vertexSize) {
|
|
||||||
vertices[i] = vertices[i] + list.worldOffsetX;
|
|
||||||
vertices[i+1] = vertices[i+1] + list.worldOffsetY;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// drawing debug stuff
|
|
||||||
if (debug) {
|
|
||||||
// if (true) {
|
|
||||||
// show bounds and its center
|
|
||||||
renderer.rect(false,
|
|
||||||
ax * skeleton.scaleX + list.worldOffsetX,
|
|
||||||
ay * skeleton.scaleY + list.worldOffsetY,
|
|
||||||
aw * skeleton.scaleX,
|
|
||||||
ah * skeleton.scaleY,
|
|
||||||
blue);
|
|
||||||
const bbCenterX = (ax + aw / 2) * skeleton.scaleX + list.worldOffsetX;
|
|
||||||
const bbCenterY = (ay + ah / 2) * skeleton.scaleY + list.worldOffsetY;
|
|
||||||
renderer.circle(true, bbCenterX, bbCenterY, 10, blue);
|
|
||||||
|
|
||||||
// show skeleton root
|
|
||||||
const root = skeleton.getRootBone()!;
|
|
||||||
renderer.circle(true, root.x + list.worldOffsetX, root.y + list.worldOffsetY, 10, red);
|
|
||||||
|
|
||||||
// show shifted origin
|
|
||||||
const originX = list.worldOffsetX - dragX - offsetX;
|
|
||||||
const originY = list.worldOffsetY - dragY - offsetY;
|
|
||||||
renderer.circle(true, originX, originY, 10, green);
|
|
||||||
|
|
||||||
// show line from origin to bounds center
|
|
||||||
renderer.line(originX, originY, bbCenterX, bbCenterY, green);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
renderer.end();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupDragUtility() {
|
|
||||||
// TODO: we should use document - body might have some margin that offset the click events - Meanwhile I take event pageX/Y
|
|
||||||
const tempVectorInput = new Vector3();
|
|
||||||
|
|
||||||
let prevX = 0;
|
|
||||||
let prevY = 0;
|
|
||||||
this.input.addListener({
|
|
||||||
down: (x, y, ev) => {
|
|
||||||
const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
|
|
||||||
tempVectorInput.set(originalEvent.pageX - window.scrollX, originalEvent.pageY - window.scrollY, 0);
|
|
||||||
this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight);
|
|
||||||
this.skeletonList.forEach(({ htmlOptionsList, bounds, skeleton }) => {
|
|
||||||
htmlOptionsList.forEach((element) => {
|
|
||||||
if (!element.draggable) return;
|
|
||||||
|
|
||||||
const { worldOffsetX, worldOffsetY } = element;
|
|
||||||
const newBounds: Rectangle = {
|
|
||||||
x: bounds.x * skeleton.scaleX + worldOffsetX,
|
|
||||||
y: bounds.y * skeleton.scaleY + worldOffsetY,
|
|
||||||
width: bounds.width * skeleton.scaleX,
|
|
||||||
height: bounds.height * skeleton.scaleY,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.inside(tempVectorInput, newBounds)) {
|
|
||||||
element.dragging = true;
|
|
||||||
ev?.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
prevX = tempVectorInput.x;
|
|
||||||
prevY = tempVectorInput.y;
|
|
||||||
},
|
|
||||||
dragged: (x, y, ev) => {
|
|
||||||
const originalEvent = ev instanceof MouseEvent ? ev : ev!.changedTouches[0];
|
|
||||||
tempVectorInput.set(originalEvent.pageX - window.scrollX, originalEvent.pageY - window.scrollY, 0);
|
|
||||||
this.spineCanvas.renderer.camera.screenToWorld(tempVectorInput, this.canvas.clientWidth, this.canvas.clientHeight);
|
|
||||||
let dragX = tempVectorInput.x - prevX;
|
|
||||||
let dragY = tempVectorInput.y - prevY;
|
|
||||||
this.skeletonList.forEach(({ htmlOptionsList, bounds, skeleton }) => {
|
|
||||||
htmlOptionsList.forEach((element) => {
|
|
||||||
const { dragging } = element;
|
|
||||||
|
|
||||||
if (dragging) {
|
|
||||||
skeleton.physicsTranslate(dragX, dragY);
|
|
||||||
element.dragX += dragX;
|
|
||||||
element.dragY += dragY;
|
|
||||||
ev?.preventDefault();
|
|
||||||
ev?.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
prevX = tempVectorInput.x;
|
|
||||||
prevY = tempVectorInput.y;
|
|
||||||
},
|
|
||||||
up: () => {
|
|
||||||
this.skeletonList.forEach(({ htmlOptionsList }) => {
|
|
||||||
htmlOptionsList.forEach((element) => {
|
|
||||||
element.dragging = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Resize utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
private updateCanvasSize() {
|
|
||||||
// const pageSize = this.getPageSize();
|
|
||||||
// this.canvas.style.width = pageSize.width + "px";
|
|
||||||
// this.canvas.style.height = pageSize.height + "px";
|
|
||||||
|
|
||||||
// const displayWidth = window.innerWidth;
|
|
||||||
// const displayHeight = window.innerHeight;
|
|
||||||
|
|
||||||
|
|
||||||
const displayWidth = document.documentElement.clientWidth;
|
|
||||||
const displayHeight = document.documentElement.clientHeight;
|
|
||||||
this.canvas.style.width = displayWidth + "px";
|
|
||||||
this.canvas.style.height = displayHeight + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPageSize() {
|
|
||||||
const width = Math.max(
|
|
||||||
document.body.scrollWidth,
|
|
||||||
document.documentElement.scrollWidth,
|
|
||||||
document.body.offsetWidth,
|
|
||||||
document.documentElement.offsetWidth,
|
|
||||||
document.documentElement.clientWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
const height = Math.max(
|
|
||||||
document.body.scrollHeight,
|
|
||||||
document.documentElement.scrollHeight,
|
|
||||||
document.body.offsetHeight,
|
|
||||||
document.documentElement.offsetHeight,
|
|
||||||
document.documentElement.clientHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Other utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
private calculateAnimationViewport (skeleton: Skeleton, animation?: Animation): Rectangle {
|
|
||||||
skeleton.setToSetupPose();
|
|
||||||
|
|
||||||
let offset = new Vector2(), size = new Vector2();
|
|
||||||
const tempArray = new Array<number>(2);
|
|
||||||
if (!animation) {
|
|
||||||
skeleton.updateWorldTransform(Physics.update);
|
|
||||||
skeleton.getBounds(offset, size, tempArray, this.spineCanvas.renderer.skeletonRenderer.getSkeletonClipping());
|
|
||||||
return {
|
|
||||||
x: offset.x,
|
|
||||||
y: offset.y,
|
|
||||||
width: size.x,
|
|
||||||
height: size.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let steps = 100, stepTime = animation.duration ? animation.duration / steps : 0, time = 0;
|
|
||||||
let minX = 100000000, maxX = -100000000, minY = 100000000, maxY = -100000000;
|
|
||||||
for (let i = 0; i < steps; i++, time += stepTime) {
|
|
||||||
animation.apply(skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn);
|
|
||||||
skeleton.updateWorldTransform(Physics.update);
|
|
||||||
skeleton.getBounds(offset, size, tempArray, this.spineCanvas.renderer.skeletonRenderer.getSkeletonClipping());
|
|
||||||
|
|
||||||
if (!isNaN(offset.x) && !isNaN(offset.y) && !isNaN(size.x) && !isNaN(size.y)) {
|
|
||||||
minX = Math.min(offset.x, minX);
|
|
||||||
maxX = Math.max(offset.x + size.x, maxX);
|
|
||||||
minY = Math.min(offset.y, minY);
|
|
||||||
maxY = Math.max(offset.y + size.y, maxY);
|
|
||||||
} else
|
|
||||||
console.error("Animation bounds are invalid: " + animation.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: minX,
|
|
||||||
y: minY,
|
|
||||||
width: maxX - minX,
|
|
||||||
height: maxY - minY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private screenToWorld(vec: Vector3, x: number, y: number) {
|
|
||||||
vec.set(x, y, 0);
|
|
||||||
this.spineCanvas.renderer.camera.screenToWorld(vec, this.canvas.clientWidth, this.canvas.clientHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private inside(point: { x: number; y: number }, rectangle: Rectangle): boolean {
|
|
||||||
return (
|
|
||||||
point.x >= rectangle.x &&
|
|
||||||
point.x <= rectangle.x + rectangle.width &&
|
|
||||||
point.y >= rectangle.y &&
|
|
||||||
point.y <= rectangle.y + rectangle.height
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
dispose () {
|
|
||||||
this.spineCanvas.dispose();
|
|
||||||
this.canvas.remove();
|
|
||||||
this.disposed = true;
|
|
||||||
this.resizeObserver.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -27,7 +27,29 @@
|
|||||||
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import { AtlasAttachmentLoader, SkeletonBinary, SkeletonJson, Skeleton, Animation, AnimationState, AnimationStateData, Physics, Vector2, Vector3, Color, MixBlend, MixDirection, SceneRenderer, SkeletonData, Input, LoadingScreenWidget, TextureAtlas, Texture, ManagedWebGLRenderingContext, AssetManager, TimeKeeper } from "./index.js";
|
import {
|
||||||
|
Animation,
|
||||||
|
AnimationState,
|
||||||
|
AnimationStateData,
|
||||||
|
AtlasAttachmentLoader,
|
||||||
|
AssetManager,
|
||||||
|
Color,
|
||||||
|
Input,
|
||||||
|
LoadingScreenWidget,
|
||||||
|
ManagedWebGLRenderingContext,
|
||||||
|
MixBlend,
|
||||||
|
MixDirection,
|
||||||
|
Physics,
|
||||||
|
SceneRenderer,
|
||||||
|
SkeletonBinary,
|
||||||
|
SkeletonData,
|
||||||
|
SkeletonJson,
|
||||||
|
Skeleton,
|
||||||
|
TextureAtlas,
|
||||||
|
TimeKeeper,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
} from "./index.js";
|
||||||
|
|
||||||
interface Point {
|
interface Point {
|
||||||
x: number,
|
x: number,
|
||||||
@ -43,7 +65,7 @@ type BeforeAfterUpdateSpineWidgetFunction = (skeleton: Skeleton, state: Animatio
|
|||||||
type UpdateSpineWidgetFunction = (delta: number, skeleton: Skeleton, state: AnimationState) => void;
|
type UpdateSpineWidgetFunction = (delta: number, skeleton: Skeleton, state: AnimationState) => void;
|
||||||
|
|
||||||
type OffScreenUpdateBehaviourType = "pause" | "update" | "pose";
|
type OffScreenUpdateBehaviourType = "pause" | "update" | "pose";
|
||||||
function isOffScreenUpdateBehaviourType(value: string): value is OffScreenUpdateBehaviourType {
|
function isOffScreenUpdateBehaviourType(value: string | null): value is OffScreenUpdateBehaviourType {
|
||||||
return (
|
return (
|
||||||
value === "pause" ||
|
value === "pause" ||
|
||||||
value === "update" ||
|
value === "update" ||
|
||||||
@ -51,8 +73,8 @@ function isOffScreenUpdateBehaviourType(value: string): value is OffScreenUpdate
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModeType = 'inside' | 'origin';
|
type ModeType = "inside" | "origin";
|
||||||
function isModeType(value: string): value is ModeType {
|
function isModeType(value: string | null): value is ModeType {
|
||||||
return (
|
return (
|
||||||
value === "inside" ||
|
value === "inside" ||
|
||||||
value === "origin"
|
value === "origin"
|
||||||
@ -60,7 +82,7 @@ function isModeType(value: string): value is ModeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FitType = "fill" | "width" | "height" | "contain" | "cover" | "none" | "scaleDown";
|
type FitType = "fill" | "width" | "height" | "contain" | "cover" | "none" | "scaleDown";
|
||||||
function isFitType(value: string): value is FitType {
|
function isFitType(value: string | null): value is FitType {
|
||||||
return (
|
return (
|
||||||
value === "fill" ||
|
value === "fill" ||
|
||||||
value === "width" ||
|
value === "width" ||
|
||||||
@ -72,24 +94,48 @@ function isFitType(value: string): value is FitType {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WidgetLayoutOptions {
|
type AttributeTypes = "string" | "number" | "boolean" | "string-number" | "fitType" | "modeType" | "offScreenUpdateBehaviourType";
|
||||||
|
|
||||||
|
interface WidgetAttributes {
|
||||||
|
atlasPath: string
|
||||||
|
skeletonPath: string
|
||||||
|
scale: number
|
||||||
|
animation?: string
|
||||||
|
skin?: string
|
||||||
|
fit: FitType
|
||||||
mode: ModeType
|
mode: ModeType
|
||||||
debug: boolean
|
|
||||||
offsetX: number
|
|
||||||
offsetY: number
|
|
||||||
xAxis: number
|
xAxis: number
|
||||||
yAxis: number
|
yAxis: number
|
||||||
draggable: boolean
|
offsetX: number
|
||||||
fit: FitType
|
offsetY: number
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
|
draggable: boolean
|
||||||
|
debug: boolean
|
||||||
identifier: string
|
identifier: string
|
||||||
|
manualStart: boolean
|
||||||
|
pages?: Array<number>
|
||||||
|
clip: boolean
|
||||||
|
offScreenUpdateBehaviour: OffScreenUpdateBehaviourType
|
||||||
|
loadingSpinner: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WidgetOverridableMethods {
|
||||||
|
update?: UpdateSpineWidgetFunction;
|
||||||
|
beforeUpdateWorldTransforms: BeforeAfterUpdateSpineWidgetFunction;
|
||||||
|
afterUpdateWorldTransforms: BeforeAfterUpdateSpineWidgetFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WidgetPublicState {
|
interface WidgetPublicState {
|
||||||
skeleton: Skeleton
|
skeleton: Skeleton
|
||||||
state: AnimationState
|
state: AnimationState
|
||||||
bounds: Rectangle
|
bounds: Rectangle
|
||||||
|
onScreen: boolean
|
||||||
|
onScreenAtLeastOnce: boolean
|
||||||
|
loadingPromise: Promise<SpineWebComponentWidget>
|
||||||
|
loading: boolean
|
||||||
|
started: boolean
|
||||||
|
textureAtlas: TextureAtlas
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WidgetInternalState {
|
interface WidgetInternalState {
|
||||||
@ -101,37 +147,183 @@ interface WidgetInternalState {
|
|||||||
|
|
||||||
// TODO: add missing assets to main assets folder (chibi)
|
// TODO: add missing assets to main assets folder (chibi)
|
||||||
|
|
||||||
class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions, WidgetInternalState, Partial<WidgetPublicState> {
|
class SpineWebComponentWidget extends HTMLElement implements WidgetAttributes, WidgetOverridableMethods, WidgetInternalState, Partial<WidgetPublicState> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL of the skeleton atlas file (.atlas)
|
* The URL of the skeleton atlas file (.atlas)
|
||||||
|
* Connected to `atlas` attribute.
|
||||||
*/
|
*/
|
||||||
public atlasPath: string;
|
public atlasPath: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL of the skeleton JSON (.json) or binary (.skel) file
|
* The URL of the skeleton JSON (.json) or binary (.skel) file
|
||||||
|
* Connected to `skeleton` attribute.
|
||||||
*/
|
*/
|
||||||
public skeletonPath: string;
|
public skeletonPath: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scale when loading the skeleton data. Default: 1
|
* The scale when loading the skeleton data. Default: 1
|
||||||
|
* Connected to `scale` attribute.
|
||||||
*/
|
*/
|
||||||
public scale = 1;
|
public scale = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional: The name of the animation to be played
|
* Optional: The name of the animation to be played
|
||||||
|
* Connected to `animation` attribute.
|
||||||
*/
|
*/
|
||||||
public animation?: string;
|
public get animation() : string | undefined {
|
||||||
|
return this._animation;
|
||||||
|
}
|
||||||
|
public set animation(value: string | undefined) {
|
||||||
|
this._animation = value;
|
||||||
|
this.initWidget();
|
||||||
|
}
|
||||||
|
private _animation?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional: The name of the skin to be set
|
* Optional: The name of the skin to be set
|
||||||
|
* Connected to `skin` attribute.
|
||||||
*/
|
*/
|
||||||
public skin?: string;
|
public get skin() : string | undefined {
|
||||||
|
return this._skin;
|
||||||
|
}
|
||||||
|
public set skin(value: string | undefined) {
|
||||||
|
this._skin = value;
|
||||||
|
this.initWidget();
|
||||||
|
}
|
||||||
|
private _skin?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional: Pass a `SkeletonData`, if you want to avoid creating a new one
|
* Specify the way the skeleton is sized within the element automatically changing its `scaleX` and `scaleY`.
|
||||||
|
* It works only with {@link mode} `inside`. Possible values are:
|
||||||
|
* - `contain`: as large as possible while still containing the skeleton entirely within the element container (Default).
|
||||||
|
* - `fill`: fill the element container by distorting the skeleton's aspect ratio.
|
||||||
|
* - `width`: make sure the full width of the source is shown, regardless of whether this means the skeleton overflows the element container vertically.
|
||||||
|
* - `height`: make sure the full height of the source is shown, regardless of whether this means the skeleton overflows the element container horizontally.
|
||||||
|
* - `cover`: as small as possible while still covering the entire element container.
|
||||||
|
* - `scaleDown`: scale the skeleton down to ensure that the skeleton fits within the element container.
|
||||||
|
* - `none`: display the skeleton without autoscaling it.
|
||||||
|
* Connected to `fit` attribute.
|
||||||
*/
|
*/
|
||||||
public skeletonData?: SkeletonData;
|
public fit: FitType = "contain";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the way the skeleton is centered within the div:
|
||||||
|
* - `inside`: the skeleton bounds center is centered with the div container (Default)
|
||||||
|
* - `origin`: the skeleton origin is centered with the div container regardless of the bounds.
|
||||||
|
* Origin does not allow to specify any {@link fit} type and guarantee the skeleton to not be autoscaled.
|
||||||
|
* Connected to `mode` attribute.
|
||||||
|
*/
|
||||||
|
public mode: ModeType = "inside";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The x offset of the skeleton world origin x axis in div width units
|
||||||
|
* Connected to `x-axis` attribute.
|
||||||
|
*/
|
||||||
|
public xAxis = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The y offset of the skeleton world origin x axis in div width units
|
||||||
|
* Connected to `y-axis` attribute.
|
||||||
|
*/
|
||||||
|
public yAxis = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The x offset of the root in pixels wrt to the skeleton world origin
|
||||||
|
* Connected to `offset-x` attribute.
|
||||||
|
*/
|
||||||
|
public offsetX = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The y offset of the root in pixels wrt to the skeleton world origin
|
||||||
|
* Connected to `offset-y` attribute.
|
||||||
|
*/
|
||||||
|
public offsetY = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify a fixed width for the widget. If at least one of `width` and `height` is > 0,
|
||||||
|
* the widget will have an actual size and the div reference is the widget itself, not the div parent.
|
||||||
|
* Connected to `width` attribute.
|
||||||
|
*/
|
||||||
|
public get width() : number {
|
||||||
|
return this._width;
|
||||||
|
}
|
||||||
|
public set width(value: number) {
|
||||||
|
this._width = value;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
private _width = -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify a fixed height for the widget. If at least one of `width` and `height` is > 0,
|
||||||
|
* the widget will have an actual size and the div reference is the widget itself, not the div parent.
|
||||||
|
* Connected to `height` attribute.
|
||||||
|
*/
|
||||||
|
public get height() : number {
|
||||||
|
return this._height;
|
||||||
|
}
|
||||||
|
public set height(value: number) {
|
||||||
|
this._height = value;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
private _height = -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the widget is draggable
|
||||||
|
* Connected to `draggable` attribute.
|
||||||
|
*/
|
||||||
|
public draggable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, some convenience elements are drawn to show the skeleton world origin (green),
|
||||||
|
* the root (red), and the bounds rectangle (blue)
|
||||||
|
* Connected to `debug` attribute.
|
||||||
|
*/
|
||||||
|
public debug = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An identifier to obtain a reference to this widget using the getSpineWidget function
|
||||||
|
* Connected to `identifier` attribute.
|
||||||
|
*/
|
||||||
|
public identifier = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, assets loading are loaded immediately and the skeleton shown as soon as the assets are loaded
|
||||||
|
* If false, it is necessary to invoke the start method to start the loading process
|
||||||
|
* Connected to `manual-start` attribute.
|
||||||
|
*/
|
||||||
|
public manualStart = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of indexes indicating the atlas pages indexes to be loaded.
|
||||||
|
* If undefined, all pages are loaded. If empty (default), no page is loaded;
|
||||||
|
* in this case the user can add later the indexes of the pages they want to load
|
||||||
|
* and call the loadTexturesInPagesAttribute, to lazily load them.
|
||||||
|
* Connected to `pages` attribute.
|
||||||
|
*/
|
||||||
|
public pages?: Array<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `true`, the skeleton is clipped to the container div bounds.
|
||||||
|
* Be careful on using this feature because it breaks batching!
|
||||||
|
* Connected to `clip` attribute.
|
||||||
|
*/
|
||||||
|
public clip = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The widget update/apply behaviour when the skeleton div container is offscreen:
|
||||||
|
* - `pause`: the state is not updated, neither applied (Default)
|
||||||
|
* - `update`: the state is updated, but not applied
|
||||||
|
* - `pose`: the state is updated and applied
|
||||||
|
* Connected to `offscreen` attribute.
|
||||||
|
*/
|
||||||
|
public offScreenUpdateBehaviour: OffScreenUpdateBehaviourType = "pause";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the a Spine loading spinner is shown during asset loading
|
||||||
|
* Connected to `spinner` attribute.
|
||||||
|
*/
|
||||||
|
public loadingSpinner = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the default state and skeleton update logic for this widget.
|
* Replace the default state and skeleton update logic for this widget.
|
||||||
@ -151,103 +343,6 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
*/
|
*/
|
||||||
afterUpdateWorldTransforms: BeforeAfterUpdateSpineWidgetFunction= () => {};
|
afterUpdateWorldTransforms: BeforeAfterUpdateSpineWidgetFunction= () => {};
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the way the skeleton is sized within the element automatically changing its `scaleX` and `scaleY`.
|
|
||||||
* It works only with {@link mode} `inside`. Possible values are:
|
|
||||||
* - `contain`: as large as possible while still containing the skeleton entirely within the element container (Default).
|
|
||||||
* - `fill`: fill the element container by distorting the skeleton's aspect ratio.
|
|
||||||
* - `width`: make sure the full width of the source is shown, regardless of whether this means the skeleton overflows the element container vertically.
|
|
||||||
* - `height`: make sure the full height of the source is shown, regardless of whether this means the skeleton overflows the element container horizontally.
|
|
||||||
* - `cover`: as small as possible while still covering the entire element container.
|
|
||||||
* - `scaleDown`: scale the skeleton down to ensure that the skeleton fits within the element container.
|
|
||||||
* - `none`: display the skeleton without autoscaling it.
|
|
||||||
*/
|
|
||||||
public fit: FitType = "contain";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify the way the skeleton is centered within the div:
|
|
||||||
* - `inside`: the skeleton bounds center is centered with the div container (Default)
|
|
||||||
* - `origin`: the skeleton origin is centered with the div container regardless of the bounds.
|
|
||||||
* Origin does not allow to specify any {@link fit} type and guarantee the skeleton to not be autoscaled.
|
|
||||||
*/
|
|
||||||
public mode: ModeType = "inside";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The x offset of the skeleton world origin x axis in div width units
|
|
||||||
*/
|
|
||||||
public xAxis = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The y offset of the skeleton world origin x axis in div width units
|
|
||||||
*/
|
|
||||||
public yAxis = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The x offset of the root in pixels wrt to the skeleton world origin
|
|
||||||
*/
|
|
||||||
public offsetX = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The y offset of the root in pixels wrt to the skeleton world origin
|
|
||||||
*/
|
|
||||||
public offsetY = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify a fixed width for the widget. If at least one of `width` and `height` is > 0,
|
|
||||||
* the widget will have an actual size and the div reference is the widget itself, not the div parent.
|
|
||||||
*/
|
|
||||||
public width = -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specify a fixed height for the widget. If at least one of `width` and `height` is > 0,
|
|
||||||
* the widget will have an actual size and the div reference is the widget itself, not the div parent.
|
|
||||||
*/
|
|
||||||
public height = -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, the widget is draggable
|
|
||||||
*/
|
|
||||||
public draggable = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, some convenience elements are drawn to show the skeleton world origin (green),
|
|
||||||
* the root (red), and the bounds rectangle (blue)
|
|
||||||
*/
|
|
||||||
public debug = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An identifier to obtain a reference to this widget using the getSpineWidget function
|
|
||||||
*/
|
|
||||||
public identifier = "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, assets loading are loaded immediately and the skeleton shown as soon as the assets are loaded
|
|
||||||
* If false, it is necessary to invoke the start method to start the loading process
|
|
||||||
*/
|
|
||||||
public manualStart = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of indexes indicating the atlas pages indexes to be loaded.
|
|
||||||
* If undefined, all pages are loaded. If empty (default), no page is loaded;
|
|
||||||
* in this case the user can add later the indexes of the pages they want to load
|
|
||||||
* and call the loadTexturesInPagesAttribute, to lazily load them.
|
|
||||||
*/
|
|
||||||
public pages?: Array<number>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If `true`, the skeleton is clipped to the container div bounds.
|
|
||||||
* Be careful on using this feature because it breaks batching!
|
|
||||||
*/
|
|
||||||
public clip = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The widget update/apply behaviour when the skeleton div container is offscreen:
|
|
||||||
* - `pause`: the state is not updated, neither applied (Default)
|
|
||||||
* - `update`: the state is updated, but not applied
|
|
||||||
* - `pose`: the state is updated and applied
|
|
||||||
*/
|
|
||||||
public offScreenUpdateBehaviour: OffScreenUpdateBehaviourType = "pause";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The skeleton hosted by this widget. It's ready once assets are loaded.
|
* The skeleton hosted by this widget. It's ready once assets are loaded.
|
||||||
* Safely acces this property by using {@link loadingPromise}.
|
* Safely acces this property by using {@link loadingPromise}.
|
||||||
@ -288,11 +383,6 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
*/
|
*/
|
||||||
public loading = true;
|
public loading = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, the a Spine loading spinner is shown during asset loading
|
|
||||||
*/
|
|
||||||
public loadingSpinner = true;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reference to the {@link LoadingScreenWidget} of this widget.
|
* A reference to the {@link LoadingScreenWidget} of this widget.
|
||||||
* This is instantiated only if it is really necessary.
|
* This is instantiated only if it is really necessary.
|
||||||
@ -362,44 +452,79 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
// ----
|
||||||
|
// ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional: Pass a `SkeletonData`, if you want to avoid creating a new one
|
||||||
|
*/
|
||||||
|
public skeletonData?: SkeletonData;
|
||||||
|
|
||||||
|
// Reference to the webcomponent shadow root
|
||||||
private root: ShadowRoot;
|
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" },
|
||||||
|
skeleton: { propertyName: "skeletonPath", type: "string" },
|
||||||
|
scale: { propertyName: "scale", type: "number" },
|
||||||
|
animation: { propertyName: "animation", type: "string" },
|
||||||
|
skin: { propertyName: "skin", type: "string" },
|
||||||
|
width: { propertyName: "width", type: "number", defaultValue: -1 },
|
||||||
|
height: { propertyName: "height", type: "number", defaultValue: -1 },
|
||||||
|
draggable: { propertyName: "draggable", type: "boolean" },
|
||||||
|
"x-axis": { propertyName: "xAxis", type: "number" },
|
||||||
|
"y-axis": { propertyName: "yAxis", type: "number" },
|
||||||
|
"offset-x": { propertyName: "offsetX", type: "number" },
|
||||||
|
"offset-y": { propertyName: "offsetY", type: "number" },
|
||||||
|
identifier: { propertyName: "identifier", type: "string" },
|
||||||
|
debug: { propertyName: "debug", type: "boolean" },
|
||||||
|
"manual-start": { propertyName: "manualStart", type: "boolean" },
|
||||||
|
spinner: { propertyName: "loadingSpinner", type: "boolean" },
|
||||||
|
clip: { propertyName: "clip", type: "boolean" },
|
||||||
|
pages: { propertyName: "pages", type: "string-number" },
|
||||||
|
fit: { propertyName: "fit", type: "fitType", defaultValue: "contain" },
|
||||||
|
mode: { propertyName: "mode", type: "modeType", defaultValue: "inside" },
|
||||||
|
offscreen: { propertyName: "offScreenUpdateBehaviour", type: "offScreenUpdateBehaviourType", defaultValue: "pause" },
|
||||||
|
}
|
||||||
|
|
||||||
static get observedAttributes(): string[] {
|
static get observedAttributes(): string[] {
|
||||||
return [
|
return [
|
||||||
"atlas",
|
"atlas", // atlasPath
|
||||||
"skeleton",
|
"skeleton", // skeletonPath
|
||||||
"scale",
|
"scale", // scale
|
||||||
"animation",
|
"animation", // animation
|
||||||
"skin",
|
"skin", // skin
|
||||||
"fit",
|
"fit", // fit
|
||||||
"width",
|
"width", // width
|
||||||
"height",
|
"height", // height
|
||||||
"draggable",
|
"draggable", // draggable
|
||||||
"mode",
|
"mode", // mode
|
||||||
"x-axis",
|
"x-axis", // xAxis
|
||||||
"y-axis",
|
"y-axis", // yAxis
|
||||||
"identifier",
|
"offset-x", // offsetX
|
||||||
"offset-x",
|
"offset-y", // offsetY
|
||||||
"offset-y",
|
"identifier", // identifier
|
||||||
"debug",
|
"debug", // debug
|
||||||
"manual-start",
|
"manual-start", // manualStart
|
||||||
"spinner",
|
"spinner", // loadingSpinner
|
||||||
"pages",
|
"pages", // pages
|
||||||
"offscreen",
|
"offscreen", // offScreenUpdateBehaviour
|
||||||
"clip",
|
"clip", // clip
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.root = this.attachShadow({ mode: "open" });
|
this.root = this.attachShadow({ mode: "closed" });
|
||||||
this.overlay = this.initializeOverlay();
|
this.overlay = this.initializeOverlay();
|
||||||
this.atlasPath = "TODO";
|
this.atlasPath = "TODO";
|
||||||
this.skeletonPath = "TODO";
|
this.skeletonPath = "TODO";
|
||||||
|
|
||||||
this.debugDragDiv = document.createElement('div');
|
this.debugDragDiv = document.createElement("div");
|
||||||
this.debugDragDiv.style.position = "absolute";
|
this.debugDragDiv.style.position = "absolute";
|
||||||
this.debugDragDiv.style.backgroundColor = "rgba(0, 1, 1, 0.3)";
|
this.debugDragDiv.style.backgroundColor = "rgba(0, 1, 1, 0.3)";
|
||||||
this.debugDragDiv.style.setProperty("pointer-events", "none");
|
this.debugDragDiv.style.setProperty("pointer-events", "none");
|
||||||
@ -431,112 +556,65 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
|
private static castBoolean(value: string | null, defaultValue = "") {
|
||||||
if (newValue !== null) {
|
return value === "true" || value === "" ? true : false;
|
||||||
if (name === "identifier") {
|
|
||||||
this.identifier = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "atlas") {
|
|
||||||
this.atlasPath = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "skeleton") {
|
|
||||||
this.skeletonPath = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "skin") {
|
|
||||||
this.skin = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "fit") {
|
|
||||||
this.fit = isFitType(newValue) ? newValue : "contain";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "mode") {
|
|
||||||
this.mode = isModeType(newValue) ? newValue : "inside";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "offscreen") {
|
|
||||||
this.offScreenUpdateBehaviour = isOffScreenUpdateBehaviourType(newValue) ? newValue : "pause";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "x-axis") {
|
|
||||||
let float = this.xAxis;
|
|
||||||
float = parseFloat(newValue);
|
|
||||||
this.xAxis = float;
|
|
||||||
}
|
|
||||||
if (name === "y-axis") {
|
|
||||||
let float = this.yAxis;
|
|
||||||
float = parseFloat(newValue);
|
|
||||||
this.yAxis = float;
|
|
||||||
}
|
|
||||||
if (name === "offset-x") {
|
|
||||||
let float = 0;
|
|
||||||
float = parseInt(newValue);
|
|
||||||
this.offsetX = float;
|
|
||||||
}
|
|
||||||
if (name === "offset-y") {
|
|
||||||
let float = 0;
|
|
||||||
float = parseInt(newValue);
|
|
||||||
this.offsetY = float;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "scale") {
|
|
||||||
let scaleFloat = 1;
|
|
||||||
scaleFloat = parseFloat(newValue);
|
|
||||||
this.scale = scaleFloat;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "width") {
|
|
||||||
let widthFloat = 1;
|
|
||||||
widthFloat = parseFloat(newValue);
|
|
||||||
this.width = widthFloat;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "height") {
|
|
||||||
let heightFloat = 1;
|
|
||||||
heightFloat = parseFloat(newValue);
|
|
||||||
this.height = heightFloat;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "animation") {
|
|
||||||
this.animation = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "draggable") {
|
|
||||||
this.draggable = Boolean(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "debug") {
|
|
||||||
this.debug = Boolean(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "spinner") {
|
|
||||||
this.loadingSpinner = Boolean(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "manual-start") {
|
|
||||||
this.manualStart = Boolean(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "clip") {
|
|
||||||
this.clip = Boolean(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === "pages") {
|
|
||||||
this.pages = newValue.split(",").reduce((acc, pageIndex) => {
|
|
||||||
const index = parseInt(pageIndex);
|
|
||||||
if (!isNaN(index)) acc.push(index);
|
|
||||||
return acc;
|
|
||||||
}, [] as Array<number>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate bounds of the current animation on track 0, then set it
|
private static castString(value: string | null, defaultValue = "") {
|
||||||
public recalculateBounds() {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
|
||||||
|
const { type, propertyName, defaultValue } = SpineWebComponentWidget.attributesDescription[name];
|
||||||
|
const val = SpineWebComponentWidget.castValue(type, newValue, defaultValue ?? this[propertyName]);
|
||||||
|
(this as any)[propertyName] = val;
|
||||||
|
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;
|
const { skeleton, state } = this;
|
||||||
if (!skeleton || !state) return;
|
if (!skeleton || !state) return;
|
||||||
const track = state.getCurrent(0);
|
const track = state.getCurrent(0);
|
||||||
@ -545,9 +623,15 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
this.setBounds(bounds);
|
this.setBounds(bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the given bounds on the current skeleton
|
/**
|
||||||
// bounds is used to center the skeleton in inside mode and as a input area for click events
|
* Set the given bounds on the current skeleton.
|
||||||
public setBounds(bounds: Rectangle) {
|
* 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;
|
const { skeleton } = this;
|
||||||
if (!skeleton) return;
|
if (!skeleton) return;
|
||||||
bounds.x /= skeleton.scaleX;
|
bounds.x /= skeleton.scaleX;
|
||||||
@ -557,15 +641,10 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
this.bounds = bounds;
|
this.bounds = bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeOverlay(): SpineWebComponentOverlay {
|
/**
|
||||||
let overlay = document.querySelector("spine-overlay") as SpineWebComponentOverlay;
|
* Starts the widget. Starting the widget means to load the assets currently set into
|
||||||
if (!overlay) {
|
* {@link atlasPath} and {@link skeletonPath}.
|
||||||
overlay = document.createElement("spine-overlay") as SpineWebComponentOverlay;
|
*/
|
||||||
document.body.appendChild(overlay);
|
|
||||||
}
|
|
||||||
return overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
if (this.started) {
|
if (this.started) {
|
||||||
console.warn("If you want to start again the widget, first reset it");
|
console.warn("If you want to start again the widget, first reset it");
|
||||||
@ -578,20 +657,37 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadTexturesInPagesAttribute(atlas: TextureAtlas) {
|
/**
|
||||||
|
* Loads the texture pages in the given `atlas` corresponding to the indexes set into {@link pages}.
|
||||||
|
* This method is automatically called during asset loading. When `pages` is undefined (default),
|
||||||
|
* all pages are loaded. This method is useful when you want to load a subset of pages programmatically.
|
||||||
|
* In that case, set `pages` to an empty array at the beginning.
|
||||||
|
* Then set the pages you want to load and invoke this method.
|
||||||
|
* @param atlas the `TextureAtlas` from which to get the `TextureAtlasPage`s
|
||||||
|
* @returns The list of loaded assets
|
||||||
|
*/
|
||||||
|
public async loadTexturesInPagesAttribute(atlas: TextureAtlas): Promise<Array<any>> {
|
||||||
const pagesIndexToLoad = this.pages ?? atlas.pages.map((_, i) => i); // if no pages provided, loads all
|
const pagesIndexToLoad = this.pages ?? atlas.pages.map((_, i) => i); // if no pages provided, loads all
|
||||||
|
const atlasPath = this.atlasPath.includes("/") ? this.atlasPath.substring(0, this.atlasPath.lastIndexOf("/") + 1) : "";
|
||||||
const atlasPath = this.atlasPath.includes('/') ? this.atlasPath.substring(0, this.atlasPath.lastIndexOf('/') + 1) : '';
|
const promisePageList: Array<Promise<any>> = [];
|
||||||
const promisePageList: Array<Promise<void>> = [];
|
|
||||||
pagesIndexToLoad.forEach((index) => {
|
pagesIndexToLoad.forEach((index) => {
|
||||||
const page = atlas.pages[index];
|
const page = atlas.pages[index];
|
||||||
const promiseTextureLoad = this.loadTexture(`${atlasPath}${page.name}`).then(texture => page.setTexture(texture));
|
const promiseTextureLoad = this.overlay.assetManager.loadTextureAsync(`${atlasPath}${page.name}`).then(texture => page.setTexture(texture));
|
||||||
promisePageList.push(promiseTextureLoad);
|
promisePageList.push(promiseTextureLoad);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(promisePageList)
|
return Promise.all(promisePageList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The `HTMLElement` where the widget is hosted.
|
||||||
|
*/
|
||||||
|
public getHTMLElementReference(): HTMLElement {
|
||||||
|
return this.width <= 0 || this.width <= 0
|
||||||
|
? this.parentElement!
|
||||||
|
: this;
|
||||||
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
@ -605,8 +701,8 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
// - []: no page is loaded
|
// - []: no page is loaded
|
||||||
// - undefined: all pages are loaded (default)
|
// - undefined: all pages are loaded (default)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
isBinary ? this.loadBinary(skeletonPath) : this.loadJson(skeletonPath),
|
isBinary ? this.overlay.assetManager.loadBinaryAsync(skeletonPath) : this.overlay.assetManager.loadJsonAsync(skeletonPath),
|
||||||
this.loadTextureAtlasButNoTextures(atlasPath).then(atlas => this.loadTexturesInPagesAttribute(atlas)),
|
this.overlay.assetManager.loadTextureAtlasButNoTexturesAsync(atlasPath).then(atlas => this.loadTexturesInPagesAttribute(atlas)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const atlas = this.overlay.assetManager.require(atlasPath);
|
const atlas = this.overlay.assetManager.require(atlasPath);
|
||||||
@ -616,21 +712,15 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
skeletonLoader.scale = scale;
|
skeletonLoader.scale = scale;
|
||||||
|
|
||||||
const skeletonFile = this.overlay.assetManager.require(skeletonPath);
|
const skeletonFile = this.overlay.assetManager.require(skeletonPath);
|
||||||
const skeletonData = skeletonDataInput ?? skeletonLoader.readSkeletonData(skeletonFile);
|
const skeletonData = (skeletonDataInput || this.skeleton?.data) ?? skeletonLoader.readSkeletonData(skeletonFile);
|
||||||
|
|
||||||
const skeleton = new Skeleton(skeletonData);
|
const skeleton = new Skeleton(skeletonData);
|
||||||
const animationStateData = new AnimationStateData(skeletonData);
|
const animationStateData = new AnimationStateData(skeletonData);
|
||||||
const state = new AnimationState(animationStateData);
|
const state = new AnimationState(animationStateData);
|
||||||
|
|
||||||
if (skin) {
|
this.skeleton = skeleton;
|
||||||
skeleton.setSkinByName(skin);
|
this.state = state;
|
||||||
}
|
this.textureAtlas = atlas;
|
||||||
|
|
||||||
let animationData;
|
|
||||||
if (animation) {
|
|
||||||
state.setAnimation(0, animation, true);
|
|
||||||
animationData = animation ? skeleton.data.findAnimation(animation)! : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ideally we would know the dpi and the zoom, however they are combined
|
// ideally we would know the dpi and the zoom, however they are combined
|
||||||
// to simplify we just assume that the user wants to load the skeleton at scale 1
|
// to simplify we just assume that the user wants to load the skeleton at scale 1
|
||||||
@ -640,19 +730,18 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
// skeleton.scaleX = this.currentScaleDpi;
|
// skeleton.scaleX = this.currentScaleDpi;
|
||||||
// skeleton.scaleY = this.currentScaleDpi;
|
// skeleton.scaleY = this.currentScaleDpi;
|
||||||
|
|
||||||
this.skeleton = skeleton;
|
this.initWidget();
|
||||||
this.state = state;
|
|
||||||
this.textureAtlas = atlas;
|
|
||||||
|
|
||||||
const bounds = this.calculateAnimationViewport(animationData);
|
|
||||||
this.bounds = bounds;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getHTMLElementReference(): HTMLElement {
|
private initWidget() {
|
||||||
return this.width <= 0 || this.width <= 0
|
const { skeleton, state, animation, skin } = this;
|
||||||
? this.parentElement!
|
|
||||||
: this;
|
if (skin) skeleton?.setSkinByName(skin);
|
||||||
|
if (animation) state?.setAnimation(0, animation, true);
|
||||||
|
|
||||||
|
this.recalculateBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
private render(): void {
|
private render(): void {
|
||||||
@ -678,54 +767,17 @@ class SpineWebComponentWidget extends HTMLElement implements WidgetLayoutOptions
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// Create a new overlay webcomponent, if no one exists yet.
|
||||||
* Load assets utilities
|
// 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 {
|
||||||
public async loadBinary(path: string) {
|
let overlay = document.querySelector("spine-overlay") as SpineWebComponentOverlay;
|
||||||
return new Promise((resolve, reject) => {
|
if (!overlay) {
|
||||||
this.overlay.assetManager.loadBinary(path,
|
overlay = document.createElement("spine-overlay") as SpineWebComponentOverlay;
|
||||||
(_, binary) => resolve(binary),
|
document.body.appendChild(overlay);
|
||||||
(_, message) => reject(message),
|
}
|
||||||
);
|
return overlay;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async loadJson(path: string) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.overlay.assetManager.loadJson(path,
|
|
||||||
(_, object) => resolve(object),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadTexture(path: string) {
|
|
||||||
return new Promise<Texture>((resolve, reject) => {
|
|
||||||
this.overlay.assetManager.loadTexture(path,
|
|
||||||
(_, texture) => resolve(texture),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadTextureAtlas(path: string) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.overlay.assetManager.loadTextureAtlas(path,
|
|
||||||
(_, atlas) => resolve(atlas),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadTextureAtlasButNoTextures(path: string) {
|
|
||||||
return new Promise<TextureAtlas>((resolve, reject) => {
|
|
||||||
this.overlay.assetManager.loadTextureAtlasButNoTextures(path,
|
|
||||||
(_, atlas) => resolve(atlas),
|
|
||||||
(_, message) => reject(message),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Other utilities
|
* Other utilities
|
||||||
@ -808,9 +860,9 @@ class SpineWebComponentOverlay extends HTMLElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.root = this.attachShadow({ mode: 'open' });
|
this.root = this.attachShadow({ mode: "closed" });
|
||||||
|
|
||||||
this.div = document.createElement('div');
|
this.div = document.createElement("div");
|
||||||
this.div.style.position = "absolute";
|
this.div.style.position = "absolute";
|
||||||
this.div.style.top = "0";
|
this.div.style.top = "0";
|
||||||
this.div.style.left = "0";
|
this.div.style.left = "0";
|
||||||
@ -820,7 +872,7 @@ class SpineWebComponentOverlay extends HTMLElement {
|
|||||||
|
|
||||||
this.root.appendChild(this.div);
|
this.root.appendChild(this.div);
|
||||||
|
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement("canvas");
|
||||||
this.div.appendChild(this.canvas);
|
this.div.appendChild(this.canvas);
|
||||||
this.canvas.style.position = "absolute";
|
this.canvas.style.position = "absolute";
|
||||||
this.canvas.style.top = "0";
|
this.canvas.style.top = "0";
|
||||||
@ -830,7 +882,7 @@ class SpineWebComponentOverlay extends HTMLElement {
|
|||||||
this.canvas.style.transform =`translate(0px,0px)`;
|
this.canvas.style.transform =`translate(0px,0px)`;
|
||||||
// this.canvas.style.setProperty("will-change", "transform"); // performance seems to be even worse with this uncommented
|
// this.canvas.style.setProperty("will-change", "transform"); // performance seems to be even worse with this uncommented
|
||||||
|
|
||||||
this.fps = document.createElement('span');
|
this.fps = document.createElement("span");
|
||||||
this.fps.style.position = "fixed";
|
this.fps.style.position = "fixed";
|
||||||
this.fps.style.top = "0";
|
this.fps.style.top = "0";
|
||||||
this.fps.style.left = "0";
|
this.fps.style.left = "0";
|
||||||
@ -866,7 +918,7 @@ class SpineWebComponentOverlay extends HTMLElement {
|
|||||||
this.scrollHandler();
|
this.scrollHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('scroll', this.scrollHandler);
|
window.addEventListener("scroll", this.scrollHandler);
|
||||||
this.scrollHandler();
|
this.scrollHandler();
|
||||||
|
|
||||||
this.input = new Input(document.body, false);
|
this.input = new Input(document.body, false);
|
||||||
@ -994,7 +1046,7 @@ class SpineWebComponentOverlay extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
if (mode === 'inside') {
|
if (mode === "inside") {
|
||||||
let { x: ax, y: ay, width: aw, height: ah } = bounds!;
|
let { x: ax, y: ay, width: aw, height: ah } = bounds!;
|
||||||
|
|
||||||
// scale ratio
|
// scale ratio
|
||||||
@ -1144,8 +1196,6 @@ class SpineWebComponentOverlay extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
// TODO: move the intersectio observer to the canvas - so that we can instantiate a single one rather than one per widget
|
|
||||||
|
|
||||||
this.intersectionObserver = new IntersectionObserver((widgets) => {
|
this.intersectionObserver = new IntersectionObserver((widgets) => {
|
||||||
widgets.forEach(({ isIntersecting, target }) => {
|
widgets.forEach(({ isIntersecting, target }) => {
|
||||||
|
|
||||||
@ -1162,6 +1212,7 @@ class SpineWebComponentOverlay extends HTMLElement {
|
|||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: drag is bugged when zoom on browser (just zoom and activare debug to see the drag surface has some offset)
|
||||||
private setupDragUtility() {
|
private setupDragUtility() {
|
||||||
// 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 point: Point = { x: 0, y: 0 };
|
const point: Point = { x: 0, y: 0 };
|
||||||
@ -1265,7 +1316,7 @@ class SpineWebComponentOverlay extends HTMLElement {
|
|||||||
private zoomHandler = () => {
|
private zoomHandler = () => {
|
||||||
this.skeletonList.forEach((widget) => {
|
this.skeletonList.forEach((widget) => {
|
||||||
// inside mode scale automatically to fit the skeleton within its parent
|
// inside mode scale automatically to fit the skeleton within its parent
|
||||||
if (widget.mode !== 'origin' && widget.fit !== 'none') return;
|
if (widget.mode !== "origin" && widget.fit !== "none") return;
|
||||||
|
|
||||||
const skeleton = widget.skeleton;
|
const skeleton = widget.skeleton;
|
||||||
if (!skeleton) return;
|
if (!skeleton) return;
|
||||||
@ -1326,33 +1377,23 @@ const inside = (point: { x: number; y: number }, rectangle: Rectangle): boolean
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('spine-widget', SpineWebComponentWidget);
|
customElements.define("spine-widget", SpineWebComponentWidget);
|
||||||
customElements.define('spine-overlay', SpineWebComponentOverlay);
|
customElements.define("spine-overlay", SpineWebComponentOverlay);
|
||||||
|
|
||||||
export function getSpineWidget(identifier: string) {
|
export function getSpineWidget(identifier: string): SpineWebComponentWidget {
|
||||||
return document.querySelector(`spine-widget[identifier=${identifier}]`);
|
return document.querySelector(`spine-widget[identifier=${identifier}]`) as SpineWebComponentWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSpineWidget(parameters: { atlas: string, skeleton: string, animation: string, skin: string, manualStart?: boolean, pages: Array<string> }): SpineWebComponentWidget {
|
export function createSpineWidget(parameters: WidgetAttributes): SpineWebComponentWidget {
|
||||||
const {
|
|
||||||
atlas,
|
|
||||||
skeleton,
|
|
||||||
animation,
|
|
||||||
skin,
|
|
||||||
manualStart = false,
|
|
||||||
pages = [],
|
|
||||||
} = parameters;
|
|
||||||
|
|
||||||
const widget = document.createElement("spine-widget") as SpineWebComponentWidget;
|
const widget = document.createElement("spine-widget") as SpineWebComponentWidget;
|
||||||
|
|
||||||
widget.setAttribute("skeleton", skeleton);
|
Object.entries(SpineWebComponentWidget.attributesDescription).forEach(entry => {
|
||||||
widget.setAttribute("atlas", atlas);
|
const [key, { propertyName }] = entry;
|
||||||
widget.setAttribute("skin", skin);
|
const value = parameters[propertyName];
|
||||||
widget.setAttribute("animation", animation);
|
if (value) widget.setAttribute(key, value as any);
|
||||||
widget.setAttribute("manual-start", `${manualStart}`);
|
});
|
||||||
widget.setAttribute("pages", `${pages.join(",")}`);
|
|
||||||
|
|
||||||
if (!manualStart) {
|
if (!widget.manualStart) {
|
||||||
widget.start();
|
widget.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user