Source: src/PanoViewer/PanoViewer.ts

  1. import Component, { ComponentEvent } from "@egjs/component";
  2. import Promise from "promise-polyfill";
  3. import { quat } from "gl-matrix";
  4. import { DeviceMotionEvent, checkXRSupport } from "../utils/browserFeature";
  5. import YawPitchControl, { YawPitchControlOptions } from "../YawPitchControl/YawPitchControl";
  6. import PanoImageRenderer from "../PanoImageRenderer/PanoImageRenderer";
  7. import WebGLUtils from "../PanoImageRenderer/WebGLUtils";
  8. import { util as mathUtil } from "../utils/math-util";
  9. import { VERSION } from "../version";
  10. import { CubemapConfig, ValueOf } from "../types/internal";
  11. import { AnimationEndEvent, ReadyEvent, ViewChangeEvent, ErrorEvent } from "../types/event";
  12. import { ERROR_TYPE, PANOVIEWER_EVENTS as EVENTS, GYRO_MODE, PROJECTION_TYPE, STEREO_FORMAT, DEFAULT_CANVAS_CLASS } from "./consts";
  13. export interface PanoViewerOptions {
  14. image: string | HTMLElement;
  15. video: string | HTMLElement;
  16. projectionType: ValueOf<typeof PROJECTION_TYPE>;
  17. cubemapConfig: Partial<CubemapConfig>;
  18. stereoFormat: ValueOf<typeof STEREO_FORMAT>;
  19. width: number;
  20. height: number;
  21. yaw: number;
  22. pitch: number;
  23. fov: number;
  24. showPolePoint: boolean;
  25. useZoom: boolean;
  26. useKeyboard: boolean;
  27. gyroMode: ValueOf<typeof GYRO_MODE>;
  28. yawRange: number[];
  29. pitchRange: number[];
  30. fovRange: number[];
  31. touchDirection: ValueOf<typeof PanoViewer.TOUCH_DIRECTION>;
  32. canvasClass: string;
  33. }
  34. export interface PanoViewerEvent {
  35. ready: ReadyEvent;
  36. viewChange: ViewChangeEvent;
  37. animationEnd: AnimationEndEvent<PanoViewer>;
  38. error: ErrorEvent;
  39. }
  40. /**
  41. * @memberof eg.view360
  42. * @extends eg.Component
  43. * PanoViewer
  44. */
  45. class PanoViewer extends Component<PanoViewerEvent> {
  46. /**
  47. * Check whether the current environment can execute PanoViewer
  48. * @ko 현재 브라우저 환경에서 PanoViewer 실행이 가능한지 여부를 반환합니다.
  49. * @return PanoViewer executable <ko>PanoViewer 실행가능 여부</ko>
  50. */
  51. public static isSupported(): boolean {
  52. return WebGLUtils.isWebGLAvailable() && WebGLUtils.isStableWebGL();
  53. }
  54. /**
  55. * Check whether the current environment supports the WebGL
  56. * @ko 현재 브라우저 환경이 WebGL 을 지원하는지 여부를 확인합니다.
  57. * @return WebGL support <ko>WebGL 지원여부</ko>
  58. */
  59. public static isWebGLAvailable(): boolean {
  60. return WebGLUtils.isWebGLAvailable();
  61. }
  62. /**
  63. * Check whether the current environment supports the gyro sensor.
  64. * @ko 현재 브라우저 환경이 자이로 센서를 지원하는지 여부를 확인합니다.
  65. * @param callback Function to take the gyro sensor availability as argument <ko>자이로 센서를 지원하는지 여부를 인자로 받는 함수</ko>
  66. */
  67. public static isGyroSensorAvailable(callback: (isAvailable: boolean) => any) {
  68. if (!DeviceMotionEvent && callback) {
  69. callback(false);
  70. return;
  71. }
  72. let onDeviceMotionChange;
  73. const checkGyro = () => new Promise(res => {
  74. onDeviceMotionChange = deviceMotion => {
  75. const isGyroSensorAvailable = !(deviceMotion.rotationRate.alpha == null);
  76. res(isGyroSensorAvailable);
  77. };
  78. window.addEventListener("devicemotion", onDeviceMotionChange);
  79. });
  80. const timeout = () => new Promise(res => {
  81. setTimeout(() => res(false), 1000);
  82. });
  83. Promise.race([checkGyro(), timeout()]).then((isGyroSensorAvailable: boolean) => {
  84. window.removeEventListener("devicemotion", onDeviceMotionChange);
  85. if (callback) {
  86. callback(isGyroSensorAvailable);
  87. }
  88. PanoViewer.isGyroSensorAvailable = fb => {
  89. if (fb) {
  90. fb(isGyroSensorAvailable);
  91. }
  92. return isGyroSensorAvailable;
  93. };
  94. });
  95. }
  96. private static _isValidTouchDirection(direction) {
  97. return direction === PanoViewer.TOUCH_DIRECTION.NONE ||
  98. direction === PanoViewer.TOUCH_DIRECTION.YAW ||
  99. direction === PanoViewer.TOUCH_DIRECTION.PITCH ||
  100. direction === PanoViewer.TOUCH_DIRECTION.ALL;
  101. }
  102. /**
  103. * Version info string
  104. * @ko 버전정보 문자열
  105. * @name VERSION
  106. * @static
  107. * @type {String}
  108. * @example
  109. * eg.view360.PanoViewer.VERSION; // ex) 3.0.1
  110. * @memberof eg.view360.PanoViewer
  111. */
  112. public static VERSION = VERSION;
  113. public static ERROR_TYPE = ERROR_TYPE;
  114. public static EVENTS = EVENTS;
  115. public static PROJECTION_TYPE = PROJECTION_TYPE;
  116. public static GYRO_MODE = GYRO_MODE;
  117. // This should be deprecated!
  118. // eslint-disable-next-line @typescript-eslint/naming-convention
  119. public static ProjectionType = PROJECTION_TYPE;
  120. public static STEREO_FORMAT = STEREO_FORMAT;
  121. /**
  122. * Constant value for touch directions
  123. * @ko 터치 방향에 대한 상수 값.
  124. * @namespace
  125. * @name TOUCH_DIRECTION
  126. */
  127. public static TOUCH_DIRECTION = {
  128. /**
  129. * Constant value for none direction.
  130. * @ko none 방향에 대한 상수 값.
  131. * @name NONE
  132. * @memberof eg.view360.PanoViewer.TOUCH_DIRECTION
  133. * @constant
  134. * @type {Number}
  135. * @default 1
  136. */
  137. NONE: YawPitchControl.TOUCH_DIRECTION_NONE,
  138. /**
  139. * Constant value for horizontal(yaw) direction.
  140. * @ko horizontal(yaw) 방향에 대한 상수 값.
  141. * @name YAW
  142. * @memberof eg.view360.PanoViewer.TOUCH_DIRECTION
  143. * @constant
  144. * @type {Number}
  145. * @default 6
  146. */
  147. YAW: YawPitchControl.TOUCH_DIRECTION_YAW,
  148. /**
  149. * Constant value for vertical direction.
  150. * @ko vertical(pitch) 방향에 대한 상수 값.
  151. * @name PITCH
  152. * @memberof eg.view360.PanoViewer.TOUCH_DIRECTION
  153. * @constant
  154. * @type {Number}
  155. * @default 24
  156. */
  157. PITCH: YawPitchControl.TOUCH_DIRECTION_PITCH,
  158. /**
  159. * Constant value for all direction.
  160. * @ko all 방향에 대한 상수 값.
  161. * @name ALL
  162. * @memberof eg.view360.PanoViewer.TOUCH_DIRECTION
  163. * @constant
  164. * @type {Number}
  165. * @default 30
  166. */
  167. ALL: YawPitchControl.TOUCH_DIRECTION_ALL
  168. };
  169. private _container: HTMLElement;
  170. // Options
  171. private _image: ConstructorParameters<typeof PanoImageRenderer>[0];
  172. private _isVideo: boolean;
  173. private _projectionType: ValueOf<typeof PROJECTION_TYPE>;
  174. private _cubemapConfig: Partial<CubemapConfig>;
  175. private _stereoFormat: ValueOf<typeof STEREO_FORMAT>;
  176. private _width: number;
  177. private _height: number;
  178. private _yaw: number;
  179. private _pitch: number;
  180. private _fov: number;
  181. private _gyroMode: ValueOf<typeof GYRO_MODE>;
  182. private _quaternion: quat | null;
  183. private _aspectRatio: number;
  184. private _isReady: boolean;
  185. private _canvasClass: string;
  186. // Internal Values
  187. private _photoSphereRenderer: PanoImageRenderer | null;
  188. private _yawPitchControl: YawPitchControl | null;
  189. /**
  190. * @classdesc 360 media viewer
  191. * @ko 360 미디어 뷰어
  192. *
  193. * @param container The container element for the renderer. <ko>렌더러의 컨테이너 엘리먼트</ko>
  194. * @param options
  195. *
  196. * @param {String|HTMLImageElement} options.image Input image url or element (Use only image property or video property)<ko>입력 이미지 URL 혹은 엘리먼트(image 와 video 둘 중 하나만 설정)</ko>
  197. * @param {String|HTMLVideoElement} options.video Input video url or element(Use only image property or video property)<ko>입력 비디오 URL 혹은 엘리먼트(image 와 video 둘 중 하나만 설정)</ko>
  198. * @param {String} [options.projectionType=equirectangular] The type of projection: equirectangular, cubemap <br/>{@link eg.view360.PanoViewer.PROJECTION_TYPE}<ko>Projection 유형 : equirectangular, cubemap <br/>{@link eg.view360.PanoViewer.PROJECTION_TYPE}</ko>
  199. * @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}<ko>cubemap projection type 의 레이아웃을 설정한다. 이 설정은 ProjectionType이 {@link eg.view360.PanoViewer.PROJECTION_TYPE.CUBEMAP} 혹은 {@link eg.view360.PanoViewer.PROJECTION_TYPE.CUBESTRIP} 인 경우에만 적용된다.</ko>
  200. * @param {Object} [options.cubemapConfig.order = "RLUDBF"(ProjectionType === CUBEMAP) | "RLUDFB" (ProjectionType === CUBESTRIP)] Order of cubemap faces <ko>Cubemap 형태의 이미지가 배치된 순서</ko>
  201. * @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}, ...]<ko>각 Cubemap 면에 대한 회전 각도/좌우반전 여부 설정, 객체를 배열 형태로 지정하여 각 면에 대한 설정을 다르게 지정할 수도 있다. 예를 들어 [{flipHorizontal:false, rotation:90}, {flipHorizontal: true, rotation: 180}, ...]과 같이 지정할 수 있다.</ko>
  202. * @param {Number} [options.cubemapConfig.trim=0] A px distance to discard from each tile side. You can use this value to avoid graphical glitch at where tiles are connected. This option is available when there's only one texture.<ko>각 타일의 끝으로부터 폐기할 px 거리. 이 옵션을 사용하여 타일의 접합부에서 나타나는 그래픽 결함을 완화할 수 있습니다. 이 옵션은 한 개의 텍스쳐만 사용할 때 적용 가능합니다.</ko>
  203. * @param {String} [options.stereoFormat="3dv"] Contents format of the stereoscopic equirectangular projection.<br/>See {@link eg.view360.PanoViewer.STEREO_FORMAT}.<ko>Stereoscopic equirectangular projection type의 콘텐츠 포맷을 설정한다.<br/>{@link eg.view360.PanoViewer.STEREO_FORMAT} 참조.</ko>
  204. * @param {Number} [options.width=width of container] the viewer's width. (in px) <ko>뷰어의 너비 (px 단위)</ko>
  205. * @param {Number} [options.height=height of container] the viewer's height.(in px) <ko>뷰어의 높이 (px 단위)</ko>
  206. * @param {Number} [options.yaw=0] Initial Yaw of camera (in degree) <ko>카메라의 초기 Yaw (degree 단위)</ko>
  207. * @param {Number} [options.pitch=0] Initial Pitch of camera (in degree) <ko>카메라의 초기 Pitch (degree 단위)</ko>
  208. * @param {Number} [options.fov=65] Initial vertical field of view of camera (in degree) <ko>카메라의 초기 수직 field of view (degree 단위)</ko>
  209. * @param {Boolean} [options.showPolePoint=false] If false, the pole is not displayed inside the viewport <ko>false 인 경우, 극점은 뷰포트 내부에 표시되지 않습니다</ko>
  210. * @param {Boolean} [options.useZoom=true] When true, enables zoom with the wheel and Pinch gesture <ko>true 일 때 휠 및 집기 제스춰로 확대 / 축소 할 수 있습니다.</ko>
  211. * @param {Boolean} [options.useKeyboard=true] When true, enables the keyboard move key control: awsd, arrow keys <ko>true 이면 키보드 이동 키 컨트롤을 활성화합니다: awsd, 화살표 키</ko>
  212. * @param {String} [options.gyroMode=yawPitch] Enables control through device motion. ("none", "yawPitch", "VR") <br/>{@link eg.view360.PanoViewer.GYRO_MODE} <ko>디바이스 움직임을 통한 컨트롤을 활성화 합니다. ("none", "yawPitch", "VR") <br/>{@link eg.view360.PanoViewer.GYRO_MODE} </ko>
  213. * @param {Array} [options.yawRange=[-180, 180]] Range of controllable Yaw values <ko>제어 가능한 Yaw 값의 범위</ko>
  214. * @param {Array} [options.pitchRange=[-90, 90]] Range of controllable Pitch values <ko>제어 가능한 Pitch 값의 범위</ko>
  215. * @param {Array} [options.fovRange=[30, 110]] Range of controllable vertical field of view values <ko>제어 가능한 수직 field of view 값의 범위</ko>
  216. * @param {Number} [options.touchDirection= {@link eg.view360.PanoViewer.TOUCH_DIRECTION.ALL}(6)] Direction of touch that can be controlled by user <br/>{@link eg.view360.PanoViewer.TOUCH_DIRECTION}<ko>사용자가 터치로 조작 가능한 방향 <br/>{@link eg.view360.PanoViewer.TOUCH_DIRECTION}</ko>
  217. * @param {String} [options.canvasClass="view360-canvas"] A class name for the canvas element inside the container element. PanoViewer will use the canvas that has this class instead of creating one if it exists<ko>콘테이너 엘리먼트 내부의 캔버스 엘리먼트의 클래스 이름. PanoViewer는 해당 클래스를 갖는 캔버스 엘리먼트가 콘테이너 엘리먼트 내부에 존재할 경우, 새로 생성하는 대신 그 엘리먼트를 사용할 것입니다</ko>
  218. *
  219. * @example
  220. * ```
  221. * // PanoViewer Creation
  222. * // create PanoViewer with option
  223. * var PanoViewer = eg.view360.PanoViewer;
  224. * // Area where the image will be displayed(HTMLElement)
  225. * var container = document.getElementById("myPanoViewer");
  226. *
  227. * var panoViewer = new PanoViewer(container, {
  228. * // If projectionType is not specified, the default is "equirectangular".
  229. * // Specifies an image of the "equirectangular" type.
  230. * image: "/path/to/image/image.jpg"
  231. * });
  232. * ```
  233. *
  234. * @example
  235. * ```
  236. * // Cubemap Config Setting Example
  237. * // For support Youtube EAC projection, You should set cubemapConfig as follows.
  238. * cubemapConfig: {
  239. * order: "LFRDBU",
  240. * tileConfig: [{rotation: 0}, {rotation: 0}, {rotation: 0}, {rotation: 0}, {rotation: -90}, {rotation: 180}]
  241. * }
  242. * ```
  243. */
  244. public constructor(container: HTMLElement, options: Partial<PanoViewerOptions> = {}) {
  245. super();
  246. // Raises the error event if webgl is not supported.
  247. if (!WebGLUtils.isWebGLAvailable()) {
  248. setTimeout(() => {
  249. this.trigger(new ComponentEvent(EVENTS.ERROR, {
  250. type: ERROR_TYPE.NO_WEBGL,
  251. message: "no webgl support"
  252. }));
  253. }, 0);
  254. return this;
  255. }
  256. if (!WebGLUtils.isStableWebGL()) {
  257. setTimeout(() => {
  258. this.trigger(new ComponentEvent(EVENTS.ERROR, {
  259. type: ERROR_TYPE.INVALID_DEVICE,
  260. message: "blacklisted browser"
  261. }));
  262. }, 0);
  263. return this;
  264. }
  265. if (!!options.image && !!options.video) {
  266. setTimeout(() => {
  267. this.trigger(new ComponentEvent(EVENTS.ERROR, {
  268. type: ERROR_TYPE.INVALID_RESOURCE,
  269. message: "Specifying multi resouces(both image and video) is not valid."
  270. }));
  271. }, 0);
  272. return this;
  273. }
  274. // Check XR support at not when imported, but when created.
  275. // This is intended to make polyfills easier to use.
  276. checkXRSupport();
  277. this._container = container;
  278. this._image = options.image! as HTMLImageElement || options.video! as HTMLVideoElement;
  279. this._isVideo = !!options.video;
  280. this._projectionType = options.projectionType || PROJECTION_TYPE.EQUIRECTANGULAR;
  281. this._cubemapConfig = {
  282. ...{
  283. /* RLUDBF is abnormal, we use it on CUBEMAP only for backward compatibility*/
  284. order: this._projectionType === PROJECTION_TYPE.CUBEMAP ? "RLUDBF" : "RLUDFB",
  285. tileConfig: {
  286. flipHorizontal: false,
  287. rotation: 0
  288. },
  289. trim: 0
  290. }, ...options.cubemapConfig
  291. };
  292. this._stereoFormat = options.stereoFormat || STEREO_FORMAT.TOP_BOTTOM;
  293. // If the width and height are not provided, will use the size of the container.
  294. this._width = options.width || parseInt(window.getComputedStyle(container).width, 10);
  295. this._height = options.height || parseInt(window.getComputedStyle(container).height, 10);
  296. /**
  297. * Cache the direction for the performance in renderLoop
  298. *
  299. * This value should be updated by "change" event of YawPitchControl.
  300. */
  301. this._yaw = options.yaw || 0;
  302. this._pitch = options.pitch || 0;
  303. this._fov = options.fov || 65;
  304. this._gyroMode = options.gyroMode || GYRO_MODE.YAWPITCH;
  305. this._quaternion = null;
  306. this._aspectRatio = this._height !== 0 ? this._width / this._height : 1;
  307. this._canvasClass = options.canvasClass || DEFAULT_CANVAS_CLASS;
  308. const fovRange = options.fovRange || [30, 110];
  309. const touchDirection = PanoViewer._isValidTouchDirection(options.touchDirection) ?
  310. options.touchDirection : YawPitchControl.TOUCH_DIRECTION_ALL;
  311. const yawPitchConfig = {
  312. ...options,
  313. ...{
  314. element: container,
  315. yaw: this._yaw,
  316. pitch: this._pitch,
  317. fov: this._fov,
  318. gyroMode: this._gyroMode,
  319. fovRange,
  320. aspectRatio: this._aspectRatio,
  321. touchDirection
  322. }
  323. };
  324. this._isReady = false;
  325. this._initYawPitchControl(yawPitchConfig);
  326. this._initRenderer(this._yaw, this._pitch, this._fov, this._projectionType, this._cubemapConfig);
  327. }
  328. /**
  329. * Get the video element that the viewer is currently playing. You can use this for playback.
  330. * @ko 뷰어가 현재 사용 중인 비디오 요소를 얻습니다. 이 요소를 이용해 비디오의 컨트롤을 할 수 있습니다.
  331. * @return HTMLVideoElement<ko>HTMLVideoElement</ko>
  332. * @example
  333. * ```
  334. * var videoTag = panoViewer.getVideo();
  335. * videoTag.play(); // play the video!
  336. * ```
  337. */
  338. public getVideo() {
  339. if (!this._isVideo) {
  340. return null;
  341. }
  342. return this._photoSphereRenderer!.getContent() as HTMLVideoElement;
  343. }
  344. /**
  345. * Set the video information to be used by the viewer.
  346. * @ko 뷰어가 사용할 이미지 정보를 설정합니다.
  347. * @param {string|HTMLVideoElement|object} video Input video url or element or config object<ko>입력 비디오 URL 혹은 엘리먼트 혹은 설정객체를 활용(image 와 video 둘 중 하나만 설정)</ko>
  348. * @param {object} param
  349. * @param {string} [param.projectionType={@link eg.view360.PanoViewer.PROJECTION_TYPE.EQUIRECTANGULAR}("equirectangular")] Projection Type<ko>프로젝션 타입</ko>
  350. * @param {object} param.cubemapConfig config cubemap projection layout. <ko>cubemap projection type 의 레이아웃 설정</ko>
  351. * @param {string} [param.stereoFormat="3dv"] Contents format of the stereoscopic equirectangular projection. See {@link eg.view360.PanoViewer.STEREO_FORMAT}.<ko>Stereoscopic equirectangular projection type의 콘텐츠 포맷을 설정한다. {@link eg.view360.PanoViewer.STEREO_FORMAT} 참조.</ko>
  352. *
  353. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  354. * @example
  355. * ```
  356. * panoViewer.setVideo("/path/to/video/video.mp4", {
  357. * projectionType: eg.view360.PanoViewer.PROJECTION_TYPE.EQUIRECTANGULAR
  358. * });
  359. * ```
  360. */
  361. public setVideo(video: string | HTMLElement | { type: string; src: string }, param: Partial<{
  362. projectionType: PanoViewer["_projectionType"];
  363. cubemapConfig: PanoViewer["_cubemapConfig"];
  364. stereoFormat: PanoViewer["_stereoFormat"];
  365. }> = {}) {
  366. if (video) {
  367. this.setImage(video, {
  368. projectionType: param.projectionType,
  369. isVideo: true,
  370. cubemapConfig: param.cubemapConfig,
  371. stereoFormat: param.stereoFormat
  372. });
  373. }
  374. return this;
  375. }
  376. /**
  377. * Get the image information that the viewer is currently using.
  378. * @ko 뷰어가 현재 사용하고있는 이미지 정보를 얻습니다.
  379. * @return Image Object<ko>이미지 객체</ko>
  380. * @example
  381. * var imageObj = panoViewer.getImage();
  382. */
  383. public getImage() {
  384. if (this._isVideo) {
  385. return null;
  386. }
  387. return this._photoSphereRenderer!.getContent();
  388. }
  389. /**
  390. * Set the image information to be used by the viewer.
  391. * @ko 뷰어가 사용할 이미지 정보를 설정합니다.
  392. * @param {string|HTMLElement|object} image Input image url or element or config object<ko>입력 이미지 URL 혹은 엘리먼트 혹은 설정객체를 활용(image 와 video 둘 중 하나만 설정한다.)</ko>
  393. * @param {object} param Additional information<ko>이미지 추가 정보</ko>
  394. * @param {string} [param.projectionType="equirectangular"] Projection Type<ko>프로젝션 타입</ko>
  395. * @param {object} param.cubemapConfig config cubemap projection layout. <ko>cubemap projection type 레이아웃</ko>
  396. * @param {string} [param.stereoFormat="3dv"] Contents format of the stereoscopic equirectangular projection. See {@link eg.view360.PanoViewer.STEREO_FORMAT}.<ko>Stereoscopic equirectangular projection type의 콘텐츠 포맷을 설정한다. {@link eg.view360.PanoViewer.STEREO_FORMAT} 참조.</ko>
  397. * @param {boolean} [param.isVideo=false] Whether the given `imaage` is video or not.<ko>이미지가 비디오인지 여부</ko>
  398. *
  399. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  400. * @example
  401. * ```
  402. * panoViewer.setImage("/path/to/image/image.png", {
  403. * projectionType: eg.view360.PanoViewer.PROJECTION_TYPE.CUBEMAP
  404. * });
  405. * ```
  406. */
  407. public setImage(image: string | HTMLElement | { src: string; type: string }, param: Partial<{
  408. projectionType: PanoViewer["_projectionType"];
  409. cubemapConfig: PanoViewer["_cubemapConfig"];
  410. stereoFormat: PanoViewer["_stereoFormat"];
  411. isVideo: boolean;
  412. }> = {}) {
  413. const cubemapConfig = {
  414. ...{
  415. order: "RLUDBF",
  416. tileConfig: {
  417. flipHorizontal: false,
  418. rotation: 0
  419. },
  420. trim: 0
  421. }, ...param.cubemapConfig
  422. };
  423. const stereoFormat = param.stereoFormat || STEREO_FORMAT.TOP_BOTTOM;
  424. const isVideo = !!(param.isVideo);
  425. if (this._image && this._isVideo !== isVideo) {
  426. /* eslint-disable no-console */
  427. console.warn("PanoViewer is not currently supporting content type changes. (Image <--> Video)");
  428. /* eslint-enable no-console */
  429. return this;
  430. }
  431. if (image) {
  432. this._deactivate();
  433. this._image = image as HTMLImageElement;
  434. this._isVideo = isVideo;
  435. this._projectionType = param.projectionType || PROJECTION_TYPE.EQUIRECTANGULAR;
  436. this._cubemapConfig = cubemapConfig;
  437. this._stereoFormat = stereoFormat;
  438. this._initRenderer(this._yaw, this._pitch, this._fov, this._projectionType, this._cubemapConfig);
  439. }
  440. return this;
  441. }
  442. /**
  443. * Set whether the renderer always updates the texture and renders.
  444. * @ko 렌더러가 항상 텍스쳐를 갱신하고 화면을 렌더링 할지 여부를 설정할 수 있습니다.
  445. * @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.<ko>true면 항상 텍스쳐를 갱신하고 화면을 그리는 반면, false면 텍스쳐 갱신은 하지 않으며, 카메라 요소에 변화가 있을 때에만 화면을 그립니다.</ko>
  446. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  447. */
  448. public keepUpdate(doUpdate: boolean) {
  449. this._photoSphereRenderer!.keepUpdate(doUpdate);
  450. return this;
  451. }
  452. /**
  453. * Get the current projection type (equirectangular/cube)
  454. * @ko 현재 프로젝션 타입(Equirectangular 혹은 Cube)을 반환합니다.
  455. * @return {@link eg.view360.PanoViewer.PROJECTION_TYPE}
  456. */
  457. public getProjectionType() {
  458. return this._projectionType;
  459. }
  460. /**
  461. * Activate the device's motion sensor, and return the Promise whether the sensor is enabled
  462. * If it's iOS13+, this method must be used in the context of user interaction, like onclick callback on the button element.
  463. * @ko 디바이스의 모션 센서를 활성화하고, 활성화 여부를 담는 Promise를 리턴합니다.
  464. * iOS13+일 경우, 사용자 인터렉션에 의해서 호출되어야 합니다. 예로, 버튼의 onclick 콜백과 같은 콘텍스트에서 호출되어야 합니다.
  465. * @return Promise containing nothing when resolved, or string of the rejected reason when rejected.<ko>Promise. resolve되었을 경우 아무것도 반환하지 않고, reject되었을 경우 그 이유를 담고있는 string을 반환한다.</ko>
  466. */
  467. public enableSensor() {
  468. return new Promise((resolve, reject) => {
  469. if (DeviceMotionEvent && typeof DeviceMotionEvent.requestPermission === "function") {
  470. DeviceMotionEvent.requestPermission().then(permissionState => {
  471. if (permissionState === "granted") {
  472. resolve();
  473. } else {
  474. reject(new Error("permission denied"));
  475. }
  476. }).catch(e => {
  477. // This can happen when this method wasn't triggered by user interaction
  478. reject(e);
  479. });
  480. } else {
  481. resolve();
  482. }
  483. });
  484. }
  485. /**
  486. * Disable the device's motion sensor.
  487. * @ko 디바이스의 모션 센서를 비활성화합니다.
  488. * @deprecated
  489. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  490. */
  491. public disableSensor() {
  492. return this;
  493. }
  494. /**
  495. * Switch to VR stereo rendering mode which uses WebXR / WebVR API (WebXR is preferred).
  496. * This method must be used in the context of user interaction, like onclick callback on the button element.
  497. * It can be rejected when an enabling device sensor fails or image/video is still loading("ready" event not triggered).
  498. * @ko WebXR / WebVR API를 사용하는 VR 스테레오 렌더링 모드로 전환합니다. (WebXR을 더 선호합니다)
  499. * 이 메소드는 사용자 인터렉션에 의해서 호출되어야 합니다. 예로, 버튼의 onclick 콜백과 같은 콘텍스트에서 호출되어야 합니다.
  500. * 디바이스 센서 활성화에 실패시 혹은 아직 이미지/비디오가 로딩중인 경우("ready"이벤트가 아직 트리거되지 않은 경우)에는 Promise가 reject됩니다.
  501. * @param {object} [options={}] Additional options for WebXR session, see {@link https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit XRSessionInit}.<ko>WebXR용 추가 옵션, {@link https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit XRSessionInit}을 참조해주세요.</ko>
  502. * @return Promise containing either a string of resolved reason or an Error instance of rejected reason.<ko>Promise가 resolve된 이유(string) 혹은 reject된 이유(Error)</ko>
  503. */
  504. public enterVR(options: {
  505. requiredFeatures?: any[];
  506. optionalFeatures?: any[];
  507. [key: string]: any;
  508. } = {}): globalThis.Promise<string> {
  509. if (!this._isReady) {
  510. return Promise.reject(new Error("PanoViewer is not ready to show image.")) as any;
  511. }
  512. return new Promise((resolve, reject) => {
  513. this.enableSensor()
  514. .then(() => this._photoSphereRenderer!.enterVR(options))
  515. .then((res: string) => resolve(res))
  516. .catch(e => reject(e));
  517. }) as any;
  518. }
  519. /**
  520. * Exit VR stereo rendering mode.
  521. * @ko VR 스테레오 렌더링 모드에서 일반 렌더링 모드로 전환합니다.
  522. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  523. */
  524. public exitVR() {
  525. this._photoSphereRenderer!.exitVR();
  526. return this;
  527. }
  528. /**
  529. * 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}.
  530. * @ko true 로 설정 시 휠 혹은 집기 동작으로 확대/축소 할 수 있습니다. false 설정 시 확대/축소 기능을 비활성화 합니다. 단, 터치인 경우 touchDirection 설정이 {@link eg.view360.PanoViewer.TOUCH_DIRECTION.ALL} 인 경우에만 pinch 가 동작합니다.
  531. * @param useZoom
  532. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  533. */
  534. public setUseZoom(useZoom: boolean): this {
  535. if (typeof useZoom === "boolean") {
  536. this._yawPitchControl!.option("useZoom", useZoom);
  537. }
  538. return this;
  539. }
  540. /**
  541. * When true, enables the keyboard move key control: awsd, arrow keys
  542. * @ko true이면 키보드 이동 키 컨트롤을 활성화합니다. (awsd, 화살표 키)
  543. * @param useKeyboard
  544. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  545. */
  546. public setUseKeyboard(useKeyboard: boolean): this {
  547. this._yawPitchControl!.option("useKeyboard", useKeyboard);
  548. return this;
  549. }
  550. /**
  551. * Enables control through device motion. ("none", "yawPitch", "VR")
  552. * @ko 디바이스 움직임을 통한 컨트롤을 활성화 합니다. ("none", "yawPitch", "VR")
  553. * @param gyroMode {@link eg.view360.PanoViewer.GYRO_MODE}
  554. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  555. * @example
  556. * ```
  557. * panoViewer.setGyroMode("yawPitch");
  558. * //equivalent
  559. * panoViewer.setGyroMode(eg.view360.PanoViewer.GYRO_MODE.YAWPITCH);
  560. * ```
  561. */
  562. public setGyroMode(gyroMode: PanoViewer["_gyroMode"]) {
  563. this._yawPitchControl!.option("gyroMode", gyroMode);
  564. return this;
  565. }
  566. /**
  567. * Set the range of controllable FOV values
  568. * @ko 제어 가능한 FOV 구간을 설정합니다.
  569. * @param range
  570. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  571. * @example
  572. * panoViewer.setFovRange([50, 90]);
  573. */
  574. public setFovRange(range: number[]) {
  575. this._yawPitchControl!.option("fovRange", range);
  576. return this;
  577. }
  578. /**
  579. * Get the range of controllable FOV values
  580. * @ko 제어 가능한 FOV 구간을 반환합니다.
  581. * @return FOV range
  582. * @example
  583. * var range = panoViewer.getFovRange(); // [50, 90]
  584. */
  585. public getFovRange(): [number, number] {
  586. return this._yawPitchControl!.option("fovRange") as [number, number];
  587. }
  588. /**
  589. * 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.
  590. * @ko 캔버스 엘리먼트의 크기를 컨테이너 엘리먼트의 크기나 지정된 크기로 업데이트합니다. 만약 size 가 지정되지 않으면 컨테이너 영역의 크기를 얻어와 해당 크기로 갱신합니다.
  591. * @param {object} [size]
  592. * @param {number} [size.width=width of the container]
  593. * @param {number} [size.height=height of the container]
  594. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  595. */
  596. public updateViewportDimensions(size: Partial<{
  597. width: number;
  598. height: number;
  599. }> = {}): this {
  600. if (!this._isReady) {
  601. return this;
  602. }
  603. let containerSize;
  604. if (size.width === undefined || size.height === undefined) {
  605. containerSize = window.getComputedStyle(this._container);
  606. }
  607. const width = size.width || parseInt(containerSize.width, 10);
  608. const height = size.height || parseInt(containerSize.height, 10);
  609. // Skip if viewport is not changed.
  610. if (width === this._width && height === this._height) {
  611. return this;
  612. }
  613. this._width = width;
  614. this._height = height;
  615. this._aspectRatio = width / height;
  616. this._photoSphereRenderer!.updateViewportDimensions(width, height);
  617. this._yawPitchControl!.option("aspectRatio", this._aspectRatio);
  618. this._yawPitchControl!.updatePanScale({height});
  619. this.lookAt({}, 0);
  620. return this;
  621. }
  622. /**
  623. * Get the current field of view(FOV)
  624. * @ko 현재 field of view(FOV) 값을 반환합니다.
  625. */
  626. public getFov(): number {
  627. return this._fov;
  628. }
  629. /**
  630. * Get current yaw value
  631. * @ko 현재 yaw 값을 반환합니다.
  632. */
  633. public getYaw() {
  634. return this._yaw;
  635. }
  636. /**
  637. * Get current pitch value
  638. * @ko 현재 pitch 값을 반환합니다.
  639. */
  640. public getPitch() {
  641. return this._pitch;
  642. }
  643. /**
  644. * Get the range of controllable Yaw values
  645. * @ko 컨트롤 가능한 Yaw 구간을 반환합니다.
  646. */
  647. public getYawRange(): [number, number] {
  648. return this._yawPitchControl!.option("yawRange") as [number, number];
  649. }
  650. /**
  651. * Get the range of controllable Pitch values
  652. * @ko 컨트롤 가능한 Pitch 구간을 가져옵니다.
  653. */
  654. public getPitchRange(): [number, number] {
  655. return this._yawPitchControl!.option("pitchRange") as [number, number];
  656. }
  657. /**
  658. * Set the range of controllable yaw
  659. * @ko 컨트롤 가능한 Yaw 구간을 반환합니다.
  660. * @param {number[]} range
  661. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  662. * @example
  663. * panoViewer.setYawRange([-90, 90]);
  664. */
  665. public setYawRange(yawRange: number[]) {
  666. this._yawPitchControl!.option("yawRange", yawRange);
  667. return this;
  668. }
  669. /**
  670. * Set the range of controllable Pitch values
  671. * @ko 컨트롤 가능한 Pitch 구간을 설정합니다.
  672. * @param {number[]} range
  673. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  674. * @example
  675. * panoViewer.setPitchRange([-40, 40]);
  676. */
  677. public setPitchRange(pitchRange: number[]) {
  678. this._yawPitchControl!.option("pitchRange", pitchRange);
  679. return this;
  680. }
  681. /**
  682. * 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.
  683. * @ko pitch 범위를 제한하여 극점을 표시할지를 지정합니다. true 인 경우 극점까지 표현할 수 있으며 false 인 경우 극점까지 표시하지 않습니다.
  684. * @param showPolePoint
  685. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  686. */
  687. public setShowPolePoint(showPolePoint: boolean) {
  688. this._yawPitchControl!.option("showPolePoint", showPolePoint);
  689. return this;
  690. }
  691. /**
  692. * Set a new view by setting camera configuration. Any parameters not specified remain the same.
  693. * @ko 카메라 설정을 지정하여 화면을 갱신합니다. 지정되지 않은 매개 변수는 동일하게 유지됩니다.
  694. * @param {object} orientation
  695. * @param {number} orientation.yaw Target yaw in degree <ko>목표 yaw (degree 단위)</ko>
  696. * @param {number} orientation.pitch Target pitch in degree <ko>목표 pitch (degree 단위)</ko>
  697. * @param {number} orientation.fov Target vertical fov in degree <ko>목표 수직 fov (degree 단위)</ko>
  698. * @param {number} duration Animation duration in milliseconds <ko>애니메이션 시간 (밀리 초)</ko>
  699. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  700. * @example
  701. * ```
  702. * // Change the yaw angle (absolute angle) to 30 degrees for one second.
  703. * panoViewer.lookAt({yaw: 30}, 1000);
  704. * ```
  705. */
  706. public lookAt(orientation: Partial<{
  707. yaw: number;
  708. pitch: number;
  709. fov: number;
  710. }>, duration: number = 0) {
  711. if (!this._isReady) {
  712. return this;
  713. }
  714. const yaw = orientation.yaw !== undefined ? orientation.yaw : this._yaw;
  715. const pitch = orientation.pitch !== undefined ? orientation.pitch : this._pitch;
  716. const pitchRange = this._yawPitchControl!.option("pitchRange");
  717. const verticalAngleOfImage = pitchRange[1] - pitchRange[0];
  718. let fov = orientation.fov !== undefined ? orientation.fov : this._fov;
  719. if (verticalAngleOfImage < fov) {
  720. fov = verticalAngleOfImage;
  721. }
  722. this._yawPitchControl!.lookAt({yaw, pitch, fov}, duration);
  723. if (duration === 0) {
  724. this._photoSphereRenderer!.renderWithYawPitch(yaw, pitch, fov);
  725. }
  726. return this;
  727. }
  728. /**
  729. * Set touch direction by which user can control.
  730. * @ko 사용자가 조작가능한 터치 방향을 지정합니다.
  731. * @param direction of the touch. {@link eg.view360.PanoViewer.TOUCH_DIRECTION}<ko>컨트롤 가능한 방향 {@link eg.view360.PanoViewer.TOUCH_DIRECTION}</ko>
  732. * @return PanoViewer instance
  733. * @example
  734. * ```
  735. * panoViewer = new PanoViewer(el);
  736. * // Limit the touch direction to the yaw direction only.
  737. * panoViewer.setTouchDirection(eg.view360.PanoViewer.TOUCH_DIRECTION.YAW);
  738. * ```
  739. */
  740. public setTouchDirection(direction: number): this {
  741. if (PanoViewer._isValidTouchDirection(direction)) {
  742. this._yawPitchControl!.option("touchDirection", direction);
  743. }
  744. return this;
  745. }
  746. /**
  747. * Returns touch direction by which user can control
  748. * @ko 사용자가 조작가능한 터치 방향을 반환한다.
  749. * @return direction of the touch. {@link eg.view360.PanoViewer.TOUCH_DIRECTION}<ko>컨트롤 가능한 방향 {@link eg.view360.PanoViewer.TOUCH_DIRECTION}</ko>
  750. * @example
  751. * ```
  752. * panoViewer = new PanoViewer(el);
  753. * // Returns the current touch direction.
  754. * var dir = panoViewer.getTouchDirection();
  755. * ```
  756. */
  757. public getTouchDirection(): number {
  758. return this._yawPitchControl!.option("touchDirection") ;
  759. }
  760. /**
  761. * Destroy viewer. Remove all registered event listeners and remove viewer canvas.
  762. * @ko 뷰어 인스턴스를 해제합니다. 모든 등록된 이벤트리스너를 제거하고 뷰어 캔버스를 삭제합니다.
  763. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko>
  764. */
  765. public destroy(): this {
  766. this._deactivate();
  767. if (this._yawPitchControl) {
  768. this._yawPitchControl.destroy();
  769. this._yawPitchControl = null;
  770. }
  771. return this;
  772. }
  773. // TODO: Remove parameters as they're just using private values
  774. private _initRenderer(
  775. yaw: number,
  776. pitch: number,
  777. fov: number,
  778. projectionType: PanoViewer["_projectionType"],
  779. cubemapConfig: PanoViewer["_cubemapConfig"]
  780. ) {
  781. this._photoSphereRenderer = new PanoImageRenderer(
  782. this._image,
  783. this._width,
  784. this._height,
  785. this._isVideo,
  786. this._container,
  787. this._canvasClass,
  788. {
  789. initialYaw: yaw,
  790. initialPitch: pitch,
  791. fieldOfView: fov,
  792. imageType: projectionType,
  793. cubemapConfig,
  794. stereoFormat: this._stereoFormat
  795. },
  796. );
  797. this._photoSphereRenderer.setYawPitchControl(this._yawPitchControl!);
  798. this._bindRendererHandler();
  799. this._photoSphereRenderer
  800. .bindTexture()
  801. .then(() => this._activate())
  802. .catch(() => {
  803. this.trigger(new ComponentEvent(EVENTS.ERROR, {
  804. type: ERROR_TYPE.FAIL_BIND_TEXTURE,
  805. message: "failed to bind texture"
  806. }));
  807. });
  808. }
  809. /**
  810. * @private
  811. * update values of YawPitchControl if needed.
  812. * For example, In Panorama mode, initial fov and pitchRange is changed by aspect ratio of image.
  813. *
  814. * This function should be called after isReady status is true.
  815. */
  816. private _updateYawPitchIfNeeded() {
  817. if (this._projectionType === PanoViewer.ProjectionType.PANORAMA) {
  818. // update fov by aspect ratio
  819. const image = this._photoSphereRenderer!.getContent()! as HTMLImageElement;
  820. let imageAspectRatio = image.naturalWidth / image.naturalHeight;
  821. let yawSize;
  822. let maxFov;
  823. // If height is larger than width, then we assume it's rotated by 90 degree.
  824. if (imageAspectRatio < 1) {
  825. // So inverse the aspect ratio.
  826. imageAspectRatio = 1 / imageAspectRatio;
  827. }
  828. if (imageAspectRatio < 6) {
  829. yawSize = mathUtil.toDegree(imageAspectRatio);
  830. // 0.5 means ratio of half height of cylinder(0.5) and radius of cylider(1). 0.5/1 = 0.5
  831. maxFov = mathUtil.toDegree(Math.atan(0.5)) * 2;
  832. } else {
  833. yawSize = 360;
  834. maxFov = (360 / imageAspectRatio); // Make it 5 fixed as axes does.
  835. }
  836. // console.log("_updateYawPitchIfNeeded", maxFov, "aspectRatio", image.naturalWidth, image.naturalHeight, "yawSize", yawSize);
  837. const minFov = (this._yawPitchControl!.option("fovRange"))[0];
  838. // this option should be called after fov is set.
  839. this._yawPitchControl!.option({
  840. "fov": maxFov, /* parameter for internal validation for pitchrange */
  841. "yawRange": [-yawSize / 2, yawSize / 2],
  842. "pitchRange": [-maxFov / 2, maxFov / 2],
  843. "fovRange": [minFov, maxFov]
  844. });
  845. this.lookAt({fov: maxFov});
  846. }
  847. }
  848. private _bindRendererHandler() {
  849. this._photoSphereRenderer!.on(PanoImageRenderer.EVENTS.ERROR, e => {
  850. this.trigger(new ComponentEvent(EVENTS.ERROR, e));
  851. });
  852. this._photoSphereRenderer!.on(PanoImageRenderer.EVENTS.RENDERING_CONTEXT_LOST, () => {
  853. this._deactivate();
  854. this.trigger(new ComponentEvent(EVENTS.ERROR, {
  855. type: ERROR_TYPE.RENDERING_CONTEXT_LOST,
  856. message: "webgl rendering context lost"
  857. }));
  858. });
  859. }
  860. private _initYawPitchControl(yawPitchConfig: Partial<YawPitchControlOptions>) {
  861. this._yawPitchControl = new YawPitchControl(yawPitchConfig);
  862. this._yawPitchControl.on(EVENTS.ANIMATION_END, e => {
  863. this.trigger(new ComponentEvent(EVENTS.ANIMATION_END, e));
  864. });
  865. this._yawPitchControl.on("change", e => {
  866. this._yaw = e.yaw;
  867. this._pitch = e.pitch;
  868. this._fov = e.fov;
  869. this._quaternion = e.quaternion;
  870. this.trigger(new ComponentEvent(EVENTS.VIEW_CHANGE, {
  871. yaw: e.yaw,
  872. pitch: e.pitch,
  873. fov: e.fov,
  874. quaternion: e.quaternion,
  875. isTrusted: e.isTrusted
  876. }));
  877. });
  878. }
  879. private _activate() {
  880. this._photoSphereRenderer!.attachTo(this._container);
  881. this._yawPitchControl!.enable();
  882. this.updateViewportDimensions();
  883. this._isReady = true;
  884. // update yawPitchControl after isReady status is true.
  885. this._updateYawPitchIfNeeded();
  886. this.trigger(new ComponentEvent(EVENTS.READY));
  887. this._photoSphereRenderer!.startRender();
  888. }
  889. /**
  890. * Destroy webgl context and block user interaction and stop rendering
  891. */
  892. private _deactivate() {
  893. // Turn off the video if it has one
  894. const video = this.getVideo();
  895. if (video) {
  896. video.pause();
  897. }
  898. if (this._isReady) {
  899. this._photoSphereRenderer!.stopRender();
  900. this._yawPitchControl!.disable();
  901. this._isReady = false;
  902. }
  903. if (this._photoSphereRenderer) {
  904. this._photoSphereRenderer.destroy();
  905. this._photoSphereRenderer = null;
  906. }
  907. }
  908. }
  909. export default PanoViewer;
comments powered by Disqus