diff --git a/CHANGELOG.md b/CHANGELOG.md index abca0868a..0e2c1177f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -433,6 +433,7 @@ * Removed `RegionAttachment.vertices` field. The vertices array is provided to `RegionAttachment.computeWorldVertices` by the API user now. * Removed `RegionAttachment.updateWorldVertices`, added `RegionAttachment.computeWorldVertices`. The new method now computes the x/y positions of the 4 vertices of the corner and places them in the provided `worldVertices` array, starting at `offset`, then moving by `stride` array elements when advancing to the next vertex. This allows to directly compose the vertex buffer and avoids a copy. The computation of the full vertices, including vertex colors and texture coordinates, is now done by the backend's respective renderer. * The completion event will fire for looped 0 duration animations every frame. + * Removed the Spine Widget in favor of [Spine Web Player](https://esotericsoftware.com/spine-player). * **Additions** * Added support for local and relative transform constraint calculation, including additional fields in `TransformConstraintData` @@ -444,6 +445,7 @@ * `AnimationState#apply` returns boolean indicating if any timeline was applied or not. * `Animation#apply` and `Timeline#apply`` now take enums `MixPose` and `MixDirection` instead of booleans * Added `AssetManager.loadTextureAtlas`. Instead of loading the `.atlas` and corresponding image files manually, you can simply specify the location of the `.atlas` file and AssetManager will load the atlas and all its images automatically. `AssetManager.get("atlasname.atlas")` will then return an instance of `spine.TextureAtlas`. + * Added the [Spine Web Player](https://esotericsoftware.com/spine-player) ### WebGL backend diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java index a1cba92db..513a27ab1 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java @@ -33,6 +33,7 @@ package com.esotericsoftware.spine.attachments; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion; import com.badlogic.gdx.graphics.g2d.TextureRegion; + import com.esotericsoftware.spine.Animation.DeformTimeline; /** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not @@ -70,8 +71,29 @@ public class MeshAttachment extends VertexAttachment { /** Calculates {@link #uvs} using {@link #regionUVs} and the {@link #region}. Must be called after changing the region UVs or * region. */ public void updateUVs () { + float[] regionUVs = this.regionUVs; + if (this.uvs == null || this.uvs.length != regionUVs.length) this.uvs = new float[regionUVs.length]; + float[] uvs = this.uvs; float u, v, width, height; - if (region == null) { + if (region instanceof AtlasRegion) { + AtlasRegion region = (AtlasRegion)this.region; + float textureWidth = region.getTexture().getWidth(), textureHeight = region.getTexture().getHeight(); + if (region.rotate) { + u = region.getU() - (region.originalHeight - region.offsetY - region.packedWidth) / textureWidth; + v = region.getV() - (region.originalWidth - region.offsetX - region.packedHeight) / textureHeight; + width = region.originalHeight / textureWidth; + height = region.originalWidth / textureHeight; + for (int i = 0, n = uvs.length; i < n; i += 2) { + uvs[i] = u + regionUVs[i + 1] * width; + uvs[i + 1] = v + height - regionUVs[i] * height; + } + return; + } + u = region.getU() - region.offsetX / textureWidth; + v = region.getV() - (region.originalHeight - region.offsetY - region.packedHeight) / textureHeight; + width = region.originalWidth / textureWidth; + height = region.originalHeight / textureHeight; + } else if (region == null) { u = v = 0; width = height = 1; } else { @@ -80,19 +102,9 @@ public class MeshAttachment extends VertexAttachment { width = region.getU2() - u; height = region.getV2() - v; } - float[] regionUVs = this.regionUVs; - if (this.uvs == null || this.uvs.length != regionUVs.length) this.uvs = new float[regionUVs.length]; - float[] uvs = this.uvs; - if (region instanceof AtlasRegion && ((AtlasRegion)region).rotate) { - for (int i = 0, n = uvs.length; i < n; i += 2) { - uvs[i] = u + regionUVs[i + 1] * width; - uvs[i + 1] = v + height - regionUVs[i] * height; - } - } else { - for (int i = 0, n = uvs.length; i < n; i += 2) { - uvs[i] = u + regionUVs[i] * width; - uvs[i + 1] = v + regionUVs[i + 1] * height; - } + for (int i = 0, n = uvs.length; i < n; i += 2) { + uvs[i] = u + regionUVs[i] * width; + uvs[i + 1] = v + regionUVs[i + 1] * height; } } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java index 4d5bf3ed2..ad18eb36b 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java @@ -70,14 +70,12 @@ public class RegionAttachment extends Attachment { float localY = -localY2; if (region instanceof AtlasRegion) { AtlasRegion region = (AtlasRegion)this.region; + localX += region.offsetX / region.originalWidth * width; + localY += region.offsetY / region.originalHeight * height; if (region.rotate) { - localX += region.offsetX / region.originalWidth * width; - localY += region.offsetY / region.originalHeight * height; localX2 -= (region.originalWidth - region.offsetX - region.packedHeight) / region.originalWidth * width; localY2 -= (region.originalHeight - region.offsetY - region.packedWidth) / region.originalHeight * height; } else { - localX += region.offsetX / region.originalWidth * width; - localY += region.offsetY / region.originalHeight * height; localX2 -= (region.originalWidth - region.offsetX - region.packedWidth) / region.originalWidth * width; localY2 -= (region.originalHeight - region.offsetY - region.packedHeight) / region.originalHeight * height; } diff --git a/spine-ts/README.md b/spine-ts/README.md index 152df60b9..950533f9d 100644 --- a/spine-ts/README.md +++ b/spine-ts/README.md @@ -7,7 +7,7 @@ up into multiple modules: 1. **WebGL**: `webgl/`, a self-contained WebGL backend, build on the core classes 1. **Canvas**: `canvas/`, a self-contained Canvas backend, build on the core classes 1. **THREE.JS**: `threejs/`, a self-contained THREE.JS backend, build on the core classes -1. **Widget**: `widget/`, a self-contained widget to easily display Spine animations on your website, build on core classes & WebGL backend. +1. **Player**: `player/`, a self-contained player to easily display Spine animations on your website, build on core classes & WebGL backend. While the source code for the core library and backends is written in TypeScript, all code is compiled to easily consumable JavaScript. @@ -20,7 +20,7 @@ The Spine Runtimes are developed with the intent to be used with data exported f spine-ts works with data exported from Spine 3.7.xx. -spine-ts WebGL & Widget backends supports all Spine features. +spine-ts WebGL & players backends supports all Spine features. spine-ts Canvas does not support color tinting, mesh attachments and clipping. Only the alpha channel from tint colors is applied. Experimental support for mesh attachments can be enabled by setting `spine.canvas.SkeletonRenderer.useTriangleRendering` to true. Note that this method is slow and may lead to artifacts on some browsers. @@ -31,10 +31,10 @@ spine-ts does not yet support loading the binary format. ## Usage 1. Download the Spine Runtimes source using [git](https://help.github.com/articles/set-up-git) or by downloading it as a zip via the download button above. 2. To use only the core library without rendering support, include the `build/spine-core.js` file in your project. -3. To use the WebGL backend, include the `spine-webgl.js` file in your project. -3. To use the Canvas backend, include the `spine-canvas.js` file in your project. -4. To use the Widget, include `spine-widget.js` file in your project. -5. To use the THREE.JS backend, include the `spine-threejs.js` file in your project. THREE.JS must be loaded first. +3. To use the WebGL backend, include the `build/spine-webgl.js` file in your project. +3. To use the Canvas backend, include the `build/spine-canvas.js` file in your project. +4. To use the Player, include `build/spine-player.js` and `player/css/spine-player.css` file in your project. +5. To use the THREE.JS backend, include the `build/spine-threejs.js` file in your project. THREE.JS must be loaded first. All `*.js` files are self-contained and include both the core and respective backend classes. @@ -60,7 +60,7 @@ cd spine-ts python -m SimpleHTTPServer ``` -Then open `http://localhost:8000/webgl/example`, `http://localhost:8000/canvas/example`, `https://localhost:8000/threejs/example` or `http://localhost:8000/widget/example` in your browser. +Then open `http://localhost:8000/webgl/example`, `http://localhost:8000/canvas/example`, `https://localhost:8000/threejs/example` or `http://localhost:8000/player/example` in your browser. ## WebGL Demos The spine-ts WebGL demos load their image, atlas, and JSON files from our webserver and so can be run directly, without needing a webserver. The demos can be viewed [all on one page](http://esotericsoftware.com/spine-demos/) or in individual, standalone pages which are easy for you to explore and edit. See the [standalone demos source code](webgl/demos) and view the pages here: @@ -92,7 +92,7 @@ setup a development environment, follow these steps. * **WebGL**: `tsc -w -p tsconfig.webgl.json`, builds `core/src` and `webgl/src`, outputs `build/spine-webgl.js|d.ts|js.map` * **Canvas**: `tsc -w -p tsconfig.canvas.json`, builds `core/src` and `canvas/src`, outputs `build/spine-canvas.js|d.ts|js.map` * **THREE.JS**: `tsc -w -p tsconfig.threejs.json`, builds `core/src` and `threejs/src`, outputs `build/spine-threejs.js|d.ts|js.map` - * **Widget**: `tsc -w -p tsconfig.widget.json`, builds `core/src` and `widget/src`, outputs `build/spine-widget.js|d.ts|js.map` + * **Player**: `tsc -w -p tsconfig.player.json`, builds `core/src` and `player/src`, outputs `build/spine-player.js|d.ts|js.map` 6. Open the `spine-ts` folder in Visual Studio Code. VS Code will use the `tsconfig.json` file all source files from core and all backends for your development pleasure. The actual JavaScript output is still created by the command line TypeScript compiler process from the previous step. @@ -104,7 +104,7 @@ cd spine-ts python -m SimpleHTTPServer ``` -Then navigate to `http://localhost:8000/webgl/example`, `http://localhost:8000/canvas/example`, `http://localhost:8000/threejs/example` or `http://localhost:8000/widget/example` +Then navigate to `http://localhost:8000/webgl/example`, `http://localhost:8000/canvas/example`, `http://localhost:8000/threejs/example` or `http://localhost:8000/player/example` ### Spine-ts WebGL backend By default, the spine-ts WebGL backend supports two-color tinting. This has a neglible effect on performance, as more per vertex data has to be submitted to the GPU, and the fragment shader has to do a few more arithmetic operations. @@ -124,98 +124,5 @@ var skeletonRenderer = new spine.SkeletonRenderer(gl, false); var shader = Shader.newColoredTextured(); ``` -### Using the Widget -To easily display Spine animations on your website, you can use the spine-ts Widget backend. - -1. Export your Spine animation with a texture atlas and put the resulting `.json`, `.atlas` and `.png` files on your server. -2. Copy the `build/spine-widget.js` file to your server and include it on your website ``, adjusting the src to match the location of the file on your server. -3. Add HTML elements, e.g. a `
`, with the class `spine-widget` to your website, specifying its configuration such as the location of the files, the animation to display, etc. - -You can configure a HTML element either directly via HTML, or using JavaScript. At a minimum, you need to specify the location of the -`.json` and `.atlas` file as well as the name of the animation to play back. - -#### HTML configuration -To specify the configuration of a Spine Widget via HTML, you can use these HTML element attributes: - - * `data-json`: required, path to the `.json` file, absolute or relative, e.g. "assets/animation.json" - * `data-atlas`: required, path to the `.atlas` file, absolute or relative, e.g. "assets/animation.atlas" - * `data-animation`: required, the name of the animation to play back - * `data-images-path`: optional, the location of images on the server to load atlas pages from. If omitted, atlas `.png` page files are loaded relative to the `.atlas` file. - * `data-skin`: optional, the name of the skin to use. Defaults to `default` if omitted. - * `data-loop`: optional, whether to loop the animation or not. Defaults to `true` if omitted. - * `data-scale`: optional, the scaling factor to apply when loading the `.json` file. Defaults to `1` if omitted. Irrelevant if `data-fit-to-canvas` is `true`. - * `data-x`: optional, the x-coordinate to display the animation at. The origin is in the bottom left corner. Defaults to `0` if omitted. Irrelevant if `data-fit-to-canvas` is `true`. - * `data-y`: optional, the y-coordinate to display the animation at. The origin is in the bottom left corner with the y-axis pointing upwards. Defaults to `0` if omitted. Irrelevant if `data-fit-to-canvas` is `true`. - * `data-fit-to-canvas`: optional, whether to fit the animation to the canvas size or not. Defaults to `true` if omitted, in which case `data-scale`, `data-x` and `data-y` are irrelevant. This setting calculates the setup pose bounding box using the specified skin to center and scale the animation on the canvas. - * `data-background-color`: optional, the background color to use. Defaults to `#000000` if omitted. - * `data-premultiplied-alpha`: optional, whether the atlas pages use premultiplied alpha or not. Defaults to `false` if omitted. - * `data-debug`: optional, whether to show debug information such as bones, attachments, etc. Defaults to `false` if omitted. - -You can specify these as attribuets on the HTML element like this: - -```html -
-``` - -All HTML elements with class `spine-widget` will be automatically loaded when the website is finished loading by the browser. To add -widgets dynamically, use the JavaScript configuration described below. - -#### JavaScript configuration -You can dynamically add Spine Widgets to your web page by using the JavaScript API. - -Create a HTML element on your website, either statically or via JavaScript: - -```html -
-``` - -Then create a new `spine.SpineWidget`, providing a [`SpineWidgetConfiguration`](widget/src/Widget.ts#L281) object, e.g.: - -```JavaScript -new spine.SpineWidget("my-widget", { - json: "assets/spineboy.json", - atlas: "assets/spineboy.atlas", - animation: "run", - backgroundColor: "#000000", - success: function (widget) { - var animIndex = 0; - widget.canvas.onclick = function () { - animIndex++; - let animations = widget.skeleton.data.animations; - if (animIndex >= animations.length) animIndex = 0; - widget.setAnimation(animations[animIndex].name); - } - } -}); -``` - -The configuration object has the following fields: - - * `json`: required, path to the `.json` file, absolute or relative, e.g. "assets/animation.json" - * `jsonContent`: optional, string or JSON object holding the content of a skeleton `.json` file. Overrides `json` if given. - * `atlas`: required, path to the `.atlas` file, absolute or relative, e.g. "assets/animation.atlas" - * `atlasContent`: optional, string holding the content of an .atlas file. Overrides `atlas` if given. - * `animation`: required, the name of the animation to play back - * `imagesPath`: optional, the location of images on the server to load atlas pages from. If omitted, atlas `.png` page files are loaded relative to the `.atlas` file. - * `atlasPages`: optional, the list of atlas page images, e.g. `atlasPages: ["assets/page1.png", "assets/page2.png"]` when using code, or `data-atlas-pages="assets/page1.png,assets/page2.png"` on case of HTML instantiation. Use this if you have a multi-page atlas. If ommited, only one atlas page image is loaded based on the atlas file name, replacing `.atlas` with `.png`. - * `atlasPagesContent`: optional, the list of atlas page images as data URIs. If given, `atlasPages` must also be given. - * `skin`: optional, the name of the skin to use. Defaults to `default` if omitted. - * `loop`: optional, whether to loop the animation or not. Defaults to `true` if omitted. - * `scale`: optional, the scaling factor to apply when loading the `.json` file. Defaults to `1` if omitted. Irrelevant if `data-fit-to-canavs` is `true`. - * `x`: optional, the x-coordinate to display the animation at. The origin is in the bottom left corner. Defaults to `0` if omitted. Irrelevant if `data-fit-to-canvas` is `true`. - * `y`: optional, the y-coordinate to display the animation at. The origin is in the bottom left corner with the y-axis pointing upwards. Defaults to `0` if omitted. Irrelevant if `data-fit-to-canvas` is `true`. - * `fitToCanvas`: optional, whether to fit the animation to the canvas size or not. Defaults to `true` if omitted, in which case `data-scale`, `data-x` and `data-y` are irrelevant. This setting calculates the setup pose bounding box using the specified skin to center and scale the animation on the canvas. - * `alpha`: optional, whether to allow the canvas to be transparent. Defaults to `true`. Set the alpha channel in ``backgroundColor` to 0 as well, e.g. `#00000000`. - * `backgroundColor`: optional, the background color to use. Defaults to `#000000` if omitted. - * `premultipliedAlpha`: optional, whether the atlas pages use premultiplied alpha or not. Defaults to `false` if omitted. - * `debug`: optional, whether to show debug information such as bones, attachments, etc. Defaults to `false` if omitted. - * `success`: optional, a callback taking a `SpineWidget` called when the animation has been loaded successfully - * `error`: optional, a callback taking a `SpineWidget` and an error string called when the animation couldn't be loaded - -You can also create a HTML element with class `spine-widget` and `data-` attributes as described above and call `spine.widget.SpineWidget.loadWidget(element)` to load -an element via JavaScript on demand. - -The resulting `SpineWidget` has various fields that let you modify the animation programmatically. Most notably, the `skeleton` and `state` fields -let you modify all aspects of your animation as you wish. See the [example](widget/example/index.html#L21). - -You can also modify what debug information is shown by accessing `SpineWidget.debugRenderer` and set the various `drawXXX` fields to `true` or `false`. +### Using the Player +Please see the documentation for the [Spine Web Player](https://esotericsoftware.com/spine-player) diff --git a/spine-ts/build/spine-all.d.ts b/spine-ts/build/spine-all.d.ts index c2dafbe47..01d98d7cd 100644 --- a/spine-ts/build/spine-all.d.ts +++ b/spine-ts/build/spine-all.d.ts @@ -1772,3 +1772,112 @@ declare module spine.threejs { static toThreeJsTextureWrap(wrap: TextureWrap): THREE.Wrapping; } } +declare module spine { + interface Viewport { + x: number; + y: number; + width: number; + height: number; + padLeft: string | number; + padRight: string | number; + padTop: string | number; + padBottom: string | number; + } + interface SpinePlayerConfig { + jsonUrl: string; + atlasUrl: string; + animation: string; + animations: string[]; + defaultMix: number; + skin: string; + skins: string[]; + controlBones: string[]; + premultipliedAlpha: boolean; + showControls: boolean; + debug: { + bones: boolean; + regions: boolean; + meshes: boolean; + bounds: boolean; + paths: boolean; + clipping: boolean; + points: boolean; + hulls: boolean; + }; + viewport: { + x: number; + y: number; + width: number; + height: number; + padLeft: string | number; + padRight: string | number; + padTop: string | number; + padBottom: string | number; + animations: Map; + debugRender: boolean; + transitionTime: number; + }; + alpha: boolean; + backgroundColor: string; + backgroundImage: { + url: string; + x: number; + y: number; + width: number; + height: number; + }; + fullScreenBackgroundColor: string; + success: (widget: SpinePlayer) => void; + error: (widget: SpinePlayer, msg: string) => void; + } + class SpinePlayer { + parent: HTMLElement | string; + private config; + static HOVER_COLOR_INNER: Color; + static HOVER_COLOR_OUTER: Color; + static NON_HOVER_COLOR_INNER: Color; + static NON_HOVER_COLOR_OUTER: Color; + private sceneRenderer; + private dom; + private playerControls; + private canvas; + private timelineSlider; + private playButton; + private skinButton; + private animationButton; + private context; + private loadingScreen; + private assetManager; + private loaded; + private skeleton; + private animationState; + private time; + private paused; + private playTime; + private speed; + private animationViewports; + private currentViewport; + private previousViewport; + private viewportTransitionStart; + private selectedBones; + constructor(parent: HTMLElement | string, config: SpinePlayerConfig); + validateConfig(config: SpinePlayerConfig): SpinePlayerConfig; + showError(error: string): void; + render(): HTMLElement; + private lastPopup; + showSpeedDialog(speedButton: HTMLElement): void; + showAnimationsDialog(animationsButton: HTMLElement): void; + showSkinsDialog(skinButton: HTMLElement): void; + showSettingsDialog(settingsButton: HTMLElement): void; + drawFrame(requestNextFrame?: boolean): void; + scale(sourceWidth: number, sourceHeight: number, targetWidth: number, targetHeight: number): Vector2; + loadSkeleton(): void; + private cancelId; + setupInput(): void; + private play(); + private pause(); + private setAnimation(animation); + private percentageToWorldUnit(size, percentageOrAbsolute); + private calculateAnimationViewport(animationName); + } +} diff --git a/spine-ts/build/spine-all.js b/spine-ts/build/spine-all.js index b2144c4cd..ffe3c018f 100644 --- a/spine-ts/build/spine-all.js +++ b/spine-ts/build/spine-all.js @@ -10090,4 +10090,971 @@ var spine; threejs.ThreeJsTexture = ThreeJsTexture; })(threejs = spine.threejs || (spine.threejs = {})); })(spine || (spine = {})); +var spine; +(function (spine) { + var Popup = (function () { + function Popup(player, parent, htmlContent) { + this.player = player; + this.dom = createElement("\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t"); + this.dom.innerHTML = htmlContent; + parent.appendChild(this.dom); + } + Popup.prototype.show = function (dismissedListener) { + var _this = this; + if (dismissedListener === void 0) { dismissedListener = function () { }; } + this.dom.classList.remove("spine-player-hidden"); + var dismissed = false; + var resize = function () { + if (!dismissed) + requestAnimationFrame(resize); + var bottomOffset = Math.abs(_this.dom.getBoundingClientRect().bottom - _this.player.getBoundingClientRect().bottom); + var rightOffset = Math.abs(_this.dom.getBoundingClientRect().right - _this.player.getBoundingClientRect().right); + var maxHeight = _this.player.clientHeight - bottomOffset - rightOffset; + _this.dom.style.maxHeight = maxHeight + "px"; + }; + requestAnimationFrame(resize); + var justClicked = true; + var windowClickListener = function (event) { + if (justClicked) { + justClicked = false; + return; + } + if (!isContained(_this.dom, event.target)) { + _this.dom.remove(); + window.removeEventListener("click", windowClickListener); + dismissedListener(); + dismissed = true; + } + }; + window.addEventListener("click", windowClickListener); + }; + return Popup; + }()); + var Switch = (function () { + function Switch(text) { + this.text = text; + this.enabled = false; + } + Switch.prototype.render = function () { + var _this = this; + this["switch"] = createElement("\n\t\t\t\t
\n\t\t\t\t\t" + this.text + "\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t"); + this["switch"].addEventListener("click", function () { + _this.setEnabled(!_this.enabled); + if (_this.change) + _this.change(_this.enabled); + }); + return this["switch"]; + }; + Switch.prototype.setEnabled = function (enabled) { + if (enabled) + this["switch"].classList.add("active"); + else + this["switch"].classList.remove("active"); + this.enabled = enabled; + }; + Switch.prototype.isEnabled = function () { + return this.enabled; + }; + return Switch; + }()); + var Slider = (function () { + function Slider(snaps, snapPercentage, big) { + if (snaps === void 0) { snaps = 0; } + if (snapPercentage === void 0) { snapPercentage = 0.1; } + if (big === void 0) { big = false; } + this.snaps = snaps; + this.snapPercentage = snapPercentage; + this.big = big; + } + Slider.prototype.render = function () { + var _this = this; + this.slider = createElement("\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t"); + this.value = findWithClass(this.slider, "spine-player-slider-value")[0]; + this.setValue(0); + var input = new spine.webgl.Input(this.slider); + var dragging = false; + input.addListener({ + down: function (x, y) { + dragging = true; + _this.value.classList.add("hovering"); + }, + up: function (x, y) { + dragging = false; + var percentage = x / _this.slider.clientWidth; + percentage = percentage = Math.max(0, Math.min(percentage, 1)); + _this.setValue(x / _this.slider.clientWidth); + if (_this.change) + _this.change(percentage); + _this.value.classList.remove("hovering"); + }, + moved: function (x, y) { + if (dragging) { + var percentage = x / _this.slider.clientWidth; + percentage = Math.max(0, Math.min(percentage, 1)); + percentage = _this.setValue(x / _this.slider.clientWidth); + if (_this.change) + _this.change(percentage); + } + }, + dragged: function (x, y) { + var percentage = x / _this.slider.clientWidth; + percentage = Math.max(0, Math.min(percentage, 1)); + percentage = _this.setValue(x / _this.slider.clientWidth); + if (_this.change) + _this.change(percentage); + } + }); + return this.slider; + }; + Slider.prototype.setValue = function (percentage) { + percentage = Math.max(0, Math.min(1, percentage)); + if (this.snaps > 0) { + var modulo = percentage % (1 / this.snaps); + if (modulo < (1 / this.snaps) * this.snapPercentage) { + percentage = percentage - modulo; + } + else if (modulo > (1 / this.snaps) - (1 / this.snaps) * this.snapPercentage) { + percentage = percentage - modulo + (1 / this.snaps); + } + percentage = Math.max(0, Math.min(1, percentage)); + } + this.value.style.width = "" + (percentage * 100) + "%"; + return percentage; + }; + return Slider; + }()); + var SpinePlayer = (function () { + function SpinePlayer(parent, config) { + this.parent = parent; + this.config = config; + this.time = new spine.TimeKeeper(); + this.paused = true; + this.playTime = 0; + this.speed = 1; + this.animationViewports = {}; + this.currentViewport = null; + this.previousViewport = null; + this.viewportTransitionStart = 0; + this.cancelId = 0; + if (typeof parent === "string") + parent = document.getElementById(parent); + parent.appendChild(this.render()); + } + SpinePlayer.prototype.validateConfig = function (config) { + if (!config) + throw new Error("Please pass a configuration to new.spine.SpinePlayer()."); + if (!config.jsonUrl) + throw new Error("Please specify the URL of the skeleton JSON file."); + if (!config.atlasUrl) + throw new Error("Please specify the URL of the atlas file."); + if (!config.alpha) + config.alpha = false; + if (!config.backgroundColor) + config.backgroundColor = "#000000"; + if (!config.fullScreenBackgroundColor) + config.fullScreenBackgroundColor = config.backgroundColor; + if (!config.premultipliedAlpha) + config.premultipliedAlpha = false; + if (!config.success) + config.success = function (widget) { }; + if (!config.error) + config.error = function (widget, msg) { }; + if (!config.debug) + config.debug = { + bones: false, + regions: false, + meshes: false, + bounds: false, + clipping: false, + paths: false, + points: false, + hulls: false + }; + if (!config.debug.bones) + config.debug.bones = false; + if (!config.debug.bounds) + config.debug.bounds = false; + if (!config.debug.clipping) + config.debug.clipping = false; + if (!config.debug.hulls) + config.debug.hulls = false; + if (!config.debug.paths) + config.debug.paths = false; + if (!config.debug.points) + config.debug.points = false; + if (!config.debug.regions) + config.debug.regions = false; + if (!config.debug.meshes) + config.debug.meshes = false; + if (config.animations && config.animation) { + if (config.animations.indexOf(config.animation) < 0) + throw new Error("Default animation '" + config.animation + "' is not contained in the list of selectable animations " + escapeHtml(JSON.stringify(this.config.animations)) + "."); + } + if (config.skins && config.skin) { + if (config.skins.indexOf(config.skin) < 0) + throw new Error("Default skin '" + config.skin + "' is not contained in the list of selectable skins " + escapeHtml(JSON.stringify(this.config.skins)) + "."); + } + if (!config.controlBones) + config.controlBones = []; + if (typeof config.showControls === "undefined") + config.showControls = true; + if (typeof config.defaultMix === "undefined") + config.defaultMix = 0.25; + return config; + }; + SpinePlayer.prototype.showError = function (error) { + var errorDom = findWithClass(this.dom, "spine-player-error")[0]; + errorDom.classList.remove("spine-player-hidden"); + errorDom.innerHTML = "

