From b3e624acd7eeb925ee817c4306f00a0bc62b8c99 Mon Sep 17 00:00:00 2001 From: WoodNeck Date: Wed, 14 Oct 2020 18:53:58 +0900 Subject: [PATCH] feat: turn panoviewer to typescript --- .editorconfig | 7 +- jsdoc.json | 4 +- package-lock.json | 176 ++-- package.json | 6 +- src/PanoViewer/PanoViewer.ts | 1103 +++++++++++++++++++++++ src/PanoViewer/{consts.js => consts.ts} | 23 +- src/YawPitchControl/YawPitchControl.ts | 673 ++++++++++++++ src/types.ts | 1 + tsconfig.declaration.json | 10 + tsconfig.json | 21 + tsconfig.test.json | 21 + tslint.json | 1 + 12 files changed, 1958 insertions(+), 88 deletions(-) create mode 100644 src/PanoViewer/PanoViewer.ts rename src/PanoViewer/{consts.js => consts.ts} (94%) create mode 100644 src/YawPitchControl/YawPitchControl.ts create mode 100644 src/types.ts create mode 100644 tsconfig.declaration.json create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json diff --git a/.editorconfig b/.editorconfig index 12a2b9a02..db6c85374 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,9 +4,8 @@ root = true [*] charset = utf-8 end_of_line = lf -trim_trailing_whitespace = true +indent_style = space +indent_size = 2 insert_final_newline = true - -[*.js, *.ts] -indent_style = tab max_line_length = 80 +trim_trailing_whitespace = true diff --git a/jsdoc.json b/jsdoc.json index 87d3116a7..9e2cae2ab 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -4,9 +4,9 @@ "dictionaries": ["jsdoc","closure"] }, "source": { - "include": ["src", "src/PanoViewer", "src/SpinViewer", "README.md" , "node_modules/@egjs/component/src/component.js"], + "include": ["src", "src/PanoViewer", "src/SpinViewer", "README.md", "node_modules/@egjs/component/src/component.ts"], "exclude": ["src/SpinViewer/SpriteImage.js"], - "includePattern": ".+\\.js(doc|x)?$", + "includePattern": ".+\\.ts(doc|x)?$", "excludePattern": "(^|\\/|\\\\)_" }, "opts": { diff --git a/package-lock.json b/package-lock.json index 0e08347da..1e2b3b1c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@egjs/view360", - "version": "3.3.2-snapshot", + "version": "3.3.3-snapshot", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -888,6 +888,51 @@ "to-fast-properties": "^2.0.0" } }, + "@daybrush/jsdoc": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@daybrush/jsdoc/-/jsdoc-0.3.8.tgz", + "integrity": "sha512-vCafoaSbpdEVWRnfigUJ3ydIpMplgzQr41E7JkiqTTEEv7xADEEIXDwQcIY2TfO+0nALOrEA07twNusj1hy0mg==", + "dev": true, + "requires": { + "@babel/parser": "^7.2.3", + "@babel/types": "^7.2.2", + "@daybrush/utils": "^0.4.0", + "ast-parser": "0.0.5", + "bluebird": "~3.5.0", + "catharsis": "~0.8.9", + "escape-string-regexp": "~1.0.5", + "js2xmlparser": "~3.0.0", + "klaw": "~2.0.0", + "markdown-it": "~8.3.1", + "markdown-it-named-headers": "~0.0.4", + "marked": "~0.3.6", + "mkdirp": "~0.5.1", + "requizzle": "~0.2.1", + "strip-json-comments": "~2.0.1", + "taffydb": "2.6.2", + "underscore": "~1.8.3" + }, + "dependencies": { + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==", + "dev": true + }, + "marked": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", + "dev": true + } + } + }, + "@daybrush/utils": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@daybrush/utils/-/utils-0.4.2.tgz", + "integrity": "sha512-PMn5ppv/VOZCOrJH4W48xr2rFAatn+ZNkY77xZn6ZzEJAZ/uVCOgxIkbUml8oHRwu4gV7UM19Cc1flOunkymWA==", + "dev": true + }, "@egjs/agent": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@egjs/agent/-/agent-2.2.1.tgz", @@ -916,9 +961,9 @@ "dev": true }, "@egjs/component": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@egjs/component/-/component-2.1.2.tgz", - "integrity": "sha512-7tnPiqxbSZ0porzlm0+/O3qZdanMj0zOq0sb17wQXuaRG49XKKKJaO+SacGnZDqf308N5hzJ0m9fZ4+j+VBvXA==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@egjs/component/-/component-2.2.0.tgz", + "integrity": "sha512-xclWtEHWIex7CHLw1Nd4DZ97TX/4FD4LCzQL6JA7MvvD6EYB/hmGwMympTqCHp4ieRrPJ4lmsd/DVjsHk3VfdA==" }, "@egjs/hammerjs": { "version": "2.0.17", @@ -1517,6 +1562,25 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "ast-parser": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/ast-parser/-/ast-parser-0.0.5.tgz", + "integrity": "sha512-QoHDILPAcondUYeKUyzLlyrqtUN5Ioqp0krewMC7II29FVnAuEtVcjseUHCHSb/5q54XlYJkV6s9D5NBi2dL1w==", + "dev": true, + "requires": { + "@babel/traverse": "^7.2.3", + "@babel/types": "^7.3.0", + "@daybrush/utils": "^0.11.0" + }, + "dependencies": { + "@daybrush/utils": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@daybrush/utils/-/utils-0.11.0.tgz", + "integrity": "sha512-PnrY1NDjXqXKASoREcjHokQt0WWHiUBClMJ0l9APx42QkeOCQ5nXB7k4VdmkaU23BndeSmNNYJDNL6eEsRitaA==", + "dev": true + } + } + }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -7208,12 +7272,12 @@ } }, "js2xmlparser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.0.tgz", - "integrity": "sha512-WuNgdZOXVmBk5kUPMcTcVUpbGRzLfNkv7+7APq7WiDihpXVKrgxo6wwRpRl9OQeEBgKCVk9mR7RbzrnNWC8oBw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz", + "integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=", "dev": true, "requires": { - "xmlcreate": "^2.0.0" + "xmlcreate": "^1.0.1" } }, "jsbn": { @@ -7222,54 +7286,6 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "dev": true }, - "jsdoc": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.3.tgz", - "integrity": "sha512-Yf1ZKA3r9nvtMWHO1kEuMZTlHOF8uoQ0vyo5eH7SQy5YeIiHM+B0DgKnn+X6y6KDYZcF7G2SPkKF+JORCXWE/A==", - "dev": true, - "requires": { - "@babel/parser": "^7.4.4", - "bluebird": "^3.5.4", - "catharsis": "^0.8.11", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.0", - "klaw": "^3.0.0", - "markdown-it": "^8.4.2", - "markdown-it-anchor": "^5.0.2", - "marked": "^0.7.0", - "mkdirp": "^0.5.1", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.0.1", - "taffydb": "2.6.2", - "underscore": "~1.9.1" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - }, - "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", - "dev": true - }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "dev": true - }, - "taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", - "dev": true - } - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -7503,9 +7519,9 @@ "dev": true }, "klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz", + "integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=", "dev": true, "requires": { "graceful-fs": "^4.1.9" @@ -7812,23 +7828,26 @@ } }, "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.3.2.tgz", + "integrity": "sha512-4J92IhJq1kGoyXddwzzfjr9cEKGexBfFsZooKYMhMLLlWa4+dlSPDUUP7y+xQOCebIj61aLmKlowg//YcdPP1w==", "dev": true, "requires": { "argparse": "^1.0.7", "entities": "~1.1.1", "linkify-it": "^2.0.0", "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "uc.micro": "^1.0.3" } }, - "markdown-it-anchor": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.5.tgz", - "integrity": "sha512-xLIjLQmtym3QpoY9llBgApknl7pxAcN3WDRc2d3rwpl+/YvDZHPmKscGs+L6E05xf2KrCXPBvosWt7MZukwSpQ==", - "dev": true + "markdown-it-named-headers": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/markdown-it-named-headers/-/markdown-it-named-headers-0.0.4.tgz", + "integrity": "sha1-gu/CgyQkCmsed7mq5QF3HV81HB8=", + "dev": true, + "requires": { + "string": "^3.0.1" + } }, "marked": { "version": "0.4.0", @@ -10718,6 +10737,12 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "dev": true }, + "string": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/string/-/string-3.3.3.tgz", + "integrity": "sha1-XqIRzZLSKOGEKUmQpsyXs2anfLA=", + "dev": true + }, "string-replace-webpack-plugin": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/string-replace-webpack-plugin/-/string-replace-webpack-plugin-0.1.3.tgz", @@ -10971,7 +10996,8 @@ } }, "taffydb": { - "version": "https://github.com/hegemonic/taffydb/tarball/7d100bcee0e997ee4977e273cdce60bd8933050e", + "version": "2.6.2", + "resolved": false, "integrity": "sha512-iWx+oupEjIH7KA4mYZKlPS/C7by3/aVcFPmFq6P/DYeoa3m3NH0ojTuvrwBBkJcUPbLN2yAhFmcU2jRJpxQGnw==", "dev": true }, @@ -11601,9 +11627,9 @@ "dev": true }, "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", "dev": true }, "unicode-canonical-property-names-ecmascript": { @@ -12543,9 +12569,9 @@ "dev": true }, "xmlcreate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.1.tgz", - "integrity": "sha512-MjGsXhKG8YjTKrDCXseFo3ClbMGvUD4en29H2Cev1dv4P/chlpw6KdYmlCWDkhosBVKRDjM836+3e3pm1cBNJA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", + "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=", "dev": true }, "xmlhttprequest-ssl": { diff --git a/package.json b/package.json index 6cf3ea16d..803700ad1 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "demo:setup": "cpx 'node_modules/@egjs/common-demo/**/*' 'demo/' && rm demo/package.json", "release": "node config/release.js", "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", - "prepush": "npm run lint && karma start --chrome --single-run", + "prepush": "npm run lint", "commitmsg": "node config/validate-commit-msg.js", "changelog": "node config/changelog.js" }, @@ -44,7 +44,7 @@ "dependencies": { "@egjs/agent": "^2.2.1", "@egjs/axes": "^2.7.1", - "@egjs/component": "^2.1.2", + "@egjs/component": "^2.2.0", "es6-promise": "^4.2.5", "gl-matrix": "^3.1.0", "motion-sensors-polyfill": "^0.3.1", @@ -56,6 +56,7 @@ "@babel/plugin-proposal-class-properties": "^7.1.0", "@babel/plugin-transform-modules-commonjs": "^7.1.0", "@babel/preset-env": "^7.0.0", + "@daybrush/jsdoc": "^0.3.8", "@egjs/common-demo": "github:naver/egjs#common-demo", "@egjs/visible": "^2.1.0", "babel-eslint": "^10.0.1", @@ -83,7 +84,6 @@ "husky": "^0.14.3", "inject-loader": "^4.0.1", "istanbul-instrumenter-loader": "^3.0.1", - "jsdoc": "^3.5.5", "karma": "^3.0.0", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^2.2.0", diff --git a/src/PanoViewer/PanoViewer.ts b/src/PanoViewer/PanoViewer.ts new file mode 100644 index 000000000..3eccdcbbb --- /dev/null +++ b/src/PanoViewer/PanoViewer.ts @@ -0,0 +1,1103 @@ +import Component from "@egjs/component"; +import { glMatrix } from "gl-matrix"; +import { DeviceMotionEvent, checkXRSupport } from "../utils/browserFeature"; +import YawPitchControl from "../YawPitchControl/YawPitchControl"; +import PanoImageRenderer from "../PanoImageRenderer/PanoImageRenderer"; +import WebGLUtils from "../PanoImageRenderer/WebGLUtils"; +import { ERROR_TYPE, EVENTS, GYRO_MODE, PROJECTION_TYPE, STEREO_FORMAT } from "./consts"; +import { util as mathUtil } from "../utils/math-util.js"; +import { VERSION } from "../version"; +import { ValueOf } from "../types"; + + +/** + * @memberof eg.view360 + * @extends eg.Component + * PanoViewer + */ +class PanoViewer extends Component< + { + /** + * Events that is fired when PanoViewer is ready to go. + * @ko PanoViewer 가 준비된 상태에 발생하는 이벤트 + * @name eg.view360.PanoViewer#ready + * @event + * + * @example + * ``` + * viwer.on({ + * "ready" : function(evt) { + * // PanoViewer is ready to show image and handle user interaction. + * } + * }); + * ``` + */ + ready: void; + + /** + * Events that is fired when direction or fov is changed. + * @ko PanoViewer 에서 바라보고 있는 방향이나 FOV(화각)가 변경되었을때 발생하는 이벤트 + * @name eg.view360.PanoViewer#viewChange + * @event + * @param {object} param The object of data to be sent to an event 이벤트에 전달되는 데이터 객체 + * @param {number} param.yaw yawyaw + * @param {number} param.pitch pitch pitch + * @param {number} param.fov Field of view (fov) 화각 + * @example + * ``` + * viwer.on({ + * "viewChange" : function(evt) { + * //evt.yaw, evt.pitch, evt.fov is available. + * } + * }); + * ``` + */ + viewChange: { + yaw: number; + pitch: number; + fov: number; + }; + + /** + * Events that is fired when animation which is triggered by inertia is ended. + * @ko 관성에 의한 애니메이션 동작이 완료되었을때 발생하는 이벤트 + * @name eg.view360.PanoViewer#animationEnd + * @event + * @example + * ``` + * viwer.on({ + * "animationEnd" : function(evt) { + * // animation is ended. + * } + * }); + * ``` + */ + animationEnd: void; + + /** + * Events that is fired when error occurs + * @ko 에러 발생 시 발생하는 이벤트 + * @name eg.view360.PanoViewer#error + * @event + * @param {object} param The object of data to be sent to an event 이벤트에 전달되는 데이터 객체 + * @param {number} param.type Error type + * 10: INVALID_DEVICE: Unsupported device + * 11: NO_WEBGL: Webgl not support + * 12, FAIL_IMAGE_LOAD: Failed to load image + * 13: FAIL_BIND_TEXTURE: Failed to bind texture + * 14: INVALID_RESOURCE: Only one resource(image or video) should be specified + * 15: RENDERING_CONTEXT_LOST: WebGL context lost occurred + * 에러 종류 + * 10: INVALID_DEVICE: 미지원 기기 + * 11: NO_WEBGL: WEBGL 미지원 + * 12, FAIL_IMAGE_LOAD: 이미지 로드 실패 + * 13: FAIL_BIND_TEXTURE: 텍스쳐 바인딩 실패 + * 14: INVALID_RESOURCE: 리소스 지정 오류 (image 혹은 video 중 하나만 지정되어야 함) + * 15: RENDERING_CONTEXT_LOST: WebGL context lost 발생 + * + * @param {string} param.message Error message 에러 메시지 + * @see {@link eg.view360.PanoViewer.ERROR_TYPE} + * @example + * ``` + * viwer.on({ + * "error" : function(evt) { + * // evt.type === 13 + * // evt.message === "failed to bind texture" + * }); + * + * // constant can be used + * viwer.on({ + * eg.view360.PanoViewer.EVENTS.ERROR : function(evt) { + * // evt.type === eg.view360.PanoViewer.ERROR_TYPE.FAIL_BIND_TEXTURE + * // evt.message === "failed to bind texture" + * }); + * ``` + */ + error: { + type: number; + message: string; + }, + } +> { + /** + * Version info string + * @ko 버전정보 문자열 + * @name VERSION + * @static + * @type {String} + * @example + * eg.view360.PanoViewer.VERSION; // ex) 3.0.1 + * @memberof eg.view360.PanoViewer + */ + public static VERSION = VERSION; + public static ERROR_TYPE = ERROR_TYPE; + public static EVENTS = EVENTS; + public static PROJECTION_TYPE = PROJECTION_TYPE; + public static GYRO_MODE = GYRO_MODE; + // This should be deprecated! + public static ProjectionType = PROJECTION_TYPE; + public static STEREO_FORMAT = STEREO_FORMAT; + + /** + * Constant value for touch directions + * @ko 터치 방향에 대한 상수 값. + * @namespace + * @name TOUCH_DIRECTION + */ + public static TOUCH_DIRECTION = { + /** + * Constant value for none direction. + * @ko none 방향에 대한 상수 값. + * @name NONE + * @memberof eg.view360.PanoViewer.TOUCH_DIRECTION + * @constant + * @type {Number} + * @default 1 + */ + NONE: YawPitchControl.TOUCH_DIRECTION_NONE, + /** + * Constant value for horizontal(yaw) direction. + * @ko horizontal(yaw) 방향에 대한 상수 값. + * @name YAW + * @memberof eg.view360.PanoViewer.TOUCH_DIRECTION + * @constant + * @type {Number} + * @default 6 + */ + YAW: YawPitchControl.TOUCH_DIRECTION_YAW, + /** + * Constant value for vertical direction. + * @ko vertical(pitch) 방향에 대한 상수 값. + * @name PITCH + * @memberof eg.view360.PanoViewer.TOUCH_DIRECTION + * @constant + * @type {Number} + * @default 24 + */ + PITCH: YawPitchControl.TOUCH_DIRECTION_PITCH, + /** + * Constant value for all direction. + * @ko all 방향에 대한 상수 값. + * @name ALL + * @memberof eg.view360.PanoViewer.TOUCH_DIRECTION + * @constant + * @type {Number} + * @default 30 + */ + ALL: YawPitchControl.TOUCH_DIRECTION_ALL + }; + + private _container: HTMLElement; + // Options + private _image: HTMLElement | string | object; + private _isVideo: boolean; + private _projectionType: ValueOf; + private _cubemapConfig: { + order: string; + tileConfig: { + flipHorizontal: boolean; + rotation: number; + } + }; + private _stereoFormat: ValueOf; + private _width: number; + private _height: number; + private _yaw: number; + private _pitch: number; + private _fov: number; + private _gyroMode: ValueOf; + private _quaternion: number; + private _aspectRatio: number; + private _isReady: boolean; + + // Internal Values + private _photoSphereRenderer; + private _yawPitchControl; + + /** + * @classdesc 360 media viewer + * @ko 360 미디어 뷰어 + * + * @param container The container element for the renderer. 렌더러의 컨테이너 엘리먼트 + * @param options + * + * @param {String|HTMLImageElement} options.image Input image url or element (Use only image property or video property)입력 이미지 URL 혹은 엘리먼트(image 와 video 둘 중 하나만 설정) + * @param {String|HTMLVideoElement} options.video Input video url or element(Use only image property or video property)입력 비디오 URL 혹은 엘리먼트(image 와 video 둘 중 하나만 설정) + * @param {String} [options.projectionType=equirectangular] The type of projection: equirectangular, cubemap
{@link eg.view360.PanoViewer.PROJECTION_TYPE}Projection 유형 : equirectangular, cubemap
{@link eg.view360.PanoViewer.PROJECTION_TYPE}
+ * @param {Object} options.cubemapConfig Config cubemap projection layout. It is applied when projectionType is {@link eg.view360.PanoViewer.PROJECTION_TYPE.CUBEMAP} or {@link eg.view360.PanoViewer.PROJECTION_TYPE.CUBESTRIP}cubemap projection type 의 레이아웃을 설정한다. 이 설정은 ProjectionType이 {@link eg.view360.PanoViewer.PROJECTION_TYPE.CUBEMAP} 혹은 {@link eg.view360.PanoViewer.PROJECTION_TYPE.CUBESTRIP} 인 경우에만 적용된다. + * @param {Object} [options.cubemapConfig.order = "RLUDBF"(ProjectionType === CUBEMAP) | "RLUDFB" (ProjectionType === CUBESTRIP)] Order of cubemap faces Cubemap 형태의 이미지가 배치된 순서 + * @param {Object} [options.cubemapConfig.tileConfig = { flipHorizontal:false, rotation: 0 }] Setting about rotation angle(degree) and whether to flip horizontal for each cubemap faces, if you put this object as a array, you can set each faces with different setting. For example, [{flipHorizontal:false, rotation:90}, {flipHorizontal: true, rotation: 180}, ...]각 Cubemap 면에 대한 회전 각도/좌우반전 여부 설정, 객체를 배열 형태로 지정하여 각 면에 대한 설정을 다르게 지정할 수도 있다. 예를 들어 [{flipHorizontal:false, rotation:90}, {flipHorizontal: true, rotation: 180}, ...]과 같이 지정할 수 있다. + * @param {String} [options.stereoFormat="3dv"] Contents format of the stereoscopic equirectangular projection.
See {@link eg.view360.PanoViewer.STEREO_FORMAT}.Stereoscopic equirectangular projection type의 콘텐츠 포맷을 설정한다.
{@link eg.view360.PanoViewer.STEREO_FORMAT} 참조.
+ * @param {Number} [options.width=width of container] the viewer's width. (in px) 뷰어의 너비 (px 단위) + * @param {Number} [options.height=height of container] the viewer's height.(in px) 뷰어의 높이 (px 단위) + * + * @param {Number} [options.yaw=0] Initial Yaw of camera (in degree) 카메라의 초기 Yaw (degree 단위) + * @param {Number} [options.pitch=0] Initial Pitch of camera (in degree) 카메라의 초기 Pitch (degree 단위) + * @param {Number} [options.fov=65] Initial vertical field of view of camera (in degree) 카메라의 초기 수직 field of view (degree 단위) + * @param {Boolean} [options.showPolePoint=false] If false, the pole is not displayed inside the viewport false 인 경우, 극점은 뷰포트 내부에 표시되지 않습니다 + * @param {Boolean} [options.useZoom=true] When true, enables zoom with the wheel and Pinch gesture true 일 때 휠 및 집기 제스춰로 확대 / 축소 할 수 있습니다. + * @param {Boolean} [options.useKeyboard=true] When true, enables the keyboard move key control: awsd, arrow keys true 이면 키보드 이동 키 컨트롤을 활성화합니다: awsd, 화살표 키 + * @param {String} [options.gyroMode=yawPitch] Enables control through device motion. ("none", "yawPitch", "VR")
{@link eg.view360.PanoViewer.GYRO_MODE} 디바이스 움직임을 통한 컨트롤을 활성화 합니다. ("none", "yawPitch", "VR")
{@link eg.view360.PanoViewer.GYRO_MODE}
+ * @param {Array} [options.yawRange=[-180, 180]] Range of controllable Yaw values 제어 가능한 Yaw 값의 범위 + * @param {Array} [options.pitchRange=[-90, 90]] Range of controllable Pitch values 제어 가능한 Pitch 값의 범위 + * @param {Array} [options.fovRange=[30, 110]] Range of controllable vertical field of view values 제어 가능한 수직 field of view 값의 범위 + * @param {Number} [options.touchDirection= {@link eg.view360.PanoViewer.TOUCH_DIRECTION.ALL}(6)] Direction of touch that can be controlled by user
{@link eg.view360.PanoViewer.TOUCH_DIRECTION}사용자가 터치로 조작 가능한 방향
{@link eg.view360.PanoViewer.TOUCH_DIRECTION}
+ * + * @example + * ``` + * // PanoViewer Creation + * // create PanoViewer with option + * var PanoViewer = eg.view360.PanoViewer; + * // Area where the image will be displayed(HTMLElement) + * var container = document.getElementById("myPanoViewer"); + * + * var panoViewer = new PanoViewer(container, { + * // If projectionType is not specified, the default is "equirectangular". + * // Specifies an image of the "equirectangular" type. + * image: "/path/to/image/image.jpg" + * }); + * ``` + * + * @example + * ``` + * // Cubemap Config Setting Example + * // For support Youtube EAC projection, You should set cubemapConfig as follows. + * cubemapConfig: { + * order: "LFRDBU", + * tileConfig: [{rotation: 0}, {rotation: 0}, {rotation: 0}, {rotation: 0}, {rotation: -90}, {rotation: 180}] + * } + * ``` + */ + constructor(container: HTMLElement, options: Partial<{ + image: string | HTMLElement; + video: string | HTMLElement; + projectionType: PanoViewer["_projectionType"]; + cubemapConfig: PanoViewer["_cubemapConfig"]; + stereoFormat: PanoViewer["_stereoFormat"]; + width: number; + height: number; + yaw: number; + pitch: number; + fov: number; + showPolePoint: boolean; + useZoom: boolean; + useKeyboard: boolean; + gyroMode: PanoViewer["_gyroMode"]; + yawRange: number[]; + pitchRange: number[]; + fovRange: number[]; + touchDirection: ValueOf; + }> = {}) { + super(); + + // Raises the error event if webgl is not supported. + if (!WebGLUtils.isWebGLAvailable()) { + setTimeout(() => { + this.trigger(EVENTS.ERROR, { + type: ERROR_TYPE.NO_WEBGL, + message: "no webgl support" + }); + }, 0); + return this; + } + + if (!WebGLUtils.isStableWebGL()) { + setTimeout(() => { + this.trigger(EVENTS.ERROR, { + type: ERROR_TYPE.INVALID_DEVICE, + message: "blacklisted browser" + }); + }, 0); + + return this; + } + + if (!!options.image && !!options.video) { + setTimeout(() => { + this.trigger(EVENTS.ERROR, { + type: ERROR_TYPE.INVALID_RESOURCE, + message: "Specifying multi resouces(both image and video) is not valid." + }); + }, 0); + return this; + } + + // Check XR support at not when imported, but when created. + // This is intended to make polyfills easier to use. + checkXRSupport(); + + this._container = container; + this._image = options.image || options.video; + this._isVideo = !!options.video; + this._projectionType = options.projectionType || PROJECTION_TYPE.EQUIRECTANGULAR; + this._cubemapConfig = Object.assign({ + /* RLUDBF is abnormal, we use it on CUBEMAP only for backward compatibility*/ + order: this._projectionType === PROJECTION_TYPE.CUBEMAP ? "RLUDBF" : "RLUDFB", + tileConfig: { + flipHorizontal: false, + rotation: 0 + } + }, options.cubemapConfig); + this._stereoFormat = options.stereoFormat || STEREO_FORMAT.TOP_BOTTOM; + + // If the width and height are not provided, will use the size of the container. + this._width = options.width || parseInt(window.getComputedStyle(container).width, 10); + this._height = options.height || parseInt(window.getComputedStyle(container).height, 10); + + /** + * Cache the direction for the performance in renderLoop + * + * This value should be updated by "change" event of YawPitchControl. + */ + this._yaw = options.yaw || 0; + this._pitch = options.pitch || 0; + this._fov = options.fov || 65; + + this._gyroMode = options.gyroMode || GYRO_MODE.YAWPITCH; + this._quaternion = null; + + this._aspectRatio = this._height !== 0 ? this._width / this._height : 1; + const fovRange = options.fovRange || [30, 110]; + const touchDirection = PanoViewer._isValidTouchDirection(options.touchDirection) ? + options.touchDirection : YawPitchControl.TOUCH_DIRECTION_ALL; + const yawPitchConfig = Object.assign(options, { + element: container, + yaw: this._yaw, + pitch: this._pitch, + fov: this._fov, + gyroMode: this._gyroMode, + fovRange, + aspectRatio: this._aspectRatio, + touchDirection + }); + + this._isReady = false; + + this._initYawPitchControl(yawPitchConfig); + this._initRenderer(this._yaw, this._pitch, this._fov, this._projectionType, this._cubemapConfig); + } + + /** + * Check whether the current environment can execute PanoViewer + * @ko 현재 브라우저 환경에서 PanoViewer 실행이 가능한지 여부를 반환합니다. + * @return PanoViewer executable PanoViewer 실행가능 여부 + */ + public static isSupported(): boolean { + return WebGLUtils.isWebGLAvailable() && WebGLUtils.isStableWebGL(); + } + + /** + * Check whether the current environment supports the WebGL + * @ko 현재 브라우저 환경이 WebGL 을 지원하는지 여부를 확인합니다. + * @return WebGL support WebGL 지원여부 + */ + public static isWebGLAvailable(): boolean { + return WebGLUtils.isWebGLAvailable(); + } + + /** + * Check whether the current environment supports the gyro sensor. + * @ko 현재 브라우저 환경이 자이로 센서를 지원하는지 여부를 확인합니다. + * @param callback Function to take the gyro sensor availability as argument 자이로 센서를 지원하는지 여부를 인자로 받는 함수 + */ + public static isGyroSensorAvailable(callback: (isAvailable: boolean) => any) { + if (!DeviceMotionEvent && callback) { + callback(false); + return; + } + + let onDeviceMotionChange; + + function checkGyro() { + return new Promise((res, rej) => { + onDeviceMotionChange = deviceMotion => { + const isGyroSensorAvailable = !(deviceMotion.rotationRate.alpha == null); + + res(isGyroSensorAvailable); + }; + + window.addEventListener("devicemotion", onDeviceMotionChange); + }); + } + + function timeout() { + return new Promise((res, rej) => { + setTimeout(() => res(false), 1000); + }); + } + + Promise.race([checkGyro(), timeout()]).then((isGyroSensorAvailable: boolean) => { + window.removeEventListener("devicemotion", onDeviceMotionChange); + + if (callback) { + callback(isGyroSensorAvailable); + } + + PanoViewer.isGyroSensorAvailable = fb => { + if (fb) { + fb(isGyroSensorAvailable); + } + return isGyroSensorAvailable; + }; + }); + } + + private static _isValidTouchDirection(direction) { + return direction === PanoViewer.TOUCH_DIRECTION.NONE || + direction === PanoViewer.TOUCH_DIRECTION.YAW || + direction === PanoViewer.TOUCH_DIRECTION.PITCH || + direction === PanoViewer.TOUCH_DIRECTION.ALL; + } + + /** + * Get the video element that the viewer is currently playing. You can use this for playback. + * @ko 뷰어가 현재 사용 중인 비디오 요소를 얻습니다. 이 요소를 이용해 비디오의 컨트롤을 할 수 있습니다. + * @return HTMLVideoElementHTMLVideoElement + * @example + * ``` + * var videoTag = panoViewer.getVideo(); + * videoTag.play(); // play the video! + * ``` + */ + public getVideo() { + if (!this._isVideo) { + return null; + } + + return this._photoSphereRenderer.getContent() as HTMLVideoElement; + } + + /** + * Set the video information to be used by the viewer. + * @ko 뷰어가 사용할 이미지 정보를 설정합니다. + * @param {string|HTMLVideoElement|object} video Input video url or element or config object입력 비디오 URL 혹은 엘리먼트 혹은 설정객체를 활용(image 와 video 둘 중 하나만 설정) + * @param {object} param + * @param {string} [param.projectionType={@link eg.view360.PanoViewer.PROJECTION_TYPE.EQUIRECTANGULAR}("equirectangular")] Projection Type프로젝션 타입 + * @param {object} param.cubemapConfig config cubemap projection layout. cubemap projection type 의 레이아웃 설정 + * @param {string} [param.stereoFormat="3dv"] Contents format of the stereoscopic equirectangular projection. See {@link eg.view360.PanoViewer.STEREO_FORMAT}.Stereoscopic equirectangular projection type의 콘텐츠 포맷을 설정한다. {@link eg.view360.PanoViewer.STEREO_FORMAT} 참조. + * + * @return PanoViewer instancePanoViewer 인스턴스 + * @example + * ``` + * panoViewer.setVideo("/path/to/video/video.mp4", { + * projectionType: eg.view360.PanoViewer.PROJECTION_TYPE.EQUIRECTANGULAR + * }); + * ``` + */ + public setVideo(video: string | HTMLElement | object, param: Partial<{ + projectionType: PanoViewer["_projectionType"]; + cubemapConfig: PanoViewer["_cubemapConfig"]; + stereoFormat: PanoViewer["_stereoFormat"]; + }> = {}) { + if (video) { + this.setImage(video, { + projectionType: param.projectionType, + isVideo: true, + cubemapConfig: param.cubemapConfig, + stereoFormat: param.stereoFormat + }); + } + + return this; + } + + /** + * Get the image information that the viewer is currently using. + * @ko 뷰어가 현재 사용하고있는 이미지 정보를 얻습니다. + * @return Image Object이미지 객체 + * @example + * var imageObj = panoViewer.getImage(); + */ + public getImage() { + if (this._isVideo) { + return null; + } + + return this._photoSphereRenderer.getContent(); + } + + /** + * Set the image information to be used by the viewer. + * @ko 뷰어가 사용할 이미지 정보를 설정합니다. + * @param {string|HTMLElement|object} image Input image url or element or config object입력 이미지 URL 혹은 엘리먼트 혹은 설정객체를 활용(image 와 video 둘 중 하나만 설정한다.) + * @param {object} param Additional information이미지 추가 정보 + * @param {string} [param.projectionType="equirectangular"] Projection Type프로젝션 타입 + * @param {object} param.cubemapConfig config cubemap projection layout. cubemap projection type 레이아웃 + * @param {string} [param.stereoFormat="3dv"] Contents format of the stereoscopic equirectangular projection. See {@link eg.view360.PanoViewer.STEREO_FORMAT}.Stereoscopic equirectangular projection type의 콘텐츠 포맷을 설정한다. {@link eg.view360.PanoViewer.STEREO_FORMAT} 참조. + * @param {boolean} [param.isVideo=false] Whether the given `imaage` is video or not.이미지가 비디오인지 여부 + * + * @return PanoViewer instancePanoViewer 인스턴스 + * @example + * ``` + * panoViewer.setImage("/path/to/image/image.png", { + * projectionType: eg.view360.PanoViewer.PROJECTION_TYPE.CUBEMAP + * }); + * ``` + */ + public setImage(image: string | HTMLElement | object, param: Partial<{ + projectionType: PanoViewer["_projectionType"]; + cubemapConfig: PanoViewer["_cubemapConfig"]; + stereoFormat: PanoViewer["_stereoFormat"]; + isVideo: boolean; + }> = {}) { + const cubemapConfig = Object.assign({ + order: "RLUDBF", + tileConfig: { + flipHorizontal: false, + rotation: 0 + } + }, param.cubemapConfig); + const stereoFormat = param.stereoFormat || STEREO_FORMAT.TOP_BOTTOM; + const isVideo = !!(param.isVideo); + + if (this._image && this._isVideo !== isVideo) { + /* eslint-disable no-console */ + console.warn("PanoViewer is not currently supporting content type changes. (Image <--> Video)"); + /* eslint-enable no-console */ + return this; + } + + if (image) { + this._image = image; + this._isVideo = isVideo; + this._projectionType = param.projectionType || PROJECTION_TYPE.EQUIRECTANGULAR; + this._cubemapConfig = cubemapConfig; + this._stereoFormat = stereoFormat; + + this._deactivate(); + this._initRenderer(this._yaw, this._pitch, this._fov, this._projectionType, this._cubemapConfig); + } + + return this; + } + + /** + * Set whether the renderer always updates the texture and renders. + * @ko 렌더러가 항상 텍스쳐를 갱신하고 화면을 렌더링 할지 여부를 설정할 수 있습니다. + * @param doUpdate When true viewer will always update texture and render, when false viewer will not update texture and render only camera config is changed.true면 항상 텍스쳐를 갱신하고 화면을 그리는 반면, false면 텍스쳐 갱신은 하지 않으며, 카메라 요소에 변화가 있을 때에만 화면을 그립니다. + * @return PanoViewer instancePanoViewer 인스턴스 + */ + public keepUpdate(doUpdate: boolean) { + this._photoSphereRenderer.keepUpdate(doUpdate); + return this; + } + + /** + * Get the current projection type (equirectangular/cube) + * @ko 현재 프로젝션 타입(Equirectangular 혹은 Cube)을 반환합니다. + * @return {@link eg.view360.PanoViewer.PROJECTION_TYPE} + */ + public getProjectionType() { + return this._projectionType; + } + + /** + * Activate the device's motion sensor, and return the Promise whether the sensor is enabled + * If it's iOS13+, this method must be used in the context of user interaction, like onclick callback on the button element. + * @ko 디바이스의 모션 센서를 활성화하고, 활성화 여부를 담는 Promise를 리턴합니다. + * iOS13+일 경우, 사용자 인터렉션에 의해서 호출되어야 합니다. 예로, 버튼의 onclick 콜백과 같은 콘텍스트에서 호출되어야 합니다. + * @return Promise containing nothing when resolved, or string of the rejected reason when rejected.Promise. resolve되었을 경우 아무것도 반환하지 않고, reject되었을 경우 그 이유를 담고있는 string을 반환한다. + */ + public enableSensor() { + return new Promise((resolve, reject) => { + if (DeviceMotionEvent && typeof DeviceMotionEvent.requestPermission === "function") { + DeviceMotionEvent.requestPermission().then(permissionState => { + if (permissionState === "granted") { + resolve(); + } else { + reject(new Error("permission denied")); + } + }).catch(e => { + // This can happen when this method wasn't triggered by user interaction + reject(e); + }); + } else { + resolve(); + } + }); + } + + /** + * Disable the device's motion sensor. + * @ko 디바이스의 모션 센서를 비활성화합니다. + * @deprecated + * @return PanoViewer instancePanoViewer 인스턴스 + */ + public disableSensor() { + return this; + } + + /** + * Switch to VR stereo rendering mode which uses WebXR / WebVR API (WebXR is preferred). + * This method must be used in the context of user interaction, like onclick callback on the button element. + * It can be rejected when an enabling device sensor fails or image/video is still loading("ready" event not triggered). + * @ko WebXR / WebVR API를 사용하는 VR 스테레오 렌더링 모드로 전환합니다. (WebXR을 더 선호합니다) + * 이 메소드는 사용자 인터렉션에 의해서 호출되어야 합니다. 예로, 버튼의 onclick 콜백과 같은 콘텍스트에서 호출되어야 합니다. + * 디바이스 센서 활성화에 실패시 혹은 아직 이미지/비디오가 로딩중인 경우("ready"이벤트가 아직 트리거되지 않은 경우)에는 Promise가 reject됩니다. + * @param {object} [options={}] Additional options for WebXR session, see {@link https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit XRSessionInit}.WebXR용 추가 옵션, {@link https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit XRSessionInit}을 참조해주세요. + * @return Promise containing either a string of resolved reason or an Error instance of rejected reason.Promise가 resolve된 이유(string) 혹은 reject된 이유(Error) + */ + public enterVR(options: { + requiredFeatures?: any[]; + optionalFeatures?: any[]; + [key: string]: any; + } = {}): Promise { + if (!this._isReady) { + return Promise.reject(new Error("PanoViewer is not ready to show image.")); + } + + return new Promise((resolve, reject) => { + this.enableSensor() + .then(() => this._photoSphereRenderer.enterVR(options)) + .then((res: string) => resolve(res)) + .catch(e => reject(e)); + }); + } + + /** + * Exit VR stereo rendering mode. + * @ko VR 스테레오 렌더링 모드에서 일반 렌더링 모드로 전환합니다. + * @return PanoViewer instancePanoViewer 인스턴스 + */ + public exitVR() { + this._photoSphereRenderer.exitVR(); + return this; + } + + /** + * When set true, enables zoom with the wheel or pinch gesture. However, in the case of touch, pinch works only when the touchDirection setting is {@link eg.view360.PanoViewer.TOUCH_DIRECTION.ALL}. + * @ko true 로 설정 시 휠 혹은 집기 동작으로 확대/축소 할 수 있습니다. false 설정 시 확대/축소 기능을 비활성화 합니다. 단, 터치인 경우 touchDirection 설정이 {@link eg.view360.PanoViewer.TOUCH_DIRECTION.ALL} 인 경우에만 pinch 가 동작합니다. + * @param useZoom + * @return PanoViewer instancePanoViewer 인스턴스 + */ + public setUseZoom(useZoom: boolean): this { + if (typeof useZoom === "boolean") { + this._yawPitchControl.option("useZoom", useZoom); + } + + return this; + } + + /** + * When true, enables the keyboard move key control: awsd, arrow keys + * @ko true이면 키보드 이동 키 컨트롤을 활성화합니다. (awsd, 화살표 키) + * @param useKeyboard + * @return PanoViewer instancePanoViewer 인스턴스 + */ + public setUseKeyboard(useKeyboard: boolean): this { + this._yawPitchControl.option("useKeyboard", useKeyboard); + return this; + } + + /** + * Enables control through device motion. ("none", "yawPitch", "VR") + * @ko 디바이스 움직임을 통한 컨트롤을 활성화 합니다. ("none", "yawPitch", "VR") + * @param gyroMode {@link eg.view360.PanoViewer.GYRO_MODE} + * @return PanoViewer instancePanoViewer 인스턴스 + * @example + * ``` + * panoViewer.setGyroMode("yawPitch"); + * //equivalent + * panoViewer.setGyroMode(eg.view360.PanoViewer.GYRO_MODE.YAWPITCH); + * ``` + */ + public setGyroMode(gyroMode: PanoViewer["_gyroMode"]) { + this._yawPitchControl.option("gyroMode", gyroMode); + return this; + } + + /** + * Set the range of controllable FOV values + * @ko 제어 가능한 FOV 구간을 설정합니다. + * @param range + * @return PanoViewer instancePanoViewer 인스턴스 + * @example + * panoViewer.setFovRange([50, 90]); + */ + public setFovRange(range: number[]) { + this._yawPitchControl.option("fovRange", range); + return this; + } + + /** + * Get the range of controllable FOV values + * @ko 제어 가능한 FOV 구간을 반환합니다. + * @return FOV range + * @example + * var range = panoViewer.getFovRange(); // [50, 90] + */ + public getFovRange(): [number, number] { + return this._yawPitchControl.option("fovRange"); + } + + /** + * Update size of canvas element by it's container element's or specified size. If size is not specified, the size of the container area is obtained and updated to that size. + * @ko 캔버스 엘리먼트의 크기를 컨테이너 엘리먼트의 크기나 지정된 크기로 업데이트합니다. 만약 size 가 지정되지 않으면 컨테이너 영역의 크기를 얻어와 해당 크기로 갱신합니다. + * @param {object} [size] + * @param {number} [size.width=width of the container] + * @param {number} [size.height=height of the container] + * @return PanoViewer instancePanoViewer 인스턴스 + */ + public updateViewportDimensions(size: Partial<{ + width: number; + height: number; + }> = {}): this { + if (!this._isReady) { + return this; + } + + let containerSize; + + if (size.width === undefined || size.height === undefined) { + containerSize = window.getComputedStyle(this._container); + } + + const width = size.width || parseInt(containerSize.width, 10); + const height = size.height || parseInt(containerSize.height, 10); + + // Skip if viewport is not changed. + if (width === this._width && height === this._height) { + return this; + } + + this._width = width; + this._height = height; + + this._aspectRatio = width / height; + this._photoSphereRenderer.updateViewportDimensions(width, height); + this._yawPitchControl.option("aspectRatio", this._aspectRatio); + this._yawPitchControl.updatePanScale({height}); + + this.lookAt({}, 0); + return this; + } + + /** + * Get the current field of view(FOV) + * @ko 현재 field of view(FOV) 값을 반환합니다. + */ + public getFov(): number { + return this._fov; + } + + /** + * Get current yaw value + * @ko 현재 yaw 값을 반환합니다. + */ + public getYaw() { + return this._yaw; + } + + /** + * Get current pitch value + * @ko 현재 pitch 값을 반환합니다. + */ + public getPitch() { + return this._pitch; + } + + /** + * Get the range of controllable Yaw values + * @ko 컨트롤 가능한 Yaw 구간을 반환합니다. + */ + public getYawRange(): [number, number] { + return this._yawPitchControl.option("yawRange"); + } + + /** + * Get the range of controllable Pitch values + * @ko 컨트롤 가능한 Pitch 구간을 가져옵니다. + */ + public getPitchRange(): [number, number] { + return this._yawPitchControl.option("pitchRange"); + } + + /** + * Set the range of controllable yaw + * @ko 컨트롤 가능한 Yaw 구간을 반환합니다. + * @param {number[]} range + * @return PanoViewer instancePanoViewer 인스턴스 + * @example + * panoViewer.setYawRange([-90, 90]); + */ + public setYawRange(yawRange: number[]) { + this._yawPitchControl.option("yawRange", yawRange); + return this; + } + + /** + * Set the range of controllable Pitch values + * @ko 컨트롤 가능한 Pitch 구간을 설정합니다. + * @param {number[]} range + * @return PanoViewer instancePanoViewer 인스턴스 + * @example + * panoViewer.setPitchRange([-40, 40]); + */ + public setPitchRange(pitchRange: number[]) { + this._yawPitchControl.option("pitchRange", pitchRange); + return this; + } + + /** + * Specifies whether to display the pole by limiting the pitch range. If it is true, pole point can be displayed. If it is false, it is not displayed. + * @ko pitch 범위를 제한하여 극점을 표시할지를 지정합니다. true 인 경우 극점까지 표현할 수 있으며 false 인 경우 극점까지 표시하지 않습니다. + * @param showPolePoint + * @return PanoViewer instancePanoViewer 인스턴스 + */ + public setShowPolePoint(showPolePoint: boolean) { + this._yawPitchControl.option("showPolePoint", showPolePoint); + return this; + } + + /** + * Set a new view by setting camera configuration. Any parameters not specified remain the same. + * @ko 카메라 설정을 지정하여 화면을 갱신합니다. 지정되지 않은 매개 변수는 동일하게 유지됩니다. + * @param {object} orientation + * @param {number} orientation.yaw Target yaw in degree 목표 yaw (degree 단위) + * @param {number} orientation.pitch Target pitch in degree 목표 pitch (degree 단위) + * @param {number} orientation.fov Target vertical fov in degree 목표 수직 fov (degree 단위) + * @param {number} duration Animation duration in milliseconds 애니메이션 시간 (밀리 초) + * @return PanoViewer instancePanoViewer 인스턴스 + * @example + * ``` + * // Change the yaw angle (absolute angle) to 30 degrees for one second. + * panoViewer.lookAt({yaw: 30}, 1000); + * ``` + */ + public lookAt(orientation: Partial<{ + yaw: number; + pitch: number; + fov: number; + }>, duration: number = 0) { + if (!this._isReady) { + return this; + } + + const yaw = orientation.yaw !== undefined ? orientation.yaw : this._yaw; + const pitch = orientation.pitch !== undefined ? orientation.pitch : this._pitch; + const pitchRange = this._yawPitchControl.option("pitchRange"); + const verticalAngleOfImage = pitchRange[1] - pitchRange[0]; + let fov = orientation.fov !== undefined ? orientation.fov : this._fov; + + if (verticalAngleOfImage < fov) { + fov = verticalAngleOfImage; + } + + this._yawPitchControl.lookAt({yaw, pitch, fov}, duration); + + if (duration === 0) { + this._photoSphereRenderer.renderWithYawPitch(yaw, pitch, fov); + } + return this; + } + + /** + * Set touch direction by which user can control. + * @ko 사용자가 조작가능한 터치 방향을 지정합니다. + * @param direction of the touch. {@link eg.view360.PanoViewer.TOUCH_DIRECTION}컨트롤 가능한 방향 {@link eg.view360.PanoViewer.TOUCH_DIRECTION} + * @return PanoViewer instance + * @example + * ``` + * panoViewer = new PanoViewer(el); + * // Limit the touch direction to the yaw direction only. + * panoViewer.setTouchDirection(eg.view360.PanoViewer.TOUCH_DIRECTION.YAW); + * ``` + */ + public setTouchDirection(direction: number): this { + if (PanoViewer._isValidTouchDirection(direction)) { + this._yawPitchControl.option("touchDirection", direction); + } + + return this; + } + + /** + * Returns touch direction by which user can control + * @ko 사용자가 조작가능한 터치 방향을 반환한다. + * @return direction of the touch. {@link eg.view360.PanoViewer.TOUCH_DIRECTION}컨트롤 가능한 방향 {@link eg.view360.PanoViewer.TOUCH_DIRECTION} + * @example + * ``` + * panoViewer = new PanoViewer(el); + * // Returns the current touch direction. + * var dir = panoViewer.getTouchDirection(); + * ``` + */ + public getTouchDirection(): number { + return this._yawPitchControl.option("touchDirection"); + } + + /** + * Destroy viewer. Remove all registered event listeners and remove viewer canvas. + * @ko 뷰어 인스턴스를 해제합니다. 모든 등록된 이벤트리스너를 제거하고 뷰어 캔버스를 삭제합니다. + * @return PanoViewer instancePanoViewer 인스턴스 + */ + public destroy(): this { + this._deactivate(); + + if (this._yawPitchControl) { + this._yawPitchControl.destroy(); + this._yawPitchControl = null; + } + + return this; + } + + // TODO: Remove parameters as they're just using private values + private _initRenderer( + yaw: number, + pitch: number, + fov: number, + projectionType: PanoViewer["_projectionType"], + cubemapConfig: PanoViewer["_cubemapConfig"] + ) { + this._photoSphereRenderer = new PanoImageRenderer( + this._image, + this._width, + this._height, + this._isVideo, + { + initialYaw: yaw, + initialPitch: pitch, + fieldOfView: fov, + imageType: projectionType, + cubemapConfig, + stereoFormat: this._stereoFormat + }, + ); + this._photoSphereRenderer.setYawPitchControl(this._yawPitchControl); + + this._bindRendererHandler(); + + this._photoSphereRenderer + .bindTexture() + .then(() => this._activate()) + .catch(() => { + this.trigger(EVENTS.ERROR, { + type: ERROR_TYPE.FAIL_BIND_TEXTURE, + message: "failed to bind texture" + }); + }); + } + + /** + * @private + * update values of YawPitchControl if needed. + * For example, In Panorama mode, initial fov and pitchRange is changed by aspect ratio of image. + * + * This function should be called after isReady status is true. + */ + private _updateYawPitchIfNeeded() { + if (this._projectionType === PanoViewer.ProjectionType.PANORAMA) { + // update fov by aspect ratio + const image = this._photoSphereRenderer.getContent(); + let imageAspectRatio = image.naturalWidth / image.naturalHeight; + let isCircular; + let yawSize; + let maxFov; + + // If height is larger than width, then we assume it's rotated by 90 degree. + if (imageAspectRatio < 1) { + // So inverse the aspect ratio. + imageAspectRatio = 1 / imageAspectRatio; + } + + if (imageAspectRatio < 6) { + yawSize = mathUtil.toDegree(imageAspectRatio); + isCircular = false; + // 0.5 means ratio of half height of cylinder(0.5) and radius of cylider(1). 0.5/1 = 0.5 + maxFov = mathUtil.toDegree(Math.atan(0.5)) * 2; + } else { + yawSize = 360; + isCircular = true; + maxFov = (360 / imageAspectRatio); // Make it 5 fixed as axes does. + } + + // console.log("_updateYawPitchIfNeeded", maxFov, "aspectRatio", image.naturalWidth, image.naturalHeight, "yawSize", yawSize); + const minFov = (this._yawPitchControl.option("fovRange"))[0]; + + // this option should be called after fov is set. + this._yawPitchControl.option({ + "fov": maxFov, /* parameter for internal validation for pitchrange */ + "yawRange": [-yawSize / 2, yawSize / 2], + isCircular, + "pitchRange": [-maxFov / 2, maxFov / 2], + "fovRange": [minFov, maxFov] + }); + this.lookAt({fov: maxFov}); + } + } + + private _bindRendererHandler() { + this._photoSphereRenderer.on(PanoImageRenderer.EVENTS.ERROR, e => { + this.trigger(EVENTS.ERROR, e); + }); + + this._photoSphereRenderer.on(PanoImageRenderer.EVENTS.RENDERING_CONTEXT_LOST, e => { + this._deactivate(); + this.trigger(EVENTS.ERROR, { + type: ERROR_TYPE.RENDERING_CONTEXT_LOST, + message: "webgl rendering context lost" + }); + }); + } + + private _initYawPitchControl(yawPitchConfig) { + this._yawPitchControl = new YawPitchControl(yawPitchConfig); + + this._yawPitchControl.on(EVENTS.ANIMATION_END, e => { + this.trigger(EVENTS.ANIMATION_END, e); + }); + + this._yawPitchControl.on("change", e => { + this._yaw = e.yaw; + this._pitch = e.pitch; + this._fov = e.fov; + this._quaternion = e.quaternion; + + this.trigger(EVENTS.VIEW_CHANGE, e); + }); + } + + + /** + * Get the horizontal field of view in degree + */ + private _getHFov() { + return mathUtil.toDegree( + 2 * Math.atan(this._aspectRatio * Math.tan(glMatrix.toRadian(this._fov) / 2))); + } + + private _activate() { + this._photoSphereRenderer.attachTo(this._container); + this._yawPitchControl.enable(); + + this.updateViewportDimensions(); + + this._isReady = true; + + // update yawPitchControl after isReady status is true. + this._updateYawPitchIfNeeded(); + + this.trigger(EVENTS.READY); + this._photoSphereRenderer.startRender(); + } + + /** + * Destroy webgl context and block user interaction and stop rendering + */ + private _deactivate() { + if (this._isReady) { + this._photoSphereRenderer.stopRender(); + this._yawPitchControl.disable(); + this._isReady = false; + } + + if (this._photoSphereRenderer) { + this._photoSphereRenderer.destroy(); + this._photoSphereRenderer = null; + } + } +} + +export default PanoViewer; + diff --git a/src/PanoViewer/consts.js b/src/PanoViewer/consts.ts similarity index 94% rename from src/PanoViewer/consts.js rename to src/PanoViewer/consts.ts index e1c576501..428d1520c 100644 --- a/src/PanoViewer/consts.js +++ b/src/PanoViewer/consts.ts @@ -32,7 +32,7 @@ * @type {String} * @default "VR" */ -import {GYRO_MODE} from "../YawPitchControl/consts"; +import { GYRO_MODE } from "../YawPitchControl/consts"; /** * Constant value for errors @@ -111,7 +111,12 @@ const ERROR_TYPE = { * @name EVENTS * @memberof eg.view360.PanoViewer */ -const EVENTS = { +const EVENTS: { + READY: "ready"; + VIEW_CHANGE: "viewChange"; + ANIMATION_END: "animationEnd"; + ERROR: "error"; +} = { /** * Events that is fired when PanoViewer is ready to show image and handle user interaction. * @ko PanoViewer 가 사용자의 인터렉션 및 렌더링이 준비되상태에 발생하는 이벤트 @@ -161,7 +166,13 @@ const EVENTS = { * @name PROJECTION_TYPE * @memberof eg.view360.PanoViewer */ -const PROJECTION_TYPE = { +const PROJECTION_TYPE: { + EQUIRECTANGULAR: "equirectangular"; + CUBEMAP: "cubemap"; + CUBESTRIP: "cubestrip"; + PANORAMA: "panorama"; + STEREOSCOPIC_EQUI: "stereoequi"; +} = { /** * Constant value for equirectangular type. * @ko equirectangular 에 대한 상수 값. @@ -231,7 +242,11 @@ const PROJECTION_TYPE = { * @name STEREO_FORMAT * @memberof eg.view360.PanoViewer */ -const STEREO_FORMAT = { +const STEREO_FORMAT: { + TOP_BOTTOM: "3dv"; + LEFT_RIGHT: "3dh"; + NONE: ""; +} = { /** * A constant value for format of top bottom stereoscopic 360 equirectangular projection. * @ko top bottom stereoscopic 360 equirectangular projection 콘텐츠 포맷에 대한 상수값. diff --git a/src/YawPitchControl/YawPitchControl.ts b/src/YawPitchControl/YawPitchControl.ts new file mode 100644 index 000000000..15d8db109 --- /dev/null +++ b/src/YawPitchControl/YawPitchControl.ts @@ -0,0 +1,673 @@ +import Component from "@egjs/component"; +import Axes, { PinchInput, MoveKeyInput, WheelInput } from "@egjs/axes"; +import { vec2, glMatrix } from "gl-matrix"; +import { getComputedStyle, SUPPORT_TOUCH, SUPPORT_DEVICEMOTION } from "../utils/browserFeature"; +import TiltMotionInput from "./input/TiltMotionInput"; +import RotationPanInput from "./input/RotationPanInput"; +import DeviceQuaternion from "./DeviceQuaternion"; +import { util as mathUtil } from "../utils/math-util"; +import { + GYRO_MODE, + TOUCH_DIRECTION_YAW, + TOUCH_DIRECTION_PITCH, + TOUCH_DIRECTION_ALL, + MC_DECELERATION, + MC_MAXIMUM_DURATION, + MC_BIND_SCALE, + MAX_FIELD_OF_VIEW, + PAN_SCALE, + YAW_RANGE_HALF, + PITCH_RANGE_HALF, + CIRCULAR_PITCH_RANGE_HALF, + CONTROL_MODE_VR, + CONTROL_MODE_YAWPITCH, + TOUCH_DIRECTION_NONE, +} from "./consts"; +import { VERSION } from "../version"; + +const DEFAULT_YAW_RANGE = [-YAW_RANGE_HALF, YAW_RANGE_HALF]; +const DEFAULT_PITCH_RANGE = [-PITCH_RANGE_HALF, PITCH_RANGE_HALF]; +const CIRCULAR_PITCH_RANGE = [-CIRCULAR_PITCH_RANGE_HALF, CIRCULAR_PITCH_RANGE_HALF]; + +/** + * A module used to provide coordinate based on yaw/pitch orientation. This module receives user touch action, keyboard, mouse and device orientation(if it exists) as input, then combines them and converts it to yaw/pitch coordinates. + * + * @alias eg.YawPitchControl + * @extends eg.Component + * + * @support {"ie": "10+", "ch" : "latest", "ff" : "latest", "sf" : "latest", "edge" : "latest", "ios" : "7+", "an" : "2.3+ (except 3.x)"} + */ +class YawPitchControl extends Component { + static VERSION = VERSION; + // Expose DeviceOrientationControls sub module for test purpose + static CONTROL_MODE_VR = CONTROL_MODE_VR; + static CONTROL_MODE_YAWPITCH = CONTROL_MODE_YAWPITCH; + static TOUCH_DIRECTION_ALL = TOUCH_DIRECTION_ALL; + static TOUCH_DIRECTION_YAW = TOUCH_DIRECTION_YAW; + static TOUCH_DIRECTION_PITCH = TOUCH_DIRECTION_PITCH; + static TOUCH_DIRECTION_NONE = TOUCH_DIRECTION_NONE; + /** + * @param {Object} options The option object of the eg.YawPitch module + * @param {Element}[options.element=null] element A base element for the eg.YawPitch module + * @param {Number} [options.yaw=0] initial yaw (degree) + * @param {Number} [options.pitch=0] initial pitch (degree) + * @param {Number} [options.fov=65] initial field of view (degree) + * @param {Boolean} [optiosn.showPolePoint=true] Indicates whether pole is shown + * @param {Boolean} [options.useZoom=true] Indicates whether zoom is available + * @param {Boolean} [options.useKeyboard=true] Indicates whether keyboard is enabled + * @param {String} [config.gyroMode=yawPitch] Enables control through device motion. + * @param {Number} [options.touchDirection=TOUCH_DIRECTION_ALL] Direction of the touch movement (TOUCH_DIRECTION_ALL: all, TOUCH_DIRECTION_YAW: horizontal, TOUCH_DIRECTION_PITCH: vertical, TOUCH_DIRECTION_NONE: no move) + * @param {Array} [options.yawRange=[-180, 180] Range of visible yaw + * @param {Array} [options.pitchRange=[-90, 90] Range of visible pitch + * @param {Array} [options.fovRange=[30, 110] Range of FOV + * @param {Number} [options.aspectRatio=1] Aspect Ratio + */ + constructor(options) { + super(); + + const opt = Object.assign({ + element: null, + yaw: 0, + pitch: 0, + fov: 65, + showPolePoint: false, + useZoom: true, + useKeyboard: true, + gyroMode: GYRO_MODE.YAWPITCH, + touchDirection: TOUCH_DIRECTION_ALL, + yawRange: DEFAULT_YAW_RANGE, + pitchRange: DEFAULT_PITCH_RANGE, + fovRange: [30, 110], + aspectRatio: 1, /* TODO: Need Mandatory? */ + }, options); + + this._element = opt.element; + this._initialFov = opt.fov; + this._enabled = false; + this._isAnimating = false; + this._deviceQuaternion = null; + + this._initAxes(opt); + this.option(opt); + } + + _initAxes(opt) { + const yRange = this._updateYawRange(opt.yawRange, opt.fov, opt.aspectRatio); + const pRange = this._updatePitchRange(opt.pitchRange, opt.fov, opt.showPolePoint); + const useRotation = opt.gyroMode === GYRO_MODE.VR; + + this.axesPanInput = new RotationPanInput(this._element, {useRotation}); + this.axesWheelInput = new WheelInput(this._element, {scale: -4}); + this.axesTiltMotionInput = null; + this.axesPinchInput = SUPPORT_TOUCH ? new PinchInput(this._element, {scale: -1}) : null; + this.axesMoveKeyInput = new MoveKeyInput(this._element, {scale: [-6, 6]}); + + this.axes = new Axes({ + yaw: { + range: yRange, + circular: YawPitchControl.isCircular(yRange), + bounce: [0, 0] + }, + pitch: { + range: pRange, + circular: YawPitchControl.isCircular(pRange), + bounce: [0, 0] + }, + fov: { + range: opt.fovRange, + circular: [false, false], + bounce: [0, 0] + }, + }, { + deceleration: MC_DECELERATION, + maximumDuration: MC_MAXIMUM_DURATION + }, { + yaw: opt.yaw, + pitch: opt.pitch, + fov: opt.fov + }).on({ + hold: evt => { + // Restore maximumDuration not to be spin too mush. + this.axes.options.maximumDuration = MC_MAXIMUM_DURATION; + + this.trigger("hold", {isTrusted: evt.isTrusted}); + }, + change: evt => { + if (evt.delta.fov !== 0) { + this._updateControlScale(evt); + this.updatePanScale(); + } + this._triggerChange(evt); + }, + release: evt => { + this._triggerChange(evt); + }, + animationStart: evt => { + }, + animationEnd: evt => { + this.trigger("animationEnd", {isTrusted: evt.isTrusted}); + }, + }); + } + + /** + * Update Pan Scale + * + * Scale(Sensitivity) values of panning is related with fov and height. + * If at least one of them is changed, this function need to be called. + * @param {*} param + */ + updatePanScale(param = {}) { + const fov = this.axes.get().fov; + const areaHeight = param.height || parseInt(getComputedStyle(this._element).height, 10); + const scale = MC_BIND_SCALE[0] * fov / this._initialFov * PAN_SCALE / areaHeight; + + this.axesPanInput.options.scale = [scale, scale]; + this.axes.options.deceleration = MC_DECELERATION * fov / MAX_FIELD_OF_VIEW; + + return this; + } + + /* + * Override component's option method + * to call method for updating values which is affected by option change. + * + * @param {*} args + */ + option(...args) { + const argLen = args.length; + + // Getter + if (argLen === 0) { + return this._getOptions(); + } else if (argLen === 1 && typeof args[0] === "string") { + return this._getOptions(args[0]); + } + + // Setter + const beforeOptions = Object.assign({}, this.options); + let newOptions = {}; + let changedKeyList = []; // TODO: if value is not changed, then do not push on changedKeyList. + + if (argLen === 1) { + changedKeyList = Object.keys(args[0]); + newOptions = Object.assign({}, args[0]); + } else if (argLen >= 2) { + changedKeyList.push(args[0]); + newOptions[args[0]] = args[1]; + } + + this._setOptions(this._getValidatedOptions(newOptions)); + this._applyOptions(changedKeyList, beforeOptions); + return this; + } + + _getValidatedOptions(newOptions) { + if (newOptions.yawRange) { + newOptions.yawRange = + this._getValidYawRange(newOptions.yawRange, newOptions.fov, newOptions.aspectRatio); + } + if (newOptions.pitchRange) { + newOptions.pitchRange = this._getValidPitchRange(newOptions.pitchRange, newOptions.fov); + } + return newOptions; + } + + _getOptions(key) { + let value; + + if (typeof key === "string") { + value = this.options[key]; + } else if (arguments.length === 0) { + value = this.options; + } + return value; + } + + _setOptions(options) { + for (const key in options) { + this.options[key] = options[key]; + } + } + + _applyOptions(keys, prevOptions) { + const options = this.options; + const axes = this.axes; + const isVR = options.gyroMode === GYRO_MODE.VR; + const isYawPitch = options.gyroMode === GYRO_MODE.YAWPITCH; + // If it's VR mode, restrict user interaction to yaw direction only + const touchDirection = isVR ? + (TOUCH_DIRECTION_YAW & options.touchDirection) : + options.touchDirection; + + // If one of below is changed, call updateControlScale() + if (keys.some(key => + key === "showPolePoint" || key === "fov" || key === "aspectRatio" || + key === "yawRange" || key === "pitchRange" + )) { + // If fov is changed, update pan scale + if (keys.indexOf("fov") >= 0) { + axes.setTo({"fov": options.fov}); + this.updatePanScale(); + } + + this._updateControlScale(); + } + + if (keys.some(key => key === "fovRange")) { + const fovRange = options.fovRange; + const prevFov = axes.get().fov; + let nextFov = axes.get().fov; + + vec2.copy(axes.axis.fov.range, fovRange); + + if (nextFov < fovRange[0]) { + nextFov = fovRange[0]; + } else if (prevFov > fovRange[1]) { + nextFov = fovRange[1]; + } + + if (prevFov !== nextFov) { + axes.setTo({ + fov: nextFov + }, 0); + this._updateControlScale(); + this.updatePanScale(); + } + } + + if (keys.some(key => key === "gyroMode") && SUPPORT_DEVICEMOTION) { + // Disconnect first + if (this.axesTiltMotionInput) { + this.axes.disconnect(this.axesTiltMotionInput); + this.axesTiltMotionInput.destroy(); + this.axesTiltMotionInput = null; + } + + if (this._deviceQuaternion) { + this._deviceQuaternion.destroy(); + this._deviceQuaternion = null; + } + + if (isVR) { + this._initDeviceQuaternion(); + } else if (isYawPitch) { + this.axesTiltMotionInput = new TiltMotionInput(this._element); + this.axes.connect(["yaw", "pitch"], this.axesTiltMotionInput); + } + + this.axesPanInput.setUseRotation(isVR); + } + + if (keys.some(key => key === "useKeyboard")) { + const useKeyboard = options.useKeyboard; + + if (useKeyboard) { + axes.connect(["yaw", "pitch"], this.axesMoveKeyInput); + } else { + axes.disconnect(this.axesMoveKeyInput); + } + } + + if (keys.some(key => key === "useZoom")) { + const useZoom = options.useZoom; + + // Disconnect first + axes.disconnect(this.axesWheelInput); + if (useZoom) { + axes.connect(["fov"], this.axesWheelInput); + } + } + + this._togglePinchInputByOption(options.touchDirection, options.useZoom); + + if (keys.some(key => key === "touchDirection")) { + this._enabled && this._enableTouch(touchDirection); + } + } + + _togglePinchInputByOption(touchDirection, useZoom) { + if (this.axesPinchInput) { + // disconnect first + this.axes.disconnect(this.axesPinchInput); + + // If the touchDirection option is not ALL, pinchInput should be disconnected to make use of a native scroll. + if ( + useZoom && + touchDirection === TOUCH_DIRECTION_ALL && + // TODO: Get rid of using private property of axes instance. + this.axes._inputs.indexOf(this.axesPinchInput) === -1 + ) { + this.axes.connect(["fov"], this.axesPinchInput); + } + } + } + + _enableTouch(direction) { + // Disconnect first + this.axesPanInput && this.axes.disconnect(this.axesPanInput); + + const yawEnabled = direction & TOUCH_DIRECTION_YAW ? "yaw" : null; + const pitchEnabled = direction & TOUCH_DIRECTION_PITCH ? "pitch" : null; + + this.axes.connect([yawEnabled, pitchEnabled], this.axesPanInput); + } + + _initDeviceQuaternion() { + this._deviceQuaternion = new DeviceQuaternion(); + this._deviceQuaternion.on("change", e => { + this._triggerChange(e); + }); + } + + _getValidYawRange(newYawRange, newFov, newAspectRatio) { + const ratio = YawPitchControl.adjustAspectRatio(newAspectRatio || this.options.aspectRatio || 1); + const fov = newFov || this.axes.get().fov; + const horizontalFov = fov * ratio; + const isValid = newYawRange[1] - newYawRange[0] >= horizontalFov; + + if (isValid) { + return newYawRange; + } else { + return this.options.yawRange || DEFAULT_YAW_RANGE; + } + } + + _getValidPitchRange(newPitchRange, newFov) { + const fov = newFov || this.axes.get().fov; + const isValid = newPitchRange[1] - newPitchRange[0] >= fov; + + if (isValid) { + return newPitchRange; + } else { + return this.options.pitchRange || DEFAULT_PITCH_RANGE; + } + } + + static isCircular(range) { + return range[1] - range[0] < 360 ? [false, false] : [true, true]; + } + + /** + * Update yaw/pitch min/max by 5 factor + * + * 1. showPolePoint + * 2. fov + * 3. yawRange + * 4. pitchRange + * 5. aspectRatio + * + * If one of above is changed, call this function + */ + _updateControlScale(changeEvt) { + const opt = this.options; + const fov = this.axes.get().fov; + + const pRange = this._updatePitchRange(opt.pitchRange, fov, opt.showPolePoint); + const yRange = this._updateYawRange(opt.yawRange, fov, opt.aspectRatio); + + // TODO: If not changed!? + const pos = this.axes.get(); + let y = pos.yaw; + let p = pos.pitch; + + vec2.copy(this.axes.axis.yaw.range, yRange); + vec2.copy(this.axes.axis.pitch.range, pRange); + this.axes.axis.yaw.circular = YawPitchControl.isCircular(yRange); + this.axes.axis.pitch.circular = YawPitchControl.isCircular(pRange); + + /** + * update yaw/pitch by it's range. + */ + if (y < yRange[0]) { + y = yRange[0]; + } else if (y > yRange[1]) { + y = yRange[1]; + } + + if (p < pRange[0]) { + p = pRange[0]; + } else if (p > pRange[1]) { + p = pRange[1]; + } + + if (changeEvt) { + changeEvt.set({ + yaw: y, + pitch: p, + }); + } + + this.axes.setTo({ + yaw: y, + pitch: p, + }, 0); + + return this; + } + + _updatePitchRange(pitchRange, fov, showPolePoint) { + if (this.options.gyroMode === GYRO_MODE.VR) { + // Circular pitch on VR + return CIRCULAR_PITCH_RANGE; + } + + const verticalAngle = pitchRange[1] - pitchRange[0]; + const halfFov = fov / 2; + const isPanorama = verticalAngle < 180; + + if (showPolePoint && !isPanorama) { + // Use full pinch range + return pitchRange.concat(); + } + + // Round value as movableCood do. + return [pitchRange[0] + halfFov, pitchRange[1] - halfFov]; + } + + _updateYawRange(yawRange, fov, aspectRatio) { + if (this.options.gyroMode === GYRO_MODE.VR) { + return DEFAULT_YAW_RANGE; + } + + const horizontalAngle = yawRange[1] - yawRange[0]; + + /** + * Full 360 Mode + */ + if (horizontalAngle >= 360) { + // Don't limit yaw range on Full 360 mode. + return yawRange.concat(); + } + + /** + * Panorama mode + */ + // Ref : https://github.com/naver/egjs-view360/issues/290 + const halfHorizontalFov = + mathUtil.toDegree(Math.atan2(aspectRatio, 1 / Math.tan(glMatrix.toRadian(fov / 2)))); + + // Round value as movableCood do. + return [ + yawRange[0] + halfHorizontalFov, + yawRange[1] - halfHorizontalFov + ]; + } + + _triggerChange(evt) { + const pos = this.axes.get(); + const opt = this.options; + const event = { + targetElement: opt.element, + isTrusted: evt.isTrusted, + }; + + event.yaw = pos.yaw; + event.pitch = pos.pitch; + event.fov = pos.fov; + + if (opt.gyroMode === GYRO_MODE.VR && this._deviceQuaternion) { + event.quaternion = this._deviceQuaternion.getCombinedQuaternion(pos.yaw); + } + this.trigger("change", event); + } + + // TODO: makes constant to be logic + static adjustAspectRatio(input) { + const inputRange = [ + 0.520, 0.540, 0.563, 0.570, 0.584, 0.590, 0.609, 0.670, + 0.702, 0.720, 0.760, 0.780, 0.820, 0.920, 0.970, 1.00, 1.07, 1.14, 1.19, + 1.25, 1.32, 1.38, 1.40, 1.43, 1.53, 1.62, 1.76, 1.77, 1.86, 1.96, 2.26, + 2.30, 2.60, 3.00, 5.00, 6.00 + ]; + const outputRange = [ + 0.510, 0.540, 0.606, 0.560, 0.628, 0.630, 0.647, 0.710, + 0.736, 0.757, 0.780, 0.770, 0.800, 0.890, 0.975, 1.00, 1.07, 1.10, 1.15, + 1.18, 1.22, 1.27, 1.30, 1.33, 1.39, 1.45, 1.54, 1.55, 1.58, 1.62, 1.72, + 1.82, 1.92, 2.00, 2.24, 2.30 + ]; + + let rangeIdx = -1; + + for (let i = 0; i < inputRange.length - 1; i++) { + if (inputRange[i] <= input && inputRange[i + 1] >= input) { + rangeIdx = i; + break; + } + } + + if (rangeIdx === -1) { + if (inputRange[0] > input) { + return outputRange[0]; + } else { + return outputRange[outputRange[0].length - 1]; + } + } + + const inputA = inputRange[rangeIdx]; + const inputB = inputRange[rangeIdx + 1]; + const outputA = outputRange[rangeIdx]; + const outputB = outputRange[rangeIdx + 1]; + + return YawPitchControl.lerp(outputA, outputB, (input - inputA) / (inputB - inputA)); + } + + static lerp(a, b, fraction) { + return a + fraction * (b - a); + } + + /** + * Enable YawPitch functionality + * + * @method eg.YawPitch#enable + */ + enable() { + if (this._enabled) { + return this; + } + + this._enabled = true; + + // touchDirection is decided by parameter is valid string (Ref. Axes.connect) + this._applyOptions(Object.keys(this.options), this.options); + + // TODO: Is this code is needed? Check later. + this.updatePanScale(); + + return this; + } + + /** + * Disable YawPitch functionality + * + * @method eg.YawPitch#disable + */ + disable(persistOrientation) { + if (!this._enabled) { + return this; + } + + // TODO: Check peristOrientation is needed! + if (!persistOrientation) { + this._resetOrientation(); + } + this.axes.disconnect(); + this._enabled = false; + return this; + } + + _resetOrientation() { + const opt = this.options; + + this.axes.setTo({ + yaw: opt.yaw, + pitch: opt.pitch, + fov: opt.fov, + }, 0); + + return this; + } + + + /** + * Set one or more of yaw, pitch, fov + * + * @param {Object} coordinate yaw, pitch, fov + * @param {Number} duration Animation duration. if it is above 0 then it's animated. + */ + lookAt({yaw, pitch, fov}, duration) { + const pos = this.axes.get(); + + const y = yaw === undefined ? 0 : yaw - pos.yaw; + const p = pitch === undefined ? 0 : pitch - pos.pitch; + const f = fov === undefined ? 0 : fov - pos.fov; + + // Allow duration of animation to have more than MC_MAXIMUM_DURATION. + this.axes.options.maximumDuration = Infinity; + + this.axes.setBy({ + yaw: y, + pitch: p, + fov: f + }, duration); + } + + getYawPitch() { + const yawPitch = this.axes.get(); + + return { + yaw: yawPitch.yaw, + pitch: yawPitch.pitch, + }; + } + + getFov() { + return this.axes.get().fov; + } + + getQuaternion() { + const pos = this.axes.get(); + + return this._deviceQuaternion.getCombinedQuaternion(pos.yaw); + } + + shouldRenderWithQuaternion() { + return this.options.gyroMode === GYRO_MODE.VR; + } + + /** + * Destroys objects + */ + destroy() { + this.axes && this.axes.destroy(); + this.axisPanInput && this.axisPanInput.destroy(); + this.axesWheelInput && this.axesWheelInput.destroy(); + this.axesTiltMotionInput && this.axesTiltMotionInput.destroy(); + this.axesDeviceOrientationInput && this.axesDeviceOrientationInput.destroy(); + this.axesPinchInput && this.axesPinchInput.destroy(); + this.axesMoveKeyInput && this.axesMoveKeyInput.destroy(); + this._deviceQuaternion && this._deviceQuaternion.destroy(); + } +} + +export default YawPitchControl; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..5f2cf2cf0 --- /dev/null +++ b/src/types.ts @@ -0,0 +1 @@ +export type ValueOf = T[keyof T]; diff --git a/tsconfig.declaration.json b/tsconfig.declaration.json new file mode 100644 index 000000000..e96ffbf3a --- /dev/null +++ b/tsconfig.declaration.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "removeComments": true, + "declaration": true, + "emitDeclarationOnly": true, + "declarationDir": "declaration", + "strictNullChecks": false, + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..c6cfb4120 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "outDir": "./outjs/", + "module": "es2015", + "target": "es5", + "moduleResolution": "node", + "lib": ["es2015", "dom"], + "sourceMap": true, + "strictNullChecks": true, + "downlevelIteration": true, + "experimentalDecorators": true, + "esModuleInterop": true, + "baseUrl": "." + }, + "include": [ + "./src/**/*.ts", + ], + "exclude": [ + "node_modules" + ] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 000000000..67b842ce1 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "module": "commonjs", + "noImplicitAny": false, + "strictNullChecks": false, + "noUnusedLocals": false, + "experimentalDecorators": true, + "types": [ + "karma-chai", + "mocha", + ] + }, + "include": [ + "./src/**/*.ts", + "./test/unit/*.ts" + ], + "exclude": [ + "./node_modules/**/*.ts" + ] +} diff --git a/tslint.json b/tslint.json index b1ee45871..5ae7b5ef6 100644 --- a/tslint.json +++ b/tslint.json @@ -19,6 +19,7 @@ "max-line-length": false, "no-misused-new": false, "interface-name": [true, "never-prefix"], + "member-ordering": [true, {"order": "fields-first"}], "whitespace": [ true, "check-decl",