UNPKG

@egjs/view360

Version:

360 integrated viewing solution from inside-out view to outside-in view. It provides user-friendly service by rotating 360 degrees through various user interaction such as motion sensor and touch.

1,021 lines (909 loc) 41.4 kB
import Component, { ComponentEvent } from "@egjs/component"; import Promise from "promise-polyfill"; import { quat } from "gl-matrix"; import { DeviceMotionEvent, checkXRSupport } from "../utils/browserFeature"; import YawPitchControl, { YawPitchControlOptions } from "../YawPitchControl/YawPitchControl"; import PanoImageRenderer from "../PanoImageRenderer/PanoImageRenderer"; import WebGLUtils from "../PanoImageRenderer/WebGLUtils"; import { util as mathUtil } from "../utils/math-util"; import { VERSION } from "../version"; import { CubemapConfig, ValueOf } from "../types/internal"; import { AnimationEndEvent, ReadyEvent, ViewChangeEvent, ErrorEvent } from "../types/event"; import { ERROR_TYPE, PANOVIEWER_EVENTS as EVENTS, GYRO_MODE, PROJECTION_TYPE, STEREO_FORMAT, DEFAULT_CANVAS_CLASS } from "./consts"; export interface PanoViewerOptions { image: string | HTMLElement; video: string | HTMLElement; projectionType: ValueOf<typeof PROJECTION_TYPE>; cubemapConfig: Partial<CubemapConfig>; stereoFormat: ValueOf<typeof STEREO_FORMAT>; width: number; height: number; yaw: number; pitch: number; fov: number; showPolePoint: boolean; useZoom: boolean; useKeyboard: boolean; gyroMode: ValueOf<typeof GYRO_MODE>; yawRange: number[]; pitchRange: number[]; fovRange: number[]; touchDirection: ValueOf<typeof PanoViewer.TOUCH_DIRECTION>; canvasClass: string; } export interface PanoViewerEvent { ready: ReadyEvent; viewChange: ViewChangeEvent; animationEnd: AnimationEndEvent<PanoViewer>; error: ErrorEvent; } /** * @memberof eg.view360 * @extends eg.Component * PanoViewer */ class PanoViewer extends Component<PanoViewerEvent> { /** * Check whether the current environment can execute PanoViewer * @ko 현재 브라우저 환경에서 PanoViewer 실행이 가능한지 여부를 반환합니다. * @return PanoViewer executable <ko>PanoViewer 실행가능 여부</ko> */ public static isSupported(): boolean { return WebGLUtils.isWebGLAvailable() && WebGLUtils.isStableWebGL(); } /** * Check whether the current environment supports the WebGL * @ko 현재 브라우저 환경이 WebGL 을 지원하는지 여부를 확인합니다. * @return WebGL support <ko>WebGL 지원여부</ko> */ 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 <ko>자이로 센서를 지원하는지 여부를 인자로 받는 함수</ko> */ public static isGyroSensorAvailable(callback: (isAvailable: boolean) => any) { if (!DeviceMotionEvent && callback) { callback(false); return; } let onDeviceMotionChange; const checkGyro = () => new Promise(res => { onDeviceMotionChange = deviceMotion => { const isGyroSensorAvailable = !(deviceMotion.rotationRate.alpha == null); res(isGyroSensorAvailable); }; window.addEventListener("devicemotion", onDeviceMotionChange); }); const timeout = () => new Promise(res => { 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; } /** * 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! // eslint-disable-next-line @typescript-eslint/naming-convention 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: ConstructorParameters<typeof PanoImageRenderer>[0]; private _isVideo: boolean; private _projectionType: ValueOf<typeof PROJECTION_TYPE>; private _cubemapConfig: Partial<CubemapConfig>; private _stereoFormat: ValueOf<typeof STEREO_FORMAT>; private _width: number; private _height: number; private _yaw: number; private _pitch: number; private _fov: number; private _gyroMode: ValueOf<typeof GYRO_MODE>; private _quaternion: quat | null; private _aspectRatio: number; private _isReady: boolean; private _canvasClass: string; // Internal Values private _photoSphereRenderer: PanoImageRenderer | null; private _yawPitchControl: YawPitchControl | null; /** * @classdesc 360 media viewer * @ko 360 미디어 뷰어 * * @param container The container element for the renderer. <ko>렌더러의 컨테이너 엘리먼트</ko> * @param options * * @param {String|HTMLImageElement} options.image Input image url or element (Use only image property or video property)<ko>입력 이미지 URL 혹은 엘리먼트(image 와 video 둘 중 하나만 설정)</ko> * @param {String|HTMLVideoElement} options.video Input video url or element(Use only image property or video property)<ko>입력 비디오 URL 혹은 엘리먼트(image 와 video 둘 중 하나만 설정)</ko> * @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> * @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> * @param {Object} [options.cubemapConfig.order = "RLUDBF"(ProjectionType === CUBEMAP) | "RLUDFB" (ProjectionType === CUBESTRIP)] Order of cubemap faces <ko>Cubemap 형태의 이미지가 배치된 순서</ko> * @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> * @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> * @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> * @param {Number} [options.width=width of container] the viewer's width. (in px) <ko>뷰어의 너비 (px 단위)</ko> * @param {Number} [options.height=height of container] the viewer's height.(in px) <ko>뷰어의 높이 (px 단위)</ko> * @param {Number} [options.yaw=0] Initial Yaw of camera (in degree) <ko>카메라의 초기 Yaw (degree 단위)</ko> * @param {Number} [options.pitch=0] Initial Pitch of camera (in degree) <ko>카메라의 초기 Pitch (degree 단위)</ko> * @param {Number} [options.fov=65] Initial vertical field of view of camera (in degree) <ko>카메라의 초기 수직 field of view (degree 단위)</ko> * @param {Boolean} [options.showPolePoint=false] If false, the pole is not displayed inside the viewport <ko>false 인 경우, 극점은 뷰포트 내부에 표시되지 않습니다</ko> * @param {Boolean} [options.useZoom=true] When true, enables zoom with the wheel and Pinch gesture <ko>true 일 때 휠 및 집기 제스춰로 확대 / 축소 할 수 있습니다.</ko> * @param {Boolean} [options.useKeyboard=true] When true, enables the keyboard move key control: awsd, arrow keys <ko>true 이면 키보드 이동 키 컨트롤을 활성화합니다: awsd, 화살표 키</ko> * @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> * @param {Array} [options.yawRange=[-180, 180]] Range of controllable Yaw values <ko>제어 가능한 Yaw 값의 범위</ko> * @param {Array} [options.pitchRange=[-90, 90]] Range of controllable Pitch values <ko>제어 가능한 Pitch 값의 범위</ko> * @param {Array} [options.fovRange=[30, 110]] Range of controllable vertical field of view values <ko>제어 가능한 수직 field of view 값의 범위</ko> * @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> * @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> * * @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}] * } * ``` */ public constructor(container: HTMLElement, options: Partial<PanoViewerOptions> = {}) { super(); // Raises the error event if webgl is not supported. if (!WebGLUtils.isWebGLAvailable()) { setTimeout(() => { this.trigger(new ComponentEvent(EVENTS.ERROR, { type: ERROR_TYPE.NO_WEBGL, message: "no webgl support" })); }, 0); return this; } if (!WebGLUtils.isStableWebGL()) { setTimeout(() => { this.trigger(new ComponentEvent(EVENTS.ERROR, { type: ERROR_TYPE.INVALID_DEVICE, message: "blacklisted browser" })); }, 0); return this; } if (!!options.image && !!options.video) { setTimeout(() => { this.trigger(new ComponentEvent(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! as HTMLImageElement || options.video! as HTMLVideoElement; this._isVideo = !!options.video; this._projectionType = options.projectionType || PROJECTION_TYPE.EQUIRECTANGULAR; this._cubemapConfig = { ...{ /* 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 }, trim: 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; this._canvasClass = options.canvasClass || DEFAULT_CANVAS_CLASS; const fovRange = options.fovRange || [30, 110]; const touchDirection = PanoViewer._isValidTouchDirection(options.touchDirection) ? options.touchDirection : YawPitchControl.TOUCH_DIRECTION_ALL; const yawPitchConfig = { ...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); } /** * Get the video element that the viewer is currently playing. You can use this for playback. * @ko 뷰어가 현재 사용 중인 비디오 요소를 얻습니다. 이 요소를 이용해 비디오의 컨트롤을 할 수 있습니다. * @return HTMLVideoElement<ko>HTMLVideoElement</ko> * @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<ko>입력 비디오 URL 혹은 엘리먼트 혹은 설정객체를 활용(image 와 video 둘 중 하나만 설정)</ko> * @param {object} param * @param {string} [param.projectionType={@link eg.view360.PanoViewer.PROJECTION_TYPE.EQUIRECTANGULAR}("equirectangular")] Projection Type<ko>프로젝션 타입</ko> * @param {object} param.cubemapConfig config cubemap projection layout. <ko>cubemap projection type 의 레이아웃 설정</ko> * @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> * * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko> * @example * ``` * panoViewer.setVideo("/path/to/video/video.mp4", { * projectionType: eg.view360.PanoViewer.PROJECTION_TYPE.EQUIRECTANGULAR * }); * ``` */ public setVideo(video: string | HTMLElement | { type: string; src: string }, 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<ko>이미지 객체</ko> * @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<ko>입력 이미지 URL 혹은 엘리먼트 혹은 설정객체를 활용(image 와 video 둘 중 하나만 설정한다.)</ko> * @param {object} param Additional information<ko>이미지 추가 정보</ko> * @param {string} [param.projectionType="equirectangular"] Projection Type<ko>프로젝션 타입</ko> * @param {object} param.cubemapConfig config cubemap projection layout. <ko>cubemap projection type 레이아웃</ko> * @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> * @param {boolean} [param.isVideo=false] Whether the given `imaage` is video or not.<ko>이미지가 비디오인지 여부</ko> * * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko> * @example * ``` * panoViewer.setImage("/path/to/image/image.png", { * projectionType: eg.view360.PanoViewer.PROJECTION_TYPE.CUBEMAP * }); * ``` */ public setImage(image: string | HTMLElement | { src: string; type: string }, param: Partial<{ projectionType: PanoViewer["_projectionType"]; cubemapConfig: PanoViewer["_cubemapConfig"]; stereoFormat: PanoViewer["_stereoFormat"]; isVideo: boolean; }> = {}) { const cubemapConfig = { ...{ order: "RLUDBF", tileConfig: { flipHorizontal: false, rotation: 0 }, trim: 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._deactivate(); this._image = image as HTMLImageElement; this._isVideo = isVideo; this._projectionType = param.projectionType || PROJECTION_TYPE.EQUIRECTANGULAR; this._cubemapConfig = cubemapConfig; this._stereoFormat = stereoFormat; 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.<ko>true면 항상 텍스쳐를 갱신하고 화면을 그리는 반면, false면 텍스쳐 갱신은 하지 않으며, 카메라 요소에 변화가 있을 때에만 화면을 그립니다.</ko> * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko> */ 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.<ko>Promise. resolve되었을 경우 아무것도 반환하지 않고, reject되었을 경우 그 이유를 담고있는 string을 반환한다.</ko> */ 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 instance<ko>PanoViewer 인스턴스</ko> */ 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}.<ko>WebXR용 추가 옵션, {@link https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit XRSessionInit}을 참조해주세요.</ko> * @return Promise containing either a string of resolved reason or an Error instance of rejected reason.<ko>Promise가 resolve된 이유(string) 혹은 reject된 이유(Error)</ko> */ public enterVR(options: { requiredFeatures?: any[]; optionalFeatures?: any[]; [key: string]: any; } = {}): globalThis.Promise<string> { if (!this._isReady) { return Promise.reject(new Error("PanoViewer is not ready to show image.")) as any; } return new Promise((resolve, reject) => { this.enableSensor() .then(() => this._photoSphereRenderer!.enterVR(options)) .then((res: string) => resolve(res)) .catch(e => reject(e)); }) as any; } /** * Exit VR stereo rendering mode. * @ko VR 스테레오 렌더링 모드에서 일반 렌더링 모드로 전환합니다. * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko> */ 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 instance<ko>PanoViewer 인스턴스</ko> */ 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 instance<ko>PanoViewer 인스턴스</ko> */ 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 instance<ko>PanoViewer 인스턴스</ko> * @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 instance<ko>PanoViewer 인스턴스</ko> * @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") as [number, number]; } /** * 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 instance<ko>PanoViewer 인스턴스</ko> */ 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") as [number, number]; } /** * Get the range of controllable Pitch values * @ko 컨트롤 가능한 Pitch 구간을 가져옵니다. */ public getPitchRange(): [number, number] { return this._yawPitchControl!.option("pitchRange") as [number, number]; } /** * Set the range of controllable yaw * @ko 컨트롤 가능한 Yaw 구간을 반환합니다. * @param {number[]} range * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko> * @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 instance<ko>PanoViewer 인스턴스</ko> * @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 instance<ko>PanoViewer 인스턴스</ko> */ 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 <ko>목표 yaw (degree 단위)</ko> * @param {number} orientation.pitch Target pitch in degree <ko>목표 pitch (degree 단위)</ko> * @param {number} orientation.fov Target vertical fov in degree <ko>목표 수직 fov (degree 단위)</ko> * @param {number} duration Animation duration in milliseconds <ko>애니메이션 시간 (밀리 초)</ko> * @return PanoViewer instance<ko>PanoViewer 인스턴스</ko> * @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}<ko>컨트롤 가능한 방향 {@link eg.view360.PanoViewer.TOUCH_DIRECTION}</ko> * @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}<ko>컨트롤 가능한 방향 {@link eg.view360.PanoViewer.TOUCH_DIRECTION}</ko> * @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 instance<ko>PanoViewer 인스턴스</ko> */ 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, this._container, this._canvasClass, { 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(new ComponentEvent(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()! as HTMLImageElement; let imageAspectRatio = image.naturalWidth / image.naturalHeight; 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); // 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; 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], "pitchRange": [-maxFov / 2, maxFov / 2], "fovRange": [minFov, maxFov] }); this.lookAt({fov: maxFov}); } } private _bindRendererHandler() { this._photoSphereRenderer!.on(PanoImageRenderer.EVENTS.ERROR, e => { this.trigger(new ComponentEvent(EVENTS.ERROR, e)); }); this._photoSphereRenderer!.on(PanoImageRenderer.EVENTS.RENDERING_CONTEXT_LOST, () => { this._deactivate(); this.trigger(new ComponentEvent(EVENTS.ERROR, { type: ERROR_TYPE.RENDERING_CONTEXT_LOST, message: "webgl rendering context lost" })); }); } private _initYawPitchControl(yawPitchConfig: Partial<YawPitchControlOptions>) { this._yawPitchControl = new YawPitchControl(yawPitchConfig); this._yawPitchControl.on(EVENTS.ANIMATION_END, e => { this.trigger(new ComponentEvent(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(new ComponentEvent(EVENTS.VIEW_CHANGE, { yaw: e.yaw, pitch: e.pitch, fov: e.fov, quaternion: e.quaternion, isTrusted: e.isTrusted })); }); } 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(new ComponentEvent(EVENTS.READY)); this._photoSphereRenderer!.startRender(); } /** * Destroy webgl context and block user interaction and stop rendering */ private _deactivate() { // Turn off the video if it has one const video = this.getVideo(); if (video) { video.pause(); } if (this._isReady) { this._photoSphereRenderer!.stopRender(); this._yawPitchControl!.disable(); this._isReady = false; } if (this._photoSphereRenderer) { this._photoSphereRenderer.destroy(); this._photoSphereRenderer = null; } } } export default PanoViewer;