Pull my fork up to datepull/723/head
@@ -6,3 +6,6 @@ build/** | |||||
# Ignore generated docs | # Ignore generated docs | ||||
utils/doc/generated_docs | utils/doc/generated_docs | ||||
# Ignore IntelliJ Files | |||||
.idea |
@@ -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 @@ | |||||
2.3.2 | |||||
2.4.1 |
@@ -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 | ||||
-------------------------- | -------------------------- | ||||
@@ -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. | ||||
@@ -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,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" | ||||
}, | }, | ||||
@@ -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. | ||||
@@ -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; | |||||
} |
@@ -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'); | ||||
@@ -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'); | |||||
} | } | ||||
} | } | ||||
@@ -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; | ||||
} | } | ||||
@@ -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(' }') | ||||
@@ -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; | ||||