mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-09 08:38:43 +08:00
[ts] Added CameraController, drag & drop example
This commit is contained in:
parent
7f5e7c3bfc
commit
cda5c0f052
7
spine-ts/.vscode/launch.json
vendored
7
spine-ts/.vscode/launch.json
vendored
@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -27,6 +27,7 @@
|
||||
<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/mix-and-match.html">Mix & match</a></li>
|
||||
<li><a href="/spine-webgl/example/drag-and-drop.html">Drag & drop</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/clipping.html">Clipping</a></li>
|
||||
|
||||
@ -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 ((<any>asset).dispose) (<any>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 ((<any>asset).dispose) (<any>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<Array<Function>> = {};
|
||||
rawDataUris: StringMap<string> = {};
|
||||
|
||||
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];
|
||||
|
||||
19
spine-ts/spine-webgl/example/drag-and-drop.html
Normal file
19
spine-ts/spine-webgl/example/drag-and-drop.html
Normal file
@ -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
spine-ts/spine-webgl/example/drag-and-drop.js
Normal file
220
spine-ts/spine-webgl/example/drag-and-drop.js
Normal file
@ -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
spine-ts/spine-webgl/src/CameraController.ts
Normal file
89
spine-ts/spine-webgl/src/CameraController.ts
Normal file
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./AssetManager";
|
||||
export * from "./Camera";
|
||||
export * from "./CameraController";
|
||||
export * from "./GLTexture";
|
||||
export * from "./Input";
|
||||
export * from "./LoadingScreen";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user