Browse Source

[ts] Added CameraController, drag & drop example

badlogic 4 years ago
parent
commit
cda5c0f052

+ 7 - 0
spine-ts/.vscode/launch.json

@@ -10,6 +10,13 @@
 			"name": "Examples",
 			"name": "Examples",
 			"url": "http://localhost:8080",
 			"url": "http://localhost:8080",
 			"webRoot": "${workspaceFolder}"
 			"webRoot": "${workspaceFolder}"
+		},
+		{
+			"type": "pwa-chrome",
+			"request": "launch",
+			"name": "drag-and-drop",
+			"url": "http://localhost:8080/spine-webgl/example/drag-and-drop.html",
+			"webRoot": "${workspaceFolder}"
 		}
 		}
 	]
 	]
 }
 }

+ 1 - 0
spine-ts/index.html

@@ -27,6 +27,7 @@
 			<li><a href="/spine-webgl/example">Example</a></li>
 			<li><a href="/spine-webgl/example">Example</a></li>
 			<li><a href="/spine-webgl/example/barebones.html">Barebones</a></li>
 			<li><a href="/spine-webgl/example/barebones.html">Barebones</a></li>
 			<li><a href="/spine-webgl/example/mix-and-match.html">Mix &amp; match</a></li>
 			<li><a href="/spine-webgl/example/mix-and-match.html">Mix &amp; match</a></li>
+			<li><a href="/spine-webgl/example/drag-and-drop.html">Drag &amp; drop</a></li>
 			<li><a href="/spine-webgl/example/dress-up.html">Dress-up</a></li>
 			<li><a href="/spine-webgl/example/dress-up.html">Dress-up</a></li>
 			<li><a href="/spine-webgl/demos/additiveblending.html">Additive blending</a></li>
 			<li><a href="/spine-webgl/demos/additiveblending.html">Additive blending</a></li>
 			<li><a href="/spine-webgl/demos/clipping.html">Clipping</a></li>
 			<li><a href="/spine-webgl/demos/clipping.html">Clipping</a></li>

+ 30 - 30
spine-ts/spine-core/src/AssetManagerBase.ts

@@ -40,36 +40,36 @@ export class AssetManagerBase implements Disposable {
 	private toLoad = 0;
 	private toLoad = 0;
 	private loaded = 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.textureLoader = textureLoader;
 		this.pathPrefix = pathPrefix;
 		this.pathPrefix = pathPrefix;
 		this.downloader = downloader || new Downloader();
 		this.downloader = downloader || new Downloader();
 	}
 	}
 
 
-	private start (path: string): string {
+	private start(path: string): string {
 		this.toLoad++;
 		this.toLoad++;
 		return this.pathPrefix + path;
 		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.toLoad--;
 		this.loaded++;
 		this.loaded++;
 		this.assets[path] = asset;
 		this.assets[path] = asset;
 		if (callback) callback(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.toLoad--;
 		this.loaded++;
 		this.loaded++;
 		this.errors[path] = message;
 		this.errors[path] = message;
 		if (callback) callback(path, message);
 		if (callback) callback(path, message);
 	}
 	}
 
 
-	setRawDataURI (path: string, data: string) {
+	setRawDataURI(path: string, data: string) {
 		this.downloader.rawDataUris[this.pathPrefix + path] = data;
 		this.downloader.rawDataUris[this.pathPrefix + path] = data;
 	}
 	}
 
 
-	loadBinary (path: string,
+	loadBinary(path: string,
 		success: (path: string, binary: Uint8Array) => void = null,
 		success: (path: string, binary: Uint8Array) => void = null,
 		error: (path: string, message: string) => void = null) {
 		error: (path: string, message: string) => void = null) {
 		path = this.start(path);
 		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,
 		success: (path: string, text: string) => void = null,
 		error: (path: string, message: string) => void = null) {
 		error: (path: string, message: string) => void = null) {
 		path = this.start(path);
 		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,
 		success: (path: string, object: object) => void = null,
 		error: (path: string, message: string) => void = null) {
 		error: (path: string, message: string) => void = null) {
 		path = this.start(path);
 		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,
 		success: (path: string, texture: Texture) => void = null,
 		error: (path: string, message: string) => void = null) {
 		error: (path: string, message: string) => void = null) {
 		path = this.start(path);
 		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,
 		success: (path: string, atlas: TextureAtlas) => void = null,
 		error: (path: string, message: string) => 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];
 		return this.assets[this.pathPrefix + path];
 	}
 	}
 
 
