From 28893c847e615551628fcdcf9d6010bde3957ec1 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 1 Oct 2017 20:00:45 +0800 Subject: [PATCH] use npm --- .babelrc | 13 + .gitignore | 61 ++ README.md | 12 +- example.html | 45 -- img/1_8_texturemap_redux.png | Bin 3684 -> 0 bytes img/hatsune_miku.png | Bin 3392 -> 0 bytes img/mojang_cape.png | Bin 1076 -> 0 bytes img/steve_old.png | Bin 965 -> 0 bytes js/skinview3d.js | 1287 ---------------------------------- package.json | 31 + rollup.config.babel.js | 27 + rollup.config.default.js | 20 + src/animation.js | 16 + src/model.js | 333 +++++++++ src/orbit_controls.js | 593 ++++++++++++++++ src/skinview3d.js | 27 + src/viewer.js | 245 +++++++ 17 files changed, 1372 insertions(+), 1338 deletions(-) create mode 100644 .babelrc create mode 100644 .gitignore delete mode 100644 example.html delete mode 100644 img/1_8_texturemap_redux.png delete mode 100644 img/hatsune_miku.png delete mode 100644 img/mojang_cape.png delete mode 100644 img/steve_old.png delete mode 100644 js/skinview3d.js create mode 100644 package.json create mode 100644 rollup.config.babel.js create mode 100644 rollup.config.default.js create mode 100644 src/animation.js create mode 100644 src/model.js create mode 100644 src/orbit_controls.js create mode 100644 src/skinview3d.js create mode 100644 src/viewer.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..e016676 --- /dev/null +++ b/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "es2015", + { + "modules": false + } + ] + ], + "plugins": [ + "external-helpers" + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c07976 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +build/ +package-lock.json diff --git a/README.md b/README.md index 39091bd..555dbd4 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ Three.js powered Minecraft skin viewer. * Capes * Slim arms -# License -* `/img/mojang_cape.png` Copyright Mojang AB. [Link](https://minecraft.gamepedia.com/File:MojangCape2016.png) -* `/img/hatsune_miku.png` Copyright xilefian. [Link](http://www.minecraftforum.net/forums/mapping-and-modding/skins/2646900-hatsune-miku-skin-1-9-transparency-layers) -* `/img/1_8_texturemap_redux.png` Copyright Mojang AB. [Link](https://minecraft.gamepedia.com/File:1_8_texturemap_redux.png) -* `/img/steve_old.png` Copyright Mojang AB. [Link](https://minecraft.gamepedia.com/File:Char.png) +# Build +`npm run build` Concatenation only. -Other parts of the repository are licensed under GPLv3 License. +`npm run build-babel` Concatenate and compile using Babel (es2015). + +# License +The repository is licensed under GPL-3.0 License. diff --git a/example.html b/example.html deleted file mode 100644 index 0da274e..0000000 --- a/example.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - skinview3d example - - - -
- - - - - - diff --git a/img/1_8_texturemap_redux.png b/img/1_8_texturemap_redux.png deleted file mode 100644 index a206b32da79d2a1be3de8e29425fc1a2f38ea7b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3684 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 l zvwLE-duOeHZMTAQwux-Mh-|%!a=ngny^eIomVnuwl8yiX00DGTPE!Ct=GbNc00K=( zL_t(|UZs+SZsaf&MQaLk1{uoC%*)Ko`~N?&*LJOKr`pYQdKAf5k?zrR@+J(nUBOeC zu`ZanDR#y8A*fhh=4IJ90^jFaa0hrd7gT(C2j3oBfG+3$Z3L=s1kc70a6jQQQlJ<^ zkZ@qzH~x)5z&puWzyB!rmnuBB`~9_~!-K%=#V7)vCi!HZ=aWyLq)U@|l5z;lUK9$_ zHcyrp=ac-~&)+|OeSDnEFD@s%otnKUo}k-HgHF0yIxT(qaCUUGns$P;*A2{G6bpi2 zVQ%km{q%hI^V#lsY5j0-ZUHa#UW_E@wpX^@{kNaKdfxZrm;3JaO1lez?2jTygWkr; zAhTY-xeH!Zt!!|z(F;8)Q@N!RCwS)#}#H&erC^`>A(RgDkT+u(F9!cmVDL zBW+lU#=Q$nfi)V;oAqXk1)k!XpaE_xVCZ|dxx#fptu|e20UYv-9LEW1Ts^K<UATi*B=5$I1Z)gGc75C5s|PY1C2zG%-n$ n0265g)foXk7ob;e4o&a}7qn*G7xJ2m00000NkvXXu0mjfWUzyu diff --git a/js/skinview3d.js b/js/skinview3d.js deleted file mode 100644 index 0f0534e..0000000 --- a/js/skinview3d.js +++ /dev/null @@ -1,1287 +0,0 @@ -/** - * skinview3d - * - * Copyright (C) 2017 the original author or authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -let skinview3d = new function(){ - /** - * @author yushijinhun - * @author Hacksore - * @author Kent Rasmussen - */ - "use strict"; - - let copyImage = (context, sX, sY, w, h, dX, dY, flipHorizontal) => { - let imgData = context.getImageData(sX, sY, w, h); - if(flipHorizontal){ - for(let y = 0; y < h; y++) { - for(let x = 0; x < (w / 2); x++) { - let index = (x + y * w) * 4; - let index2 = ((w - x - 1) + y * w) * 4; - let pA1 = imgData.data[index]; - let pA2 = imgData.data[index+1]; - let pA3 = imgData.data[index+2]; - let pA4 = imgData.data[index+3]; - - let pB1 = imgData.data[index2]; - let pB2 = imgData.data[index2+1]; - let pB3 = imgData.data[index2+2]; - let pB4 = imgData.data[index2+3]; - - imgData.data[index] = pB1; - imgData.data[index+1] = pB2; - imgData.data[index+2] = pB3; - imgData.data[index+3] = pB4; - - imgData.data[index2] = pA1; - imgData.data[index2+1] = pA2; - imgData.data[index2+2] = pA3; - imgData.data[index2+3] = pA4; - } - } - } - context.putImageData(imgData,dX,dY); - }; - - let convertSkinTo1_8 = (context, width) => { - let scale = width/64.0; - let copySkin = (context, sX, sY, w, h, dX, dY, flipHorizontal) => copyImage(context, sX*scale, sY*scale, w*scale, h*scale, dX*scale, dY*scale, flipHorizontal); - - copySkin(context, 4, 16, 4, 4, 20, 48, true); // Top Leg - copySkin(context, 8, 16, 4, 4, 24, 48, true); // Bottom Leg - copySkin(context, 0, 20, 4, 12, 24, 52, true); // Outer Leg - copySkin(context, 4, 20, 4, 12, 20, 52, true); // Front Leg - copySkin(context, 8, 20, 4, 12, 16, 52, true); // Inner Leg - copySkin(context, 12, 20, 4, 12, 28, 52, true); // Back Leg - copySkin(context, 44, 16, 4, 4, 36, 48, true); // Top Arm - copySkin(context, 48, 16, 4, 4, 40, 48, true); // Bottom Arm - copySkin(context, 40, 20, 4, 12, 40, 52, true); // Outer Arm - copySkin(context, 44, 20, 4, 12, 36, 52, true); // Front Arm - copySkin(context, 48, 20, 4, 12, 32, 52, true); // Inner Arm - copySkin(context, 52, 20, 4, 12, 44, 52, true); // Back Arm - }; - - let toFaceVertices = (x1,y1,x2,y2,w,h) => [ - new THREE.Vector2(x1/w, 1.0-y2/h), - new THREE.Vector2(x2/w, 1.0-y2/h), - new THREE.Vector2(x2/w, 1.0-y1/h), - new THREE.Vector2(x1/w, 1.0-y1/h) - ]; - - let toSkinVertices = (x1,y1,x2,y2) => toFaceVertices(x1, y1, x2, y2, 64.0, 64.0); - let toCapeVertices = (x1,y1,x2,y2) => toFaceVertices(x1, y1, x2, y2, 64.0, 32.0); - - let addVertices = (box,top,bottom,left,front,right,back) => { - box.faceVertexUvs[0] = []; - box.faceVertexUvs[0][0] = [right[3], right[0], right[2]]; - box.faceVertexUvs[0][1] = [right[0], right[1], right[2]]; - box.faceVertexUvs[0][2] = [left[3], left[0], left[2]]; - box.faceVertexUvs[0][3] = [left[0], left[1], left[2]]; - box.faceVertexUvs[0][4] = [top[3], top[0], top[2]]; - box.faceVertexUvs[0][5] = [top[0], top[1], top[2]]; - box.faceVertexUvs[0][6] = [bottom[0], bottom[3], bottom[1]]; - box.faceVertexUvs[0][7] = [bottom[3], bottom[2], bottom[1]]; - box.faceVertexUvs[0][8] = [front[3], front[0], front[2]]; - box.faceVertexUvs[0][9] = [front[0], front[1], front[2]]; - box.faceVertexUvs[0][10] = [back[3], back[0], back[2]]; - box.faceVertexUvs[0][11] = [back[0], back[1], back[2]]; - }; - - const esp = 0.002; - - this.SkinObject = class extends THREE.Group { - constructor(isSlim, layer1Material, layer2Material) { - super(); - - // Head - this.head = new THREE.Group(); - - let headBox = new THREE.BoxGeometry(8, 8, 8, 0, 0, 0); - addVertices(headBox, - toSkinVertices(8, 0, 16, 8), - toSkinVertices(16, 0, 24, 8), - toSkinVertices(0, 8, 8, 16), - toSkinVertices(8, 8, 16, 16), - toSkinVertices(16, 8, 24, 16), - toSkinVertices(24, 8, 32, 16) - ); - let headMesh = new THREE.Mesh(headBox, layer1Material); - this.head.add(headMesh); - - let head2Box = new THREE.BoxGeometry(9, 9, 9, 0, 0, 0); - addVertices(head2Box, - toSkinVertices(40, 0, 48, 8), - toSkinVertices(48, 0, 56, 8), - toSkinVertices(32, 8, 40, 16), - toSkinVertices(40, 8, 48, 16), - toSkinVertices(48, 8, 56, 16), - toSkinVertices(56, 8, 64, 16) - ); - let head2Mesh = new THREE.Mesh(head2Box, layer2Material); - head2Mesh.renderOrder = -1; - this.head.add(head2Mesh); - - this.add(this.head); - - - // Body - this.body = new THREE.Group(); - - let bodyBox = new THREE.BoxGeometry(8, 12, 4, 0, 0, 0); - addVertices(bodyBox, - toSkinVertices(20, 16, 28, 20), - toSkinVertices(28, 16, 36, 20), - toSkinVertices(16, 20, 20, 32), - toSkinVertices(20, 20, 28, 32), - toSkinVertices(28, 20, 32, 32), - toSkinVertices(32, 20, 40, 32) - ); - let bodyMesh = new THREE.Mesh(bodyBox, layer1Material); - this.body.add(bodyMesh); - - let body2Box = new THREE.BoxGeometry(9, 13.5, 4.5, 0, 0, 0); - addVertices(body2Box, - toSkinVertices(20, 32, 28, 36), - toSkinVertices(28, 32, 36, 36), - toSkinVertices(16, 36, 20, 48), - toSkinVertices(20, 36, 28, 48), - toSkinVertices(28, 36, 32, 48), - toSkinVertices(32, 36, 40, 48) - ); - let body2Mesh = new THREE.Mesh(body2Box, layer2Material); - this.body.add(body2Mesh); - - this.body.position.y = -10; - this.add(this.body); - - - // Right Arm - this.rightArm = new THREE.Group(); - let rightArmPivot = new THREE.Group(); - - let rightArmBox = new THREE.BoxGeometry((isSlim?3:4)-esp, 12-esp, 4-esp, 0, 0, 0); - if (isSlim) { - addVertices(rightArmBox, - toSkinVertices(44, 16, 47, 20), - toSkinVertices(47, 16, 50, 20), - toSkinVertices(40, 20, 44, 32), - toSkinVertices(44, 20, 47, 32), - toSkinVertices(47, 20, 51, 32), - toSkinVertices(51, 20, 54, 32) - ); - } else { - addVertices(rightArmBox, - toSkinVertices(44, 16, 48, 20), - toSkinVertices(48, 16, 52, 20), - toSkinVertices(40, 20, 44, 32), - toSkinVertices(44, 20, 48, 32), - toSkinVertices(48, 20, 52, 32), - toSkinVertices(52, 20, 56, 32) - ); - } - let rightArmMesh = new THREE.Mesh(rightArmBox, layer1Material); - rightArmPivot.add(rightArmMesh); - - let rightArm2Box = new THREE.BoxGeometry((isSlim?3.375:4.5)-esp, 13.5-esp, 4.5-esp, 0, 0, 0); - if (isSlim) { - addVertices(rightArm2Box, - toSkinVertices(44, 32, 47, 36), - toSkinVertices(47, 32, 50, 36), - toSkinVertices(40, 36, 44, 48), - toSkinVertices(44, 36, 47, 48), - toSkinVertices(47, 36, 51, 48), - toSkinVertices(51, 36, 54, 48) - ); - } else { - addVertices(rightArm2Box, - toSkinVertices(44, 32, 48, 36), - toSkinVertices(48, 32, 52, 36), - toSkinVertices(40, 36, 44, 48), - toSkinVertices(44, 36, 48, 48), - toSkinVertices(48, 36, 52, 48), - toSkinVertices(52, 36, 56, 48) - ); - } - let rightArm2Mesh = new THREE.Mesh(rightArm2Box, layer2Material); - rightArm2Mesh.renderOrder = 1; - rightArmPivot.add(rightArm2Mesh); - - rightArmPivot.position.y = -6; - this.rightArm.add(rightArmPivot); - this.rightArm.position.y = -4; - this.rightArm.position.x = isSlim?-5.5:-6; - this.add(this.rightArm); - - - // Left Arm - this.leftArm = new THREE.Group(); - let leftArmPivot = new THREE.Group(); - - let leftArmBox = new THREE.BoxGeometry((isSlim?3:4)-esp, 12-esp, 4-esp, 0, 0, 0); - if (isSlim) { - addVertices(leftArmBox, - toSkinVertices(36, 48, 39, 52), - toSkinVertices(39, 48, 42, 52), - toSkinVertices(32, 52, 36, 64), - toSkinVertices(36, 52, 39, 64), - toSkinVertices(39, 52, 43, 64), - toSkinVertices(43, 52, 46, 64) - ); - } else { - addVertices(leftArmBox, - toSkinVertices(36, 48, 40, 52), - toSkinVertices(40, 48, 44, 52), - toSkinVertices(32, 52, 36, 64), - toSkinVertices(36, 52, 40, 64), - toSkinVertices(40, 52, 44, 64), - toSkinVertices(44, 52, 48, 64) - ); - } - let leftArmMesh = new THREE.Mesh(leftArmBox, layer1Material); - leftArmPivot.add(leftArmMesh); - - let leftArm2Box = new THREE.BoxGeometry((isSlim?3.375:4.5)-esp, 13.5-esp, 4.5-esp, 0, 0, 0); - if (isSlim) { - addVertices(leftArm2Box, - toSkinVertices(52, 48, 55, 52), - toSkinVertices(55, 48, 58, 52), - toSkinVertices(48, 52, 52, 64), - toSkinVertices(52, 52, 55, 64), - toSkinVertices(55, 52, 59, 64), - toSkinVertices(59, 52, 62, 64) - ); - } else { - addVertices(leftArm2Box, - toSkinVertices(52, 48, 56, 52), - toSkinVertices(56, 48, 60, 52), - toSkinVertices(48, 52, 52, 64), - toSkinVertices(52, 52, 56, 64), - toSkinVertices(56, 52, 60, 64), - toSkinVertices(60, 52, 64, 64) - ); - } - let leftArm2Mesh = new THREE.Mesh(leftArm2Box, layer2Material); - leftArm2Mesh.renderOrder = 1; - leftArmPivot.add(leftArm2Mesh); - - leftArmPivot.position.y = -6; - this.leftArm.add(leftArmPivot); - this.leftArm.position.y = -4; - this.leftArm.position.x = isSlim?5.5:6; - this.add(this.leftArm); - - - // Right Leg - this.rightLeg = new THREE.Group(); - let rightLegPivot = new THREE.Group(); - - let rightLegBox = new THREE.BoxGeometry(4-esp, 12-esp, 4-esp, 0, 0, 0); - addVertices(rightLegBox, - toSkinVertices(4, 16, 8, 20), - toSkinVertices(8, 16, 12, 20), - toSkinVertices(0, 20, 4, 32), - toSkinVertices(4, 20, 8, 32), - toSkinVertices(8, 20, 12, 32), - toSkinVertices(12, 20, 16, 32) - ); - let rightLegMesh = new THREE.Mesh(rightLegBox, layer1Material); - rightLegPivot.add(rightLegMesh); - - let rightLeg2Box = new THREE.BoxGeometry(4.5-esp, 13.5-esp, 4.5-esp, 0, 0, 0); - addVertices(rightLeg2Box, - toSkinVertices(4, 32, 8, 36), - toSkinVertices(8, 32, 12, 36), - toSkinVertices(0, 36, 4, 48), - toSkinVertices(4, 36, 8, 48), - toSkinVertices(8, 36, 12, 48), - toSkinVertices(12, 36, 16, 48) - ); - let rightLeg2Mesh = new THREE.Mesh(rightLeg2Box, layer2Material); - rightLeg2Mesh.renderOrder = 1; - rightLegPivot.add(rightLeg2Mesh); - - rightLegPivot.position.y = -6; - this.rightLeg.add(rightLegPivot); - this.rightLeg.position.y = -16; - this.rightLeg.position.x = -2; - this.add(this.rightLeg); - - // Left Leg - this.leftLeg = new THREE.Group(); - let leftLegPivot = new THREE.Group(); - - let leftLegBox = new THREE.BoxGeometry(4-esp, 12-esp, 4-esp, 0, 0, 0); - addVertices(leftLegBox, - toSkinVertices(20, 48, 24, 52), - toSkinVertices(24, 48, 28, 52), - toSkinVertices(16, 52, 20, 64), - toSkinVertices(20, 52, 24, 64), - toSkinVertices(24, 52, 28, 64), - toSkinVertices(28, 52, 32, 64) - ); - let leftLegMesh = new THREE.Mesh(leftLegBox, layer1Material); - leftLegPivot.add(leftLegMesh); - - let leftLeg2Box = new THREE.BoxGeometry(4.5-esp, 13.5-esp, 4.5-esp, 0, 0, 0); - addVertices(leftLeg2Box, - toSkinVertices(4, 48, 8, 52), - toSkinVertices(8, 48, 12, 52), - toSkinVertices(0, 52, 4, 64), - toSkinVertices(4, 52, 8, 64), - toSkinVertices(8, 52, 12, 64), - toSkinVertices(12, 52, 16, 64) - ); - let leftLeg2Mesh = new THREE.Mesh(leftLeg2Box, layer2Material); - leftLeg2Mesh.renderOrder = 1; - leftLegPivot.add(leftLeg2Mesh); - - leftLegPivot.position.y = -6; - this.leftLeg.add(leftLegPivot); - this.leftLeg.position.y = -16; - this.leftLeg.position.x = 2; - this.add(this.leftLeg); - } - }; - - this.CapeObject = class extends THREE.Group { - constructor(capeMaterial) { - super(); - - // back = outside - // front = inside - let capeBox = new THREE.BoxGeometry(10, 16, 1, 0, 0, 0); - addVertices(capeBox, - toCapeVertices(1, 0, 11, 1), - toCapeVertices(11, 0, 21, 1), - toCapeVertices(11, 1, 12, 17), - toCapeVertices(12, 1, 22, 17), - toCapeVertices(0, 1, 1, 17), - toCapeVertices(1, 1, 11, 17) - ); - this.cape = new THREE.Mesh(capeBox, capeMaterial); - this.cape.position.y = -8; - this.cape.position.z = -0.5; - this.add(this.cape); - } - }; - - this.PlayerObject = class extends THREE.Group { - constructor(slim, layer1Material, layer2Material, capeMaterial){ - super(); - - this.slim = slim; - - this.skin = new skinview3d.SkinObject(slim, layer1Material, layer2Material); - this.skin.visible = false; - this.add(this.skin); - - this.cape = new skinview3d.CapeObject(capeMaterial); - this.cape.position.z = -2; - this.cape.position.y = -4; - this.cape.rotation.x = 25*Math.PI/180; - this.cape.visible = false; - this.add(this.cape); - } - }; - - this.WalkAnimation = (player,time) => { - let skin = player.skin; - let angleRot = time + Math.PI/2; - - // Leg Swing - skin.leftLeg.rotation.x = Math.cos(angleRot); - skin.rightLeg.rotation.x = Math.cos(angleRot + (Math.PI)); - - // Arm Swing - skin.leftArm.rotation.x = Math.cos(angleRot + (Math.PI)); - skin.rightArm.rotation.x = Math.cos(angleRot); - }; - - this.SkinViewer = class { - constructor(options) { - this.domElement = options.domElement; - this.animation = options.animation || null; - this.animationPaused = false; - this.animationSpeed = 3; - this.animationTime = 0; - this.disposed = false; - - // texture - this.skinImg = new Image(); - this.skinCanvas = document.createElement("canvas"); - this.skinTexture = new THREE.Texture(this.skinCanvas); - this.skinTexture.magFilter = THREE.NearestFilter; - this.skinTexture.minFilter = THREE.NearestMipMapNearestFilter; - - this.capeImg = new Image(); - this.capeCanvas = document.createElement("canvas"); - this.capeTexture = new THREE.Texture(this.capeCanvas); - this.capeTexture.magFilter = THREE.NearestFilter; - this.capeTexture.minFilter = THREE.NearestMipMapNearestFilter; - - this.layer1Material = new THREE.MeshBasicMaterial({map: this.skinTexture, side: THREE.FrontSide}); - this.layer2Material = new THREE.MeshBasicMaterial({map: this.skinTexture, transparent: true, opacity: 1, side: THREE.DoubleSide}); - this.capeMaterial = new THREE.MeshBasicMaterial({map: this.capeTexture}); - - // scene - this.scene = new THREE.Scene(); - - this.camera = new THREE.PerspectiveCamera(75); - this.camera.position.y = -12; - this.camera.position.z = 30; - - this.renderer = new THREE.WebGLRenderer({angleRot: true, alpha: true, antialias: false}); - this.renderer.setSize(300, 300); // default size - this.renderer.context.getShaderInfoLog = () => ""; // shut firefox up - this.domElement.appendChild(this.renderer.domElement); - - this.playerObject = new skinview3d.PlayerObject(options.slim === true, this.layer1Material, this.layer2Material, this.capeMaterial); - this.scene.add(this.playerObject); - - // texture loading - this.skinImg.crossOrigin = ""; - this.skinImg.onerror = () => console.log("Failed loading " + this.skinImg.src); - this.skinImg.onload = () => { - let isOldFormat = false; - if (this.skinImg.width !== this.skinImg.height) { - if (this.skinImg.width === 2*this.skinImg.height) { - isOldFormat = true; - } else { - console.log("Bad skin size"); - return; - } - } - - let skinContext = this.skinCanvas.getContext("2d"); - if(isOldFormat){ - let width = this.skinImg.width; - this.skinCanvas.width = width; - this.skinCanvas.height = width; - skinContext.clearRect(0, 0, width, width); - skinContext.drawImage(this.skinImg, 0, 0, width, width/2.0); - convertSkinTo1_8(skinContext, width); - } else { - this.skinCanvas.width = this.skinImg.width; - this.skinCanvas.height = this.skinImg.height; - skinContext.clearRect(0, 0, this.skinCanvas.width, this.skinCanvas.height); - skinContext.drawImage(this.skinImg, 0, 0, this.skinCanvas.width, this.skinCanvas.height); - } - - this.skinTexture.needsUpdate = true; - this.layer1Material.needsUpdate = true; - this.layer2Material.needsUpdate = true; - - this.playerObject.skin.visible = true; - }; - - this.capeImg.crossOrigin = ""; - this.capeImg.onerror = () => console.log("Failed loading " + this.capeImg.src); - this.capeImg.onload = () => { - if (this.capeImg.width !== 2*this.capeImg.height) { - console.log("Bad cape size"); - return; - } - - this.capeCanvas.width = this.capeImg.width; - this.capeCanvas.height = this.capeImg.height; - let capeContext = this.capeCanvas.getContext("2d"); - capeContext.clearRect(0, 0, this.capeCanvas.width, this.capeCanvas.height); - capeContext.drawImage(this.capeImg, 0, 0, this.capeCanvas.width, this.capeCanvas.height); - - this.capeTexture.needsUpdate = true; - this.capeMaterial.needsUpdate = true; - - this.playerObject.cape.visible = true; - }; - - if(options.skinUrl) this.skinUrl = options.skinUrl; - if(options.capeUrl) this.capeUrl = options.capeUrl; - if(options.width) this.width = options.width; - if(options.height) this.height = options.height; - - let draw = () => { - if(this.disposed) return; - window.requestAnimationFrame(draw); - if(!this.animationPaused){ - this.animationTime++; - if(this.animation){ - this.animation(this.playerObject,this.animationTime/100*this.animationSpeed); - } - } - this.renderer.render(this.scene, this.camera); - }; - draw(); - } - - setSize(width, height){ - this.camera.aspect = width / height; - this.camera.updateProjectionMatrix(); - this.renderer.setSize(width, height); - } - - dispose() { - this.disposed = true; - this.domElement.removeChild(this.renderer.domElement); - this.renderer.dispose(); - this.skinTexture.dispose(); - this.capeTexture.dispose(); - } - - get skinUrl(){ - return this.skinImg.src; - } - - set skinUrl(url){ - this.skinImg.src = url; - } - - get capeUrl(){ - return this.capeImg.src; - } - - set capeUrl(url){ - this.capeImg.src = url; - } - - get width(){ - return this.renderer.getSize().width; - } - - set width(newWidth){ - this.setSize(newWidth, this.height); - } - - get height(){ - return this.renderer.getSize().height; - } - - set height(newHeight){ - this.setSize(this.width, newHeight); - } - }; - - // ====== OrbitControls ====== - // The code was originally from https://github.com/mrdoob/three.js/blob/d45a042cf962e9b1aa9441810ba118647b48aacb/examples/js/controls/OrbitControls.js - /** - * The MIT License - * - * Copyright (C) 2010-2017 three.js authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - this.OrbitControls = function (object, domElement) { - /** - * @author qiao / https://github.com/qiao - * @author mrdoob / http://mrdoob.com - * @author alteredq / http://alteredqualia.com/ - * @author WestLangley / http://github.com/WestLangley - * @author erich666 / http://erichaines.com - */ - // This set of controls performs orbiting, dollying (zooming), and panning. - // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). - // - // Orbit - left mouse / touch: one finger move - // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish - // Pan - right mouse, or arrow keys / touch: three finger swipe - - this.object = object; - - this.domElement = (domElement !== undefined) ? domElement : document; - - // Set to false to disable this control - this.enabled = true; - - // "target" sets the location of focus, where the object orbits around - this.target = new THREE.Vector3(); - - // How far you can dolly in and out (PerspectiveCamera only) - this.minDistance = 0; - this.maxDistance = Infinity; - - // How far you can zoom in and out (OrthographicCamera only) - this.minZoom = 0; - this.maxZoom = Infinity; - - // How far you can orbit vertically, upper and lower limits. - // Range is 0 to Math.PI radians. - this.minPolarAngle = 0; // radians - this.maxPolarAngle = Math.PI; // radians - - // How far you can orbit horizontally, upper and lower limits. - // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. - this.minAzimuthAngle = - Infinity; // radians - this.maxAzimuthAngle = Infinity; // radians - - // Set to true to enable damping (inertia) - // If damping is enabled, you must call controls.update() in your animation loop - this.enableDamping = false; - this.dampingFactor = 0.25; - - // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. - // Set to false to disable zooming - this.enableZoom = true; - this.zoomSpeed = 1.0; - - // Set to false to disable rotating - this.enableRotate = true; - this.rotateSpeed = 1.0; - - // Set to false to disable panning - this.enablePan = true; - this.keyPanSpeed = 7.0; // pixels moved per arrow key push - - // Set to true to automatically rotate around the target - // If auto-rotate is enabled, you must call controls.update() in your animation loop - this.autoRotate = false; - this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 - - // Set to false to disable use of the keys - this.enableKeys = true; - - // The four arrow keys - this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; - - // Mouse buttons - this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; - - // for reset - this.target0 = this.target.clone(); - this.position0 = this.object.position.clone(); - this.zoom0 = this.object.zoom; - - // - // public methods - // - - this.getPolarAngle = function () { - return spherical.phi; - }; - - this.getAzimuthalAngle = function () { - return spherical.theta; - }; - - this.saveState = function () { - scope.target0.copy(scope.target); - scope.position0.copy(scope.object.position); - scope.zoom0 = scope.object.zoom; - }; - - this.reset = function () { - scope.target.copy(scope.target0); - scope.object.position.copy(scope.position0); - scope.object.zoom = scope.zoom0; - - scope.object.updateProjectionMatrix(); - scope.dispatchEvent(changeEvent); - - scope.update(); - - state = STATE.NONE; - }; - - // this method is exposed, but perhaps it would be better if we can make it private... - this.update = function () { - let offset = new THREE.Vector3(); - - // so camera.up is the orbit axis - let quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0)); - let quatInverse = quat.clone().inverse(); - - let lastPosition = new THREE.Vector3(); - let lastQuaternion = new THREE.Quaternion(); - - return function update() { - - let position = scope.object.position; - - offset.copy(position).sub(scope.target); - - // rotate offset to "y-axis-is-up" space - offset.applyQuaternion(quat); - - // angle from z-axis around y-axis - spherical.setFromVector3(offset); - - if (scope.autoRotate && state === STATE.NONE) { - - rotateLeft(getAutoRotationAngle()); - - } - - spherical.theta += sphericalDelta.theta; - spherical.phi += sphericalDelta.phi; - - // restrict theta to be between desired limits - spherical.theta = Math.max(scope.minAzimuthAngle, Math.min(scope.maxAzimuthAngle, spherical.theta)); - - // restrict phi to be between desired limits - spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); - - spherical.makeSafe(); - - spherical.radius *= scale; - - // restrict radius to be between desired limits - spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)); - - // move target to panned location - scope.target.add(panOffset); - - offset.setFromSpherical(spherical); - - // rotate offset back to "camera-up-vector-is-up" space - offset.applyQuaternion(quatInverse); - - position.copy(scope.target).add(offset); - - scope.object.lookAt(scope.target); - - if (scope.enableDamping === true) { - - sphericalDelta.theta *= (1 - scope.dampingFactor); - sphericalDelta.phi *= (1 - scope.dampingFactor); - - } else { - - sphericalDelta.set(0, 0, 0); - - } - - scale = 1; - panOffset.set(0, 0, 0); - - // update condition is: - // min(camera displacement, camera rotation in radians)^2 > EPS - // using small-angle approximation cos(x/2) = 1 - x^2 / 8 - - if (zoomChanged || - lastPosition.distanceToSquared(scope.object.position) > EPS || - 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { - - scope.dispatchEvent(changeEvent); - - lastPosition.copy(scope.object.position); - lastQuaternion.copy(scope.object.quaternion); - zoomChanged = false; - - return true; - } - return false; - }; - }(); - - this.dispose = function () { - scope.domElement.removeEventListener("contextmenu", onContextMenu, false); - scope.domElement.removeEventListener("mousedown", onMouseDown, false); - scope.domElement.removeEventListener("wheel", onMouseWheel, false); - - scope.domElement.removeEventListener("touchstart", onTouchStart, false); - scope.domElement.removeEventListener("touchend", onTouchEnd, false); - scope.domElement.removeEventListener("touchmove", onTouchMove, false); - - document.removeEventListener("mousemove", onMouseMove, false); - document.removeEventListener("mouseup", onMouseUp, false); - - window.removeEventListener("keydown", onKeyDown, false); - - //scope.dispatchEvent({ type: "dispose" }); // should this be added here? - }; - - // - // internals - // - - let scope = this; - - let changeEvent = { type: "change" }; - let startEvent = { type: "start" }; - let endEvent = { type: "end" }; - - let STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 }; - - let state = STATE.NONE; - - let EPS = 0.000001; - - // current position in spherical coordinates - let spherical = new THREE.Spherical(); - let sphericalDelta = new THREE.Spherical(); - - let scale = 1; - let panOffset = new THREE.Vector3(); - let zoomChanged = false; - - let rotateStart = new THREE.Vector2(); - let rotateEnd = new THREE.Vector2(); - let rotateDelta = new THREE.Vector2(); - - let panStart = new THREE.Vector2(); - let panEnd = new THREE.Vector2(); - let panDelta = new THREE.Vector2(); - - let dollyStart = new THREE.Vector2(); - let dollyEnd = new THREE.Vector2(); - let dollyDelta = new THREE.Vector2(); - - function getAutoRotationAngle() { - return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; - } - - function getZoomScale() { - return Math.pow(0.95, scope.zoomSpeed); - } - - function rotateLeft(angle) { - sphericalDelta.theta -= angle; - } - - function rotateUp(angle) { - sphericalDelta.phi -= angle; - } - - let panLeft = function () { - let v = new THREE.Vector3(); - return function panLeft(distance, objectMatrix) { - v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix - v.multiplyScalar(- distance); - - panOffset.add(v); - }; - }(); - - let panUp = function () { - let v = new THREE.Vector3(); - return function panUp(distance, objectMatrix) { - v.setFromMatrixColumn(objectMatrix, 1); // get Y column of objectMatrix - v.multiplyScalar(distance); - - panOffset.add(v); - }; - }(); - - // deltaX and deltaY are in pixels; right and down are positive - let pan = function () { - let offset = new THREE.Vector3(); - return function pan(deltaX, deltaY) { - let element = scope.domElement === document ? scope.domElement.body : scope.domElement; - if (scope.object instanceof THREE.PerspectiveCamera) { - // perspective - let position = scope.object.position; - offset.copy(position).sub(scope.target); - let targetDistance = offset.length(); - - // half of the fov is center to top of screen - targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0); - - // we actually don't use screenWidth, since perspective camera is fixed to screen height - panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); - panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); - } else if (scope.object instanceof THREE.OrthographicCamera) { - // orthographic - panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix); - panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix); - } else { - // camera neither orthographic nor perspective - console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."); - scope.enablePan = false; - } - }; - }(); - - function dollyIn(dollyScale) { - if (scope.object instanceof THREE.PerspectiveCamera) { - scale /= dollyScale; - } else if (scope.object instanceof THREE.OrthographicCamera) { - scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - } else { - console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); - scope.enableZoom = false; - } - } - - function dollyOut(dollyScale) { - if (scope.object instanceof THREE.PerspectiveCamera) { - scale *= dollyScale; - } else if (scope.object instanceof THREE.OrthographicCamera) { - scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - } else { - console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); - scope.enableZoom = false; - } - } - - // - // event callbacks - update the object state - // - - function handleMouseDownRotate(event) { - rotateStart.set(event.clientX, event.clientY); - - } - - function handleMouseDownDolly(event) { - dollyStart.set(event.clientX, event.clientY); - - } - - function handleMouseDownPan(event) { - panStart.set(event.clientX, event.clientY); - - } - - function handleMouseMoveRotate(event) { - rotateEnd.set(event.clientX, event.clientY); - rotateDelta.subVectors(rotateEnd, rotateStart); - let element = scope.domElement === document ? scope.domElement.body : scope.domElement; - // rotating across whole screen goes 360 degrees around - rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); - // rotating up and down along whole screen attempts to go 360, but limited to 180 - rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); - rotateStart.copy(rotateEnd); - scope.update(); - } - - function handleMouseMoveDolly(event) { - dollyEnd.set(event.clientX, event.clientY); - dollyDelta.subVectors(dollyEnd, dollyStart); - if (dollyDelta.y > 0) { - dollyIn(getZoomScale()); - } else if (dollyDelta.y < 0) { - dollyOut(getZoomScale()); - } - dollyStart.copy(dollyEnd); - scope.update(); - } - - function handleMouseMovePan(event) { - panEnd.set(event.clientX, event.clientY); - panDelta.subVectors(panEnd, panStart); - pan(panDelta.x, panDelta.y); - panStart.copy(panEnd); - scope.update(); - } - - function handleMouseUp(event) { - } - - function handleMouseWheel(event) { - if (event.deltaY < 0) { - dollyOut(getZoomScale()); - } else if (event.deltaY > 0) { - dollyIn(getZoomScale()); - } - scope.update(); - } - - function handleKeyDown(event) { - switch (event.keyCode) { - case scope.keys.UP: - pan(0, scope.keyPanSpeed); - scope.update(); - break; - - case scope.keys.BOTTOM: - pan(0, - scope.keyPanSpeed); - scope.update(); - break; - - case scope.keys.LEFT: - pan(scope.keyPanSpeed, 0); - scope.update(); - break; - - case scope.keys.RIGHT: - pan(- scope.keyPanSpeed, 0); - scope.update(); - break; - } - - } - - function handleTouchStartRotate(event) { - rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); - - } - - function handleTouchStartDolly(event) { - let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; - let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; - let distance = Math.sqrt(dx * dx + dy * dy); - dollyStart.set(0, distance); - } - - function handleTouchStartPan(event) { - panStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); - } - - function handleTouchMoveRotate(event) { - rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); - rotateDelta.subVectors(rotateEnd, rotateStart); - let element = scope.domElement === document ? scope.domElement.body : scope.domElement; - rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); - rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); - rotateStart.copy(rotateEnd); - scope.update(); - - } - - function handleTouchMoveDolly(event) { - let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; - let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; - let distance = Math.sqrt(dx * dx + dy * dy); - dollyEnd.set(0, distance); - dollyDelta.subVectors(dollyEnd, dollyStart); - if (dollyDelta.y > 0) { - dollyOut(getZoomScale()); - } else if (dollyDelta.y < 0) { - dollyIn(getZoomScale()); - } - dollyStart.copy(dollyEnd); - scope.update(); - - } - - function handleTouchMovePan(event) { - panEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); - panDelta.subVectors(panEnd, panStart); - pan(panDelta.x, panDelta.y); - panStart.copy(panEnd); - scope.update(); - } - - function handleTouchEnd(event) { - } - - // - // event handlers - FSM: listen for events and reset state - // - - function onMouseDown(event) { - if (scope.enabled === false) return; - switch (event.button) { - case scope.mouseButtons.ORBIT: - if (scope.enableRotate === false) return; - handleMouseDownRotate(event); - state = STATE.ROTATE; - break; - - case scope.mouseButtons.ZOOM: - if (scope.enableZoom === false) return; - handleMouseDownDolly(event); - state = STATE.DOLLY; - break; - - case scope.mouseButtons.PAN: - if (scope.enablePan === false) return; - handleMouseDownPan(event); - state = STATE.PAN; - break; - } - - event.preventDefault(); - if (state !== STATE.NONE) { - document.addEventListener("mousemove", onMouseMove, false); - document.addEventListener("mouseup", onMouseUp, false); - scope.dispatchEvent(startEvent); - } - } - - function onMouseMove(event) { - if (scope.enabled === false) return; - switch (state) { - case STATE.ROTATE: - if (scope.enableRotate === false) return; - handleMouseMoveRotate(event); - break; - - case STATE.DOLLY: - if (scope.enableZoom === false) return; - handleMouseMoveDolly(event); - break; - - case STATE.PAN: - if (scope.enablePan === false) return; - handleMouseMovePan(event); - break; - } - event.preventDefault(); - } - - function onMouseUp(event) { - if (scope.enabled === false) return; - handleMouseUp(event); - document.removeEventListener("mousemove", onMouseMove, false); - document.removeEventListener("mouseup", onMouseUp, false); - scope.dispatchEvent(endEvent); - state = STATE.NONE; - } - - function onMouseWheel(event) { - if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) return; - event.preventDefault(); - event.stopPropagation(); - handleMouseWheel(event); - scope.dispatchEvent(startEvent); // not sure why these are here... - scope.dispatchEvent(endEvent); - } - - function onKeyDown(event) { - if (scope.enabled === false || scope.enableKeys === false || scope.enablePan === false) return; - handleKeyDown(event); - } - - function onTouchStart(event) { - if (scope.enabled === false) return; - switch (event.touches.length) { - case 1: // one-fingered touch: rotate - if (scope.enableRotate === false) return; - handleTouchStartRotate(event); - state = STATE.TOUCH_ROTATE; - break; - - case 2: // two-fingered touch: dolly - if (scope.enableZoom === false) return; - handleTouchStartDolly(event); - state = STATE.TOUCH_DOLLY; - break; - - case 3: // three-fingered touch: pan - if (scope.enablePan === false) return; - handleTouchStartPan(event); - state = STATE.TOUCH_PAN; - break; - - default: - state = STATE.NONE; - } - - if (state !== STATE.NONE) { - scope.dispatchEvent(startEvent); - } - - } - - function onTouchMove(event) { - if (scope.enabled === false) return; - switch (event.touches.length) { - case 1: // one-fingered touch: rotate - if (scope.enableRotate === false) return; - if (state !== STATE.TOUCH_ROTATE) return; // is this needed?... - handleTouchMoveRotate(event); - break; - - case 2: // two-fingered touch: dolly - if (scope.enableZoom === false) return; - if (state !== STATE.TOUCH_DOLLY) return; // is this needed?... - handleTouchMoveDolly(event); - break; - - case 3: // three-fingered touch: pan - if (scope.enablePan === false) return; - if (state !== STATE.TOUCH_PAN) return; // is this needed?... - handleTouchMovePan(event); - break; - - default: - state = STATE.NONE; - } - event.preventDefault(); - event.stopPropagation(); - } - - function onTouchEnd(event) { - if (scope.enabled === false) return; - handleTouchEnd(event); - scope.dispatchEvent(endEvent); - state = STATE.NONE; - } - - function onContextMenu(event) { - if (scope.enabled === false || scope.enablePan === false) return; - event.preventDefault(); - } - - // - - scope.domElement.addEventListener("contextmenu", onContextMenu, false); - - scope.domElement.addEventListener("mousedown", onMouseDown, false); - scope.domElement.addEventListener("wheel", onMouseWheel, false); - - scope.domElement.addEventListener("touchstart", onTouchStart, false); - scope.domElement.addEventListener("touchend", onTouchEnd, false); - scope.domElement.addEventListener("touchmove", onTouchMove, false); - - window.addEventListener("keydown", onKeyDown, false); - - // force an update at start - - this.update(); - }; - - this.OrbitControls.prototype = Object.create(THREE.EventDispatcher.prototype); - this.OrbitControls.prototype.constructor = this.OrbitControls; - // ============ - - this.SkinControl = class { - constructor(skinViewer) { - this.enableAnimationControl = true; - this.skinViewer = skinViewer; - - this.orbitControls = new skinview3d.OrbitControls(skinViewer.camera, skinViewer.renderer.domElement); - this.orbitControls.enablePan = false; - this.orbitControls.target = new THREE.Vector3(0, -12 ,0); - this.orbitControls.minDistance = 10; - this.orbitControls.maxDistance = 256; - this.orbitControls.update(); - - this.animationPauseListener = e => { - if(this.enableAnimationControl) { - e.preventDefault(); - this.skinViewer.animationPaused = !this.skinViewer.animationPaused; - } - }; - this.skinViewer.domElement.addEventListener("contextmenu", this.animationPauseListener, false); - } - - dispose(){ - this.skinViewer.domElement.removeEventListener("contextmenu", this.animationPauseListener, false); - this.orbitControls.dispose(); - } - }; - -}(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..39c2680 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "skinview3d", + "version": "1.0.0", + "description": "Three.js powered Minecraft skin viewer", + "main": "src/skinview3d.js", + "scripts": { + "build": "./node_modules/.bin/rollup -c rollup.config.default.js", + "build-babel": "./node_modules/.bin/rollup -c rollup.config.babel.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/to2mbn/skinview3d.git" + }, + "author": "yushijinhun", + "license": "GPL-3.0", + "bugs": { + "url": "https://github.com/to2mbn/skinview3d/issues" + }, + "homepage": "https://github.com/to2mbn/skinview3d", + "dependencies": { + "three": "^0.87.1" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-plugin-external-helpers": "^6.22.0", + "babel-preset-es2015": "^6.24.1", + "rollup": "^0.50.0", + "rollup-plugin-babel": "^3.0.2" + } +} diff --git a/rollup.config.babel.js b/rollup.config.babel.js new file mode 100644 index 0000000..17907b9 --- /dev/null +++ b/rollup.config.babel.js @@ -0,0 +1,27 @@ +import babel from 'rollup-plugin-babel'; + +export default { + input: 'src/skinview3d.js', + indent: '\t', + sourcemap: true, + external: ['three'], + globals: { + three: 'THREE' + }, + output: [ + { + format: 'umd', + name: 'skinview3d', + file: 'build/skinview3d.babel.js' + }, + { + format: 'es', + file: 'build/skinview3d.babel.module.js' + } + ], + plugins: [ + babel({ + exclude: 'node_modules/**', + }), + ] +}; diff --git a/rollup.config.default.js b/rollup.config.default.js new file mode 100644 index 0000000..d7f01d0 --- /dev/null +++ b/rollup.config.default.js @@ -0,0 +1,20 @@ +export default { + input: 'src/skinview3d.js', + indent: '\t', + sourcemap: true, + external: ['three'], + globals: { + three: 'THREE' + }, + output: [ + { + format: 'umd', + name: 'skinview3d', + file: 'build/skinview3d.js' + }, + { + format: 'es', + file: 'build/skinview3d.module.js' + } + ], +}; diff --git a/src/animation.js b/src/animation.js new file mode 100644 index 0000000..4677964 --- /dev/null +++ b/src/animation.js @@ -0,0 +1,16 @@ +import { SkinObject, CapeObject, PlayerObject } from "./skinview3d"; + +let WalkAnimation = (player, time) => { + let skin = player.skin; + let angleRot = time + Math.PI / 2; + + // Leg Swing + skin.leftLeg.rotation.x = Math.cos(angleRot); + skin.rightLeg.rotation.x = Math.cos(angleRot + (Math.PI)); + + // Arm Swing + skin.leftArm.rotation.x = Math.cos(angleRot + (Math.PI)); + skin.rightArm.rotation.x = Math.cos(angleRot); +}; + +export { WalkAnimation }; diff --git a/src/model.js b/src/model.js new file mode 100644 index 0000000..b985581 --- /dev/null +++ b/src/model.js @@ -0,0 +1,333 @@ +import THREE from "three"; + +function toFaceVertices(x1, y1, x2, y2, w, h) { + return [ + new THREE.Vector2(x1 / w, 1.0 - y2 / h), + new THREE.Vector2(x2 / w, 1.0 - y2 / h), + new THREE.Vector2(x2 / w, 1.0 - y1 / h), + new THREE.Vector2(x1 / w, 1.0 - y1 / h) + ]; +} + +function toSkinVertices(x1, y1, x2, y2) { + return toFaceVertices(x1, y1, x2, y2, 64.0, 64.0); +} + +function toCapeVertices(x1, y1, x2, y2) { + return toFaceVertices(x1, y1, x2, y2, 64.0, 32.0); +} + +function addVertices(box, top, bottom, left, front, right, back) { + box.faceVertexUvs[0] = []; + box.faceVertexUvs[0][0] = [right[3], right[0], right[2]]; + box.faceVertexUvs[0][1] = [right[0], right[1], right[2]]; + box.faceVertexUvs[0][2] = [left[3], left[0], left[2]]; + box.faceVertexUvs[0][3] = [left[0], left[1], left[2]]; + box.faceVertexUvs[0][4] = [top[3], top[0], top[2]]; + box.faceVertexUvs[0][5] = [top[0], top[1], top[2]]; + box.faceVertexUvs[0][6] = [bottom[0], bottom[3], bottom[1]]; + box.faceVertexUvs[0][7] = [bottom[3], bottom[2], bottom[1]]; + box.faceVertexUvs[0][8] = [front[3], front[0], front[2]]; + box.faceVertexUvs[0][9] = [front[0], front[1], front[2]]; + box.faceVertexUvs[0][10] = [back[3], back[0], back[2]]; + box.faceVertexUvs[0][11] = [back[0], back[1], back[2]]; +} + +const esp = 0.002; + +class SkinObject extends THREE.Group { + constructor(isSlim, layer1Material, layer2Material) { + super(); + + // Head + this.head = new THREE.Group(); + + let headBox = new THREE.BoxGeometry(8, 8, 8, 0, 0, 0); + addVertices(headBox, + toSkinVertices(8, 0, 16, 8), + toSkinVertices(16, 0, 24, 8), + toSkinVertices(0, 8, 8, 16), + toSkinVertices(8, 8, 16, 16), + toSkinVertices(16, 8, 24, 16), + toSkinVertices(24, 8, 32, 16) + ); + let headMesh = new THREE.Mesh(headBox, layer1Material); + this.head.add(headMesh); + + let head2Box = new THREE.BoxGeometry(9, 9, 9, 0, 0, 0); + addVertices(head2Box, + toSkinVertices(40, 0, 48, 8), + toSkinVertices(48, 0, 56, 8), + toSkinVertices(32, 8, 40, 16), + toSkinVertices(40, 8, 48, 16), + toSkinVertices(48, 8, 56, 16), + toSkinVertices(56, 8, 64, 16) + ); + let head2Mesh = new THREE.Mesh(head2Box, layer2Material); + head2Mesh.renderOrder = -1; + this.head.add(head2Mesh); + + this.add(this.head); + + + // Body + this.body = new THREE.Group(); + + let bodyBox = new THREE.BoxGeometry(8, 12, 4, 0, 0, 0); + addVertices(bodyBox, + toSkinVertices(20, 16, 28, 20), + toSkinVertices(28, 16, 36, 20), + toSkinVertices(16, 20, 20, 32), + toSkinVertices(20, 20, 28, 32), + toSkinVertices(28, 20, 32, 32), + toSkinVertices(32, 20, 40, 32) + ); + let bodyMesh = new THREE.Mesh(bodyBox, layer1Material); + this.body.add(bodyMesh); + + let body2Box = new THREE.BoxGeometry(9, 13.5, 4.5, 0, 0, 0); + addVertices(body2Box, + toSkinVertices(20, 32, 28, 36), + toSkinVertices(28, 32, 36, 36), + toSkinVertices(16, 36, 20, 48), + toSkinVertices(20, 36, 28, 48), + toSkinVertices(28, 36, 32, 48), + toSkinVertices(32, 36, 40, 48) + ); + let body2Mesh = new THREE.Mesh(body2Box, layer2Material); + this.body.add(body2Mesh); + + this.body.position.y = -10; + this.add(this.body); + + + // Right Arm + this.rightArm = new THREE.Group(); + let rightArmPivot = new THREE.Group(); + + let rightArmBox = new THREE.BoxGeometry((isSlim ? 3 : 4) - esp, 12 - esp, 4 - esp, 0, 0, 0); + if (isSlim) { + addVertices(rightArmBox, + toSkinVertices(44, 16, 47, 20), + toSkinVertices(47, 16, 50, 20), + toSkinVertices(40, 20, 44, 32), + toSkinVertices(44, 20, 47, 32), + toSkinVertices(47, 20, 51, 32), + toSkinVertices(51, 20, 54, 32) + ); + } else { + addVertices(rightArmBox, + toSkinVertices(44, 16, 48, 20), + toSkinVertices(48, 16, 52, 20), + toSkinVertices(40, 20, 44, 32), + toSkinVertices(44, 20, 48, 32), + toSkinVertices(48, 20, 52, 32), + toSkinVertices(52, 20, 56, 32) + ); + } + let rightArmMesh = new THREE.Mesh(rightArmBox, layer1Material); + rightArmPivot.add(rightArmMesh); + + let rightArm2Box = new THREE.BoxGeometry((isSlim ? 3.375 : 4.5) - esp, 13.5 - esp, 4.5 - esp, 0, 0, 0); + if (isSlim) { + addVertices(rightArm2Box, + toSkinVertices(44, 32, 47, 36), + toSkinVertices(47, 32, 50, 36), + toSkinVertices(40, 36, 44, 48), + toSkinVertices(44, 36, 47, 48), + toSkinVertices(47, 36, 51, 48), + toSkinVertices(51, 36, 54, 48) + ); + } else { + addVertices(rightArm2Box, + toSkinVertices(44, 32, 48, 36), + toSkinVertices(48, 32, 52, 36), + toSkinVertices(40, 36, 44, 48), + toSkinVertices(44, 36, 48, 48), + toSkinVertices(48, 36, 52, 48), + toSkinVertices(52, 36, 56, 48) + ); + } + let rightArm2Mesh = new THREE.Mesh(rightArm2Box, layer2Material); + rightArm2Mesh.renderOrder = 1; + rightArmPivot.add(rightArm2Mesh); + + rightArmPivot.position.y = -6; + this.rightArm.add(rightArmPivot); + this.rightArm.position.y = -4; + this.rightArm.position.x = isSlim ? -5.5 : -6; + this.add(this.rightArm); + + + // Left Arm + this.leftArm = new THREE.Group(); + let leftArmPivot = new THREE.Group(); + + let leftArmBox = new THREE.BoxGeometry((isSlim ? 3 : 4) - esp, 12 - esp, 4 - esp, 0, 0, 0); + if (isSlim) { + addVertices(leftArmBox, + toSkinVertices(36, 48, 39, 52), + toSkinVertices(39, 48, 42, 52), + toSkinVertices(32, 52, 36, 64), + toSkinVertices(36, 52, 39, 64), + toSkinVertices(39, 52, 43, 64), + toSkinVertices(43, 52, 46, 64) + ); + } else { + addVertices(leftArmBox, + toSkinVertices(36, 48, 40, 52), + toSkinVertices(40, 48, 44, 52), + toSkinVertices(32, 52, 36, 64), + toSkinVertices(36, 52, 40, 64), + toSkinVertices(40, 52, 44, 64), + toSkinVertices(44, 52, 48, 64) + ); + } + let leftArmMesh = new THREE.Mesh(leftArmBox, layer1Material); + leftArmPivot.add(leftArmMesh); + + let leftArm2Box = new THREE.BoxGeometry((isSlim ? 3.375 : 4.5) - esp, 13.5 - esp, 4.5 - esp, 0, 0, 0); + if (isSlim) { + addVertices(leftArm2Box, + toSkinVertices(52, 48, 55, 52), + toSkinVertices(55, 48, 58, 52), + toSkinVertices(48, 52, 52, 64), + toSkinVertices(52, 52, 55, 64), + toSkinVertices(55, 52, 59, 64), + toSkinVertices(59, 52, 62, 64) + ); + } else { + addVertices(leftArm2Box, + toSkinVertices(52, 48, 56, 52), + toSkinVertices(56, 48, 60, 52), + toSkinVertices(48, 52, 52, 64), + toSkinVertices(52, 52, 56, 64), + toSkinVertices(56, 52, 60, 64), + toSkinVertices(60, 52, 64, 64) + ); + } + let leftArm2Mesh = new THREE.Mesh(leftArm2Box, layer2Material); + leftArm2Mesh.renderOrder = 1; + leftArmPivot.add(leftArm2Mesh); + + leftArmPivot.position.y = -6; + this.leftArm.add(leftArmPivot); + this.leftArm.position.y = -4; + this.leftArm.position.x = isSlim ? 5.5 : 6; + this.add(this.leftArm); + + + // Right Leg + this.rightLeg = new THREE.Group(); + let rightLegPivot = new THREE.Group(); + + let rightLegBox = new THREE.BoxGeometry(4 - esp, 12 - esp, 4 - esp, 0, 0, 0); + addVertices(rightLegBox, + toSkinVertices(4, 16, 8, 20), + toSkinVertices(8, 16, 12, 20), + toSkinVertices(0, 20, 4, 32), + toSkinVertices(4, 20, 8, 32), + toSkinVertices(8, 20, 12, 32), + toSkinVertices(12, 20, 16, 32) + ); + let rightLegMesh = new THREE.Mesh(rightLegBox, layer1Material); + rightLegPivot.add(rightLegMesh); + + let rightLeg2Box = new THREE.BoxGeometry(4.5 - esp, 13.5 - esp, 4.5 - esp, 0, 0, 0); + addVertices(rightLeg2Box, + toSkinVertices(4, 32, 8, 36), + toSkinVertices(8, 32, 12, 36), + toSkinVertices(0, 36, 4, 48), + toSkinVertices(4, 36, 8, 48), + toSkinVertices(8, 36, 12, 48), + toSkinVertices(12, 36, 16, 48) + ); + let rightLeg2Mesh = new THREE.Mesh(rightLeg2Box, layer2Material); + rightLeg2Mesh.renderOrder = 1; + rightLegPivot.add(rightLeg2Mesh); + + rightLegPivot.position.y = -6; + this.rightLeg.add(rightLegPivot); + this.rightLeg.position.y = -16; + this.rightLeg.position.x = -2; + this.add(this.rightLeg); + + // Left Leg + this.leftLeg = new THREE.Group(); + let leftLegPivot = new THREE.Group(); + + let leftLegBox = new THREE.BoxGeometry(4 - esp, 12 - esp, 4 - esp, 0, 0, 0); + addVertices(leftLegBox, + toSkinVertices(20, 48, 24, 52), + toSkinVertices(24, 48, 28, 52), + toSkinVertices(16, 52, 20, 64), + toSkinVertices(20, 52, 24, 64), + toSkinVertices(24, 52, 28, 64), + toSkinVertices(28, 52, 32, 64) + ); + let leftLegMesh = new THREE.Mesh(leftLegBox, layer1Material); + leftLegPivot.add(leftLegMesh); + + let leftLeg2Box = new THREE.BoxGeometry(4.5 - esp, 13.5 - esp, 4.5 - esp, 0, 0, 0); + addVertices(leftLeg2Box, + toSkinVertices(4, 48, 8, 52), + toSkinVertices(8, 48, 12, 52), + toSkinVertices(0, 52, 4, 64), + toSkinVertices(4, 52, 8, 64), + toSkinVertices(8, 52, 12, 64), + toSkinVertices(12, 52, 16, 64) + ); + let leftLeg2Mesh = new THREE.Mesh(leftLeg2Box, layer2Material); + leftLeg2Mesh.renderOrder = 1; + leftLegPivot.add(leftLeg2Mesh); + + leftLegPivot.position.y = -6; + this.leftLeg.add(leftLegPivot); + this.leftLeg.position.y = -16; + this.leftLeg.position.x = 2; + this.add(this.leftLeg); + } +}; + +class CapeObject extends THREE.Group { + constructor(capeMaterial) { + super(); + + // back = outside + // front = inside + let capeBox = new THREE.BoxGeometry(10, 16, 1, 0, 0, 0); + addVertices(capeBox, + toCapeVertices(1, 0, 11, 1), + toCapeVertices(11, 0, 21, 1), + toCapeVertices(11, 1, 12, 17), + toCapeVertices(12, 1, 22, 17), + toCapeVertices(0, 1, 1, 17), + toCapeVertices(1, 1, 11, 17) + ); + this.cape = new THREE.Mesh(capeBox, capeMaterial); + this.cape.position.y = -8; + this.cape.position.z = -0.5; + this.add(this.cape); + } +}; + +class PlayerObject extends THREE.Group { + constructor(slim, layer1Material, layer2Material, capeMaterial) { + super(); + + this.slim = slim; + + this.skin = new SkinObject(slim, layer1Material, layer2Material); + this.skin.visible = false; + this.add(this.skin); + + this.cape = new CapeObject(capeMaterial); + this.cape.position.z = -2; + this.cape.position.y = -4; + this.cape.rotation.x = 25 * Math.PI / 180; + this.cape.visible = false; + this.add(this.cape); + } +}; + +export { SkinObject, CapeObject, PlayerObject }; diff --git a/src/orbit_controls.js b/src/orbit_controls.js new file mode 100644 index 0000000..1cb9076 --- /dev/null +++ b/src/orbit_controls.js @@ -0,0 +1,593 @@ +/** + * Copyright (C) 2010-2017 three.js authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +// The code was originally from https://github.com/mrdoob/three.js/blob/d45a042cf962e9b1aa9441810ba118647b48aacb/examples/js/controls/OrbitControls.js + +import THREE from "three"; + +class OrbitControls extends THREE.EventDispatcher { + /** + * @author qiao / https://github.com/qiao + * @author mrdoob / http://mrdoob.com + * @author alteredq / http://alteredqualia.com/ + * @author WestLangley / http://github.com/WestLangley + * @author erich666 / http://erichaines.com + */ + + // This set of controls performs orbiting, dollying (zooming), and panning. + // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). + // + // Orbit - left mouse / touch: one finger move + // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish + // Pan - right mouse, or arrow keys / touch: three finger swipe + + constructor(object, domElement) { + super(); + this.object = object; + this.domElement = (domElement !== undefined) ? domElement : document; + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the object orbits around + this.target = new THREE.Vector3(); + + // How far you can dolly in and out (PerspectiveCamera only) + this.minDistance = 0; + this.maxDistance = Infinity; + + // How far you can zoom in and out (OrthographicCamera only) + this.minZoom = 0; + this.maxZoom = Infinity; + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + this.minAzimuthAngle = -Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + this.enableDamping = false; + this.dampingFactor = 0.25; + + // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. + // Set to false to disable zooming + this.enableZoom = true; + this.zoomSpeed = 1.0; + + // Set to false to disable rotating + this.enableRotate = true; + this.rotateSpeed = 1.0; + + // Set to false to disable panning + this.enablePan = true; + this.keyPanSpeed = 7.0; // pixels moved per arrow key push + + // Set to true to automatically rotate around the target + // If auto-rotate is enabled, you must call controls.update() in your animation loop + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 + + // Set to false to disable use of the keys + this.enableKeys = true; + + // The four arrow keys + this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; + + // Mouse buttons + this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; + + // for reset + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; + + // + // public methods + // + this.getPolarAngle = function () { + return spherical.phi; + }; + this.getAzimuthalAngle = function () { + return spherical.theta; + }; + this.saveState = function () { + scope.target0.copy(scope.target); + scope.position0.copy(scope.object.position); + scope.zoom0 = scope.object.zoom; + }; + this.reset = function () { + scope.target.copy(scope.target0); + scope.object.position.copy(scope.position0); + scope.object.zoom = scope.zoom0; + scope.object.updateProjectionMatrix(); + scope.dispatchEvent(changeEvent); + scope.update(); + state = STATE.NONE; + }; + + // this method is exposed, but perhaps it would be better if we can make it private... + this.update = function () { + let offset = new THREE.Vector3(); + // so camera.up is the orbit axis + let quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0)); + let quatInverse = quat.clone().inverse(); + let lastPosition = new THREE.Vector3(); + let lastQuaternion = new THREE.Quaternion(); + return function update() { + let position = scope.object.position; + offset.copy(position).sub(scope.target); + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion(quat); + // angle from z-axis around y-axis + spherical.setFromVector3(offset); + if (scope.autoRotate && state === STATE.NONE) { + rotateLeft(getAutoRotationAngle()); + } + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + // restrict theta to be between desired limits + spherical.theta = Math.max(scope.minAzimuthAngle, Math.min(scope.maxAzimuthAngle, spherical.theta)); + // restrict phi to be between desired limits + spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); + spherical.makeSafe(); + spherical.radius *= scale; + // restrict radius to be between desired limits + spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)); + // move target to panned location + scope.target.add(panOffset); + offset.setFromSpherical(spherical); + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion(quatInverse); + position.copy(scope.target).add(offset); + scope.object.lookAt(scope.target); + if (scope.enableDamping === true) { + sphericalDelta.theta *= (1 - scope.dampingFactor); + sphericalDelta.phi *= (1 - scope.dampingFactor); + } + else { + sphericalDelta.set(0, 0, 0); + } + scale = 1; + panOffset.set(0, 0, 0); + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + if (zoomChanged || + lastPosition.distanceToSquared(scope.object.position) > EPS || + 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { + scope.dispatchEvent(changeEvent); + lastPosition.copy(scope.object.position); + lastQuaternion.copy(scope.object.quaternion); + zoomChanged = false; + return true; + } + return false; + }; + }(); + this.dispose = function () { + scope.domElement.removeEventListener("contextmenu", onContextMenu, false); + scope.domElement.removeEventListener("mousedown", onMouseDown, false); + scope.domElement.removeEventListener("wheel", onMouseWheel, false); + scope.domElement.removeEventListener("touchstart", onTouchStart, false); + scope.domElement.removeEventListener("touchend", onTouchEnd, false); + scope.domElement.removeEventListener("touchmove", onTouchMove, false); + document.removeEventListener("mousemove", onMouseMove, false); + document.removeEventListener("mouseup", onMouseUp, false); + window.removeEventListener("keydown", onKeyDown, false); + //scope.dispatchEvent({ type: "dispose" }); // should this be added here? + }; + // + // internals + // + let scope = this; + let changeEvent = { type: "change" }; + let startEvent = { type: "start" }; + let endEvent = { type: "end" }; + let STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 }; + let state = STATE.NONE; + let EPS = 0.000001; + // current position in spherical coordinates + let spherical = new THREE.Spherical(); + let sphericalDelta = new THREE.Spherical(); + let scale = 1; + let panOffset = new THREE.Vector3(); + let zoomChanged = false; + let rotateStart = new THREE.Vector2(); + let rotateEnd = new THREE.Vector2(); + let rotateDelta = new THREE.Vector2(); + let panStart = new THREE.Vector2(); + let panEnd = new THREE.Vector2(); + let panDelta = new THREE.Vector2(); + let dollyStart = new THREE.Vector2(); + let dollyEnd = new THREE.Vector2(); + let dollyDelta = new THREE.Vector2(); + function getAutoRotationAngle() { + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + } + function getZoomScale() { + return Math.pow(0.95, scope.zoomSpeed); + } + function rotateLeft(angle) { + sphericalDelta.theta -= angle; + } + function rotateUp(angle) { + sphericalDelta.phi -= angle; + } + let panLeft = function () { + let v = new THREE.Vector3(); + return function panLeft(distance, objectMatrix) { + v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix + v.multiplyScalar(-distance); + panOffset.add(v); + }; + }(); + let panUp = function () { + let v = new THREE.Vector3(); + return function panUp(distance, objectMatrix) { + v.setFromMatrixColumn(objectMatrix, 1); // get Y column of objectMatrix + v.multiplyScalar(distance); + panOffset.add(v); + }; + }(); + // deltaX and deltaY are in pixels; right and down are positive + let pan = function () { + let offset = new THREE.Vector3(); + return function pan(deltaX, deltaY) { + let element = scope.domElement === document ? scope.domElement.body : scope.domElement; + if (scope.object instanceof THREE.PerspectiveCamera) { + // perspective + let position = scope.object.position; + offset.copy(position).sub(scope.target); + let targetDistance = offset.length(); + // half of the fov is center to top of screen + targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0); + // we actually don't use screenWidth, since perspective camera is fixed to screen height + panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); + panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); + } + else if (scope.object instanceof THREE.OrthographicCamera) { + // orthographic + panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix); + panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix); + } + else { + // camera neither orthographic nor perspective + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."); + scope.enablePan = false; + } + }; + }(); + function dollyIn(dollyScale) { + if (scope.object instanceof THREE.PerspectiveCamera) { + scale /= dollyScale; + } + else if (scope.object instanceof THREE.OrthographicCamera) { + scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + } + else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); + scope.enableZoom = false; + } + } + function dollyOut(dollyScale) { + if (scope.object instanceof THREE.PerspectiveCamera) { + scale *= dollyScale; + } + else if (scope.object instanceof THREE.OrthographicCamera) { + scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + } + else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); + scope.enableZoom = false; + } + } + // + // event callbacks - update the object state + // + function handleMouseDownRotate(event) { + rotateStart.set(event.clientX, event.clientY); + } + function handleMouseDownDolly(event) { + dollyStart.set(event.clientX, event.clientY); + } + function handleMouseDownPan(event) { + panStart.set(event.clientX, event.clientY); + } + function handleMouseMoveRotate(event) { + rotateEnd.set(event.clientX, event.clientY); + rotateDelta.subVectors(rotateEnd, rotateStart); + let element = scope.domElement === document ? scope.domElement.body : scope.domElement; + // rotating across whole screen goes 360 degrees around + rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); + rotateStart.copy(rotateEnd); + scope.update(); + } + function handleMouseMoveDolly(event) { + dollyEnd.set(event.clientX, event.clientY); + dollyDelta.subVectors(dollyEnd, dollyStart); + if (dollyDelta.y > 0) { + dollyIn(getZoomScale()); + } + else if (dollyDelta.y < 0) { + dollyOut(getZoomScale()); + } + dollyStart.copy(dollyEnd); + scope.update(); + } + function handleMouseMovePan(event) { + panEnd.set(event.clientX, event.clientY); + panDelta.subVectors(panEnd, panStart); + pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + scope.update(); + } + function handleMouseUp(event) { + } + function handleMouseWheel(event) { + if (event.deltaY < 0) { + dollyOut(getZoomScale()); + } + else if (event.deltaY > 0) { + dollyIn(getZoomScale()); + } + scope.update(); + } + function handleKeyDown(event) { + switch (event.keyCode) { + case scope.keys.UP: + pan(0, scope.keyPanSpeed); + scope.update(); + break; + case scope.keys.BOTTOM: + pan(0, -scope.keyPanSpeed); + scope.update(); + break; + case scope.keys.LEFT: + pan(scope.keyPanSpeed, 0); + scope.update(); + break; + case scope.keys.RIGHT: + pan(-scope.keyPanSpeed, 0); + scope.update(); + break; + } + } + function handleTouchStartRotate(event) { + rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); + } + function handleTouchStartDolly(event) { + let dx = event.touches[0].pageX - event.touches[1].pageX; + let dy = event.touches[0].pageY - event.touches[1].pageY; + let distance = Math.sqrt(dx * dx + dy * dy); + dollyStart.set(0, distance); + } + function handleTouchStartPan(event) { + panStart.set(event.touches[0].pageX, event.touches[0].pageY); + } + function handleTouchMoveRotate(event) { + rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY); + rotateDelta.subVectors(rotateEnd, rotateStart); + let element = scope.domElement === document ? scope.domElement.body : scope.domElement; + rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); + rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); + rotateStart.copy(rotateEnd); + scope.update(); + } + function handleTouchMoveDolly(event) { + let dx = event.touches[0].pageX - event.touches[1].pageX; + let dy = event.touches[0].pageY - event.touches[1].pageY; + let distance = Math.sqrt(dx * dx + dy * dy); + dollyEnd.set(0, distance); + dollyDelta.subVectors(dollyEnd, dollyStart); + if (dollyDelta.y > 0) { + dollyOut(getZoomScale()); + } + else if (dollyDelta.y < 0) { + dollyIn(getZoomScale()); + } + dollyStart.copy(dollyEnd); + scope.update(); + } + function handleTouchMovePan(event) { + panEnd.set(event.touches[0].pageX, event.touches[0].pageY); + panDelta.subVectors(panEnd, panStart); + pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + scope.update(); + } + function handleTouchEnd(event) { + } + // + // event handlers - FSM: listen for events and reset state + // + function onMouseDown(event) { + if (scope.enabled === false) + return; + switch (event.button) { + case scope.mouseButtons.ORBIT: + if (scope.enableRotate === false) + return; + handleMouseDownRotate(event); + state = STATE.ROTATE; + break; + case scope.mouseButtons.ZOOM: + if (scope.enableZoom === false) + return; + handleMouseDownDolly(event); + state = STATE.DOLLY; + break; + case scope.mouseButtons.PAN: + if (scope.enablePan === false) + return; + handleMouseDownPan(event); + state = STATE.PAN; + break; + } + event.preventDefault(); + if (state !== STATE.NONE) { + document.addEventListener("mousemove", onMouseMove, false); + document.addEventListener("mouseup", onMouseUp, false); + scope.dispatchEvent(startEvent); + } + } + function onMouseMove(event) { + if (scope.enabled === false) + return; + switch (state) { + case STATE.ROTATE: + if (scope.enableRotate === false) + return; + handleMouseMoveRotate(event); + break; + case STATE.DOLLY: + if (scope.enableZoom === false) + return; + handleMouseMoveDolly(event); + break; + case STATE.PAN: + if (scope.enablePan === false) + return; + handleMouseMovePan(event); + break; + } + event.preventDefault(); + } + function onMouseUp(event) { + if (scope.enabled === false) + return; + handleMouseUp(event); + document.removeEventListener("mousemove", onMouseMove, false); + document.removeEventListener("mouseup", onMouseUp, false); + scope.dispatchEvent(endEvent); + state = STATE.NONE; + } + function onMouseWheel(event) { + if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) + return; + event.preventDefault(); + event.stopPropagation(); + handleMouseWheel(event); + scope.dispatchEvent(startEvent); // not sure why these are here... + scope.dispatchEvent(endEvent); + } + function onKeyDown(event) { + if (scope.enabled === false || scope.enableKeys === false || scope.enablePan === false) + return; + handleKeyDown(event); + } + function onTouchStart(event) { + if (scope.enabled === false) + return; + switch (event.touches.length) { + case 1:// one-fingered touch: rotate + if (scope.enableRotate === false) + return; + handleTouchStartRotate(event); + state = STATE.TOUCH_ROTATE; + break; + case 2:// two-fingered touch: dolly + if (scope.enableZoom === false) + return; + handleTouchStartDolly(event); + state = STATE.TOUCH_DOLLY; + break; + case 3:// three-fingered touch: pan + if (scope.enablePan === false) + return; + handleTouchStartPan(event); + state = STATE.TOUCH_PAN; + break; + default: + state = STATE.NONE; + } + if (state !== STATE.NONE) { + scope.dispatchEvent(startEvent); + } + } + function onTouchMove(event) { + if (scope.enabled === false) + return; + switch (event.touches.length) { + case 1:// one-fingered touch: rotate + if (scope.enableRotate === false) + return; + if (state !== STATE.TOUCH_ROTATE) + return; // is this needed?... + handleTouchMoveRotate(event); + break; + case 2:// two-fingered touch: dolly + if (scope.enableZoom === false) + return; + if (state !== STATE.TOUCH_DOLLY) + return; // is this needed?... + handleTouchMoveDolly(event); + break; + case 3:// three-fingered touch: pan + if (scope.enablePan === false) + return; + if (state !== STATE.TOUCH_PAN) + return; // is this needed?... + handleTouchMovePan(event); + break; + default: + state = STATE.NONE; + } + event.preventDefault(); + event.stopPropagation(); + } + function onTouchEnd(event) { + if (scope.enabled === false) + return; + handleTouchEnd(event); + scope.dispatchEvent(endEvent); + state = STATE.NONE; + } + function onContextMenu(event) { + if (scope.enabled === false || scope.enablePan === false) + return; + event.preventDefault(); + } + // + scope.domElement.addEventListener("contextmenu", onContextMenu, false); + scope.domElement.addEventListener("mousedown", onMouseDown, false); + scope.domElement.addEventListener("wheel", onMouseWheel, false); + scope.domElement.addEventListener("touchstart", onTouchStart, false); + scope.domElement.addEventListener("touchend", onTouchEnd, false); + scope.domElement.addEventListener("touchmove", onTouchMove, false); + window.addEventListener("keydown", onKeyDown, false); + // force an update at start + this.update(); + } +} + +export { OrbitControls }; diff --git a/src/skinview3d.js b/src/skinview3d.js new file mode 100644 index 0000000..c27cedf --- /dev/null +++ b/src/skinview3d.js @@ -0,0 +1,27 @@ +/** + * skinview3d + * + * Copyright (C) 2017 the original author or authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + * @author yushijinhun + * @author Hacksore + * @author Kent Rasmussen + */ + +export { SkinObject, CapeObject, PlayerObject } from "./model"; +export { SkinViewer, SkinControl } from "./viewer"; +export { WalkAnimation } from "./animation"; diff --git a/src/viewer.js b/src/viewer.js new file mode 100644 index 0000000..a6602b9 --- /dev/null +++ b/src/viewer.js @@ -0,0 +1,245 @@ +import THREE from "three"; +import { SkinObject, CapeObject, PlayerObject } from "./model"; +import { OrbitControls } from "./orbit_controls"; + +function copyImage(context, sX, sY, w, h, dX, dY, flipHorizontal) { + let imgData = context.getImageData(sX, sY, w, h); + if (flipHorizontal) { + for (let y = 0; y < h; y++) { + for (let x = 0; x < (w / 2); x++) { + let index = (x + y * w) * 4; + let index2 = ((w - x - 1) + y * w) * 4; + let pA1 = imgData.data[index]; + let pA2 = imgData.data[index + 1]; + let pA3 = imgData.data[index + 2]; + let pA4 = imgData.data[index + 3]; + + let pB1 = imgData.data[index2]; + let pB2 = imgData.data[index2 + 1]; + let pB3 = imgData.data[index2 + 2]; + let pB4 = imgData.data[index2 + 3]; + + imgData.data[index] = pB1; + imgData.data[index + 1] = pB2; + imgData.data[index + 2] = pB3; + imgData.data[index + 3] = pB4; + + imgData.data[index2] = pA1; + imgData.data[index2 + 1] = pA2; + imgData.data[index2 + 2] = pA3; + imgData.data[index2 + 3] = pA4; + } + } + } + context.putImageData(imgData, dX, dY); +} + +function convertSkinTo1_8(context, width) { + let scale = width / 64.0; + let copySkin = (context, sX, sY, w, h, dX, dY, flipHorizontal) => copyImage(context, sX * scale, sY * scale, w * scale, h * scale, dX * scale, dY * scale, flipHorizontal); + + copySkin(context, 4, 16, 4, 4, 20, 48, true); // Top Leg + copySkin(context, 8, 16, 4, 4, 24, 48, true); // Bottom Leg + copySkin(context, 0, 20, 4, 12, 24, 52, true); // Outer Leg + copySkin(context, 4, 20, 4, 12, 20, 52, true); // Front Leg + copySkin(context, 8, 20, 4, 12, 16, 52, true); // Inner Leg + copySkin(context, 12, 20, 4, 12, 28, 52, true); // Back Leg + copySkin(context, 44, 16, 4, 4, 36, 48, true); // Top Arm + copySkin(context, 48, 16, 4, 4, 40, 48, true); // Bottom Arm + copySkin(context, 40, 20, 4, 12, 40, 52, true); // Outer Arm + copySkin(context, 44, 20, 4, 12, 36, 52, true); // Front Arm + copySkin(context, 48, 20, 4, 12, 32, 52, true); // Inner Arm + copySkin(context, 52, 20, 4, 12, 44, 52, true); // Back Arm +} + +class SkinViewer { + constructor(options) { + this.domElement = options.domElement; + this.animation = options.animation || null; + this.animationPaused = false; + this.animationSpeed = 3; + this.animationTime = 0; + this.disposed = false; + + // texture + this.skinImg = new Image(); + this.skinCanvas = document.createElement("canvas"); + this.skinTexture = new THREE.Texture(this.skinCanvas); + this.skinTexture.magFilter = THREE.NearestFilter; + this.skinTexture.minFilter = THREE.NearestMipMapNearestFilter; + + this.capeImg = new Image(); + this.capeCanvas = document.createElement("canvas"); + this.capeTexture = new THREE.Texture(this.capeCanvas); + this.capeTexture.magFilter = THREE.NearestFilter; + this.capeTexture.minFilter = THREE.NearestMipMapNearestFilter; + + this.layer1Material = new THREE.MeshBasicMaterial({ map: this.skinTexture, side: THREE.FrontSide }); + this.layer2Material = new THREE.MeshBasicMaterial({ map: this.skinTexture, transparent: true, opacity: 1, side: THREE.DoubleSide }); + this.capeMaterial = new THREE.MeshBasicMaterial({ map: this.capeTexture }); + + // scene + this.scene = new THREE.Scene(); + + this.camera = new THREE.PerspectiveCamera(75); + this.camera.position.y = -12; + this.camera.position.z = 30; + + this.renderer = new THREE.WebGLRenderer({ angleRot: true, alpha: true, antialias: false }); + this.renderer.setSize(300, 300); // default size + this.renderer.context.getShaderInfoLog = () => ""; // shut firefox up + this.domElement.appendChild(this.renderer.domElement); + + this.playerObject = new PlayerObject(options.slim === true, this.layer1Material, this.layer2Material, this.capeMaterial); + this.scene.add(this.playerObject); + + // texture loading + this.skinImg.crossOrigin = ""; + this.skinImg.onerror = () => console.log("Failed loading " + this.skinImg.src); + this.skinImg.onload = () => { + let isOldFormat = false; + if (this.skinImg.width !== this.skinImg.height) { + if (this.skinImg.width === 2 * this.skinImg.height) { + isOldFormat = true; + } else { + console.log("Bad skin size"); + return; + } + } + + let skinContext = this.skinCanvas.getContext("2d"); + if (isOldFormat) { + let width = this.skinImg.width; + this.skinCanvas.width = width; + this.skinCanvas.height = width; + skinContext.clearRect(0, 0, width, width); + skinContext.drawImage(this.skinImg, 0, 0, width, width / 2.0); + convertSkinTo1_8(skinContext, width); + } else { + this.skinCanvas.width = this.skinImg.width; + this.skinCanvas.height = this.skinImg.height; + skinContext.clearRect(0, 0, this.skinCanvas.width, this.skinCanvas.height); + skinContext.drawImage(this.skinImg, 0, 0, this.skinCanvas.width, this.skinCanvas.height); + } + + this.skinTexture.needsUpdate = true; + this.layer1Material.needsUpdate = true; + this.layer2Material.needsUpdate = true; + + this.playerObject.skin.visible = true; + }; + + this.capeImg.crossOrigin = ""; + this.capeImg.onerror = () => console.log("Failed loading " + this.capeImg.src); + this.capeImg.onload = () => { + if (this.capeImg.width !== 2 * this.capeImg.height) { + console.log("Bad cape size"); + return; + } + + this.capeCanvas.width = this.capeImg.width; + this.capeCanvas.height = this.capeImg.height; + let capeContext = this.capeCanvas.getContext("2d"); + capeContext.clearRect(0, 0, this.capeCanvas.width, this.capeCanvas.height); + capeContext.drawImage(this.capeImg, 0, 0, this.capeCanvas.width, this.capeCanvas.height); + + this.capeTexture.needsUpdate = true; + this.capeMaterial.needsUpdate = true; + + this.playerObject.cape.visible = true; + }; + + if (options.skinUrl) this.skinUrl = options.skinUrl; + if (options.capeUrl) this.capeUrl = options.capeUrl; + if (options.width) this.width = options.width; + if (options.height) this.height = options.height; + + let draw = () => { + if (this.disposed) return; + window.requestAnimationFrame(draw); + if (!this.animationPaused) { + this.animationTime++; + if (this.animation) { + this.animation(this.playerObject, this.animationTime / 100 * this.animationSpeed); + } + } + this.renderer.render(this.scene, this.camera); + }; + draw(); + } + + setSize(width, height) { + this.camera.aspect = width / height; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(width, height); + } + + dispose() { + this.disposed = true; + this.domElement.removeChild(this.renderer.domElement); + this.renderer.dispose(); + this.skinTexture.dispose(); + this.capeTexture.dispose(); + } + + get skinUrl() { + return this.skinImg.src; + } + + set skinUrl(url) { + this.skinImg.src = url; + } + + get capeUrl() { + return this.capeImg.src; + } + + set capeUrl(url) { + this.capeImg.src = url; + } + + get width() { + return this.renderer.getSize().width; + } + + set width(newWidth) { + this.setSize(newWidth, this.height); + } + + get height() { + return this.renderer.getSize().height; + } + + set height(newHeight) { + this.setSize(this.width, newHeight); + } +}; + +class SkinControl { + constructor(skinViewer) { + this.enableAnimationControl = true; + this.skinViewer = skinViewer; + + this.orbitControls = new OrbitControls(skinViewer.camera, skinViewer.renderer.domElement); + this.orbitControls.enablePan = false; + this.orbitControls.target = new THREE.Vector3(0, -12, 0); + this.orbitControls.minDistance = 10; + this.orbitControls.maxDistance = 256; + this.orbitControls.update(); + + this.animationPauseListener = e => { + if (this.enableAnimationControl) { + e.preventDefault(); + this.skinViewer.animationPaused = !this.skinViewer.animationPaused; + } + }; + this.skinViewer.domElement.addEventListener("contextmenu", this.animationPauseListener, false); + } + + dispose() { + this.skinViewer.domElement.removeEventListener("contextmenu", this.animationPauseListener, false); + this.orbitControls.dispose(); + } +}; + +export { SkinViewer, SkinControl };