Browse Source

Merge pull request #1 from mpetroff/master

Pull my fork up to date
pull/723/head
GazHay 5 years ago
committed by GitHub
parent
commit
90a7a1f45f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 779 additions and 250 deletions
  1. +3
    -0
      .gitignore
  2. +1
    -1
      COPYING
  3. +1
    -1
      VERSION
  4. +75
    -0
      changelog.md
  5. +17
    -0
      doc/events.md
  6. +66
    -11
      doc/json-config-parameters.md
  7. +1
    -1
      package.json
  8. +4
    -0
      readme.md
  9. +5
    -0
      src/css/pannellum.css
  10. +177
    -49
      src/js/libpannellum.js
  11. +278
    -123
      src/js/pannellum.js
  12. +8
    -7
      src/standalone/standalone.js
  13. +139
    -55
      utils/multires/generate.py
  14. +4
    -2
      utils/video/videojs-pannellum-plugin.js

+ 3
- 0
.gitignore View File

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

# Ignore generated docs
utils/doc/generated_docs

# Ignore IntelliJ Files
.idea

+ 1
- 1
COPYING View File

@@ -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
this software and associated documentation files (the "Software"), to deal in


+ 1
- 1
VERSION View File

@@ -1 +1 @@
2.3.2
2.4.1

+ 75
- 0
changelog.md View File

@@ -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
--------------------------



+ 17
- 0
doc/events.md View File

@@ -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.


## `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`

If a scene transition fade interval is specified, this event is fired when the
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`

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`

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


+ 66
- 11
doc/json-config-parameters.md View File

@@ -98,6 +98,13 @@ viewer is fullscreen.
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)

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`.


### `touchPanSpeedCoeffFactor` (number)

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


### `yaw` (number)

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)

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)
@@ -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.


### `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.


@@ -230,6 +257,11 @@ spot.
If specified for an `info` hot spot, the hot spot links to the specified URL.
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)

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)

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`

@@ -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
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
@@ -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,
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
@@ -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,
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
used.

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


## `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
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


+ 1
- 1
package.json View File

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


+ 4
- 0
readme.md View File

@@ -45,6 +45,10 @@ Since Pannellum is built with recent web standards, it requires a modern browser
#### No support:
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

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


+ 5
- 0
src/css/pannellum.css View File

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

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

.pnlm-pointer {
cursor: pointer;
}

+ 177
- 49
src/js/libpannellum.js View File

@@ -1,6 +1,6 @@
/*
* 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
* of this software and associated documentation files (the "Software"), to deal
@@ -42,6 +42,7 @@ function Renderer(container) {
var pose;
var image, imageType, dynamic;
var texCoordBuffer, cubeVertBuf, cubeVertTexCoordBuf, cubeVertIndBuf;
var globalParams;

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

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

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

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
// 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
// instead.
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 9_/) ||
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.
// This will discard the image loaded so far and load the fallback image.
// While browser specific tests are usually frowned upon, the
// fallback viewer only really works with WebKit/Blink and IE 10/11
// (it doesn't work properly in Firefox).
@@ -204,6 +241,16 @@ function Renderer(container) {
// Draw image width duplicated edge pixels on canvas
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++;
if (loaded == 6) {
fallbackImgSize = this.width;
@@ -211,23 +258,27 @@ function Renderer(container) {
callback();
}
};
faceMissing = false;
for (s = 0; s < 6; s++) {
var faceImg = new Image();
faceImg.crossOrigin = 'anonymous';
faceImg.crossOrigin = globalParams.crossOrigin ? globalParams.crossOrigin : 'anonymous';
faceImg.side = s;
faceImg.onload = onLoad;
faceImg.onerror = incLoaded; // ignore missing face to support partial fallback image
if (imageType == 'multires') {
faceImg.src = encodeURI(path.replace('%s', sides[s]) + '.' + image.extension);
} else {
faceImg.src = encodeURI(image[s].src);
}
}
fillMissingFaces(fallbackImgSize);
return;
} else if (!gl) {
console.log('Error: no WebGL support detected!');
throw {type: 'no webgl'};
}
if (imageType == 'cubemap')
fillMissingFaces(cubeImgWidth);
if (image.basePath) {
image.fullpath = image.basePath + image.path;
} else {
@@ -243,19 +294,18 @@ function Renderer(container) {
}
// Make sure image isn't too big
var width, maxWidth;
var maxWidth = 0;
if (imageType == 'equirectangular') {
width = Math.max(image.width, image.height);
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') {
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};
}
}
@@ -311,6 +361,11 @@ function Renderer(container) {

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
program.texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(program.texCoordLocation);
@@ -325,7 +380,7 @@ function Renderer(container) {

// Pass aspect ratio
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
program.psi = gl.getUniformLocation(program, 'u_psi');
@@ -344,7 +399,6 @@ function Renderer(container) {
// Set background color
if (imageType == 'equirectangular') {
program.backgroundColor = gl.getUniformLocation(program, 'u_backgroundColor');
var color = params.backgroundColor ? params.backgroundColor : [0, 0, 0];
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_NEGATIVE_Z, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image[2]);
} 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
@@ -540,16 +630,18 @@ function Renderer(container) {
// Apply face transforms
var faces = Object.keys(transforms);
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;
}
if (imageType != 'multires') {
// 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);

// Pass psi, theta, roll, and focal length
@@ -571,7 +663,7 @@ function Renderer(container) {
} else {
// 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
checkZoom(hfov);
@@ -606,12 +698,29 @@ function Renderer(container) {
var ntmp = new MultiresNode(vtmps[s], sides[s], 1, 0, 0, image.fullpath);
testMultiresNode(rotPersp, ntmp, pitch, yaw, hfov);
}
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() {
if (!program.drawInProgress) {
program.drawInProgress = true;
gl.clear(gl.COLOR_BUFFER_BIT);
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;
//gl.uniform4f(program.colorUniform, color[0], color[1], color[2], 1.0);
@@ -984,7 +1094,7 @@ function Renderer(container) {
* @returns {number[]} Generated perspective matrix.
*/
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);
return [
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.bindTexture(gl.TEXTURE_2D, null);
}
var pendingTextureRequests = [];

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

