Просмотр исходного кода

Merge pull request #1 from mpetroff/master

Pull my fork up to date
pull/723/head
GazHay 5 лет назад
committed by GitHub
Родитель
Сommit
90a7a1f45f
Не найден GPG ключ соответствующий данной подписи Идентификатор GPG ключа: 4AEE18F83AFDEB23
14 измененных файлов: 779 добавлений и 250 удалений
  1. +3
    -0
      .gitignore
  2. +1
    -1
      COPYING
  3. +1
    -1
      VERSION
  4. +75
    -0
      changelog.md
  5. +17
    -0
      doc/events.md
  6. +66
    -11
      doc/json-config-parameters.md
  7. +1
    -1
      package.json
  8. +4
    -0
      readme.md
  9. +5
    -0
      src/css/pannellum.css
  10. +177
    -49
      src/js/libpannellum.js
  11. +278
    -123
      src/js/pannellum.js
  12. +8
    -7
      src/standalone/standalone.js
  13. +139
    -55
      utils/multires/generate.py
  14. +4
    -2
      utils/video/videojs-pannellum-plugin.js

+ 3
- 0
.gitignore Просмотреть файл

@@ -6,3 +6,6 @@ build/**


# Ignore generated docs # Ignore generated docs
utils/doc/generated_docs utils/doc/generated_docs

# Ignore IntelliJ Files
.idea

+ 1
- 1
COPYING Просмотреть файл

@@ -1,4 +1,4 @@
Copyright (c) 2011-2017 Matthew Petroff
Copyright (c) 2011-2018 Matthew Petroff


Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in


+ 1
- 1
VERSION Просмотреть файл

@@ -1 +1 @@
2.3.2
2.4.1

+ 75
- 0
changelog.md Просмотреть файл

@@ -2,6 +2,81 @@ Changelog
========= =========




Changes in Pannellum 2.4.1
--------------------------

Bugfixes:

- Fix touch input issue in Chrome
- The API's `loadScene` method now works when no scenes have been loaded yet


Changes in Pannellum 2.4.0
--------------------------

New Features:

- Translation support
- Event for when scene change fade completes (`scenechangefadedone`)
- Events for touch starts and ends (`touchstart` and `touchend`)
- Added ability to set custom animation timing
function (`animationTimingFunction` parameter)
- Added option for only enable mouse wheel zoom while in
fullscreen (`mouseZoom` parameter)
- Added option to set title and author displayed while the load button
is displayed (`previewTitle` and `previewAuthor` parameters)
- Mouse and touch dragging can now be disabled (`draggable` parameter)
- Added option to disable keyboard controls (`disableKeyboardCtrl` parameter)
- CORS setting can now be configured

New API functions:

- Check if image is loaded (`isLoaded`)
- Method to update viewer after it is resized (`resize`)
- Set horizon pitch and roll (`setPose`)
- Turn device orientation control on and off, check if it is supported, and
check if it is activated (`startOrientation`, `stopOrientation`,
`isOrientationSupported`, and `isOrientationActive`)
- Method to retrieve viewer's container element (`getContainer`)

Improvements:

- Double-clicking now causes the viewer to zoom in (and back out when
double-clicking while zoomed in)
- New lines are now allowed in hot spot text
- Support for HTML in configuration strings can be enabled when using
the API (`escapeHTML` parameter)
- Fallback cursor is provided for browsers that don't support SVG data URIs
- Image type configuration parameter is now validated
- Optional callbacks added to `lookAt`, `setPitch`, `setYaw`, and `setHfov`
API functions
- Scroll events are now only captured when they're being used
- Viewer object is now assigned to a variable in the standalone viewer
- Hot spots can now be added with API before panorama is loaded
- Viewer UI is now created in a container element

Bugfixes:

- Fixed race condition when scene change hot spot is double-clicked
- Fixed bug with preview image absolute URLs
- Removed redundant constraints on yaw in API
- Tabbing now works, and only events for keys that are used are captured
- Fixed bug in HTML escaping
- Fixed bug that sometimes occurred when `orientationOnByDefault` was `true`
- Yaw no longer changes when device orientation mode is activated
- Fixed iOS 10 canvas size too big issue
- Fixed iOS 10 NPOT cube map issue
- Hot spots added via API are now permanent between scene changes
- Fixed multiple bugs with removing event listeners
- Fixed bug with multiresolution tile loading
- Fixed `sameAzimuth` target yaw not working when `northOffset` wasn't set
- Fixed bug yaw out of bounds in `mouseEventToCoords`
- Fixed bug with `animateMove` function
- Fixed bug with scene change fade
- Yaw animation is now always in the shortest direction
- Fixed bug related to removing hot spots


Changes in Pannellum 2.3.2 Changes in Pannellum 2.3.2
-------------------------- --------------------------




+ 17
- 0
doc/events.md Просмотреть файл

@@ -11,12 +11,28 @@ Fired when a scene change is initiated. A `load` event will be fired when the
new scene finishes loading. Passes scene ID string to handler. new scene finishes loading. Passes scene ID string to handler.




## `fullscreenchange`

Fired when browser fullscreen status changed. Passes status boolean to handler.


## `zoomchange`

Fired when scene hfov update. Passes new HFOV value to handler.


## `scenechangefadedone` ## `scenechangefadedone`


If a scene transition fade interval is specified, this event is fired when the If a scene transition fade interval is specified, this event is fired when the
fading is completed after changing scenes. fading is completed after changing scenes.




## `animatefinished`

Fired when any movements / animations finish, i.e. when the renderer stops
rendering new frames. Passes final pitch, yaw, and HFOV values to handler.


## `error` ## `error`


Fired when an error occured. The error message string is passed to the Fired when an error occured. The error message string is passed to the
@@ -46,3 +62,4 @@ Fired when a touch starts. Passes `TouchEvent` to handler.
## `touchend` ## `touchend`


Fired when a touch ends. Passes `TouchEvent` to handler. Fired when a touch ends. Passes `TouchEvent` to handler.


+ 66
- 11
doc/json-config-parameters.md Просмотреть файл

@@ -98,6 +98,13 @@ viewer is fullscreen.
If set to `false`, mouse and touch dragging is disabled. Defaults to `true`. If set to `false`, mouse and touch dragging is disabled. Defaults to `true`.




### `friction` (number)

Controls the "friction" that slows down the viewer motion after it is dragged
and released. Higher values mean the motion stops faster. Should be set
(0.0, 1.0]; defaults to 0.15.


### `disableKeyboardCtrl` (boolean) ### `disableKeyboardCtrl` (boolean)


If set to `true`, keyboard controls are disabled. Defaults to `false`. If set to `true`, keyboard controls are disabled. Defaults to `false`.
@@ -115,6 +122,11 @@ the fullscreen API.
If set to `false`, no controls are displayed. Defaults to `true`. If set to `false`, no controls are displayed. Defaults to `true`.




### `touchPanSpeedCoeffFactor` (number)

Adjusts panning speed from touch inputs. Defaults to `1`.


### `yaw` (number) ### `yaw` (number)


Sets the panorama's starting yaw position in degrees. Defaults to `0`. Sets the panorama's starting yaw position in degrees. Defaults to `0`.
@@ -146,7 +158,16 @@ Defaults to `undefined`, so the viewer center can reach `-90` / `90`.
### `minHfov` and `maxHfov` (number) ### `minHfov` and `maxHfov` (number)


Sets the minimum / maximum horizontal field of view, in degrees, that the Sets the minimum / maximum horizontal field of view, in degrees, that the
viewer can be set to. Defaults to `50` / `120`.
viewer can be set to. Defaults to `50` / `120`. Unless the `multiResMinHfov`
parameter is set to `true`, the `minHfov` parameter is ignored for
`multires` panoramas.


### `multiResMinHfov` (boolean)

When set to `false`, the `minHfov` parameter is ignored for `multires`
panoramas; an automatically calculated minimum horizontal field of view is used
instead. Defaults to `false`.




### `compass` (boolean) ### `compass` (boolean)
@@ -198,9 +219,15 @@ the configuration is provided via the URL; it defaults to `false` but can be
set to `true` when using the API. set to `true` when using the API.




### `hotSpots` (array)
### `crossOrigin` (string)

This specifies the type of CORS request used and can be set to either
`anonymous` or `use-credentials`. Defaults to `anonymous`.


### `hotSpots` (object)


This specifies an array of hot spots that can be links to other scenes,
This specifies a dictionary of hot spots that can be links to other scenes,
information, or external links. Each array element has the following properties. information, or external links. Each array element has the following properties.




@@ -230,6 +257,11 @@ spot.
If specified for an `info` hot spot, the hot spot links to the specified URL. If specified for an `info` hot spot, the hot spot links to the specified URL.
Not applicable for `scene` hot spots. Not applicable for `scene` hot spots.


#### `attributes` (dict)

Specifies URL's link attributes. If not set, the `target` attribute is set to
`_blank`, to open link in new tab to avoid opening in viewer frame / page.

#### `sceneId` (string) #### `sceneId` (string)


Specifies the ID of the scene to link to for `scene` hot spots. Not applicable Specifies the ID of the scene to link to for `scene` hot spots. Not applicable
@@ -251,7 +283,9 @@ maintain the same direction with regard to north.


#### `targetHfov` (number) #### `targetHfov` (number)


Specifies the HFOV of the target scene, in degrees.
Specifies the HFOV of the target scene, in degrees. Can also be set to `same`,
which uses the current HFOV of the current scene as the initial HFOV of the
target scene.


#### `id` #### `id`


@@ -285,6 +319,27 @@ Specifies the fade duration, in milliseconds, when transitioning between
scenes. Not defined by default. Only applicable for tours. Only works with scenes. Not defined by default. Only applicable for tours. Only works with
WebGL renderer. WebGL renderer.


### `capturedKeyNumbers` (array)

Specifies the key numbers that are captured in key events. Defaults to the
standard keys that are used by the viewer.

### `backgroundColor` ([number, number, number])

Specifies an array containing RGB values [0, 1] that sets the background color
for areas where no image data is available. Defaults to `[0, 0, 0]` (black).
For partial `equirectangular` panoramas this applies to areas past the edges of
the defined rectangle. For `multires` and `cubemap` (including fallback) panoramas
this applies to areas corresponding to missing tiles or faces.

### `avoidShowingBackground` (boolean)

If set to `true`, prevent displaying out-of-range areas of a partial panorama
by constraining the yaw and the field-of-view. Even at the corners and edges
of the canvas only areas actually belonging to the image
(i.e., within [`minYaw`, `maxYaw`] and [`minPitch`, `maxPitch`]) are shown,
thus setting the `backgroundColor` option is not needed if this option is set.
Defaults to `false`.




## `equirectangular` specific options ## `equirectangular` specific options
@@ -318,11 +373,6 @@ and the equirectangular image is not cropped symmetrically.
If set to `true`, any embedded Photo Sphere XMP data will be ignored; else, If set to `true`, any embedded Photo Sphere XMP data will be ignored; else,
said data will override any existing settings. Defaults to `false`. said data will override any existing settings. Defaults to `false`.


### `backgroundColor` ([number, number, number])

Specifies an array containing RGB values [0, 1] that sets the background color
shown past the edges of a partial panorama. Defaults to `[0, 0, 0]` (black).





## `cubemap` specific options ## `cubemap` specific options
@@ -332,8 +382,7 @@ shown past the edges of a partial panorama. Defaults to `[0, 0, 0]` (black).
This is an array of URLs for the six cube faces in the order front, right, This is an array of URLs for the six cube faces in the order front, right,
back, left, up, down. These are relative to `basePath` if it is set, else they back, left, up, down. These are relative to `basePath` if it is set, else they
are relative to the location of `pannellum.htm`. Absolute URLs can also be are relative to the location of `pannellum.htm`. Absolute URLs can also be
used.

used. Partial cubemap images may be specified by giving `null` instead of a URL.




## `multires` specific options ## `multires` specific options
@@ -397,6 +446,12 @@ Currently, only equirectangular dynamic content is supported.
The panorama source is considered dynamic when this is set to `true`. Defaults The panorama source is considered dynamic when this is set to `true`. Defaults
to `false`. This should be set to `true` for video. to `false`. This should be set to `true` for video.


### `dynamicUpdate` (boolean)

For dynamic content, viewer will start automatically updating when set to
`true`. Defaults to `false`. If the updates are controlled via the `setUpdate`
method, as with the Video.js plugin, this should be set to `false`.





## Additional information for tour configuration files ## Additional information for tour configuration files


+ 1
- 1
package.json Просмотреть файл

@@ -1,7 +1,7 @@
{ {
"name": "pannellum", "name": "pannellum",
"description": "Pannellum is a lightweight, free, and open source panorama viewer for the web.", "description": "Pannellum is a lightweight, free, and open source panorama viewer for the web.",
"version": "2.3.2",
"version": "2.4.1",
"bugs": { "bugs": {
"url": "https://github.com/mpetroff/pannellum/issues" "url": "https://github.com/mpetroff/pannellum/issues"
}, },


+ 4
- 0
readme.md Просмотреть файл

@@ -45,6 +45,10 @@ Since Pannellum is built with recent web standards, it requires a modern browser
#### No support: #### No support:
Internet Explorer 10 and previous Internet Explorer 10 and previous


#### Not officially supported:

Mobile / app frameworks are not officially supported. They may work, but they're not tested and are not the targeted platform.

## Translations ## Translations


All user-facing strings can be changed using the `strings` configuration parameter. There exists a [third-party respository of user-contributed translations](https://github.com/DanielBiegler/pannellum-translation) that can be used with this configuration option. All user-facing strings can be changed using the `strings` configuration parameter. There exists a [third-party respository of user-contributed translations](https://github.com/DanielBiegler/pannellum-translation) that can be used with this configuration option.


+ 5
- 0
src/css/pannellum.css Просмотреть файл

@@ -17,6 +17,7 @@
outline: 0; outline: 0;
line-height: 1.4; line-height: 1.4;
contain: content; contain: content;
touch-action: none;
} }


.pnlm-container * { .pnlm-container * {
@@ -437,3 +438,7 @@ div.pnlm-tooltip:hover span:after {
top: 0; top: 0;
left: 0; left: 0;
} }

.pnlm-pointer {
cursor: pointer;
}

+ 177
- 49
src/js/libpannellum.js Просмотреть файл

@@ -1,6 +1,6 @@
/* /*
* libpannellum - A WebGL and CSS 3D transform based Panorama Renderer * libpannellum - A WebGL and CSS 3D transform based Panorama Renderer
* Copyright (c) 2012-2017 Matthew Petroff
* Copyright (c) 2012-2018 Matthew Petroff
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -42,6 +42,7 @@ function Renderer(container) {
var pose; var pose;
var image, imageType, dynamic; var image, imageType, dynamic;
var texCoordBuffer, cubeVertBuf, cubeVertTexCoordBuf, cubeVertIndBuf; var texCoordBuffer, cubeVertBuf, cubeVertTexCoordBuf, cubeVertIndBuf;
var globalParams;


/** /**
* Initialize renderer. * Initialize renderer.
@@ -63,7 +64,7 @@ function Renderer(container) {
*/ */
this.init = function(_image, _imageType, _dynamic, haov, vaov, voffset, callback, params) { this.init = function(_image, _imageType, _dynamic, haov, vaov, voffset, callback, params) {
// Default argument for image type // Default argument for image type
if (typeof _imageType === undefined)
if (_imageType === undefined)
_imageType = 'equirectangular'; _imageType = 'equirectangular';


if (_imageType != 'equirectangular' && _imageType != 'cubemap' && if (_imageType != 'equirectangular' && _imageType != 'cubemap' &&
@@ -75,6 +76,7 @@ function Renderer(container) {
imageType = _imageType; imageType = _imageType;
image = _image; image = _image;
dynamic = _dynamic; dynamic = _dynamic;
globalParams = params || {};


// Clear old data // Clear old data
if (program) { if (program) {
@@ -99,6 +101,40 @@ function Renderer(container) {
pose = undefined; pose = undefined;


var s; var s;
var faceMissing = false;
var cubeImgWidth;
if (imageType == 'cubemap') {
for (s = 0; s < 6; s++) {
if (image[s].width > 0) {
if (cubeImgWidth === undefined)
cubeImgWidth = image[s].width;
if (cubeImgWidth != image[s].width)
console.log('Cube faces have inconsistent widths: ' + cubeImgWidth + ' vs. ' + image[s].width);
} else
faceMissing = true;
}
}
function fillMissingFaces(imgSize) {
if (faceMissing) { // Fill any missing fallback/cubemap faces with background
var nbytes = imgSize * imgSize * 4; // RGB, plus non-functional alpha
var imageArray = new Uint8ClampedArray(nbytes);
var rgb = params.backgroundColor ? params.backgroundColor : [0, 0, 0];
rgb[0] *= 255;
rgb[1] *= 255;
rgb[2] *= 255;
// Maybe filling could be done faster, see e.g. https://stackoverflow.com/questions/1295584/most-efficient-way-to-create-a-zero-filled-javascript-array
for (var i = 0; i < nbytes; i++) {
imageArray[i++] = rgb[0];
imageArray[i++] = rgb[1];
imageArray[i++] = rgb[2];
}
var backgroundSquare = new ImageData(imageArray, imgSize, imgSize);
for (s = 0; s < 6; s++) {
if (image[s].width == 0)
image[s] = backgroundSquare;
}
}
}
// This awful browser specific test exists because iOS 8/9 and IE 11 // This awful browser specific test exists because iOS 8/9 and IE 11
// don't display non-power-of-two cubemap textures but also don't // don't display non-power-of-two cubemap textures but also don't
@@ -108,7 +144,7 @@ function Renderer(container) {
// NPOT cubemaps, and the CSS 3D transform fallback renderer is used // NPOT cubemaps, and the CSS 3D transform fallback renderer is used
// instead. // instead.
if (!(imageType == 'cubemap' && if (!(imageType == 'cubemap' &&
(image[0].width & (image[0].width - 1)) !== 0 &&
(cubeImgWidth & (cubeImgWidth - 1)) !== 0 &&
(navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 8_/) || (navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 8_/) ||
navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 9_/) || navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 9_/) ||
navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 10_/) || navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad).* os 10_/) ||
@@ -121,6 +157,7 @@ function Renderer(container) {
} }
// If there is no WebGL, fall back to CSS 3D transform renderer. // If there is no WebGL, fall back to CSS 3D transform renderer.
// This will discard the image loaded so far and load the fallback image.
// While browser specific tests are usually frowned upon, the // While browser specific tests are usually frowned upon, the
// fallback viewer only really works with WebKit/Blink and IE 10/11 // fallback viewer only really works with WebKit/Blink and IE 10/11
// (it doesn't work properly in Firefox). // (it doesn't work properly in Firefox).
@@ -204,6 +241,16 @@ function Renderer(container) {
// Draw image width duplicated edge pixels on canvas // Draw image width duplicated edge pixels on canvas
faceContext.putImageData(imgData, 0, 0); faceContext.putImageData(imgData, 0, 0);
incLoaded.call(this);
};
var incLoaded = function() {
if (this.width > 0) {
if (fallbackImgSize === undefined)
fallbackImgSize = this.width;
if (fallbackImgSize != this.width)
console.log('Fallback faces have inconsistent widths: ' + fallbackImgSize + ' vs. ' + this.width);
} else
faceMissing = true;
loaded++; loaded++;
if (loaded == 6) { if (loaded == 6) {
fallbackImgSize = this.width; fallbackImgSize = this.width;
@@ -211,23 +258,27 @@ function Renderer(container) {
callback(); callback();
} }
}; };
faceMissing = false;
for (s = 0; s < 6; s++) { for (s = 0; s < 6; s++) {
var faceImg = new Image(); var faceImg = new Image();
faceImg.crossOrigin = 'anonymous';
faceImg.crossOrigin = globalParams.crossOrigin ? globalParams.crossOrigin : 'anonymous';
faceImg.side = s; faceImg.side = s;
faceImg.onload = onLoad; faceImg.onload = onLoad;
faceImg.onerror = incLoaded; // ignore missing face to support partial fallback image
if (imageType == 'multires') { if (imageType == 'multires') {
faceImg.src = encodeURI(path.replace('%s', sides[s]) + '.' + image.extension); faceImg.src = encodeURI(path.replace('%s', sides[s]) + '.' + image.extension);
} else { } else {
faceImg.src = encodeURI(image[s].src); faceImg.src = encodeURI(image[s].src);
} }
} }
fillMissingFaces(fallbackImgSize);
return; return;
} else if (!gl) { } else if (!gl) {
console.log('Error: no WebGL support detected!'); console.log('Error: no WebGL support detected!');
throw {type: 'no webgl'}; throw {type: 'no webgl'};
} }
if (imageType == 'cubemap')
fillMissingFaces(cubeImgWidth);
if (image.basePath) { if (image.basePath) {
image.fullpath = image.basePath + image.path; image.fullpath = image.basePath + image.path;
} else { } else {
@@ -243,19 +294,18 @@ function Renderer(container) {
} }
// Make sure image isn't too big // Make sure image isn't too big
var width, maxWidth;
var maxWidth = 0;
if (imageType == 'equirectangular') { if (imageType == 'equirectangular') {
width = Math.max(image.width, image.height);
maxWidth = gl.getParameter(gl.MAX_TEXTURE_SIZE); maxWidth = gl.getParameter(gl.MAX_TEXTURE_SIZE);
if (width > maxWidth) {
console.log('Error: The image is too big; it\'s ' + width + 'px wide, but this device\'s maximum supported width is ' + maxWidth + 'px.');
throw {type: 'webgl size error', width: width, maxWidth: maxWidth};
if (Math.max(image.width / 2, image.height) > maxWidth) {
console.log('Error: The image is too big; it\'s ' + image.width + 'px wide, '+
'but this device\'s maximum supported size is ' + (maxWidth * 2) + 'px.');
throw {type: 'webgl size error', width: image.width, maxWidth: maxWidth * 2};
} }
} else if (imageType == 'cubemap') { } else if (imageType == 'cubemap') {
width = image[0].width;
maxWidth = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE);
if (width > maxWidth) {
console.log('Error: The cube face image is too big; it\'s ' + width + 'px wide, but this device\'s maximum supported width is ' + maxWidth + 'px.');
if (cubeImgWidth > gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)) {
console.log('Error: The image is too big; it\'s ' + width + 'px wide, '+
'but this device\'s maximum supported size is ' + maxWidth + 'px.');
throw {type: 'webgl size error', width: width, maxWidth: maxWidth}; throw {type: 'webgl size error', width: width, maxWidth: maxWidth};
} }
} }
@@ -311,6 +361,11 @@ function Renderer(container) {


program.drawInProgress = false; program.drawInProgress = false;


// Set background clear color (does not apply to cubemap/fallback image)
var color = params.backgroundColor ? params.backgroundColor : [0, 0, 0];
gl.clearColor(color[0], color[1], color[2], 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

// Look up texture coordinates location // Look up texture coordinates location
program.texCoordLocation = gl.getAttribLocation(program, 'a_texCoord'); program.texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(program.texCoordLocation); gl.enableVertexAttribArray(program.texCoordLocation);
@@ -325,7 +380,7 @@ function Renderer(container) {


// Pass aspect ratio // Pass aspect ratio
program.aspectRatio = gl.getUniformLocation(program, 'u_aspectRatio'); program.aspectRatio = gl.getUniformLocation(program, 'u_aspectRatio');
gl.uniform1f(program.aspectRatio, canvas.clientWidth / canvas.clientHeight);
gl.uniform1f(program.aspectRatio, gl.drawingBufferWidth / gl.drawingBufferHeight);


// Locate psi, theta, focal length, horizontal extent, vertical extent, and vertical offset // Locate psi, theta, focal length, horizontal extent, vertical extent, and vertical offset
program.psi = gl.getUniformLocation(program, 'u_psi'); program.psi = gl.getUniformLocation(program, 'u_psi');
@@ -344,7 +399,6 @@ function Renderer(container) {
// Set background color // Set background color
if (imageType == 'equirectangular') { if (imageType == 'equirectangular') {
program.backgroundColor = gl.getUniformLocation(program, 'u_backgroundColor'); program.backgroundColor = gl.getUniformLocation(program, 'u_backgroundColor');
var color = params.backgroundColor ? params.backgroundColor : [0, 0, 0];
gl.uniform4fv(program.backgroundColor, color.concat([1])); gl.uniform4fv(program.backgroundColor, color.concat([1]));
} }


@@ -362,8 +416,44 @@ function Renderer(container) {
gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[0]); gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[0]);
gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[2]); gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[2]);
} else { } else {
// Upload image to the texture
gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
if (image.width <= maxWidth) {
gl.uniform1i(gl.getUniformLocation(program, 'u_splitImage'), 0);
// Upload image to the texture
gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
} else {
// Image needs to be split into two parts due to texture size limits
gl.uniform1i(gl.getUniformLocation(program, 'u_splitImage'), 1);

// Draw image on canvas
var cropCanvas = document.createElement('canvas');
cropCanvas.width = image.width;
cropCanvas.height = image.height;
var cropContext = cropCanvas.getContext('2d');
cropContext.drawImage(image, 0, 0);

// Upload first half of image to the texture
var cropImage = cropContext.getImageData(0, 0, image.width / 2, image.height);
gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage);

// Create and bind texture for second half of image
program.texture2 = gl.createTexture();
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(glBindType, program.texture2);
gl.uniform1i(gl.getUniformLocation(program, 'u_image1'), 1);

// Upload second half of image to the texture
cropImage = cropContext.getImageData(image.width / 2, 0, image.width / 2, image.height);
gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage);

// Set parameters for rendering any size
gl.texParameteri(glBindType, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(glBindType, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

// Reactive first texture unit
gl.activeTexture(gl.TEXTURE0);
}
} }


// Set parameters for rendering any size // Set parameters for rendering any size
@@ -540,16 +630,18 @@ function Renderer(container) {
// Apply face transforms // Apply face transforms
var faces = Object.keys(transforms); var faces = Object.keys(transforms);
for (i = 0; i < 6; i++) { for (i = 0; i < 6; i++) {
var face = world.querySelector('.pnlm-' + faces[i] + 'face').style;
face.webkitTransform = transform + transforms[faces[i]];
face.transform = transform + transforms[faces[i]];
var face = world.querySelector('.pnlm-' + faces[i] + 'face');
if (!face)
continue; // ignore missing face to support partial cubemap/fallback image
face.style.webkitTransform = transform + transforms[faces[i]];
face.style.transform = transform + transforms[faces[i]];
} }
return; return;
} }
if (imageType != 'multires') { if (imageType != 'multires') {
// Calculate focal length from vertical field of view // Calculate focal length from vertical field of view
var vfov = 2 * Math.atan(Math.tan(hfov * 0.5) / (canvas.clientWidth / canvas.clientHeight));
var vfov = 2 * Math.atan(Math.tan(hfov * 0.5) / (gl.drawingBufferWidth / gl.drawingBufferHeight));
focal = 1 / Math.tan(vfov * 0.5); focal = 1 / Math.tan(vfov * 0.5);


// Pass psi, theta, roll, and focal length // Pass psi, theta, roll, and focal length
@@ -571,7 +663,7 @@ function Renderer(container) {
} else { } else {
// Create perspective matrix // Create perspective matrix
var perspMatrix = makePersp(hfov, canvas.clientWidth / canvas.clientHeight, 0.1, 100.0);
var perspMatrix = makePersp(hfov, gl.drawingBufferWidth / gl.drawingBufferHeight, 0.1, 100.0);
// Find correct zoom level // Find correct zoom level
checkZoom(hfov); checkZoom(hfov);
@@ -606,12 +698,29 @@ function Renderer(container) {
var ntmp = new MultiresNode(vtmps[s], sides[s], 1, 0, 0, image.fullpath); var ntmp = new MultiresNode(vtmps[s], sides[s], 1, 0, 0, image.fullpath);
testMultiresNode(rotPersp, ntmp, pitch, yaw, hfov); testMultiresNode(rotPersp, ntmp, pitch, yaw, hfov);
} }
program.currentNodes.sort(multiresNodeRenderSort); program.currentNodes.sort(multiresNodeRenderSort);
// Only process one tile per frame to improve responsiveness
for (i = 0; i < program.currentNodes.length; i++) {
if (!program.currentNodes[i].texture) {
setTimeout(processNextTile, 0, program.currentNodes[i]);
break;
// Unqueue any pending requests for nodes that are no longer visible
for (i = pendingTextureRequests.length - 1; i >= 0; i--) {
if (program.currentNodes.indexOf(pendingTextureRequests[i].node) === -1) {
pendingTextureRequests[i].node.textureLoad = false;
pendingTextureRequests.splice(i, 1);
}
}
// Allow one request to be pending, so that we can create a texture buffer for that in advance of loading actually beginning
if (pendingTextureRequests.length === 0) {
for (i = 0; i < program.currentNodes.length; i++) {
var node = program.currentNodes[i];
if (!node.texture && !node.textureLoad) {
node.textureLoad = true;
setTimeout(processNextTile, 0, node);
// Only process one tile per frame to improve responsiveness
break;
}
} }
} }
@@ -695,8 +804,9 @@ function Renderer(container) {
function multiresDraw() { function multiresDraw() {
if (!program.drawInProgress) { if (!program.drawInProgress) {
program.drawInProgress = true; program.drawInProgress = true;
gl.clear(gl.COLOR_BUFFER_BIT);
for ( var i = 0; i < program.currentNodes.length; i++ ) { for ( var i = 0; i < program.currentNodes.length; i++ ) {
if (program.currentNodes[i].textureLoaded) {
if (program.currentNodes[i].textureLoaded > 1) {
//var color = program.currentNodes[i].color; //var color = program.currentNodes[i].color;
//gl.uniform4f(program.colorUniform, color[0], color[1], color[2], 1.0); //gl.uniform4f(program.colorUniform, color[0], color[1], color[2], 1.0);
@@ -984,7 +1094,7 @@ function Renderer(container) {
* @returns {number[]} Generated perspective matrix. * @returns {number[]} Generated perspective matrix.
*/ */
function makePersp(hfov, aspect, znear, zfar) { function makePersp(hfov, aspect, znear, zfar) {
var fovy = 2 * Math.atan(Math.tan(hfov/2) * canvas.clientHeight / canvas.clientWidth);
var fovy = 2 * Math.atan(Math.tan(hfov/2) * gl.drawingBufferHeight / gl.drawingBufferWidth);
var f = 1 / Math.tan(fovy/2); var f = 1 / Math.tan(fovy/2);
return [ return [
f/aspect, 0, 0, 0, f/aspect, 0, 0, 0,
@@ -1009,23 +1119,31 @@ function Renderer(container) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null); gl.bindTexture(gl.TEXTURE_2D, null);
} }
var pendingTextureRequests = [];


// Based on http://blog.tojicode.com/2012/03/javascript-memory-optimization-and.html // Based on http://blog.tojicode.com/2012/03/javascript-memory-optimization-and.html
var loadTexture = (function() { var loadTexture = (function() {
var cacheTop = 4; // Maximum number of concurrents loads var cacheTop = 4; // Maximum number of concurrents loads
var textureImageCache = {}; var textureImageCache = {};
var pendingTextureRequests = [];
var crossOrigin;


function TextureImageLoader() { function TextureImageLoader() {
var self = this; var self = this;
this.texture = this.callback = null; this.texture = this.callback = null;
this.image = new Image(); this.image = new Image();
this.image.crossOrigin = 'anonymous';
this.image.addEventListener('load', function() {
processLoadedTexture(self.image, self.texture);
self.callback(self.texture);
this.image.crossOrigin = crossOrigin ? crossOrigin : 'anonymous';
var loadFn = (function() {
if (self.image.width > 0 && self.image.height > 0) { // ignore missing tile to supporting partial image
processLoadedTexture(self.image, self.texture);
self.callback(self.texture, true);
} else {
self.callback(self.texture, false);
}
releaseTextureImageLoader(self); releaseTextureImageLoader(self);
}); });
this.image.addEventListener('load', loadFn);
this.image.addEventListener('error', loadFn); // ignore missing tile file to support partial image, otherwise retry loop causes high CPU load
}; };


TextureImageLoader.prototype.loadTexture = function(src, texture, callback) { TextureImageLoader.prototype.loadTexture = function(src, texture, callback) {
@@ -1034,7 +1152,8 @@ function Renderer(container) {
this.image.src = src; this.image.src = src;
}; };


function PendingTextureRequest(src, texture, callback) {
function PendingTextureRequest(node, src, texture, callback) {
this.node = node;
this.src = src; this.src = src;
this.texture = texture; this.texture = texture;
this.callback = callback; this.callback = callback;
@@ -1051,12 +1170,13 @@ function Renderer(container) {
for (var i = 0; i < cacheTop; i++) for (var i = 0; i < cacheTop; i++)
textureImageCache[i] = new TextureImageLoader(); textureImageCache[i] = new TextureImageLoader();


return function(src, callback) {
return function(node, src, callback, _crossOrigin) {
crossOrigin = _crossOrigin;
var texture = gl.createTexture(); var texture = gl.createTexture();
if (cacheTop) if (cacheTop)
textureImageCache[--cacheTop].loadTexture(src, texture, callback); textureImageCache[--cacheTop].loadTexture(src, texture, callback);
else else
pendingTextureRequests.push(new PendingTextureRequest(src, texture, callback));
pendingTextureRequests.push(new PendingTextureRequest(node, src, texture, callback));
return texture; return texture;
}; };
})(); })();
@@ -1067,13 +1187,10 @@ function Renderer(container) {
* @param {MultiresNode} node - Input node. * @param {MultiresNode} node - Input node.
*/ */
function processNextTile(node) { function processNextTile(node) {
if (!node.textureLoad) {
node.textureLoad = true;
loadTexture(encodeURI(node.path + '.' + image.extension), function(texture) {
node.texture = texture;
node.textureLoaded = true;
});
}
loadTexture(node, encodeURI(node.path + '.' + image.extension), function(texture, loaded) {
node.texture = texture;
node.textureLoaded = loaded ? 2 : 1;
}, globalParams.crossOrigin);
} }
/** /**
@@ -1085,7 +1202,7 @@ function Renderer(container) {
// Find optimal level // Find optimal level
var newLevel = 1; var newLevel = 1;
while ( newLevel < image.maxLevel && while ( newLevel < image.maxLevel &&
canvas.width > image.tileResolution *
gl.drawingBufferWidth > image.tileResolution *
Math.pow(2, newLevel - 1) * Math.tan(hfov / 2) * 0.707 ) { Math.pow(2, newLevel - 1) * Math.tan(hfov / 2) * 0.707 ) {
newLevel++; newLevel++;
} }
@@ -1241,7 +1358,9 @@ var fragEquiCubeBase = [
'const float PI = 3.14159265358979323846264;', 'const float PI = 3.14159265358979323846264;',


// Texture // Texture
'uniform sampler2D u_image;',
'uniform sampler2D u_image0;',
'uniform sampler2D u_image1;',
'uniform bool u_splitImage;',
'uniform samplerCube u_imageCube;', 'uniform samplerCube u_imageCube;',


// Coordinates passed in from vertex shader // Coordinates passed in from vertex shader
@@ -1286,8 +1405,17 @@ var fragEquirectangular = fragEquiCubeBase + [
// Map from [-1,1] to [0,1] and flip y-axis // Map from [-1,1] to [0,1] and flip y-axis
'if(coord.x < -u_h || coord.x > u_h || coord.y < -u_v + u_vo || coord.y > u_v + u_vo)', 'if(coord.x < -u_h || coord.x > u_h || coord.y < -u_v + u_vo || coord.y > u_v + u_vo)',
'gl_FragColor = u_backgroundColor;', 'gl_FragColor = u_backgroundColor;',
'else',
'gl_FragColor = texture2D(u_image, vec2((coord.x + u_h) / (u_h * 2.0), (-coord.y + u_v + u_vo) / (u_v * 2.0)));',
'else {',
'if(u_splitImage) {',
// Image was split into two textures to work around texture size limits
'if(coord.x < 0.0)',
'gl_FragColor = texture2D(u_image0, vec2((coord.x + u_h) / u_h, (-coord.y + u_v + u_vo) / (u_v * 2.0)));',
'else',
'gl_FragColor = texture2D(u_image1, vec2((coord.x + u_h) / u_h - 1.0, (-coord.y + u_v + u_vo) / (u_v * 2.0)));',
'} else {',
'gl_FragColor = texture2D(u_image0, vec2((coord.x + u_h) / (u_h * 2.0), (-coord.y + u_v + u_vo) / (u_v * 2.0)));',
'}',
'}',
'}' '}'
].join('\n'); ].join('\n');




+ 278
- 123
src/js/pannellum.js Просмотреть файл

@@ -1,6 +1,6 @@
/* /*
* Pannellum - An HTML5 based Panorama Viewer * Pannellum - An HTML5 based Panorama Viewer
* Copyright (c) 2011-2017 Matthew Petroff
* Copyright (c) 2011-2018 Matthew Petroff
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -49,7 +49,7 @@ var config,
onPointerDownPitch = 0, onPointerDownPitch = 0,
keysDown = new Array(10), keysDown = new Array(10),
fullscreenActive = false, fullscreenActive = false,
loaded = false,
loaded,
error = false, error = false,
isTimedOut = false, isTimedOut = false,
listenersAdded = false, listenersAdded = false,
@@ -67,11 +67,13 @@ var config,
externalEventListeners = {}, externalEventListeners = {},
specifiedPhotoSphereExcludes = [], specifiedPhotoSphereExcludes = [],
update = false, // Should we update when still to render dynamic content update = false, // Should we update when still to render dynamic content
eps = 1e-6,
hotspotsCreated = false; hotspotsCreated = false;


var defaultConfig = { var defaultConfig = {
hfov: 100, hfov: 100,
minHfov: 50, minHfov: 50,
multiResMinHfov: false,
maxHfov: 120, maxHfov: 120,
pitch: 0, pitch: 0,
minPitch: undefined, minPitch: undefined,
@@ -90,6 +92,7 @@ var defaultConfig = {
northOffset: 0, northOffset: 0,
showFullscreenCtrl: true, showFullscreenCtrl: true,
dynamic: false, dynamic: false,
dynamicUpdate: false,
doubleClickZoom: true, doubleClickZoom: true,
keyboardZoom: true, keyboardZoom: true,
mouseZoom: true, mouseZoom: true,
@@ -99,9 +102,14 @@ var defaultConfig = {
orientationOnByDefault: false, orientationOnByDefault: false,
hotSpotDebug: false, hotSpotDebug: false,
backgroundColor: [0, 0, 0], backgroundColor: [0, 0, 0],
avoidShowingBackground: false,
animationTimingFunction: timingFunction, animationTimingFunction: timingFunction,
draggable: true, draggable: true,
disableKeyboardCtrl: false, disableKeyboardCtrl: false,
crossOrigin: 'anonymous',
touchPanSpeedCoeffFactor: 1,
capturedKeyNumbers: [16, 17, 27, 37, 38, 39, 40, 61, 65, 68, 83, 87, 107, 109, 173, 187, 189],
friction: 0.15
}; };


// Translatable / configurable strings // Translatable / configurable strings
@@ -128,8 +136,6 @@ defaultConfig.strings = {
unknownError: 'Unknown error. Check developer console.', unknownError: 'Unknown error. Check developer console.',
} }


var usedKeyNumbers = [16, 17, 27, 37, 38, 39, 40, 61, 65, 68, 83, 87, 107, 109, 173, 187, 189];

// Initialize container // Initialize container
container = typeof container === 'string' ? document.getElementById(container) : container; container = typeof container === 'string' ? document.getElementById(container) : container;
container.classList.add('pnlm-container'); container.classList.add('pnlm-container');
@@ -306,7 +312,7 @@ function init() {
panoImage = []; panoImage = [];
for (i = 0; i < 6; i++) { for (i = 0; i < 6; i++) {
panoImage.push(new Image()); panoImage.push(new Image());
panoImage[i].crossOrigin = 'anonymous';
panoImage[i].crossOrigin = config.crossOrigin;
} }
infoDisplay.load.lbox.style.display = 'block'; infoDisplay.load.lbox.style.display = 'block';
infoDisplay.load.lbar.style.display = 'none'; infoDisplay.load.lbar.style.display = 'none';
@@ -350,18 +356,23 @@ function init() {
var onError = function(e) { var onError = function(e) {
var a = document.createElement('a'); var a = document.createElement('a');
a.href = e.target.src; a.href = e.target.src;
a.innerHTML = a.href;
a.textContent = a.href;
anError(config.strings.fileAccessError.replace('%s', a.outerHTML)); anError(config.strings.fileAccessError.replace('%s', a.outerHTML));
}; };
for (i = 0; i < panoImage.length; i++) { for (i = 0; i < panoImage.length; i++) {
panoImage[i].onload = onLoad;
panoImage[i].onerror = onError;
p = config.cubeMap[i]; p = config.cubeMap[i];
if (config.basePath && !absoluteURL(p)) {
p = config.basePath + p;
if (p == "null") { // support partial cubemap image with explicitly empty faces
console.log('Will use background instead of missing cubemap face ' + i);
onLoad();
} else {
if (config.basePath && !absoluteURL(p)) {
p = config.basePath + p;
}
panoImage[i].onload = onLoad;
panoImage[i].onerror = onError;
panoImage[i].src = sanitizeURL(p);
} }
panoImage[i].src = encodeURI(p);
} }
} else if (config.type == 'multires') { } else if (config.type == 'multires') {
onImageLoad(); onImageLoad();
@@ -385,8 +396,8 @@ function init() {
if (xhr.status != 200) { if (xhr.status != 200) {
// Display error if image can't be loaded // Display error if image can't be loaded
var a = document.createElement('a'); var a = document.createElement('a');
a.href = encodeURI(p);
a.innerHTML = a.href;
a.href = p;
a.textContent = a.href;
anError(config.strings.fileAccessError.replace('%s', a.outerHTML)); anError(config.strings.fileAccessError.replace('%s', a.outerHTML));
} }
var img = this.response; var img = this.response;
@@ -427,6 +438,7 @@ function init() {
} }
xhr.responseType = 'blob'; xhr.responseType = 'blob';
xhr.setRequestHeader('Accept', 'image/*,*/*;q=0.9'); xhr.setRequestHeader('Accept', 'image/*,*/*;q=0.9');
xhr.withCredentials = config.crossOrigin === 'use-credentials';
xhr.send(); xhr.send();
} }
} }
@@ -468,10 +480,10 @@ function onImageLoad() {
if (config.doubleClickZoom) { if (config.doubleClickZoom) {
dragFix.addEventListener('dblclick', onDocumentDoubleClick, false); dragFix.addEventListener('dblclick', onDocumentDoubleClick, false);
} }
uiContainer.addEventListener('mozfullscreenchange', onFullScreenChange, false);
uiContainer.addEventListener('webkitfullscreenchange', onFullScreenChange, false);
uiContainer.addEventListener('msfullscreenchange', onFullScreenChange, false);
uiContainer.addEventListener('fullscreenchange', onFullScreenChange, false);
container.addEventListener('mozfullscreenchange', onFullScreenChange, false);
container.addEventListener('webkitfullscreenchange', onFullScreenChange, false);
container.addEventListener('msfullscreenchange', onFullScreenChange, false);
container.addEventListener('fullscreenchange', onFullScreenChange, false);
window.addEventListener('resize', onDocumentResize, false); window.addEventListener('resize', onDocumentResize, false);
window.addEventListener('orientationchange', onDocumentResize, false); window.addEventListener('orientationchange', onDocumentResize, false);
if (!config.disableKeyboardCtrl) { if (!config.disableKeyboardCtrl) {
@@ -480,13 +492,17 @@ function onImageLoad() {
container.addEventListener('blur', clearKeys, false); container.addEventListener('blur', clearKeys, false);
} }
document.addEventListener('mouseleave', onDocumentMouseUp, false); document.addEventListener('mouseleave', onDocumentMouseUp, false);
dragFix.addEventListener('touchstart', onDocumentTouchStart, false);
dragFix.addEventListener('touchmove', onDocumentTouchMove, false);
dragFix.addEventListener('touchend', onDocumentTouchEnd, false);
dragFix.addEventListener('pointerdown', onDocumentPointerDown, false);
dragFix.addEventListener('pointermove', onDocumentPointerMove, false);
dragFix.addEventListener('pointerup', onDocumentPointerUp, false);
dragFix.addEventListener('pointerleave', onDocumentPointerUp, false);
if (document.documentElement.style.pointerAction === '' &&
document.documentElement.style.touchAction === '') {
dragFix.addEventListener('pointerdown', onDocumentPointerDown, false);
dragFix.addEventListener('pointermove', onDocumentPointerMove, false);
dragFix.addEventListener('pointerup', onDocumentPointerUp, false);
dragFix.addEventListener('pointerleave', onDocumentPointerUp, false);
} else {
dragFix.addEventListener('touchstart', onDocumentTouchStart, false);
dragFix.addEventListener('touchmove', onDocumentTouchMove, false);
dragFix.addEventListener('touchend', onDocumentTouchEnd, false);
}


// Deal with MS pointer events // Deal with MS pointer events
if (window.navigator.pointerEnabled) if (window.navigator.pointerEnabled)
@@ -494,6 +510,7 @@ function onImageLoad() {
} }


renderInit(); renderInit();
setHfov(config.hfov); // possibly adapt hfov after configuration and canvas is complete; prevents empty space on top or bottom by zomming out too much
setTimeout(function(){isTimedOut = true;}, 500); setTimeout(function(){isTimedOut = true;}, 500);
} }


@@ -644,8 +661,9 @@ function aboutMessage(event) {
function mousePosition(event) { function mousePosition(event) {
var bounds = container.getBoundingClientRect(); var bounds = container.getBoundingClientRect();
var pos = {}; var pos = {};
pos.x = event.clientX - bounds.left;
pos.y = event.clientY - bounds.top;
// pageX / pageY needed for iOS
pos.x = (event.clientX || event.pageX) - bounds.left;
pos.y = (event.clientY || event.pageY) - bounds.top;
return pos; return pos;
} }


@@ -869,7 +887,7 @@ function onDocumentTouchMove(event) {
// //
// Currently this seems to *roughly* keep initial drag/pan start position close to // Currently this seems to *roughly* keep initial drag/pan start position close to
// the user's finger while panning regardless of zoom level / config.hfov value. // the user's finger while panning regardless of zoom level / config.hfov value.
var touchmovePanSpeedCoeff = config.hfov / 360;
var touchmovePanSpeedCoeff = (config.hfov / 360) * config.touchPanSpeedCoeffFactor;


var yaw = (onPointerDownPointerX - clientX) * touchmovePanSpeedCoeff + onPointerDownYaw; var yaw = (onPointerDownPointerX - clientX) * touchmovePanSpeedCoeff + onPointerDownYaw;
speed.yaw = (yaw - config.yaw) % 360 * 0.2; speed.yaw = (yaw - config.yaw) % 360 * 0.2;
@@ -926,7 +944,7 @@ function onDocumentPointerMove(event) {
pointerCoordinates[i].clientY = event.clientY; pointerCoordinates[i].clientY = event.clientY;
event.targetTouches = pointerCoordinates; event.targetTouches = pointerCoordinates;
onDocumentTouchMove(event); onDocumentTouchMove(event);
//event.preventDefault();
event.preventDefault();
return; return;
} }
} }
@@ -986,7 +1004,6 @@ function onDocumentMouseWheel(event) {
setHfov(config.hfov + event.detail * 1.5); setHfov(config.hfov + event.detail * 1.5);
speed.hfov = event.detail > 0 ? 1 : -1; speed.hfov = event.detail > 0 ? 1 : -1;
} }
animateInit(); animateInit();
} }


@@ -1007,8 +1024,8 @@ function onDocumentKeyPress(event) {
var keynumber = event.which || event.keycode; var keynumber = event.which || event.keycode;


// Override default action for keys that are used // Override default action for keys that are used
if (usedKeyNumbers.indexOf(keynumber) < 0)
return
if (config.capturedKeyNumbers.indexOf(keynumber) < 0)
return;
event.preventDefault(); event.preventDefault();
// If escape key is pressed // If escape key is pressed
@@ -1043,8 +1060,8 @@ function onDocumentKeyUp(event) {
var keynumber = event.which || event.keycode; var keynumber = event.which || event.keycode;
// Override default action for keys that are used // Override default action for keys that are used
if (usedKeyNumbers.indexOf(keynumber) < 0)
return
if (config.capturedKeyNumbers.indexOf(keynumber) < 0)
return;
event.preventDefault(); event.preventDefault();
// Change key // Change key
@@ -1232,19 +1249,19 @@ function keyRepeat() {
// "Inertia" // "Inertia"
if (diff > 0 && !config.autoRotate) { if (diff > 0 && !config.autoRotate) {
// "Friction" // "Friction"
var friction = 0.85;
var slowDownFactor = 1 - config.friction;
// Yaw // Yaw
if (!keysDown[4] && !keysDown[5] && !keysDown[8] && !keysDown[9] && !animatedMove.yaw) { if (!keysDown[4] && !keysDown[5] && !keysDown[8] && !keysDown[9] && !animatedMove.yaw) {
config.yaw += speed.yaw * diff * friction;
config.yaw += speed.yaw * diff * slowDownFactor;
} }
// Pitch // Pitch
if (!keysDown[2] && !keysDown[3] && !keysDown[6] && !keysDown[7] && !animatedMove.pitch) { if (!keysDown[2] && !keysDown[3] && !keysDown[6] && !keysDown[7] && !animatedMove.pitch) {
config.pitch += speed.pitch * diff * friction;
config.pitch += speed.pitch * diff * slowDownFactor;
} }
// Zoom // Zoom
if (!keysDown[0] && !keysDown[1] && !animatedMove.hfov) { if (!keysDown[0] && !keysDown[1] && !animatedMove.hfov) {
setHfov(config.hfov + speed.hfov * diff * friction);
setHfov(config.hfov + speed.hfov * diff * slowDownFactor);
} }
} }


@@ -1262,7 +1279,7 @@ function keyRepeat() {
} }
// Stop movement if opposite controls are pressed // Stop movement if opposite controls are pressed
if (keysDown[0] && keysDown[0]) {
if (keysDown[0] && keysDown[1]) {
speed.hfov = 0; speed.hfov = 0;
} }
if ((keysDown[2] || keysDown[6]) && (keysDown[3] || keysDown[7])) { if ((keysDown[2] || keysDown[6]) && (keysDown[3] || keysDown[7])) {
@@ -1287,11 +1304,7 @@ function animateMove(axis) {
t.endPosition === t.startPosition) { t.endPosition === t.startPosition) {
result = t.endPosition; result = t.endPosition;
speed[axis] = 0; speed[axis] = 0;
var callback = animatedMove[axis].callback,
callbackArgs = animatedMove[axis].callbackArgs;
delete animatedMove[axis]; delete animatedMove[axis];
if (typeof callback == 'function')
callback(callbackArgs);
} }
config[axis] = result; config[axis] = result;
} }
@@ -1316,7 +1329,7 @@ function onDocumentResize() {
//animateInit(); //animateInit();


// Kludge to deal with WebKit regression: https://bugs.webkit.org/show_bug.cgi?id=93525 // Kludge to deal with WebKit regression: https://bugs.webkit.org/show_bug.cgi?id=93525
onFullScreenChange();
onFullScreenChange('resize');
} }


/** /**
@@ -1359,6 +1372,7 @@ function animate() {
} else if (renderer && (renderer.isLoading() || (config.dynamic === true && update))) { } else if (renderer && (renderer.isLoading() || (config.dynamic === true && update))) {
requestAnimationFrame(animate); requestAnimationFrame(animate);
} else { } else {
fireEvent('animatefinished', {pitch: _this.getPitch(), yaw: _this.getYaw(), hfov: _this.getHfov()});
animating = false; animating = false;
prevTime = undefined; prevTime = undefined;
var autoRotateStartTime = config.autoRotateInactivityDelay - var autoRotateStartTime = config.autoRotateInactivityDelay -
@@ -1385,32 +1399,50 @@ function render() {
var tmpyaw; var tmpyaw;


if (loaded) { if (loaded) {
if (config.yaw > 180) {
config.yaw -= 360;
} else if (config.yaw < -180) {
config.yaw += 360;
}

// Keep a tmp value of yaw for autoRotate comparison later // Keep a tmp value of yaw for autoRotate comparison later
tmpyaw = config.yaw; tmpyaw = config.yaw;


// Optionally avoid showing background (empty space) on left or right by adapting min/max yaw
var hoffcut = 0,
voffcut = 0;
if (config.avoidShowingBackground) {
var canvas = renderer.getCanvas(),
hfov2 = config.hfov / 2,
vfov2 = Math.atan2(Math.tan(hfov2 / 180 * Math.PI), (canvas.width / canvas.height)) * 180 / Math.PI,
transposed = config.vaov > config.haov;
if (transposed) {
voffcut = vfov2 * (1 - Math.min(Math.cos((config.pitch - hfov2) / 180 * Math.PI),
Math.cos((config.pitch + hfov2) / 180 * Math.PI)));
} else {
hoffcut = hfov2 * (1 - Math.min(Math.cos((config.pitch - vfov2) / 180 * Math.PI),
Math.cos((config.pitch + vfov2) / 180 * Math.PI)));
}
}

// Ensure the yaw is within min and max allowed // Ensure the yaw is within min and max allowed
var yawRange = config.maxYaw - config.minYaw, var yawRange = config.maxYaw - config.minYaw,
minYaw = -180, minYaw = -180,
maxYaw = 180; maxYaw = 180;
if (yawRange < 360) { if (yawRange < 360) {
minYaw = config.minYaw + config.hfov / 2;
maxYaw = config.maxYaw - config.hfov / 2;
minYaw = config.minYaw + config.hfov / 2 + hoffcut;
maxYaw = config.maxYaw - config.hfov / 2 - hoffcut;
if (yawRange < config.hfov) { if (yawRange < config.hfov) {
// Lock yaw to average of min and max yaw when both can be seen at once // Lock yaw to average of min and max yaw when both can be seen at once
minYaw = maxYaw = (minYaw + maxYaw) / 2; minYaw = maxYaw = (minYaw + maxYaw) / 2;
} }
config.yaw = Math.max(minYaw, Math.min(maxYaw, config.yaw));
} }
config.yaw = Math.max(minYaw, Math.min(maxYaw, config.yaw));
if (config.yaw > 180) {
config.yaw -= 360;
} else if (config.yaw < -180) {
config.yaw += 360;
}

// Check if we autoRotate in a limited by min and max yaw // Check if we autoRotate in a limited by min and max yaw
// If so reverse direction // If so reverse direction
if (config.autoRotate !== false && tmpyaw != config.yaw) {
if (config.autoRotate !== false && tmpyaw != config.yaw &&
prevTime !== undefined) { // this condition prevents changing the direction initially
config.autoRotate *= -1; config.autoRotate *= -1;
} }


@@ -1653,7 +1685,7 @@ function createHotSpot(hs) {
p = hs.video; p = hs.video;
if (config.basePath && !absoluteURL(p)) if (config.basePath && !absoluteURL(p))
p = config.basePath + p; p = config.basePath + p;
video.src = encodeURI(p);
video.src = sanitizeURL(p);
video.controls = true; video.controls = true;
video.style.width = hs.width + 'px'; video.style.width = hs.width + 'px';
renderContainer.appendChild(div); renderContainer.appendChild(div);
@@ -1663,11 +1695,11 @@ function createHotSpot(hs) {
if (config.basePath && !absoluteURL(p)) if (config.basePath && !absoluteURL(p))
p = config.basePath + p; p = config.basePath + p;
a = document.createElement('a'); a = document.createElement('a');
a.href = encodeURI(hs.URL ? hs.URL : p);
a.href = sanitizeURL(hs.URL ? hs.URL : p);
a.target = '_blank'; a.target = '_blank';
span.appendChild(a); span.appendChild(a);
var image = document.createElement('img'); var image = document.createElement('img');
image.src = encodeURI(p);
image.src = sanitizeURL(p);
image.style.width = hs.width + 'px'; image.style.width = hs.width + 'px';
image.style.paddingTop = '5px'; image.style.paddingTop = '5px';
renderContainer.appendChild(div); renderContainer.appendChild(div);
@@ -1675,11 +1707,17 @@ function createHotSpot(hs) {
span.style.maxWidth = 'initial'; span.style.maxWidth = 'initial';
} else if (hs.URL) { } else if (hs.URL) {
a = document.createElement('a'); a = document.createElement('a');
a.href = encodeURI(hs.URL);
a.target = '_blank';
a.href = sanitizeURL(hs.URL);
if (hs.attributes) {
for (var key in hs.attributes) {
a.setAttribute(key, hs.attributes[key]);
}
} else {
a.target = '_blank';
}
renderContainer.appendChild(a); renderContainer.appendChild(a);
div.style.cursor = 'pointer';
span.style.cursor = 'pointer';
div.className += ' pnlm-pointer';
span.className += ' pnlm-pointer';
a.appendChild(div); a.appendChild(div);
} else { } else {
if (hs.sceneId) { if (hs.sceneId) {
@@ -1690,8 +1728,8 @@ function createHotSpot(hs) {
} }
return false; return false;
}; };
div.style.cursor = 'pointer';
span.style.cursor = 'pointer';
div.className += ' pnlm-pointer';
span.className += ' pnlm-pointer';
} }
renderContainer.appendChild(div); renderContainer.appendChild(div);
} }
@@ -1709,8 +1747,8 @@ function createHotSpot(hs) {
div.addEventListener('click', function(e) { div.addEventListener('click', function(e) {
hs.clickHandlerFunc(e, hs.clickHandlerArgs); hs.clickHandlerFunc(e, hs.clickHandlerArgs);
}, 'false'); }, 'false');
div.style.cursor = 'pointer';
span.style.cursor = 'pointer';
div.className += ' pnlm-pointer';
span.className += ' pnlm-pointer';
} }
hs.div = div; hs.div = div;
}; };
@@ -1746,10 +1784,12 @@ function destroyHotSpots() {
if (hs) { if (hs) {
for (var i = 0; i < hs.length; i++) { for (var i = 0; i < hs.length; i++) {
var current = hs[i].div; var current = hs[i].div;
while(current.parentNode != renderContainer) {
current = current.parentNode;
if (current) {
while (current.parentNode && current.parentNode != renderContainer) {
current = current.parentNode;
}
renderContainer.removeChild(current);
} }
renderContainer.removeChild(current);
delete hs[i].div; delete hs[i].div;
} }
} }
@@ -1899,7 +1939,7 @@ function processOptions(isPreview) {
p = config.basePath + p; p = config.basePath + p;
preview = document.createElement('div'); preview = document.createElement('div');
preview.className = 'pnlm-preview-img'; preview.className = 'pnlm-preview-img';
preview.style.backgroundImage = "url('" + encodeURI(p) + "')";
preview.style.backgroundImage = "url('" + sanitizeURLForCss(p) + "')";
renderContainer.appendChild(preview); renderContainer.appendChild(preview);
} }


@@ -1940,7 +1980,16 @@ function processOptions(isPreview) {
break; break;
case 'fallback': case 'fallback':
infoDisplay.errorMsg.innerHTML = '<p>Your browser does not support WebGL.<br><a href="' + encodeURI(config[key]) + '" target="_blank">Click here to view this panorama in an alternative viewer.</a></p>';
var link = document.createElement('a');
link.href = sanitizeURL(config[key]);
link.target = '_blank';
link.textContent = 'Click here to view this panorama in an alternative viewer.';
var message = document.createElement('p');
message.textContent = 'Your browser does not support WebGL.'
message.appendChild(document.createElement('br'));
message.appendChild(link);
infoDisplay.errorMsg.innerHTML = ''; // Removes all children nodes
infoDisplay.errorMsg.appendChild(message);
break; break;
case 'hfov': case 'hfov':
@@ -2058,15 +2107,16 @@ function toggleFullscreen() {
* Event handler for fullscreen changes. * Event handler for fullscreen changes.
* @private * @private
*/ */
function onFullScreenChange() {
if (document.fullscreen || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement) {
function onFullScreenChange(resize) {
if (document.fullscreenElement || document.fullscreen || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement) {
controls.fullscreen.classList.add('pnlm-fullscreen-toggle-button-active'); controls.fullscreen.classList.add('pnlm-fullscreen-toggle-button-active');
fullscreenActive = true; fullscreenActive = true;
} else { } else {
controls.fullscreen.classList.remove('pnlm-fullscreen-toggle-button-active'); controls.fullscreen.classList.remove('pnlm-fullscreen-toggle-button-active');
fullscreenActive = false; fullscreenActive = false;
} }

if (resize !== 'resize')
fireEvent('fullscreenchange', fullscreenActive);
// Resize renderer (deal with browser quirks and fixes #155) // Resize renderer (deal with browser quirks and fixes #155)
renderer.resize(); renderer.resize();
setHfov(config.hfov); setHfov(config.hfov);
@@ -2104,20 +2154,31 @@ function zoomOut() {
function constrainHfov(hfov) { function constrainHfov(hfov) {
// Keep field of view within bounds // Keep field of view within bounds
var minHfov = config.minHfov; var minHfov = config.minHfov;
if (config.type == 'multires' && renderer) {
if (config.type == 'multires' && renderer && config.multiResMinHfov) {
minHfov = Math.min(minHfov, renderer.getCanvas().width / (config.multiRes.cubeResolution / 90 * 0.9)); minHfov = Math.min(minHfov, renderer.getCanvas().width / (config.multiRes.cubeResolution / 90 * 0.9));
} }
if (minHfov > config.maxHfov) { if (minHfov > config.maxHfov) {
// Don't change view if bounds don't make sense // Don't change view if bounds don't make sense
console.log('HFOV bounds do not make sense (minHfov > maxHfov).') console.log('HFOV bounds do not make sense (minHfov > maxHfov).')
return config.hfov; return config.hfov;
} if (hfov < minHfov) {
return minHfov;
}
var newHfov = config.hfov;
if (hfov < minHfov) {
newHfov = minHfov;
} else if (hfov > config.maxHfov) { } else if (hfov > config.maxHfov) {
return config.maxHfov;
newHfov = config.maxHfov;
} else { } else {
return hfov;
newHfov = hfov;
}
// Optionally avoid showing background (empty space) on top or bottom by adapting newHfov
if (config.avoidShowingBackground && renderer) {
var canvas = renderer.getCanvas();
newHfov = Math.min(newHfov,
Math.atan(Math.tan((config.maxPitch - config.minPitch) / 360 * Math.PI) /
canvas.height * canvas.width)
* 360 / Math.PI);
} }
return newHfov;
} }


/** /**
@@ -2127,6 +2188,7 @@ function constrainHfov(hfov) {
*/ */
function setHfov(hfov) { function setHfov(hfov) {
config.hfov = constrainHfov(hfov); config.hfov = constrainHfov(hfov);
fireEvent('zoomchange', config.hfov);
} }


/** /**
@@ -2148,6 +2210,7 @@ function load() {
// since it is a new scene and the error from previous maybe because of lacking // since it is a new scene and the error from previous maybe because of lacking
// memory etc and not because of a lack of WebGL support etc // memory etc and not because of a lack of WebGL support etc
clearError(); clearError();
loaded = false;


controls.load.style.display = 'none'; controls.load.style.display = 'none';
infoDisplay.load.box.style.display = 'inline'; infoDisplay.load.box.style.display = 'inline';
@@ -2228,6 +2291,13 @@ function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) {
} }
fireEvent('scenechange', sceneId); fireEvent('scenechange', sceneId);
load(); load();

// Properly handle switching to dynamic scenes
update = config.dynamicUpdate === true;
if (config.dynamic) {
panoImage = config.panorama;
onImageLoad();
}
} }


/** /**
@@ -2269,13 +2339,41 @@ function escapeHTML(s) {
} }


/** /**
* Removes possibility of XSS attacks with URLs.
* The URL cannot be of protocol 'javascript'.
* @private
* @param {string} url - URL to sanitize
* @returns {string} Sanitized URL
*/
function sanitizeURL(url) {
if (url.trim().toLowerCase().indexOf('javascript:') === 0) {
return 'about:blank';
}
return url;
}

/**
* Removes possibility of XSS atacks with URLs for CSS.
* The URL will be sanitized with `sanitizeURL()` and single quotes
* and double quotes escaped.
* @private
* @param {string} url - URL to sanitize
* @returns {string} Sanitized URL
*/
function sanitizeURLForCss(url) {
return sanitizeURL(url)
.replace(/"/g, '%22')
.replace(/'/g, '%27');
}

/**
* Checks whether or not a panorama is loaded. * Checks whether or not a panorama is loaded.
* @memberof Viewer * @memberof Viewer
* @instance * @instance
* @returns {boolean} `true` if a panorama is loaded, else `false` * @returns {boolean} `true` if a panorama is loaded, else `false`
*/ */
this.isLoaded = function() { this.isLoaded = function() {
return loaded;
return Boolean(loaded);
}; };


/** /**
@@ -2299,16 +2397,22 @@ this.getPitch = function() {
* @returns {Viewer} `this` * @returns {Viewer} `this`
*/ */
this.setPitch = function(pitch, animated, callback, callbackArgs) { this.setPitch = function(pitch, animated, callback, callbackArgs) {
latestInteraction = Date.now();
if (Math.abs(pitch - config.pitch) <= eps) {
if (typeof callback == 'function')
callback(callbackArgs);
return this;
}
animated = animated == undefined ? 1000: Number(animated); animated = animated == undefined ? 1000: Number(animated);
if (animated) { if (animated) {
animatedMove.pitch = { animatedMove.pitch = {
'startTime': Date.now(), 'startTime': Date.now(),
'startPosition': config.pitch, 'startPosition': config.pitch,
'endPosition': pitch, 'endPosition': pitch,
'duration': animated,
'callback': callback,
'callbackArgs': callbackArgs
'duration': animated
} }
if (typeof callback == 'function')
setTimeout(function(){callback(callbackArgs)}, animated);
} else { } else {
config.pitch = pitch; config.pitch = pitch;
} }
@@ -2360,6 +2464,12 @@ this.getYaw = function() {
* @returns {Viewer} `this` * @returns {Viewer} `this`
*/ */
this.setYaw = function(yaw, animated, callback, callbackArgs) { this.setYaw = function(yaw, animated, callback, callbackArgs) {
latestInteraction = Date.now();
if (Math.abs(yaw - config.yaw) <= eps) {
if (typeof callback == 'function')
callback(callbackArgs);
return this;
}
animated = animated == undefined ? 1000: Number(animated); animated = animated == undefined ? 1000: Number(animated);
yaw = ((yaw + 180) % 360) - 180 // Keep in bounds yaw = ((yaw + 180) % 360) - 180 // Keep in bounds
if (animated) { if (animated) {
@@ -2373,10 +2483,10 @@ this.setYaw = function(yaw, animated, callback, callbackArgs) {
'startTime': Date.now(), 'startTime': Date.now(),
'startPosition': config.yaw, 'startPosition': config.yaw,
'endPosition': yaw, 'endPosition': yaw,
'duration': animated,
'callback': callback,
'callbackArgs': callbackArgs
'duration': animated
} }
if (typeof callback == 'function')
setTimeout(function(){callback(callbackArgs)}, animated);
} else { } else {
config.yaw = yaw; config.yaw = yaw;
} }
@@ -2428,16 +2538,22 @@ this.getHfov = function() {
* @returns {Viewer} `this` * @returns {Viewer} `this`
*/ */
this.setHfov = function(hfov, animated, callback, callbackArgs) { this.setHfov = function(hfov, animated, callback, callbackArgs) {
latestInteraction = Date.now();
if (Math.abs(hfov - config.hfov) <= eps) {
if (typeof callback == 'function')
callback(callbackArgs);
return this;
}
animated = animated == undefined ? 1000: Number(animated); animated = animated == undefined ? 1000: Number(animated);
if (animated) { if (animated) {
animatedMove.hfov = { animatedMove.hfov = {
'startTime': Date.now(), 'startTime': Date.now(),
'startPosition': config.hfov, 'startPosition': config.hfov,
'endPosition': constrainHfov(hfov), 'endPosition': constrainHfov(hfov),
'duration': animated,
'callback': callback,
'callbackArgs': callbackArgs
'duration': animated
} }
if (typeof callback == 'function')
setTimeout(function(){callback(callbackArgs)}, animated);
} else { } else {
setHfov(hfov); setHfov(hfov);
} }
@@ -2483,16 +2599,20 @@ this.setHfovBounds = function(bounds) {
*/ */
this.lookAt = function(pitch, yaw, hfov, animated, callback, callbackArgs) { this.lookAt = function(pitch, yaw, hfov, animated, callback, callbackArgs) {
animated = animated == undefined ? 1000: Number(animated); animated = animated == undefined ? 1000: Number(animated);
if (pitch !== undefined) {
if (pitch !== undefined && Math.abs(pitch - config.pitch) > eps) {
this.setPitch(pitch, animated, callback, callbackArgs); this.setPitch(pitch, animated, callback, callbackArgs);
callback = undefined; callback = undefined;
} }
if (yaw !== undefined) {
if (yaw !== undefined && Math.abs(yaw - config.yaw) > eps) {
this.setYaw(yaw, animated, callback, callbackArgs); this.setYaw(yaw, animated, callback, callbackArgs);
callback = undefined; callback = undefined;
} }
if (hfov !== undefined)
if (hfov !== undefined && Math.abs(hfov - config.hfov) > eps) {
this.setHfov(hfov, animated, callback, callbackArgs); this.setHfov(hfov, animated, callback, callbackArgs);
callback = undefined;
}
if (typeof callback == 'function')
callback(callbackArgs);
return this; return this;
} }


@@ -2596,6 +2716,16 @@ this.stopAutoRotate = function() {
}; };


/** /**
* Stops all movement.
* @memberof Viewer
* @instance
*/
this.stopMovement = function() {
stopAnimation();
speed = {'yaw': 0, 'pitch': 0, 'hfov': 0};
}

/**
* Returns the panorama renderer. * Returns the panorama renderer.
* @memberof Viewer * @memberof Viewer
* @instance * @instance
@@ -2643,7 +2773,7 @@ this.mouseEventToCoords = function(event) {
* @returns {Viewer} `this` * @returns {Viewer} `this`
*/ */
this.loadScene = function(sceneId, pitch, yaw, hfov) { this.loadScene = function(sceneId, pitch, yaw, hfov) {
if (loaded)
if (loaded !== false)
loadScene(sceneId, pitch, yaw, hfov); loadScene(sceneId, pitch, yaw, hfov);
return this; return this;
} }
@@ -2707,6 +2837,16 @@ this.getConfig = function() {
} }


/** /**
* Get viewer's container element.
* @memberof Viewer
* @instance
* @returns {HTMLElement} Container `div` element
*/
this.getContainer = function() {
return container;
}

/**
* Add a new hot spot. * Add a new hot spot.
* @memberof Viewer * @memberof Viewer
* @instance * @instance
@@ -2747,26 +2887,43 @@ this.addHotSpot = function(hs, sceneId) {
* @memberof Viewer * @memberof Viewer
* @instance * @instance
* @param {string} hotSpotId - The ID of the hot spot * @param {string} hotSpotId - The ID of the hot spot
* @param {string} [sceneId] - Removes hot spot from specified scene if provided, else from current scene
* @returns {boolean} True if deletion is successful, else false * @returns {boolean} True if deletion is successful, else false
*/ */
this.removeHotSpot = function(hotSpotId) {
if (!config.hotSpots)
return false;
for (var i = 0; i < config.hotSpots.length; i++) {
if (config.hotSpots[i].hasOwnProperty('id') &&
config.hotSpots[i].id === hotSpotId) {
// Delete hot spot DOM elements
var current = config.hotSpots[i].div;
while (current.parentNode != renderContainer)
current = current.parentNode;
renderContainer.removeChild(current);
delete config.hotSpots[i].div;
// Remove hot spot from configuration
config.hotSpots.splice(i, 1);
return true;
this.removeHotSpot = function(hotSpotId, sceneId) {
if (sceneId === undefined || config.scene == sceneId) {
if (!config.hotSpots)
return false;
for (var i = 0; i < config.hotSpots.length; i++) {
if (config.hotSpots[i].hasOwnProperty('id') &&
config.hotSpots[i].id === hotSpotId) {
// Delete hot spot DOM elements
var current = config.hotSpots[i].div;
while (current.parentNode != renderContainer)
current = current.parentNode;
renderContainer.removeChild(current);
delete config.hotSpots[i].div;
// Remove hot spot from configuration
config.hotSpots.splice(i, 1);
return true;
}
}
} else {
if (initialConfig.scenes.hasOwnProperty(sceneId)) {
if (!initialConfig.scenes[sceneId].hasOwnProperty('hotSpots'))
return false;
for (var i = 0; i < initialConfig.scenes[sceneId].hotSpots.length; i++) {
if (initialConfig.scenes[sceneId].hotSpots[i].hasOwnProperty('id') &&
initialConfig.scenes[sceneId].hotSpots[i].id === hotSpotId) {
// Remove hot spot from configuration
initialConfig.scenes[sceneId].hotSpots.splice(i, 1);
return true;
}
}
} else {
return false;
} }
} }
return false;
} }


/** /**
@@ -2775,7 +2932,8 @@ this.removeHotSpot = function(hotSpotId) {
* @instance * @instance
*/ */
this.resize = function() { this.resize = function() {
onDocumentResize();
if (renderer)
onDocumentResize();
} }


/** /**
@@ -2818,6 +2976,16 @@ this.startOrientation = function() {
} }


/** /**
* Check if device orientation control is currently activated.
* @memberof Viewer
* @instance
* @returns {boolean} True if active, else false
*/
this.isOrientationActive = function() {
return Boolean(orientation);
}

/**
* Subscribe listener to specified event. * Subscribe listener to specified event.
* @memberof Viewer * @memberof Viewer
* @instance * @instance
@@ -2882,14 +3050,10 @@ function fireEvent(type) {
*/ */
this.destroy = function() { this.destroy = function() {
if (renderer) if (renderer)
renderer.destroy()
renderer.destroy();
if (listenersAdded) { if (listenersAdded) {
dragFix.removeEventListener('mousedown', onDocumentMouseDown, false);
dragFix.removeEventListener('dblclick', onDocumentDoubleClick, false);
document.removeEventListener('mousemove', onDocumentMouseMove, false); document.removeEventListener('mousemove', onDocumentMouseMove, false);
document.removeEventListener('mouseup', onDocumentMouseUp, false); document.removeEventListener('mouseup', onDocumentMouseUp, false);
container.removeEventListener('mousewheel', onDocumentMouseWheel, false);
container.removeEventListener('DOMMouseScroll', onDocumentMouseWheel, false);
container.removeEventListener('mozfullscreenchange', onFullScreenChange, false); container.removeEventListener('mozfullscreenchange', onFullScreenChange, false);
container.removeEventListener('webkitfullscreenchange', onFullScreenChange, false); container.removeEventListener('webkitfullscreenchange', onFullScreenChange, false);
container.removeEventListener('msfullscreenchange', onFullScreenChange, false); container.removeEventListener('msfullscreenchange', onFullScreenChange, false);
@@ -2900,18 +3064,9 @@ this.destroy = function() {
container.removeEventListener('keyup', onDocumentKeyUp, false); container.removeEventListener('keyup', onDocumentKeyUp, false);
container.removeEventListener('blur', clearKeys, false); container.removeEventListener('blur', clearKeys, false);
document.removeEventListener('mouseleave', onDocumentMouseUp, false); document.removeEventListener('mouseleave', onDocumentMouseUp, false);
dragFix.removeEventListener('touchstart', onDocumentTouchStart, false);
dragFix.removeEventListener('touchmove', onDocumentTouchMove, false);
dragFix.removeEventListener('touchend', onDocumentTouchEnd, false);
dragFix.removeEventListener('pointerdown', onDocumentPointerDown, false);
dragFix.removeEventListener('pointermove', onDocumentPointerMove, false);
dragFix.removeEventListener('pointerup', onDocumentPointerUp, false);
dragFix.removeEventListener('pointerleave', onDocumentPointerUp, false);
} }
container.innerHTML = ''; container.innerHTML = '';
container.classList.remove('pnlm-container'); container.classList.remove('pnlm-container');
uiContainer.classList.remove('pnlm-grab');
uiContainer.classList.remove('pnlm-grabbing');
} }


} }


+ 8
- 7
src/standalone/standalone.js Просмотреть файл

@@ -1,7 +1,9 @@
function anError(error) { function anError(error) {
var errorMsg = document.createElement('div'); var errorMsg = document.createElement('div');
errorMsg.className = 'pnlm-info-box'; errorMsg.className = 'pnlm-info-box';
errorMsg.innerHTML = '<p>' + error + '</p>';
var p = document.createElement('p');
p.textContent = error;
errorMsg.appendChild(p);
document.getElementById('container').appendChild(errorMsg); document.getElementById('container').appendChild(errorMsg);
} }


@@ -10,17 +12,16 @@ function parseURLParameters() {
var URL; var URL;
if (window.location.hash.length > 0) { if (window.location.hash.length > 0) {
// Prefered method since parameters aren't sent to server // Prefered method since parameters aren't sent to server
URL = [window.location.hash.slice(1)];
URL = window.location.hash.slice(1);
} else { } else {
URL = decodeURI(window.location.href).split('?');
URL.shift();
URL = window.location.search.slice(1);
} }
if (URL.length < 1) {
if (!URL) {
// Display error if no configuration parameters are specified // Display error if no configuration parameters are specified
anError('No configuration options were specified.'); anError('No configuration options were specified.');
return; return;
} }
URL = URL[0].split('&');
URL = URL.split('&');
var configFromURL = {}; var configFromURL = {};
for (var i = 0; i < URL.length; i++) { for (var i = 0; i < URL.length; i++) {
var option = URL[i].split('=')[0]; var option = URL[i].split('=')[0];
@@ -57,7 +58,7 @@ function parseURLParameters() {
// Display error if JSON can't be loaded // Display error if JSON can't be loaded
var a = document.createElement('a'); var a = document.createElement('a');
a.href = configFromURL.config; a.href = configFromURL.config;
a.innerHTML = a.href;
a.textContent = a.href;
anError('The file ' + a.outerHTML + ' could not be accessed.'); anError('The file ' + a.outerHTML + ' could not be accessed.');
return; return;
} }


+ 139
- 55
utils/multires/generate.py Просмотреть файл

@@ -4,7 +4,8 @@
# and nona (from Hugin) # and nona (from Hugin)


# generate.py - A multires tile set generator for Pannellum # generate.py - A multires tile set generator for Pannellum
# Copyright (c) 2014-2017 Matthew Petroff
# Extensions to cylindrical input and partial panoramas by David von Oheimb
# Copyright (c) 2014-2018 Matthew Petroff
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -31,9 +32,14 @@ from PIL import Image
import os import os
import sys import sys
import math import math
import ast
from distutils.spawn import find_executable from distutils.spawn import find_executable
import subprocess import subprocess


# Allow large images (this could lead to a denial of service attach if you're
# running this script on user-submitted images.)
Image.MAX_IMAGE_PIXELS = None

# Find external programs # Find external programs
try: try:
nona = find_executable('nona') nona = find_executable('nona')
@@ -42,16 +48,36 @@ except KeyError:
nona = None nona = None


# Parse input # Parse input
parser = argparse.ArgumentParser(description='Generate a Pannellum multires tile set from an full equirectangular panorama.',
parser = argparse.ArgumentParser(description='Generate a Pannellum multires tile set from a full or partial equirectangular or cylindrical panorama.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('inputFile', metavar='INPUT', parser.add_argument('inputFile', metavar='INPUT',
help='full equirectangular panorama to be processed')
help='panorama to be processed')
parser.add_argument('-C', '--cylindrical', action='store_true',
help='input projection is cylindrical (default is equirectangular)')
parser.add_argument('-H', '--haov', dest='haov', default=-1, type=float,
help='horizontal angle of view (defaults to 360.0 for full panorama)')
parser.add_argument('-F', '--hfov', dest='hfov', default=100.0, type=float,
help='starting horizontal field of view (defaults to 100.0)')
parser.add_argument('-V', '--vaov', dest='vaov', default=-1, type=float,
help='vertical angle of view (defaults to 180.0 for full panorama)')
parser.add_argument('-O', '--voffset', dest='vOffset', default=0.0, type=float,
help='starting pitch position (defaults to 0.0)')
parser.add_argument('-e', '--horizon', dest='horizon', default=0.0, type=int,
help='offset of the horizon in pixels (negative if above middle, defaults to 0)')
parser.add_argument('-o', '--output', dest='output', default='./output', parser.add_argument('-o', '--output', dest='output', default='./output',
help='output directory')
help='output directory, optionally to be used as basePath (defaults to "./output")')
parser.add_argument('-s', '--tilesize', dest='tileSize', default=512, type=int, parser.add_argument('-s', '--tilesize', dest='tileSize', default=512, type=int,
help='tile size in pixels') help='tile size in pixels')
parser.add_argument('-f', '--fallbacksize', dest='fallbackSize', default=1024, type=int,
help='fallback tile size in pixels (defaults to 1024)')
parser.add_argument('-c', '--cubesize', dest='cubeSize', default=0, type=int, parser.add_argument('-c', '--cubesize', dest='cubeSize', default=0, type=int,
help='cube size in pixels, or 0 to retain all details') help='cube size in pixels, or 0 to retain all details')
parser.add_argument('-b', '--backgroundcolor', dest='backgroundColor', default="[0.0, 0.0, 0.0]", type=str,
help='RGB triple of values [0, 1] defining background color shown past the edges of a partial panorama (defaults to "[0.0, 0.0, 0.0]")')
parser.add_argument('-B', '--avoidbackground', action='store_true',
help='viewer should limit view to avoid showing background, so using --backgroundcolor is not needed')
parser.add_argument('-a', '--autoload', action='store_true',
help='automatically load panorama in viewer')
parser.add_argument('-q', '--quality', dest='quality', default=75, type=int, parser.add_argument('-q', '--quality', dest='quality', default=75, type=int,
help='output JPEG quality 0-100') help='output JPEG quality 0-100')
parser.add_argument('--png', action='store_true', parser.add_argument('--png', action='store_true',
@@ -59,42 +85,75 @@ parser.add_argument('--png', action='store_true',
parser.add_argument('-n', '--nona', default=nona, required=nona is None, parser.add_argument('-n', '--nona', default=nona, required=nona is None,
metavar='EXECUTABLE', metavar='EXECUTABLE',
help='location of the nona executable to use') help='location of the nona executable to use')
parser.add_argument('-G', '--gpu', action='store_true',
help='perform image remapping by nona on the GPU')
parser.add_argument('-d', '--debug', action='store_true',
help='debug mode (print status info and keep intermediate files)')
args = parser.parse_args() args = parser.parse_args()


# Create output directory
if os.path.exists(args.output):
print('Output directory "' + args.output + '" already exists')
if not args.debug:
sys.exit(1)
else:
os.makedirs(args.output)

# Process input image information # Process input image information
print('Processing input image information...') print('Processing input image information...')
origWidth, origHeight = Image.open(args.inputFile).size origWidth, origHeight = Image.open(args.inputFile).size
if float(origWidth) / origHeight != 2:
print('Error: the image width is not twice the image height.')
print('Input image must be a full, not partial, equirectangular panorama!')
sys.exit(1)
haov = args.haov
if haov == -1:
if args.cylindrical or float(origWidth) / origHeight == 2:
print('Assuming --haov 360.0')
haov = 360.0
else:
print('Unless given the --haov option, equirectangular input image must be a full (not partial) panorama!')
sys.exit(1)
vaov = args.vaov
if vaov == -1:
if args.cylindrical or float(origWidth) / origHeight == 2:
print('Assuming --vaov 180.0')
vaov = 180.0
else:
print('Unless given the --vaov option, equirectangular input image must be a full (not partial) panorama!')
sys.exit(1)
if args.cubeSize != 0: if args.cubeSize != 0:
cubeSize = args.cubeSize cubeSize = args.cubeSize
else: else:
cubeSize = 8 * int(origWidth / math.pi / 8)
levels = int(math.ceil(math.log(float(cubeSize) / args.tileSize, 2))) + 1
cubeSize = 8 * int((360 / haov) * origWidth / math.pi / 8)
tileSize = min(args.tileSize, cubeSize)
levels = int(math.ceil(math.log(float(cubeSize) / tileSize, 2))) + 1
origHeight = str(origHeight) origHeight = str(origHeight)
origWidth = str(origWidth) origWidth = str(origWidth)
origFilename = os.path.join(os.getcwd(), args.inputFile) origFilename = os.path.join(os.getcwd(), args.inputFile)
extension = '.jpg' extension = '.jpg'
if args.png: if args.png:
extension = '.png' extension = '.png'
partialPano = True if args.haov != -1 and args.vaov != -1 else False
colorList = ast.literal_eval(args.backgroundColor)
colorTuple = (int(colorList[0]*255), int(colorList[1]*255), int(colorList[2]*255))


# Create output directory
os.makedirs(args.output)
if args.debug:
print('maxLevel: '+ str(levels))
print('tileResolution: '+ str(tileSize))
print('cubeResolution: '+ str(cubeSize))


# Generate PTO file for nona to generate cube faces # Generate PTO file for nona to generate cube faces
# Face order: front, back, up, down, left, right # Face order: front, back, up, down, left, right
faceLetters = ['f', 'b', 'u', 'd', 'l', 'r'] faceLetters = ['f', 'b', 'u', 'd', 'l', 'r']
projection = "f1" if args.cylindrical else "f4"
pitch = 0
text = [] text = []
text.append('p E0 R0 f0 h' + str(cubeSize) + ' n"TIFF_m" u0 v90 w' + str(cubeSize))
facestr = 'i a0 b0 c0 d0 e'+ str(args.horizon) +' '+ projection + ' h' + origHeight +' w'+ origWidth +' n"'+ origFilename +'" r0 v' + str(haov)
text.append('p E0 R0 f0 h' + str(cubeSize) + ' w' + str(cubeSize) + ' n"TIFF_m" u0 v90')
text.append('m g1 i0 m2 p0.00784314') text.append('m g1 i0 m2 p0.00784314')
text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p0 r0 v360 w' + origWidth + ' y0')
text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p0 r0 v360 w' + origWidth + ' y180')
text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p-90 r0 v360 w' + origWidth + ' y0')
text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p90 r0 v360 w' + origWidth + ' y0')
text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p0 r0 v360 w' + origWidth + ' y90')
text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p0 r0 v360 w' + origWidth + ' y-90')
text.append(facestr +' p' + str(pitch+ 0) +' y0' )
text.append(facestr +' p' + str(pitch+ 0) +' y180')
text.append(facestr +' p' + str(pitch-90) +' y0' )
text.append(facestr +' p' + str(pitch+90) +' y0' )
text.append(facestr +' p' + str(pitch+ 0) +' y90' )
text.append(facestr +' p' + str(pitch+ 0) +' y-90')
text.append('v') text.append('v')
text.append('*') text.append('*')
text = '\n'.join(text) text = '\n'.join(text)
@@ -103,65 +162,90 @@ with open(os.path.join(args.output, 'cubic.pto'), 'w') as f:


# Create cube faces # Create cube faces
print('Generating cube faces...') print('Generating cube faces...')
subprocess.check_call([args.nona, '-o', os.path.join(args.output, 'face'), os.path.join(args.output, 'cubic.pto')])
subprocess.check_call([args.nona, ('-g' if args.gpu else '-d') , '-o', os.path.join(args.output, 'face'), os.path.join(args.output, 'cubic.pto')])
faces = ['face0000.tif', 'face0001.tif', 'face0002.tif', 'face0003.tif', 'face0004.tif', 'face0005.tif'] faces = ['face0000.tif', 'face0001.tif', 'face0002.tif', 'face0003.tif', 'face0004.tif', 'face0005.tif']


# Generate tiles # Generate tiles
print('Generating tiles...') print('Generating tiles...')
for f in range(0, 6): for f in range(0, 6):
size = cubeSize size = cubeSize
face = Image.open(os.path.join(args.output, faces[f]))
if 'A' in face.mode:
if face.mode == 'RGBA':
face = face.convert('RGB')
elif face.mode == 'LA':
face = face.convert('L')
for level in range(levels, 0, -1):
if not os.path.exists(os.path.join(args.output, str(level))):
os.makedirs(os.path.join(args.output, str(level)))
tiles = int(math.ceil(float(size) / args.tileSize))
if (level < levels):
face = face.resize([size, size], Image.ANTIALIAS)
for i in range(0, tiles):
for j in range(0, tiles):
left = j * args.tileSize
upper = i * args.tileSize
right = min(j * args.tileSize + args.tileSize, size)
lower = min(i * args.tileSize + args.tileSize, size)
tile = face.crop([left, upper, right, lower])
tile.load()
tile.save(os.path.join(args.output, str(level), faceLetters[f] + str(i) + '_' + str(j) + extension), quality = args.quality)
size = int(size / 2)
faceExists = os.path.exists(os.path.join(args.output, faces[f]))
if faceExists:
face = Image.open(os.path.join(args.output, faces[f]))
for level in range(levels, 0, -1):
if not os.path.exists(os.path.join(args.output, str(level))):
os.makedirs(os.path.join(args.output, str(level)))
tiles = int(math.ceil(float(size) / tileSize))
if (level < levels):
face = face.resize([size, size], Image.ANTIALIAS)
for i in range(0, tiles):
for j in range(0, tiles):
left = j * tileSize
upper = i * tileSize
right = min(j * args.tileSize + args.tileSize, size) # min(...) not really needed
lower = min(i * args.tileSize + args.tileSize, size) # min(...) not really needed
tile = face.crop([left, upper, right, lower])
if args.debug:
print('level: '+ str(level) + ' tiles: '+ str(tiles) + ' tileSize: ' + str(tileSize) + ' size: '+ str(size))
print('left: '+ str(left) + ' upper: '+ str(upper) + ' right: '+ str(right) + ' lower: '+ str(lower))
colors = tile.getcolors(1)
if not partialPano or colors == None or colors[0][1] != colorTuple:
# More than just one color (the background), i.e., non-empty tile
if tile.mode in ('RGBA', 'LA'):
background = Image.new(tile.mode[:-1], tile.size, colorTuple)
background.paste(tile, tile.split()[-1])
tile = background
tile.save(os.path.join(args.output, str(level), faceLetters[f] + str(i) + '_' + str(j) + extension), quality=args.quality)
size = int(size / 2)


# Generate fallback tiles # Generate fallback tiles
print('Generating fallback tiles...') print('Generating fallback tiles...')
for f in range(0, 6): for f in range(0, 6):
if not os.path.exists(os.path.join(args.output, 'fallback')): if not os.path.exists(os.path.join(args.output, 'fallback')):
os.makedirs(os.path.join(args.output, 'fallback')) os.makedirs(os.path.join(args.output, 'fallback'))
face = Image.open(os.path.join(args.output, faces[f]))
if 'A' in face.mode:
if face.mode == 'RGBA':
face = face.convert('RGB')
elif face.mode == 'LA':
face = face.convert('L')
face = face.resize([1024, 1024], Image.ANTIALIAS)
face.save(os.path.join(args.output, 'fallback', faceLetters[f] + extension), quality = args.quality)
if os.path.exists(os.path.join(args.output, faces[f])):
face = Image.open(os.path.join(args.output, faces[f]))
if face.mode in ('RGBA', 'LA'):
background = Image.new(face.mode[:-1], face.size, colorTuple)
background.paste(face, face.split()[-1])
face = background
face = face.resize([args.fallbackSize, args.fallbackSize], Image.ANTIALIAS)
face.save(os.path.join(args.output, 'fallback', faceLetters[f] + extension), quality = args.quality)


# Clean up temporary files # Clean up temporary files
os.remove(os.path.join(args.output, 'cubic.pto'))
for face in faces:
os.remove(os.path.join(args.output, face))
if not args.debug:
os.remove(os.path.join(args.output, 'cubic.pto'))
for face in faces:
if os.path.exists(os.path.join(args.output, face)):
os.remove(os.path.join(args.output, face))


# Generate config file # Generate config file
text = [] text = []
text.append('{') text.append('{')
text.append(' "hfov": ' + str(args.hfov)+ ',')
if haov < 360:
text.append(' "haov": ' + str(haov)+ ',')
text.append(' "minYaw": ' + str(-haov/2+0)+ ',')
text.append(' "yaw": ' + str(-haov/2+args.hfov/2)+ ',')
text.append(' "maxYaw": ' + str(+haov/2+0)+ ',')
if vaov < 180:
text.append(' "vaov": ' + str(vaov)+ ',')
text.append(' "vOffset": ' + str(args.vOffset)+ ',')
text.append(' "minPitch": ' + str(-vaov/2+args.vOffset)+ ',')
text.append(' "pitch": ' + str( args.vOffset)+ ',')
text.append(' "maxPitch": ' + str(+vaov/2+args.vOffset)+ ',')
if colorTuple != (0, 0, 0):
text.append(' "backgroundColor": "' + args.backgroundColor+ '",')
if args.avoidbackground:
text.append(' "avoidShowingBackground": true,')
if args.autoload:
text.append(' "autoLoad": true,')
text.append(' "type": "multires",') text.append(' "type": "multires",')
text.append(' ')
text.append(' "multiRes": {') text.append(' "multiRes": {')
text.append(' "path": "/%l/%s%y_%x",') text.append(' "path": "/%l/%s%y_%x",')
text.append(' "fallbackPath": "/fallback/%s",') text.append(' "fallbackPath": "/fallback/%s",')
text.append(' "extension": "' + extension[1:] + '",') text.append(' "extension": "' + extension[1:] + '",')
text.append(' "tileResolution": ' + str(args.tileSize) + ',')
text.append(' "tileResolution": ' + str(tileSize) + ',')
text.append(' "maxLevel": ' + str(levels) + ',') text.append(' "maxLevel": ' + str(levels) + ',')
text.append(' "cubeResolution": ' + str(cubeSize)) text.append(' "cubeResolution": ' + str(cubeSize))
text.append(' }') text.append(' }')


+ 4
- 2
utils/video/videojs-pannellum-plugin.js Просмотреть файл

@@ -1,18 +1,20 @@
/* /*
* Video.js plugin for Pannellum * Video.js plugin for Pannellum
* Copyright (c) 2015-2017 Matthew Petroff
* Copyright (c) 2015-2018 Matthew Petroff
* MIT License * MIT License
*/ */


(function(document, videojs, pannellum) { (function(document, videojs, pannellum) {
'use strict'; 'use strict';


videojs.plugin('pannellum', function(config) {
var registerPlugin = videojs.registerPlugin || videojs.plugin; // Use registerPlugin for Video.js >= 6
registerPlugin('pannellum', function(config) {
// Create Pannellum instance // Create Pannellum instance
var player = this; var player = this;
var container = player.el(); var container = player.el();
var vid = container.getElementsByTagName('video')[0], var vid = container.getElementsByTagName('video')[0],
pnlmContainer = document.createElement('div'); pnlmContainer = document.createElement('div');
pnlmContainer.style.zIndex = '0';
config = config || {}; config = config || {};
config.type = 'equirectangular'; config.type = 'equirectangular';
config.dynamic = true; config.dynamic = true;


Загрузка…
Отмена
Сохранить