" + error + "

"; + this.config.error(this, error); + }; + SpinePlayer.prototype.render = function () { + var _this = this; + var config = this.config; + var dom = this.dom = createElement("\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t"); + try { + this.config = this.validateConfig(config); + } + catch (e) { + this.showError(e); + return dom; + } + try { + this.canvas = findWithClass(dom, "spine-player-canvas")[0]; + var webglConfig = { alpha: config.alpha }; + this.context = new spine.webgl.ManagedWebGLRenderingContext(this.canvas, webglConfig); + this.sceneRenderer = new spine.webgl.SceneRenderer(this.canvas, this.context, true); + this.loadingScreen = new spine.webgl.LoadingScreen(this.sceneRenderer); + } + catch (e) { + this.showError("Sorry, your browser does not support WebGL.

Please use the latest version of Firefox, Chrome, Edge, or Safari."); + return dom; + } + this.assetManager = new spine.webgl.AssetManager(this.context); + this.assetManager.loadText(config.jsonUrl); + this.assetManager.loadTextureAtlas(config.atlasUrl); + if (config.backgroundImage && config.backgroundImage.url) + this.assetManager.loadTexture(config.backgroundImage.url); + requestAnimationFrame(function () { return _this.drawFrame(); }); + this.playerControls = findWithClass(dom, "spine-player-controls")[0]; + var timeline = findWithClass(dom, "spine-player-timeline")[0]; + this.timelineSlider = new Slider(); + timeline.appendChild(this.timelineSlider.render()); + this.playButton = findWithId(dom, "spine-player-button-play-pause")[0]; + var speedButton = findWithId(dom, "spine-player-button-speed")[0]; + this.animationButton = findWithId(dom, "spine-player-button-animation")[0]; + this.skinButton = findWithId(dom, "spine-player-button-skin")[0]; + var settingsButton = findWithId(dom, "spine-player-button-settings")[0]; + var fullscreenButton = findWithId(dom, "spine-player-button-fullscreen")[0]; + var logoButton = findWithId(dom, "spine-player-button-logo")[0]; + this.playButton.onclick = function () { + if (_this.paused) + _this.play(); + else + _this.pause(); + }; + speedButton.onclick = function () { + _this.showSpeedDialog(speedButton); + }; + this.animationButton.onclick = function () { + _this.showAnimationsDialog(_this.animationButton); + }; + this.skinButton.onclick = function () { + _this.showSkinsDialog(_this.skinButton); + }; + settingsButton.onclick = function () { + _this.showSettingsDialog(settingsButton); + }; + var oldWidth = this.canvas.clientWidth; + var oldHeight = this.canvas.clientHeight; + var oldStyleWidth = this.canvas.style.width; + var oldStyleHeight = this.canvas.style.height; + var isFullscreen = false; + fullscreenButton.onclick = function () { + var fullscreenChanged = function () { + isFullscreen = !isFullscreen; + if (!isFullscreen) { + _this.canvas.style.width = "" + oldWidth + "px"; + _this.canvas.style.height = "" + oldHeight + "px"; + _this.drawFrame(false); + requestAnimationFrame(function () { + _this.canvas.style.width = oldStyleWidth; + _this.canvas.style.height = oldStyleHeight; + }); + } + }; + var doc = document; + dom.onfullscreenchange = fullscreenChanged; + dom.onwebkitfullscreenchange = fullscreenChanged; + if (doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement) { + if (doc.exitFullscreen) + doc.exitFullscreen(); + else if (doc.mozCancelFullScreen) + doc.mozCancelFullScreen(); + else if (doc.webkitExitFullscreen) + doc.webkitExitFullscreen(); + else if (doc.msExitFullscreen) + doc.msExitFullscreen(); + } + else { + oldWidth = _this.canvas.clientWidth; + oldHeight = _this.canvas.clientHeight; + oldStyleWidth = _this.canvas.style.width; + oldStyleHeight = _this.canvas.style.height; + var player = dom; + if (player.requestFullscreen) + player.requestFullscreen(); + else if (player.webkitRequestFullScreen) + player.webkitRequestFullScreen(); + else if (player.mozRequestFullScreen) + player.mozRequestFullScreen(); + else if (player.msRequestFullscreen) + player.msRequestFullscreen(); + } + }; + logoButton.onclick = function () { + window.open("http://esotericsoftware.com"); + }; + window.onresize = function () { + _this.drawFrame(false); + }; + return dom; + }; + SpinePlayer.prototype.showSpeedDialog = function (speedButton) { + var _this = this; + if (this.lastPopup) + this.lastPopup.dom.remove(); + if (this.lastPopup && findWithClass(this.lastPopup.dom, "spine-player-popup-title")[0].textContent == "Speed") { + this.lastPopup = null; + speedButton.classList.remove("spine-player-button-icon-speed-selected"); + return; + } + var popup = new Popup(this.dom, this.playerControls, "\n\t\t\t\t
Speed
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
0.1x
\n\t\t\t\t\t\t\t
1x
\n\t\t\t\t\t\t\t
2x
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t"); + var sliderParent = findWithClass(popup.dom, "spine-player-speed-slider")[0]; + var slider = new Slider(2, 0.1, true); + sliderParent.appendChild(slider.render()); + slider.setValue(this.speed / 2); + slider.change = function (percentage) { + _this.speed = percentage * 2; + }; + speedButton.classList.add("spine-player-button-icon-speed-selected"); + popup.show(function () { + speedButton.classList.remove("spine-player-button-icon-speed-selected"); + }); + this.lastPopup = popup; + }; + SpinePlayer.prototype.showAnimationsDialog = function (animationsButton) { + var _this = this; + if (this.lastPopup) + this.lastPopup.dom.remove(); + if (this.lastPopup && findWithClass(this.lastPopup.dom, "spine-player-popup-title")[0].textContent == "Animations") { + this.lastPopup = null; + animationsButton.classList.remove("spine-player-button-icon-animations-selected"); + return; + } + if (!this.skeleton || this.skeleton.data.animations.length == 0) + return; + var popup = new Popup(this.dom, this.playerControls, "\n\t\t\t\t
Animations
\n\t\t\t\t
\n\t\t\t\t\n\t\t\t"); + var rows = findWithClass(popup.dom, "spine-player-list")[0]; + this.skeleton.data.animations.forEach(function (animation) { + if (_this.config.animations && _this.config.animations.indexOf(animation.name) < 0) { + return; + } + var row = createElement("\n\t\t\t\t\t
  • \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t
  • \n\t\t\t\t"); + if (animation.name == _this.config.animation) + row.classList.add("selected"); + findWithClass(row, "selectable-text")[0].innerText = animation.name; + rows.appendChild(row); + row.onclick = function () { + removeClass(rows.children, "selected"); + row.classList.add("selected"); + _this.config.animation = animation.name; + _this.playTime = 0; + _this.setAnimation(animation.name); + }; + }); + animationsButton.classList.add("spine-player-button-icon-animations-selected"); + popup.show(function () { + animationsButton.classList.remove("spine-player-button-icon-animations-selected"); + }); + this.lastPopup = popup; + }; + SpinePlayer.prototype.showSkinsDialog = function (skinButton) { + var _this = this; + if (this.lastPopup) + this.lastPopup.dom.remove(); + if (this.lastPopup && findWithClass(this.lastPopup.dom, "spine-player-popup-title")[0].textContent == "Skins") { + this.lastPopup = null; + skinButton.classList.remove("spine-player-button-icon-skins-selected"); + return; + } + if (!this.skeleton || this.skeleton.data.animations.length == 0) + return; + var popup = new Popup(this.dom, this.playerControls, "\n\t\t\t\t
    Skins
    \n\t\t\t\t
    \n\t\t\t\t\n\t\t\t"); + var rows = findWithClass(popup.dom, "spine-player-list")[0]; + this.skeleton.data.skins.forEach(function (skin) { + if (_this.config.skins && _this.config.skins.indexOf(skin.name) < 0) { + return; + } + var row = createElement("\n\t\t\t\t\t
  • \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t\t
    \n\t\t\t\t\t
  • \n\t\t\t\t"); + if (skin.name == _this.config.skin) + row.classList.add("selected"); + findWithClass(row, "selectable-text")[0].innerText = skin.name; + rows.appendChild(row); + row.onclick = function () { + removeClass(rows.children, "selected"); + row.classList.add("selected"); + _this.config.skin = skin.name; + _this.skeleton.setSkinByName(_this.config.skin); + _this.skeleton.setSlotsToSetupPose(); + }; + }); + skinButton.classList.add("spine-player-button-icon-skins-selected"); + popup.show(function () { + skinButton.classList.remove("spine-player-button-icon-skins-selected"); + }); + this.lastPopup = popup; + }; + SpinePlayer.prototype.showSettingsDialog = function (settingsButton) { + var _this = this; + if (this.lastPopup) + this.lastPopup.dom.remove(); + if (this.lastPopup && findWithClass(this.lastPopup.dom, "spine-player-popup-title")[0].textContent == "Debug") { + this.lastPopup = null; + settingsButton.classList.remove("spine-player-button-icon-settings-selected"); + return; + } + if (!this.skeleton || this.skeleton.data.animations.length == 0) + return; + var popup = new Popup(this.dom, this.playerControls, "\n\t\t\t\t
    Debug
    \n\t\t\t\t
    \n\t\t\t\t