function TextureImageLoader() {
var self = this;
this.texture = this.callback = null;
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);
});
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) {
@@ -1034,7 +1152,8 @@ function Renderer(container) {
this.image.src = src;
};

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

return function(src, callback) {
return function(node, src, callback, _crossOrigin) {
crossOrigin = _crossOrigin;
var texture = gl.createTexture();
if (cacheTop)
textureImageCache[--cacheTop].loadTexture(src, texture, callback);
else
pendingTextureRequests.push(new PendingTextureRequest(src, texture, callback));
pendingTextureRequests.push(new PendingTextureRequest(node, src, texture, callback));
return texture;
};
})();
@@ -1067,13 +1187,10 @@ function Renderer(container) {
* @param {MultiresNode} node - Input 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
var newLevel = 1;
while ( newLevel < image.maxLevel &&
canvas.width > image.tileResolution *
gl.drawingBufferWidth > image.tileResolution *
Math.pow(2, newLevel - 1) * Math.tan(hfov / 2) * 0.707 ) {
newLevel++;
}
@@ -1241,7 +1358,9 @@ var fragEquiCubeBase = [
'const float PI = 3.14159265358979323846264;',

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

// Coordinates passed in from vertex shader
@@ -1286,8 +1405,17 @@ var fragEquirectangular = fragEquiCubeBase + [
// 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)',
'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');



+ 278
- 123
src/js/pannellum.js View File

@@ -1,6 +1,6 @@
/*
* 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
* of this software and associated documentation files (the "Software"), to deal
@@ -49,7 +49,7 @@ var config,
onPointerDownPitch = 0,
keysDown = new Array(10),
fullscreenActive = false,
loaded = false,
loaded,
error = false,
isTimedOut = false,
listenersAdded = false,
@@ -67,11 +67,13 @@ var config,
externalEventListeners = {},
specifiedPhotoSphereExcludes = [],
update = false, // Should we update when still to render dynamic content
eps = 1e-6,
hotspotsCreated = false;

var defaultConfig = {
hfov: 100,
minHfov: 50,
multiResMinHfov: false,
maxHfov: 120,
pitch: 0,
minPitch: undefined,
@@ -90,6 +92,7 @@ var defaultConfig = {
northOffset: 0,
showFullscreenCtrl: true,
dynamic: false,
dynamicUpdate: false,
doubleClickZoom: true,
keyboardZoom: true,
mouseZoom: true,
@@ -99,9 +102,14 @@ var defaultConfig = {
orientationOnByDefault: false,
hotSpotDebug: false,
backgroundColor: [0, 0, 0],
avoidShowingBackground: false,
animationTimingFunction: timingFunction,
draggable: true,
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
@@ -128,8 +136,6 @@ defaultConfig.strings = {
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
container = typeof container === 'string' ? document.getElementById(container) : container;
container.classList.add('pnlm-container');
@@ -306,7 +312,7 @@ function init() {
panoImage = [];
for (i = 0; i < 6; i++) {
panoImage.push(new Image());
panoImage[i].crossOrigin = 'anonymous';
panoImage[i].crossOrigin = config.crossOrigin;
}
infoDisplay.load.lbox.style.display = 'block';
infoDisplay.load.lbar.style.display = 'none';
@@ -350,18 +356,23 @@ function init() {
var onError = function(e) {
var a = document.createElement('a');
a.href = e.target.src;
a.innerHTML = a.href;
a.textContent = a.href;
anError(config.strings.fileAccessError.replace('%s', a.outerHTML));
};
for (i = 0; i < panoImage.length; i++) {
panoImage[i].onload = onLoad;
panoImage[i].onerror = onError;
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') {
onImageLoad();
@@ -385,8 +396,8 @@ function init() {
if (xhr.status != 200) {
// Display error if image can't be loaded
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));
}
var img = this.response;
@@ -427,6 +438,7 @@ function init() {
}
xhr.responseType = 'blob';
xhr.setRequestHeader('Accept', 'image/*,*/*;q=0.9');
xhr.withCredentials = config.crossOrigin === 'use-credentials';
xhr.send();
}
}
@@ -468,10 +480,10 @@ function onImageLoad() {
if (config.doubleClickZoom) {
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('orientationchange', onDocumentResize, false);
if (!config.disableKeyboardCtrl) {
@@ -480,13 +492,17 @@ function onImageLoad() {
container.addEventListener('blur', clearKeys, 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
if (window.navigator.pointerEnabled)
@@ -494,6 +510,7 @@ function onImageLoad() {
}

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);
}

@@ -644,8 +661,9 @@ function aboutMessage(event) {
function mousePosition(event) {
var bounds = container.getBoundingClientRect();
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;
}

@@ -869,7 +887,7 @@ function onDocumentTouchMove(event) {
//
// 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.
var touchmovePanSpeedCoeff = config.hfov / 360;
var touchmovePanSpeedCoeff = (config.hfov / 360) * config.touchPanSpeedCoeffFactor;

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

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

// Override default action for keys that are used
if (usedKeyNumbers.indexOf(keynumber) < 0)
return
if (config.capturedKeyNumbers.indexOf(keynumber) < 0)
return;
event.preventDefault();
// If escape key is pressed
@@ -1043,8 +1060,8 @@ function onDocumentKeyUp(event) {
var keynumber = event.which || event.keycode;
// Override default action for keys that are used
if (usedKeyNumbers.indexOf(keynumber) < 0)
return
if (config.capturedKeyNumbers.indexOf(keynumber) < 0)
return;
event.preventDefault();
// Change key
@@ -1232,19 +1249,19 @@ function keyRepeat() {
// "Inertia"
if (diff > 0 && !config.autoRotate) {
// "Friction"
var friction = 0.85;
var slowDownFactor = 1 - config.friction;
// 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
if (!keysDown[2] && !keysDown[3] && !keysDown[6] && !keysDown[7] && !animatedMove.pitch) {
config.pitch += speed.pitch * diff * friction;
config.pitch += speed.pitch * diff * slowDownFactor;
}
// Zoom
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
if (keysDown[0] && keysDown[0]) {
if (keysDown[0] && keysDown[1]) {
speed.hfov = 0;
}
if ((keysDown[2] || keysDown[6]) && (keysDown[3] || keysDown[7])) {
@@ -1287,11 +1304,7 @@ function animateMove(axis) {
t.endPosition === t.startPosition) {
result = t.endPosition;
speed[axis] = 0;
var callback = animatedMove[axis].callback,
callbackArgs = animatedMove[axis].callbackArgs;
delete animatedMove[axis];
if (typeof callback == 'function')
callback(callbackArgs);
}
config[axis] = result;
}
@@ -1316,7 +1329,7 @@ function onDocumentResize() {
//animateInit();

// 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))) {
requestAnimationFrame(animate);
} else {
fireEvent('animatefinished', {pitch: _this.getPitch(), yaw: _this.getYaw(), hfov: _this.getHfov()});
animating = false;
prevTime = undefined;
var autoRotateStartTime = config.autoRotateInactivityDelay -
@@ -1385,32 +1399,50 @@ function render() {
var tmpyaw;

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
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
var yawRange = config.maxYaw - config.minYaw,
minYaw = -180,
maxYaw = 180;
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) {
// Lock yaw to average of min and max yaw when both can be seen at once
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
// 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;
}

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

@@ -1940,7 +1980,16 @@ function processOptions(isPreview) {
break;
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;
case 'hfov':
@@ -2058,15 +2107,16 @@ function toggleFullscreen() {
* Event handler for fullscreen changes.
* @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');
fullscreenActive = true;
} else {
controls.fullscreen.classList.remove('pnlm-fullscreen-toggle-button-active');
fullscreenActive = false;
}

if (resize !== 'resize')
fireEvent('fullscreenchange', fullscreenActive);
// Resize renderer (deal with browser quirks and fixes #155)
renderer.resize();
setHfov(config.hfov);
@@ -2104,20 +2154,31 @@ function zoomOut() {
function constrainHfov(hfov) {
// Keep field of view within bounds
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));
}
if (minHfov > config.maxHfov) {
// Don't change view if bounds don't make sense
console.log('HFOV bounds do not make sense (minHfov > maxHfov).')
return config.hfov;
} if (hfov < minHfov) {
return minHfov;
}
var newHfov = config.hfov;
if (hfov < minHfov) {
newHfov = minHfov;
} else if (hfov > config.maxHfov) {
return config.maxHfov;
newHfov = config.maxHfov;
} 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) {
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
// memory etc and not because of a lack of WebGL support etc
clearError();
loaded = false;

controls.load.style.display = 'none';
infoDisplay.load.box.style.display = 'inline';
@@ -2228,6 +2291,13 @@ function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) {
}
fireEvent('scenechange', sceneId);
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.
* @memberof Viewer
* @instance
* @returns {boolean} `true` if a panorama is loaded, else `false`
*/
this.isLoaded = function() {
return loaded;
return Boolean(loaded);
};

/**
@@ -2299,16 +2397,22 @@ this.getPitch = function() {
* @returns {Viewer} `this`
*/
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);
if (animated) {
animatedMove.pitch = {
'startTime': Date.now(),
'startPosition': config.pitch,
'endPosition': pitch,
'duration': animated,
'callback': callback,
'callbackArgs': callbackArgs
'duration': animated
}
if (typeof callback == 'function')
setTimeout(function(){callback(callbackArgs)}, animated);
} else {
config.pitch = pitch;
}
@@ -2360,6 +2464,12 @@ this.getYaw = function() {
* @returns {Viewer} `this`
*/
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);
yaw = ((yaw + 180) % 360) - 180 // Keep in bounds
if (animated) {
@@ -2373,10 +2483,10 @@ this.setYaw = function(yaw, animated, callback, callbackArgs) {
'startTime': Date.now(),
'startPosition': config.yaw,
'endPosition': yaw,
'duration': animated,
'callback': callback,
'callbackArgs': callbackArgs
'duration': animated
}
if (typeof callback == 'function')
setTimeout(function(){callback(callbackArgs)}, animated);
} else {
config.yaw = yaw;
}
@@ -2428,16 +2538,22 @@ this.getHfov = function() {
* @returns {Viewer} `this`
*/
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);
if (animated) {
animatedMove.hfov = {
'startTime': Date.now(),
'startPosition': config.hfov,
'endPosition': constrainHfov(hfov),
'duration': animated,
'callback': callback,
'callbackArgs': callbackArgs
'duration': animated
}
if (typeof callback == 'function')
setTimeout(function(){callback(callbackArgs)}, animated);
} else {
setHfov(hfov);
}
@@ -2483,16 +2599,20 @@ this.setHfovBounds = function(bounds) {
*/
this.lookAt = function(pitch, yaw, hfov, animated, callback, callbackArgs) {
animated = animated == undefined ? 1000: Number(animated);
if (pitch !== undefined) {
if (pitch !== undefined && Math.abs(pitch - config.pitch) > eps) {
this.setPitch(pitch, animated, callback, callbackArgs);
callback = undefined;
}
if (yaw !== undefined) {
if (yaw !== undefined && Math.abs(yaw - config.yaw) > eps) {
this.setYaw(yaw, animated, callback, callbackArgs);
callback = undefined;
}
if (hfov !== undefined)
if (hfov !== undefined && Math.abs(hfov - config.hfov) > eps) {
this.setHfov(hfov, animated, callback, callbackArgs);
callback = undefined;
}
if (typeof callback == 'function')
callback(callbackArgs);
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.
* @memberof Viewer
* @instance
@@ -2643,7 +2773,7 @@ this.mouseEventToCoords = function(event) {
* @returns {Viewer} `this`
*/
this.loadScene = function(sceneId, pitch, yaw, hfov) {
if (loaded)
if (loaded !== false)
loadScene(sceneId, pitch, yaw, hfov);
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.
* @memberof Viewer
* @instance
@@ -2747,26 +2887,43 @@ this.addHotSpot = function(hs, sceneId) {
* @memberof Viewer
* @instance
* @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
*/
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
*/
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.
* @memberof Viewer
* @instance
@@ -2882,14 +3050,10 @@ function fireEvent(type) {
*/
this.destroy = function() {
if (renderer)
renderer.destroy()
renderer.destroy();
if (listenersAdded) {
dragFix.removeEventListener('mousedown', onDocumentMouseDown, false);
dragFix.removeEventListener('dblclick', onDocumentDoubleClick, false);
document.removeEventListener('mousemove', onDocumentMouseMove, false);
document.removeEventListener('mouseup', onDocumentMouseUp, false);
container.removeEventListener('mousewheel', onDocumentMouseWheel, false);
container.removeEventListener('DOMMouseScroll', onDocumentMouseWheel, false);
container.removeEventListener('mozfullscreenchange', onFullScreenChange, false);
container.removeEventListener('webkitfullscreenchange', onFullScreenChange, false);
container.removeEventListener('msfullscreenchange', onFullScreenChange, false);
@@ -2900,18 +3064,9 @@ this.destroy = function() {
container.removeEventListener('keyup', onDocumentKeyUp, false);
container.removeEventListener('blur', clearKeys, 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.classList.remove('pnlm-container');
uiContainer.classList.remove('pnlm-grab');
uiContainer.classList.remove('pnlm-grabbing');
}

}


+ 8
- 7
src/standalone/standalone.js View File

@@ -1,7 +1,9 @@
function anError(error) {
var errorMsg = document.createElement('div');
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);
}

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


+ 139
- 55
utils/multires/generate.py View File

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

# 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
# of this software and associated documentation files (the "Software"), to deal
@@ -31,9 +32,14 @@ from PIL import Image
import os
import sys
import math
import ast
from distutils.spawn import find_executable
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
try:
nona = find_executable('nona')
@@ -42,16 +48,36 @@ except KeyError:
nona = None

# 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)
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',
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,
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,
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,
help='output JPEG quality 0-100')
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,
metavar='EXECUTABLE',
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()

# 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
print('Processing input image information...')
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:
cubeSize = args.cubeSize
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)
origWidth = str(origWidth)
origFilename = os.path.join(os.getcwd(), args.inputFile)
extension = '.jpg'
if args.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
# Face order: front, back, up, down, left, right
faceLetters = ['f', 'b', 'u', 'd', 'l', 'r']
projection = "f1" if args.cylindrical else "f4"
pitch = 0
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('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('*')
text = '\n'.join(text)
@@ -103,65 +162,90 @@ with open(os.path.join(args.output, 'cubic.pto'), 'w') as f:

# Create 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']

# Generate tiles
print('Generating tiles...')
for f in range(0, 6):
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
print('Generating fallback tiles...')
for f in range(0, 6):
if not os.path.exists(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
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
text = []
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(' ')
text.append(' "multiRes": {')
text.append(' "path": "/%l/%s%y_%x",')
text.append(' "fallbackPath": "/fallback/%s",')
text.append(' "extension": "' + extension[1:] + '",')
text.append(' "tileResolution": ' + str(args.tileSize) + ',')
text.append(' "tileResolution": ' + str(tileSize) + ',')
text.append(' "maxLevel": ' + str(levels) + ',')
text.append(' "cubeResolution": ' + str(cubeSize))
text.append(' }')


+ 4
- 2
utils/video/videojs-pannellum-plugin.js View File

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

(function(document, videojs, pannellum) {
'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
var player = this;
var container = player.el();
var vid = container.getElementsByTagName('video')[0],
pnlmContainer = document.createElement('div');
pnlmContainer.style.zIndex = '0';
config = config || {};
config.type = 'equirectangular';
config.dynamic = true;


Loading…
Cancel
Save