-	require (path: string) {
+	require(path: string) {
 		path = this.pathPrefix + path;
 		path = this.pathPrefix + path;
 		let asset = this.assets[path];
 		let asset = this.assets[path];
 		if (asset) return asset;
 		if (asset) return asset;
@@ -182,7 +182,7 @@ export class AssetManagerBase implements Disposable {
 		throw Error("Asset not found: " + path + (error ? "\n" + error : ""));
 		throw Error("Asset not found: " + path + (error ? "\n" + error : ""));
 	}
 	}
 
 
-	remove (path: string) {
+	remove(path: string) {
 		path = this.pathPrefix + path;
 		path = this.pathPrefix + path;
 		let asset = this.assets[path];
 		let asset = this.assets[path];
 		if ((<any>asset).dispose) (<any>asset).dispose();
 		if ((<any>asset).dispose) (<any>asset).dispose();
@@ -190,7 +190,7 @@ export class AssetManagerBase implements Disposable {
 		return asset;
 		return asset;
 	}
 	}
 
 
-	removeAll () {
+	removeAll() {
 		for (let key in this.assets) {
 		for (let key in this.assets) {
 			let asset = this.assets[key];
 			let asset = this.assets[key];
 			if ((<any>asset).dispose) (<any>asset).dispose();
 			if ((<any>asset).dispose) (<any>asset).dispose();
@@ -198,27 +198,27 @@ export class AssetManagerBase implements Disposable {
 		this.assets = {};
 		this.assets = {};
 	}
 	}
 
 
-	isLoadingComplete (): boolean {
+	isLoadingComplete(): boolean {
 		return this.toLoad == 0;
 		return this.toLoad == 0;
 	}
 	}
 
 
-	getToLoad (): number {
+	getToLoad(): number {
 		return this.toLoad;
 		return this.toLoad;
 	}
 	}
 
 
-	getLoaded (): number {
+	getLoaded(): number {
 		return this.loaded;
 		return this.loaded;
 	}
 	}
 
 
-	dispose () {
+	dispose() {
 		this.removeAll();
 		this.removeAll();
 	}
 	}
 
 
-	hasErrors () {
+	hasErrors() {
 		return Object.keys(this.errors).length > 0;
 		return Object.keys(this.errors).length > 0;
 	}
 	}
 
 
-	getErrors () {
+	getErrors() {
 		return this.errors;
 		return this.errors;
 	}
 	}
 }
 }
@@ -227,7 +227,7 @@ export class Downloader {
 	private callbacks: StringMap<Array<Function>> = {};
 	private callbacks: StringMap<Array<Function>> = {};
 	rawDataUris: StringMap<string> = {};
 	rawDataUris: StringMap<string> = {};
 
 
-	dataUriToString (dataUri: string) {
+	dataUriToString(dataUri: string) {
 		if (!dataUri.startsWith("data:")) {
 		if (!dataUri.startsWith("data:")) {
 			throw new Error("Not a data URI.");
 			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 binary_string = window.atob(base64);
 		var len = binary_string.length;
 		var len = binary_string.length;
 		var bytes = new Uint8Array(len);
 		var bytes = new Uint8Array(len);
 		for (var i = 0; i < len; i++) {
 		for (var i = 0; i < len; i++) {
 			bytes[i] = binary_string.charCodeAt(i);
 			bytes[i] = binary_string.charCodeAt(i);
 		}
 		}
-		return bytes.buffer;
+		return bytes;
 	}
 	}
 
 
-	dataUriToUint8Array (dataUri: string) {
+	dataUriToUint8Array(dataUri: string) {
 		if (!dataUri.startsWith("data:")) {
 		if (!dataUri.startsWith("data:")) {
 			throw new Error("Not a data URI.");
 			throw new Error("Not a data URI.");
 		}
 		}
@@ -259,10 +259,10 @@ export class Downloader {
 		let base64Idx = dataUri.indexOf("base64,");
 		let base64Idx = dataUri.indexOf("base64,");
 		if (base64Idx == -1) throw new Error("Not a binary data URI.");
 		if (base64Idx == -1) throw new Error("Not a binary data URI.");
 		base64Idx += "base64,".length;
 		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.start(url, success, error)) return;
 		if (this.rawDataUris[url]) {
 		if (this.rawDataUris[url]) {
 			try {
 			try {
@@ -284,13 +284,13 @@ export class Downloader {
 		request.send();
 		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 => {
 		this.downloadText(url, (data: string): void => {
 			success(JSON.parse(data));
 			success(JSON.parse(data));
 		}, error);
 		}, 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.start(url, success, error)) return;
 		if (this.rawDataUris[url]) {
 		if (this.rawDataUris[url]) {
 			try {
 			try {
@@ -317,7 +317,7 @@ export class Downloader {
 		request.send();
 		request.send();
 	}
 	}
 
 
-	private start (url: string, success: any, error: any) {
+	private start(url: string, success: any, error: any) {
 		let callbacks = this.callbacks[url];
 		let callbacks = this.callbacks[url];
 		try {
 		try {
 			if (callbacks) return true;
 			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];
 		let callbacks = this.callbacks[url];
 		delete this.callbacks[url];
 		delete this.callbacks[url];
 		let args = status == 200 ? [data] : [status, data];
 		let args = status == 200 ? [data] : [status, data];

+ 19 - 0
spine-ts/spine-webgl/example/drag-and-drop.html

@@ -0,0 +1,19 @@
+<html>
+<script src="../dist/iife/spine-webgl.js"></script>
+<style>
+    * {
+        margin: 0;
+        padding: 0;
+    }
+</style>
+
+<body>
+    <canvas id="canvas" style="position: absolute; width: 100%; height: 100%;"></canvas>
+    <div style="position: absolute; top: 1em; left: 1em; z-index: 1; color: #ccc;">
+        <label style="margin-right: 0.5em;">Animations</label>
+        <select id="animations"></select>
+    </div>
+    <script src="drag-and-drop.js"></script>
+</body>
+
+</html>

+ 220 - 0
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;
+})();

+ 89 - 0
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;
+            },
+        });
+    }
+}

+ 35 - 43
spine-ts/spine-webgl/src/Input.ts

@@ -41,12 +41,12 @@ export class Input {
 		return new Touch(0, 0, 0);
 		return new Touch(0, 0, 0);
 	});
 	});
 
 
-	constructor (element: HTMLElement) {
+	constructor(element: HTMLElement) {
 		this.element = element;
 		this.element = element;
 		this.setupCallbacks(element);
 		this.setupCallbacks(element);
 	}
 	}
 
 
-	private setupCallbacks (element: HTMLElement) {
+	private setupCallbacks(element: HTMLElement) {
 		let mouseDown = (ev: UIEvent) => {
 		let mouseDown = (ev: UIEvent) => {
 			if (ev instanceof MouseEvent) {
 			if (ev instanceof MouseEvent) {
 				let rect = element.getBoundingClientRect();
 				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("mousedown", mouseDown, true);
 		element.addEventListener("mousemove", mouseMove, true);
 		element.addEventListener("mousemove", mouseMove, true);
 		element.addEventListener("mouseup", mouseUp, true);
 		element.addEventListener("mouseup", mouseUp, true);
+		element.addEventListener("wheel", mouseWheel, true);
+
 		element.addEventListener("touchstart", (ev: TouchEvent) => {
 		element.addEventListener("touchstart", (ev: TouchEvent) => {
 			if (!this.currTouch) {
 			if (!this.currTouch) {
 				var touches = ev.changedTouches;
 				var touches = ev.changedTouches;
@@ -133,32 +145,32 @@ export class Input {
 			}
 			}
 			ev.preventDefault();
 			ev.preventDefault();
 		}, false);
 		}, false);
-		element.addEventListener("touchend", (ev: TouchEvent) => {
+
+		element.addEventListener("touchmove", (ev: TouchEvent) => {
 			if (this.currTouch) {
 			if (this.currTouch) {
 				var touches = ev.changedTouches;
 				var touches = ev.changedTouches;
 				for (var i = 0; i < touches.length; i++) {
 				for (var i = 0; i < touches.length; i++) {
 					var touch = touches[i];
 					var touch = touches[i];
 					if (this.currTouch.identifier === touch.identifier) {
 					if (this.currTouch.identifier === touch.identifier) {
 						let rect = element.getBoundingClientRect();
 						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 x = touch.clientX - rect.left;
+						let y = touch.clientY - rect.top;
+
 						let listeners = this.listeners;
 						let listeners = this.listeners;
 						for (let i = 0; i < listeners.length; i++) {
 						for (let i = 0; i < listeners.length; i++) {
-							if (listeners[i].up) listeners[i].up(x, y);
+							if (listeners[i].dragged) listeners[i].dragged(x, y);
 						}
 						}
 
 
-						this.lastX = x;
-						this.lastY = y;
-						this.buttonDown = false;
-						this.currTouch = null;
+						this.lastX = this.currTouch.x = x;
+						this.lastY = this.currTouch.y = y;
 						break;
 						break;
 					}
 					}
 				}
 				}
 			}
 			}
 			ev.preventDefault();
 			ev.preventDefault();
 		}, false);
 		}, false);
-		element.addEventListener("touchcancel", (ev: TouchEvent) => {
+
+		let touchEnd = (ev: TouchEvent) => {
 			if (this.currTouch) {
 			if (this.currTouch) {
 				var touches = ev.changedTouches;
 				var touches = ev.changedTouches;
 				for (var i = 0; i < touches.length; i++) {
 				for (var i = 0; i < touches.length; i++) {
@@ -182,37 +194,16 @@ export class Input {
 				}
 				}
 			}
 			}
 			ev.preventDefault();
 			ev.preventDefault();
-		}, false);
-		element.addEventListener("touchmove", (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 = touch.clientX - rect.left;
-						let y = touch.clientY - rect.top;
-
-						let listeners = this.listeners;
-						for (let i = 0; i < listeners.length; i++) {
-							if (listeners[i].dragged) listeners[i].dragged(x, y);
-						}
-
-						this.lastX = this.currTouch.x = x;
-						this.lastY = this.currTouch.y = y;
-						break;
-					}
-				}
-			}
-			ev.preventDefault();
-		}, false);
+		};
+		element.addEventListener("touchend", touchEnd, false);
+		element.addEventListener("touchcancel", touchEnd);
 	}
 	}
 
 
-	addListener (listener: InputListener) {
+	addListener(listener: InputListener) {
 		this.listeners.push(listener);
 		this.listeners.push(listener);
 	}
 	}
 
 
-	removeListener (listener: InputListener) {
+	removeListener(listener: InputListener) {
 		let idx = this.listeners.indexOf(listener);
 		let idx = this.listeners.indexOf(listener);
 		if (idx > -1) {
 		if (idx > -1) {
 			this.listeners.splice(idx, 1);
 			this.listeners.splice(idx, 1);
@@ -221,13 +212,14 @@ export class Input {
 }
 }
 
 
 export class Touch {
 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 {
 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;
 }
 }

+ 1 - 0
spine-ts/spine-webgl/src/index.ts

@@ -1,5 +1,6 @@
 export * from "./AssetManager";
 export * from "./AssetManager";
 export * from "./Camera";
 export * from "./Camera";
+export * from "./CameraController";
 export * from "./GLTexture";
 export * from "./GLTexture";
 export * from "./Input";
 export * from "./Input";
 export * from "./LoadingScreen";
 export * from "./LoadingScreen";