diff --git a/spine-ts/.vscode/launch.json b/spine-ts/.vscode/launch.json
index e7bdc6cea..109a37100 100644
--- a/spine-ts/.vscode/launch.json
+++ b/spine-ts/.vscode/launch.json
@@ -10,6 +10,13 @@
"name": "Examples",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
+ },
+ {
+ "type": "pwa-chrome",
+ "request": "launch",
+ "name": "drag-and-drop",
+ "url": "http://localhost:8080/spine-webgl/example/drag-and-drop.html",
+ "webRoot": "${workspaceFolder}"
}
]
}
\ No newline at end of file
diff --git a/spine-ts/index.html b/spine-ts/index.html
index f0dc7cde3..553914515 100644
--- a/spine-ts/index.html
+++ b/spine-ts/index.html
@@ -27,6 +27,7 @@
Example
Barebones
Mix & match
+ Drag & drop
Dress-up
Additive blending
Clipping
diff --git a/spine-ts/spine-core/src/AssetManagerBase.ts b/spine-ts/spine-core/src/AssetManagerBase.ts
index c760ad83c..8783dbdbf 100644
--- a/spine-ts/spine-core/src/AssetManagerBase.ts
+++ b/spine-ts/spine-core/src/AssetManagerBase.ts
@@ -40,36 +40,36 @@ export class AssetManagerBase implements Disposable {
private toLoad = 0;
private loaded = 0;
- constructor (textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture, pathPrefix: string = "", downloader: Downloader = null) {
+ constructor(textureLoader: (image: HTMLImageElement | ImageBitmap) => Texture, pathPrefix: string = "", downloader: Downloader = null) {
this.textureLoader = textureLoader;
this.pathPrefix = pathPrefix;
this.downloader = downloader || new Downloader();
}
- private start (path: string): string {
+ private start(path: string): string {
this.toLoad++;
return this.pathPrefix + path;
}
- private success (callback: (path: string, data: any) => void, path: string, asset: any) {
+ private success(callback: (path: string, data: any) => void, path: string, asset: any) {
this.toLoad--;
this.loaded++;
this.assets[path] = asset;
if (callback) callback(path, asset);
}
- private error (callback: (path: string, message: string) => void, path: string, message: string) {
+ private error(callback: (path: string, message: string) => void, path: string, message: string) {
this.toLoad--;
this.loaded++;
this.errors[path] = message;
if (callback) callback(path, message);
}
- setRawDataURI (path: string, data: string) {
+ setRawDataURI(path: string, data: string) {
this.downloader.rawDataUris[this.pathPrefix + path] = data;
}
- loadBinary (path: string,
+ loadBinary(path: string,
success: (path: string, binary: Uint8Array) => void = null,
error: (path: string, message: string) => void = null) {
path = this.start(path);
@@ -81,7 +81,7 @@ export class AssetManagerBase implements Disposable {
});
}
- loadText (path: string,
+ loadText(path: string,
success: (path: string, text: string) => void = null,
error: (path: string, message: string) => void = null) {
path = this.start(path);
@@ -93,7 +93,7 @@ export class AssetManagerBase implements Disposable {
});
}
- loadJson (path: string,
+ loadJson(path: string,
success: (path: string, object: object) => void = null,
error: (path: string, message: string) => void = null) {
path = this.start(path);
@@ -105,7 +105,7 @@ export class AssetManagerBase implements Disposable {
});
}
- loadTexture (path: string,
+ loadTexture(path: string,
success: (path: string, texture: Texture) => void = null,
error: (path: string, message: string) => void = null) {
path = this.start(path);
@@ -136,7 +136,7 @@ export class AssetManagerBase implements Disposable {
}
}
- loadTextureAtlas (path: string,
+ loadTextureAtlas(path: string,
success: (path: string, atlas: TextureAtlas) => void = null,
error: (path: string, message: string) => void = null
) {
@@ -170,11 +170,11 @@ export class AssetManagerBase implements Disposable {
});
}
- get (path: string) {
+ get(path: string) {
return this.assets[this.pathPrefix + path];
}
- require (path: string) {
+ require(path: string) {
path = this.pathPrefix + path;
let asset = this.assets[path];
if (asset) return asset;
@@ -182,7 +182,7 @@ export class AssetManagerBase implements Disposable {
throw Error("Asset not found: " + path + (error ? "\n" + error : ""));
}
- remove (path: string) {
+ remove(path: string) {
path = this.pathPrefix + path;
let asset = this.assets[path];
if ((asset).dispose) (asset).dispose();
@@ -190,7 +190,7 @@ export class AssetManagerBase implements Disposable {
return asset;
}
- removeAll () {
+ removeAll() {
for (let key in this.assets) {
let asset = this.assets[key];
if ((asset).dispose) (asset).dispose();
@@ -198,27 +198,27 @@ export class AssetManagerBase implements Disposable {
this.assets = {};
}
- isLoadingComplete (): boolean {
+ isLoadingComplete(): boolean {
return this.toLoad == 0;
}
- getToLoad (): number {
+ getToLoad(): number {
return this.toLoad;
}
- getLoaded (): number {
+ getLoaded(): number {
return this.loaded;
}
- dispose () {
+ dispose() {
this.removeAll();
}
- hasErrors () {
+ hasErrors() {
return Object.keys(this.errors).length > 0;
}
- getErrors () {
+ getErrors() {
return this.errors;
}
}
@@ -227,7 +227,7 @@ export class Downloader {
private callbacks: StringMap> = {};
rawDataUris: StringMap = {};
- dataUriToString (dataUri: string) {
+ dataUriToString(dataUri: string) {
if (!dataUri.startsWith("data:")) {
throw new Error("Not a data URI.");
}
@@ -241,17 +241,17 @@ export class Downloader {
}
}
- base64ToArrayBuffer (base64: string) {
+ base64ToUint8Array(base64: string) {
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
- return bytes.buffer;
+ return bytes;
}
- dataUriToUint8Array (dataUri: string) {
+ dataUriToUint8Array(dataUri: string) {
if (!dataUri.startsWith("data:")) {
throw new Error("Not a data URI.");
}
@@ -259,10 +259,10 @@ export class Downloader {
let base64Idx = dataUri.indexOf("base64,");
if (base64Idx == -1) throw new Error("Not a binary data URI.");
base64Idx += "base64,".length;
- return this.base64ToArrayBuffer(dataUri.substr(base64Idx));
+ return this.base64ToUint8Array(dataUri.substr(base64Idx));
}
- downloadText (url: string, success: (data: string) => void, error: (status: number, responseText: string) => void) {
+ downloadText(url: string, success: (data: string) => void, error: (status: number, responseText: string) => void) {
if (this.start(url, success, error)) return;
if (this.rawDataUris[url]) {
try {
@@ -284,13 +284,13 @@ export class Downloader {
request.send();
}
- downloadJson (url: string, success: (data: object) => void, error: (status: number, responseText: string) => void) {
+ downloadJson(url: string, success: (data: object) => void, error: (status: number, responseText: string) => void) {
this.downloadText(url, (data: string): void => {
success(JSON.parse(data));
}, error);
}
- downloadBinary (url: string, success: (data: Uint8Array) => void, error: (status: number, responseText: string) => void) {
+ downloadBinary(url: string, success: (data: Uint8Array) => void, error: (status: number, responseText: string) => void) {
if (this.start(url, success, error)) return;
if (this.rawDataUris[url]) {
try {
@@ -317,7 +317,7 @@ export class Downloader {
request.send();
}
- private start (url: string, success: any, error: any) {
+ private start(url: string, success: any, error: any) {
let callbacks = this.callbacks[url];
try {
if (callbacks) return true;
@@ -327,7 +327,7 @@ export class Downloader {
}
}
- private finish (url: string, status: number, data: any) {
+ private finish(url: string, status: number, data: any) {
let callbacks = this.callbacks[url];
delete this.callbacks[url];
let args = status == 200 ? [data] : [status, data];
diff --git a/spine-ts/spine-webgl/example/drag-and-drop.html b/spine-ts/spine-webgl/example/drag-and-drop.html
new file mode 100644
index 000000000..13cc10f40
--- /dev/null
+++ b/spine-ts/spine-webgl/example/drag-and-drop.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spine-ts/spine-webgl/example/drag-and-drop.js b/spine-ts/spine-webgl/example/drag-and-drop.js
new file mode 100644
index 000000000..8ddc8def3
--- /dev/null
+++ b/spine-ts/spine-webgl/example/drag-and-drop.js
@@ -0,0 +1,220 @@
+class App {
+ constructor() {
+ this.skeleton = null;
+ this.animationState = null;
+ this.canvas = null;
+ }
+
+ loadAssets(canvas) {
+ this.canvas = canvas;
+
+ // Load assets of Spineboy.
+ canvas.assetManager.loadBinary("assets/spineboy-pro.skel");
+ canvas.assetManager.loadTextureAtlas("assets/spineboy-pma.atlas");
+ }
+
+ initialize(canvas) {
+ // Load the Spineboy skeleton
+ this.loadSkeleton("assets/spineboy-pro.skel", "assets/spineboy-pma.atlas", "run");
+
+ // Setup listener for animation selection box
+ let animationSelectBox = document.body.querySelector("#animations");
+ animationSelectBox.onchange = () => {
+ this.animationState.setAnimation(0, animationSelectBox.value, true);
+ }
+
+ // Setup the drag and drop listener
+ new FileDragAndDrop(canvas.htmlCanvas, (files) => this.onDrop(files))
+
+ // Setup a camera controller for paning and zooming
+ new spine.CameraController(canvas.htmlCanvas, canvas.renderer.camera);
+ }
+
+ onDrop(files) {
+ let atlasFile;
+ let skeletonFile;
+ let pngs = [];
+ let assetManager = this.canvas.assetManager;
+
+ // We use data URIs to load the dropped files. Some file types
+ // are binary, so we have to encode them to base64 for loading
+ // through AssetManager.
+ let bufferToBase64 = (buffer) => {
+ var binary = '';
+ var bytes = new Uint8Array(buffer);
+ var len = bytes.byteLength;
+ for (var i = 0; i < len; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return window.btoa(binary);
+ }
+
+ for (var file of files) {
+ if (file.name.endsWith(".atlas") || file.name.endsWith(".atlas.txt")) {
+ atlasFile = file;
+ assetManager.setRawDataURI(file.name, "data:text/plain;," + file.contentText);
+ } else if (file.name.endsWith(".skel")) {
+ skeletonFile = file;
+ assetManager.setRawDataURI(file.name, "data:application/octet-stream;base64," + bufferToBase64(file.contentBinary));
+ assetManager.loadBinary(file.name);
+ } else if (file.name.endsWith(".json")) {
+ skeletonFile = file;
+ assetManager.setRawDataURI(file.name, "data:text/plain;," + file.contentText);
+ assetManager.loadJson(file.name);
+ } else if (file.name.endsWith(".png")) {
+ pngs.push(file);
+ assetManager.setRawDataURI(file.name, "data:image/png;base64," + bufferToBase64(file.contentBinary));
+ }
+ }
+
+ if (!atlasFile) {
+ alert("Please provide a .atlas or .atlas.txt atlas file.");
+ return;
+ }
+ if (pngs.length == 0) {
+ alert("Please provide the atlas page .png file(s).");
+ }
+ if (!skeletonFile) {
+ alert("Please provide a .skel or .json skeleton file.");
+ return;
+ }
+
+ assetManager.loadTextureAtlas(atlasFile.name);
+
+ let waitForLoad = () => {
+ if (this.canvas.assetManager.isLoadingComplete()) {
+ this.loadSkeleton(skeletonFile.name, atlasFile.name);
+ } else {
+ requestAnimationFrame(waitForLoad);
+ }
+ }
+ waitForLoad();
+ }
+
+ loadSkeleton(skeletonFile, atlasFile, animationName) {
+ // Load the skeleton and setup the animation state
+ let assetManager = this.canvas.assetManager;
+ var atlas = assetManager.require(atlasFile);
+ var atlasLoader = new spine.AtlasAttachmentLoader(atlas);
+ var skeletonData;
+ var skeletonBinaryOrJson = skeletonFile.endsWith(".skel") ?
+ new spine.SkeletonBinary(atlasLoader) :
+ new spine.SkeletonJson(atlasLoader);
+ skeletonBinaryOrJson.scale = 1;
+ skeletonData = skeletonBinaryOrJson.readSkeletonData(assetManager.require(skeletonFile));
+ this.skeleton = new spine.Skeleton(skeletonData);
+ var animationStateData = new spine.AnimationStateData(skeletonData);
+ this.animationState = new spine.AnimationState(animationStateData);
+
+ // Fill the animation selection box.
+ let animationSelectBox = document.body.querySelector("#animations");
+ animationSelectBox.innerHTML = "";
+ for (var animation of this.skeleton.data.animations) {
+ if (!animationName) animationName = animation.name;
+ let option = document.createElement("option");
+ option.value = option.innerText = animation.name;
+ option.selected = animation.name == animationName;
+ animationSelectBox.appendChild(option);
+ }
+
+ if (animationName) this.animationState.setAnimation(0, animationName, true);
+
+ // Center the skeleton in the viewport
+ this.centerSkeleton();
+ }
+
+ centerSkeleton() {
+ // Calculate the bounds of the skeleton
+ this.animationState.update(0);
+ this.animationState.apply(this.skeleton);
+ this.skeleton.updateWorldTransform();
+ let offset = new spine.Vector2(), size = new spine.Vector2();
+ this.skeleton.getBounds(offset, size);
+
+ // Make sure the canvas is sized properly and position and zoom
+ // the camera so the skeleton is centered in the viewport.
+ let renderer = this.canvas.renderer;
+ renderer.resize(spine.ResizeMode.Expand);
+ let camera = this.canvas.renderer.camera;
+ camera.position.x = offset.x + size.x / 2;
+ camera.position.y = offset.y + size.y / 2;
+ camera.zoom = size.x > size.y ? size.x / this.canvas.htmlCanvas.width * 3 : size.y / this.canvas.htmlCanvas.height * 3;
+ camera.update();
+ }
+
+ update(canvas, delta) {
+ this.animationState.update(delta);
+ this.animationState.apply(this.skeleton);
+ this.skeleton.updateWorldTransform();
+ }
+
+ render(canvas) {
+ let renderer = canvas.renderer;
+ renderer.resize(spine.ResizeMode.Expand);
+
+ canvas.clear(0.2, 0.2, 0.2, 1);
+ renderer.begin();
+ renderer.line(-10000, 0, 10000, 0, spine.Color.RED);
+ renderer.line(0, -10000, 0, 10000, spine.Color.GREEN);
+ renderer.drawSkeleton(this.skeleton, true);
+ renderer.end();
+ }
+}
+
+new spine.SpineCanvas(document.getElementById("canvas"), {
+ app: new App()
+});
+
+class FileDragAndDrop {
+ constructor(element, callback) {
+ this.callback = callback;
+ element.ondrop = (ev) => this.onDrop(ev);
+ element.ondragover = (ev) => ev.preventDefault();
+ }
+
+ async onDrop(event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const items = Object.keys(event.dataTransfer.items);
+ let files = [];
+ await Promise.all(items.map(async (key) => {
+ var file = event.dataTransfer.items[key].getAsFile();
+ if (file.kind == "string") return;
+ let contentBinary = await file.arrayBuffer();
+ let contentText = await file.text();
+ files.push({ name: file.name, contentBinary: contentBinary, contentText: contentText });
+ }));
+ this.callback(files);
+ }
+}
+
+// Shim for older browsers for File/Blob.arrayBuffer() and .text()
+(function () {
+ function arrayBuffer() {
+ return new Promise(function () {
+ let fr = new FileReader();
+ fr.onload = () => {
+ resolve(fr.result);
+ };
+ fr.readAsArrayBuffer();
+ })
+ }
+
+ function text() {
+ return new Promise(function () {
+ let fr = new FileReader();
+ fr.onload = () => {
+ resolve(fr.result);
+ };
+ fr.readAsText(this);
+ })
+ }
+
+ if ('File' in self) {
+ File.prototype.arrayBuffer = File.prototype.arrayBuffer || arrayBuffer;
+ File.prototype.text = File.prototype.text || text;
+ }
+ Blob.prototype.arrayBuffer = Blob.prototype.arrayBuffer || arrayBuffer;
+ Blob.prototype.text = Blob.prototype.text || text;
+})();
\ No newline at end of file
diff --git a/spine-ts/spine-webgl/src/CameraController.ts b/spine-ts/spine-webgl/src/CameraController.ts
new file mode 100644
index 000000000..31e03c9c9
--- /dev/null
+++ b/spine-ts/spine-webgl/src/CameraController.ts
@@ -0,0 +1,89 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, 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 { Input, Vector3 } from "src";
+import { OrthoCamera } from "./Camera";
+
+export class CameraController {
+ constructor(public canvas: HTMLElement, public camera: OrthoCamera) {
+ let cameraX = 0, cameraY = 0, cameraZoom = 0;
+ let mouseX = 0, mouseY = 0;
+ let lastX = 0, lastY = 0;
+
+ new Input(canvas).addListener({
+ down: (x: number, y: number) => {
+ cameraX = camera.position.x;
+ cameraY = camera.position.y;
+ mouseX = lastX = x;
+ mouseY = lastY = y;
+ },
+ dragged: (x: number, y: number) => {
+ let deltaX = x - mouseX;
+ let deltaY = y - mouseY;
+ let originWorld = camera.screenToWorld(new Vector3(0, 0), canvas.clientWidth, canvas.clientHeight);
+ let deltaWorld = camera.screenToWorld(new Vector3(deltaX, deltaY), canvas.clientWidth, canvas.clientHeight).sub(originWorld);
+ camera.position.set(cameraX - deltaWorld.x, cameraY - deltaWorld.y, 0);
+ camera.update();
+ lastX = x;
+ lastY = y;
+ },
+ zoom: (zoom: number) => {
+ let zoomAmount = zoom / 200 * camera.zoom;
+ let newZoom = camera.zoom + zoomAmount;
+ if (newZoom > 0) {
+ let x = 0, y = 0;
+ if (zoom < 0) {
+ x = lastX; y = lastY;
+ } else {
+ let viewCenter = new Vector3(canvas.clientWidth / 2 + 15, canvas.clientHeight / 2);
+ let mouseToCenterX = lastX - viewCenter.x;
+ let mouseToCenterY = canvas.clientHeight - 1 - lastY - viewCenter.y;
+ x = viewCenter.x - mouseToCenterX;
+ y = canvas.clientHeight - 1 - viewCenter.y + mouseToCenterY;
+ }
+ let oldDistance = camera.screenToWorld(new Vector3(x, y), canvas.clientWidth, canvas.clientHeight);
+ camera.zoom = newZoom;
+ camera.update();
+ let newDistance = camera.screenToWorld(new Vector3(x, y), canvas.clientWidth, canvas.clientHeight);
+ camera.position.add(oldDistance.sub(newDistance));
+ camera.update();
+ }
+ console.log(`${camera.zoom}, ${zoomAmount}, ${zoom}`);
+ },
+ up: (x: number, y: number) => {
+ lastX = x;
+ lastY = y;
+ },
+ moved: (x: number, y: number) => {
+ lastX = x;
+ lastY = y;
+ },
+ });
+ }
+}
\ No newline at end of file
diff --git a/spine-ts/spine-webgl/src/Input.ts b/spine-ts/spine-webgl/src/Input.ts
index 0fa3a7098..836d98daa 100644
--- a/spine-ts/spine-webgl/src/Input.ts
+++ b/spine-ts/spine-webgl/src/Input.ts
@@ -41,12 +41,12 @@ export class Input {
return new Touch(0, 0, 0);
});
- constructor (element: HTMLElement) {
+ constructor(element: HTMLElement) {
this.element = element;
this.setupCallbacks(element);
}
- private setupCallbacks (element: HTMLElement) {
+ private setupCallbacks(element: HTMLElement) {
let mouseDown = (ev: UIEvent) => {
if (ev instanceof MouseEvent) {
let rect = element.getBoundingClientRect();
@@ -104,9 +104,21 @@ export class Input {
}
}
+ let mouseWheel = (e: WheelEvent) => {
+ e.preventDefault();
+ let deltaY = e.deltaY;
+ if (e.deltaMode == WheelEvent.DOM_DELTA_LINE) deltaY *= 8;
+ if (e.deltaMode == WheelEvent.DOM_DELTA_PAGE) deltaY *= 24;
+ let listeners = this.listeners;
+ for (let i = 0; i < listeners.length; i++)
+ if (listeners[i].zoom) listeners[i].zoom(e.deltaY);
+ };
+
element.addEventListener("mousedown", mouseDown, true);
element.addEventListener("mousemove", mouseMove, true);
element.addEventListener("mouseup", mouseUp, true);
+ element.addEventListener("wheel", mouseWheel, true);
+
element.addEventListener("touchstart", (ev: TouchEvent) => {
if (!this.currTouch) {
var touches = ev.changedTouches;
@@ -133,56 +145,7 @@ export class Input {
}
ev.preventDefault();
}, false);
- element.addEventListener("touchend", (ev: TouchEvent) => {
- if (this.currTouch) {
- var touches = ev.changedTouches;
- for (var i = 0; i < touches.length; i++) {
- var touch = touches[i];
- if (this.currTouch.identifier === touch.identifier) {
- let rect = element.getBoundingClientRect();
- let x = this.currTouch.x = touch.clientX - rect.left;
- let y = this.currTouch.y = touch.clientY - rect.top;
- this.touchesPool.free(this.currTouch);
- let listeners = this.listeners;
- for (let i = 0; i < listeners.length; i++) {
- if (listeners[i].up) listeners[i].up(x, y);
- }
- this.lastX = x;
- this.lastY = y;
- this.buttonDown = false;
- this.currTouch = null;
- break;
- }
- }
- }
- ev.preventDefault();
- }, false);
- element.addEventListener("touchcancel", (ev: TouchEvent) => {
- if (this.currTouch) {
- var touches = ev.changedTouches;
- for (var i = 0; i < touches.length; i++) {
- var touch = touches[i];
- if (this.currTouch.identifier === touch.identifier) {
- let rect = element.getBoundingClientRect();
- let x = this.currTouch.x = touch.clientX - rect.left;
- let y = this.currTouch.y = touch.clientY - rect.top;
- this.touchesPool.free(this.currTouch);
- let listeners = this.listeners;
- for (let i = 0; i < listeners.length; i++) {
- if (listeners[i].up) listeners[i].up(x, y);
- }
-
- this.lastX = x;
- this.lastY = y;
- this.buttonDown = false;
- this.currTouch = null;
- break;
- }
- }
- }
- ev.preventDefault();
- }, false);
element.addEventListener("touchmove", (ev: TouchEvent) => {
if (this.currTouch) {
var touches = ev.changedTouches;
@@ -206,13 +169,41 @@ export class Input {
}
ev.preventDefault();
}, false);
+
+ let touchEnd = (ev: TouchEvent) => {
+ if (this.currTouch) {
+ var touches = ev.changedTouches;
+ for (var i = 0; i < touches.length; i++) {
+ var touch = touches[i];
+ if (this.currTouch.identifier === touch.identifier) {
+ let rect = element.getBoundingClientRect();
+ let x = this.currTouch.x = touch.clientX - rect.left;
+ let y = this.currTouch.y = touch.clientY - rect.top;
+ this.touchesPool.free(this.currTouch);
+ let listeners = this.listeners;
+ for (let i = 0; i < listeners.length; i++) {
+ if (listeners[i].up) listeners[i].up(x, y);
+ }
+
+ this.lastX = x;
+ this.lastY = y;
+ this.buttonDown = false;
+ this.currTouch = null;
+ break;
+ }
+ }
+ }
+ ev.preventDefault();
+ };
+ element.addEventListener("touchend", touchEnd, false);
+ element.addEventListener("touchcancel", touchEnd);
}
- addListener (listener: InputListener) {
+ addListener(listener: InputListener) {
this.listeners.push(listener);
}
- removeListener (listener: InputListener) {
+ removeListener(listener: InputListener) {
let idx = this.listeners.indexOf(listener);
if (idx > -1) {
this.listeners.splice(idx, 1);
@@ -221,13 +212,14 @@ export class Input {
}
export class Touch {
- constructor (public identifier: number, public x: number, public y: number) {
+ constructor(public identifier: number, public x: number, public y: number) {
}
}
export interface InputListener {
- down (x: number, y: number): void;
- up (x: number, y: number): void;
- moved (x: number, y: number): void;
- dragged (x: number, y: number): void;
+ down?(x: number, y: number): void;
+ up?(x: number, y: number): void;
+ moved?(x: number, y: number): void;
+ dragged?(x: number, y: number): void;
+ zoom?(zoom: number): void;
}
diff --git a/spine-ts/spine-webgl/src/index.ts b/spine-ts/spine-webgl/src/index.ts
index 64b3a3c02..6ba803726 100644
--- a/spine-ts/spine-webgl/src/index.ts
+++ b/spine-ts/spine-webgl/src/index.ts
@@ -1,5 +1,6 @@
export * from "./AssetManager";
export * from "./Camera";
+export * from "./CameraController";
export * from "./GLTexture";
export * from "./Input";
export * from "./LoadingScreen";