@@ -6,3 +6,6 @@ build/** | |||
# Ignore 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 | |||
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 | |||
-------------------------- | |||
@@ -115,6 +115,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`. | |||
@@ -198,6 +203,12 @@ the configuration is provided via the URL; it defaults to `false` but can be | |||
set to `true` when using the API. | |||
### `crossOrigin` (string) | |||
This specifies the type of CORS request used and can be set to either | |||
`anonymous` or `use-credentials`. Defaults to `anonymous`. | |||
### `hotSpots` (array) | |||
This specifies an array of hot spots that can be links to other scenes, | |||
@@ -251,7 +262,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 +298,11 @@ 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. | |||
## `equirectangular` specific options | |||
@@ -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" | |||
}, | |||
@@ -17,6 +17,7 @@ | |||
outline: 0; | |||
line-height: 1.4; | |||
contain: content; | |||
touch-action: none; | |||
} | |||
.pnlm-container * { | |||
@@ -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) { | |||
@@ -204,6 +206,13 @@ function Renderer(container) { | |||
// Draw image width duplicated edge pixels on canvas | |||
faceContext.putImageData(imgData, 0, 0); | |||
incLoaded(); | |||
}; | |||
var incLoaded = function() { | |||
if (this.width != 0) // support partial fallback/cubemap image | |||
fallbackImgSize = this.width; | |||
if (loaded == 5 && this.width == 0) // support partial fallback/cubemap image | |||
this.width = fallbackImgSize; | |||
loaded++; | |||
if (loaded == 6) { | |||
fallbackImgSize = this.width; | |||
@@ -213,9 +222,10 @@ function Renderer(container) { | |||
}; | |||
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 file to support partial fallback/cubemap image | |||
if (imageType == 'multires') { | |||
faceImg.src = encodeURI(path.replace('%s', sides[s]) + '.' + image.extension); | |||
} else { | |||
@@ -325,7 +335,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'); | |||
@@ -534,22 +544,24 @@ function Renderer(container) { | |||
r: 'translate3d(' + s + 'px, -' + (s + 2) + 'px, -' + (s + 2) + 'px) rotateY(270deg)' | |||
}; | |||
focal = 1 / Math.tan(hfov / 2); | |||
var zoom = focal * canvas.clientWidth / 2 + 'px'; | |||
var zoom = focal * gl.drawingBufferWidth / 2 + 'px'; | |||
var transform = 'perspective(' + zoom + ') translateZ(' + zoom + ') rotateX(' + pitch + 'rad) rotateY(' + yaw + 'rad) '; | |||
// 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 fallback/cubemap 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 +583,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); | |||
@@ -984,7 +996,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, | |||
@@ -1015,17 +1027,21 @@ function Renderer(container) { | |||
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); | |||
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); | |||
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) { | |||
@@ -1051,7 +1067,8 @@ function Renderer(container) { | |||
for (var i = 0; i < cacheTop; i++) | |||
textureImageCache[i] = new TextureImageLoader(); | |||
return function(src, callback) { | |||
return function(src, callback, _crossOrigin) { | |||
crossOrigin = _crossOrigin; | |||
var texture = gl.createTexture(); | |||
if (cacheTop) | |||
textureImageCache[--cacheTop].loadTexture(src, texture, callback); | |||
@@ -1072,7 +1089,7 @@ function Renderer(container) { | |||
loadTexture(encodeURI(node.path + '.' + image.extension), function(texture) { | |||
node.texture = texture; | |||
node.textureLoaded = true; | |||
}); | |||
}, globalParams.crossOrigin); | |||
} | |||
} | |||
@@ -1085,7 +1102,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++; | |||
} | |||
@@ -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 | |||
@@ -50,7 +50,7 @@ var config, | |||
onPointerDownPitch = 0, | |||
keysDown = new Array(10), | |||
fullscreenActive = false, | |||
loaded = false, | |||
loaded, | |||
error = false, | |||
isTimedOut = false, | |||
listenersAdded = false, | |||
@@ -105,6 +105,9 @@ var defaultConfig = { | |||
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], | |||
}; | |||
// Translatable / configurable strings | |||
@@ -131,8 +134,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'); | |||
@@ -311,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'; | |||
@@ -438,6 +439,7 @@ function init() { | |||
} | |||
xhr.responseType = 'blob'; | |||
xhr.setRequestHeader('Accept', 'image/*,*/*;q=0.9'); | |||
xhr.withCredentials = config.crossOrigin === 'use-credentials'; | |||
xhr.send(); | |||
} | |||
} | |||
@@ -498,13 +500,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) | |||
@@ -899,7 +905,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; | |||
@@ -960,7 +966,7 @@ function onDocumentPointerMove(event) { | |||
pointerCoordinates[i].clientY = event.clientY; | |||
event.targetTouches = pointerCoordinates; | |||
onDocumentTouchMove(event); | |||
//event.preventDefault(); | |||
event.preventDefault(); | |||
return; | |||
} | |||
} | |||
@@ -1045,8 +1051,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 | |||
@@ -1081,8 +1087,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 | |||
@@ -1300,7 +1306,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])) { | |||
@@ -2272,6 +2278,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'; | |||
@@ -2399,7 +2406,7 @@ function escapeHTML(s) { | |||
* @returns {boolean} `true` if a panorama is loaded, else `false` | |||
*/ | |||
this.isLoaded = function() { | |||
return loaded; | |||
return Boolean(loaded); | |||
}; | |||
/** | |||
@@ -2821,7 +2828,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; | |||
} | |||
@@ -2885,6 +2892,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 | |||
@@ -2996,6 +3013,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 | |||
@@ -4,7 +4,7 @@ | |||
# and nona (from Hugin) | |||
# generate.py - A multires tile set generator for Pannellum | |||
# Copyright (c) 2014-2017 Matthew Petroff | |||
# 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 | |||
@@ -1,6 +1,6 @@ | |||
/* | |||
* Video.js plugin for Pannellum | |||
* Copyright (c) 2015-2017 Matthew Petroff | |||
* Copyright (c) 2015-2018 Matthew Petroff | |||
* MIT License | |||
*/ | |||