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;