function Viewer(options) {
    let container;

    options = options || {};
    options.controlBar =
        options.controlBar !== undefined ? options.controlBar : true;
    options.controlButtons = options.controlButtons || [
        "fullscreen",
        "setting",
        "video",
    ];
    options.autoHideControlBar =
        options.autoHideControlBar !== undefined
            ? options.autoHideControlBar
            : false;
    options.autoHideInfospot =
        options.autoHideInfospot !== undefined
            ? options.autoHideInfospot
            : true;
    options.horizontalView =
        options.horizontalView !== undefined ? options.horizontalView : false;
    options.clickTolerance = options.clickTolerance || 10;
    options.cameraFov = options.cameraFov || 60;
    options.reverseDragging = options.reverseDragging || false;
    options.enableReticle = options.enableReticle || false;
    options.dwellTime = options.dwellTime || 1500;
    options.autoReticleSelect =
        options.autoReticleSelect !== undefined
            ? options.autoReticleSelect
            : true;
    options.viewIndicator =
        options.viewIndicator !== undefined ? options.viewIndicator : false;
    options.indicatorSize = options.indicatorSize || 30;
    options.output = options.output ? options.output : "none";
    options.autoRotate = options.autoRotate || false;
    options.autoRotateSpeed = options.autoRotateSpeed || 2.0;
    options.autoRotateActivationDuration =
        options.autoRotateActivationDuration || 5000;

    this.options = options;

    /*
     * CSS Icon
     * const styleLoader = new StyleLoader();
     * styleLoader.inject( 'icono' );
     */

    // Container
    if (options.container) {
        container = options.container;
        container._width = container.clientWidth;
        container._height = container.clientHeight;
    } else {
        container = document.createElement("div");
        container.classList.add("panolens-container");
        container.style.width = "100%";
        container.style.height = "100%";
        container._width = window.innerWidth;
        container._height = window.innerHeight;
        document.body.appendChild(container);
    }

    this.container = container;

    this.camera =
        options.camera ||
        new PerspectiveCamera(
            this.options.cameraFov,
            this.container.clientWidth / this.container.clientHeight,
            1,
            10000
        );
    this.scene = options.scene || new Scene();
    this.renderer =
        options.renderer ||
        new WebGLRenderer({ alpha: true, antialias: false });
    this.sceneReticle = new Scene();

    this.viewIndicatorSize = this.options.indicatorSize;

    this.reticle = {};
    this.tempEnableReticle = this.options.enableReticle;

    this.mode = MODES.NORMAL;

    this.panorama = null;
    this.widget = null;

    this.hoverObject = null;
    this.infospot = null;
    this.pressEntityObject = null;
    this.pressObject = null;

    this.raycaster = new Raycaster();
    this.raycasterPoint = new Vector2();
    this.userMouse = new Vector2();
    this.updateCallbacks = [];
    this.requestAnimationId = null;

    this.cameraFrustum = new Frustum();
    this.cameraViewProjectionMatrix = new Matrix4();

    this.autoRotateRequestId = null;

    this.outputDivElement = null;

    this.touchSupported =
        "ontouchstart" in window ||
        (window.DocumentTouch && document instanceof DocumentTouch);

    // Handler references
    this.HANDLER_MOUSE_DOWN = this.onMouseDown.bind(this);
    this.HANDLER_MOUSE_UP = this.onMouseUp.bind(this);
    this.HANDLER_MOUSE_MOVE = this.onMouseMove.bind(this);
    this.HANDLER_WINDOW_RESIZE = this.onWindowResize.bind(this);
    this.HANDLER_KEY_DOWN = this.onKeyDown.bind(this);
    this.HANDLER_KEY_UP = this.onKeyUp.bind(this);
    this.HANDLER_TAP = this.onTap.bind(this, {
        clientX: this.container.clientWidth / 2,
        clientY: this.container.clientHeight / 2,
    });

    // Flag for infospot output
    this.OUTPUT_INFOSPOT = false;

    // Animations
    this.tweenLeftAnimation = new Tween.Tween();
    this.tweenUpAnimation = new Tween.Tween();

    // Renderer
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize(
        this.container.clientWidth,
        this.container.clientHeight
    );
    this.renderer.setClearColor(0x000000, 0);
    this.renderer.autoClear = false;

    // Append Renderer Element to container
    this.renderer.domElement.classList.add("panolens-canvas");
    this.renderer.domElement.style.display = "block";
    this.container.style.backgroundColor = "#000";
    this.container.appendChild(this.renderer.domElement);

    // Camera Controls
    this.OrbitControls = new OrbitControls(this.camera, this.container);
    this.OrbitControls.id = "orbit";
    this.OrbitControls.minDistance = 1;
    this.OrbitControls.noPan = true;
    this.OrbitControls.autoRotate = this.options.autoRotate;
    this.OrbitControls.autoRotateSpeed = this.options.autoRotateSpeed;

    this.DeviceOrientationControls = new DeviceOrientationControls(
        this.camera,
        this.container
    );
    this.DeviceOrientationControls.id = "device-orientation";
    this.DeviceOrientationControls.enabled = false;
    this.camera.position.z = 1;

    // Register change event if passiveRenering
    if (this.options.passiveRendering) {
        console.warn("passiveRendering is now deprecated");
    }

    // Controls
    this.controls = [this.OrbitControls, this.DeviceOrientationControls];
    this.control = this.OrbitControls;

    // Cardboard effect
    this.CardboardEffect = new CardboardEffect(this.renderer);
    this.CardboardEffect.setSize(
        this.container.clientWidth,
        this.container.clientHeight
    );

    // Stereo effect
    this.StereoEffect = new StereoEffect(this.renderer);
    this.StereoEffect.setSize(
        this.container.clientWidth,
        this.container.clientHeight
    );

    this.effect = this.CardboardEffect;

    // Add default hidden reticle
    this.addReticle();

    // Lock horizontal view
    if (this.options.horizontalView) {
        this.OrbitControls.minPolarAngle = Math.PI / 2;
        this.OrbitControls.maxPolarAngle = Math.PI / 2;
    }

    // Add Control UI
    if (this.options.controlBar !== false) {
        this.addDefaultControlBar(this.options.controlButtons);
    }

    // Add View Indicator
    if (this.options.viewIndicator) {
        this.addViewIndicator();
    }

    // Reverse dragging direction
    if (this.options.reverseDragging) {
        this.reverseDraggingDirection();
    }

    // Register event if reticle is enabled, otherwise defaults to mouse
    if (this.options.enableReticle) {
        this.enableReticleControl();
    } else {
        this.registerMouseAndTouchEvents();
    }

    // Output infospot position to an overlay container if specified
    if (this.options.output === "overlay") {
        this.addOutputElement();
    }

    // Register dom event listeners
    this.registerEventListeners();

    // Animate
    this.animate.call(this);
}
@classdesc Viewer contains pre-defined scene, camera and renderer
@constructor
@param {object} [options] - Use custom or default config options
@param {HTMLElement} [options.container] - A HTMLElement to host the canvas
@param {THREE.Scene} [options.scene=THREE.Scene] - A THREE.Scene which contains panorama and 3D objects
@param {THREE.Camera} [options.camera=THREE.PerspectiveCamera] - A THREE.Camera to view the scene
@param {THREE.WebGLRenderer} [options.renderer=THREE.WebGLRenderer] - A THREE.WebGLRenderer to render canvas
@param {boolean} [options.controlBar=true] - Show/hide control bar on the bottom of the container
@param {array}   [options.controlButtons=[]] - Button names to mount on controlBar if controlBar exists, Defaults to ['fullscreen', 'setting', 'video']
@param {boolean} [options.autoHideControlBar=false] - Auto hide control bar when click on non-active area
@param {boolean} [options.autoHideInfospot=true] - Auto hide infospots when click on non-active area
@param {boolean} [options.horizontalView=false] - Allow only horizontal camera control
@param {number}  [options.clickTolerance=10] - Distance tolerance to tigger click / tap event
@param {number}  [options.cameraFov=60] - Camera field of view value
@param {boolean} [options.reverseDragging=false] - Reverse dragging direction
@param {boolean} [options.enableReticle=false] - Enable reticle for mouseless interaction other than VR mode
@param {number}  [options.dwellTime=1500] - Dwell time for reticle selection in ms
@param {boolean} [options.autoReticleSelect=true] - Auto select a clickable target after dwellTime
@param {boolean} [options.viewIndicator=false] - Adds an angle view indicator in upper left corner
@param {number}  [options.indicatorSize=30] - Size of View Indicator
@param {string}  [options.output='none'] - Whether and where to output raycast position. Could be 'console' or 'overlay'
@param {boolean} [options.autoRotate=false] - Auto rotate
@param {number}  [options.autoRotateSpeed=2.0] - Auto rotate speed as in degree per second. Positive is counter-clockwise and negative is clockwise.
@param {number}  [options.autoRotateActivationDuration=5000] - Duration before auto rotatation when no user interactivity in ms