diff --git a/doc/events.md b/doc/events.md index 4ff73c9a..b17f73ea 100644 --- a/doc/events.md +++ b/doc/events.md @@ -11,12 +11,28 @@ Fired when a scene change is initiated. A `load` event will be fired when the new scene finishes loading. Passes scene ID string to handler. +## `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 diff --git a/doc/json-config-parameters.md b/doc/json-config-parameters.md index 65e7c76e..9cddd9db 100644 --- a/doc/json-config-parameters.md +++ b/doc/json-config-parameters.md @@ -98,6 +98,13 @@ viewer is fullscreen. If set to `false`, mouse and touch dragging is disabled. Defaults to `true`. +### `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`. @@ -151,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) @@ -209,9 +225,9 @@ This specifies the type of CORS request used and can be set to either `anonymous` or `use-credentials`. Defaults to `anonymous`. -### `hotSpots` (array) +### `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. @@ -241,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 diff --git a/readme.md b/readme.md index b7b43090..f3bcaf7f 100644 --- a/readme.md +++ b/readme.md @@ -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. diff --git a/src/js/libpannellum.js b/src/js/libpannellum.js index 662fb3c4..128073ab 100644 --- a/src/js/libpannellum.js +++ b/src/js/libpannellum.js @@ -294,18 +294,20 @@ function Renderer(container) { } // Make sure image isn't too big - var width = 0, maxWidth = 0; + var maxWidth = 0; if (imageType == 'equirectangular') { - width = Math.max(image.width, image.height); maxWidth = gl.getParameter(gl.MAX_TEXTURE_SIZE); + 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 = cubeImgWidth; - maxWidth = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE); - } - if (width > maxWidth) { - 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}; + 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}; + } } // Store horizon pitch and roll if applicable @@ -414,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 @@ -1320,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 @@ -1365,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'); diff --git a/src/js/pannellum.js b/src/js/pannellum.js index 5371c841..d7083f7e 100644 --- a/src/js/pannellum.js +++ b/src/js/pannellum.js @@ -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, @@ -106,6 +108,7 @@ var defaultConfig = { 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 @@ -352,7 +355,7 @@ 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)); }; @@ -367,7 +370,7 @@ function init() { } panoImage[i].onload = onLoad; panoImage[i].onerror = onError; - panoImage[i].src = encodeURI(p); + panoImage[i].src = sanitizeURL(p); } } } else if (config.type == 'multires') { @@ -392,8 +395,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; @@ -669,8 +672,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; } @@ -1011,7 +1015,6 @@ function onDocumentMouseWheel(event) { setHfov(config.hfov + event.detail * 1.5); speed.hfov = event.detail > 0 ? 1 : -1; } - animateInit(); } @@ -1257,19 +1260,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); } } @@ -1312,11 +1315,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; } @@ -1387,6 +1386,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 - @@ -1701,7 +1701,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); @@ -1711,11 +1711,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); @@ -1723,8 +1723,14 @@ 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.className += ' pnlm-pointer'; span.className += ' pnlm-pointer'; @@ -1947,7 +1953,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); } @@ -1988,7 +1994,16 @@ function processOptions(isPreview) { break; case 'fallback': - infoDisplay.errorMsg.innerHTML = '
Your browser does not support WebGL.
Click here to view this panorama in an alternative viewer.
' + error + '
'; + 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; } diff --git a/utils/multires/generate.py b/utils/multires/generate.py index 22bd8ab0..2d395cbf 100755 --- a/utils/multires/generate.py +++ b/utils/multires/generate.py @@ -121,7 +121,7 @@ if args.cubeSize != 0: cubeSize = args.cubeSize else: - cubeSize = 8 * int(origWidth / math.pi / 8) + 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) @@ -222,19 +222,24 @@ # Generate config file text = [] text.append('{') -text.append(' "haov": ' + str(haov)+ ',') text.append(' "hfov": ' + str(args.hfov)+ ',') -text.append(' "minYaw": ' + str(-haov/2+0)+ ',') -text.append(' "yaw": ' + str(-haov/2+args.hfov/2)+ ',') -text.append(' "maxYaw": ' + str(+haov/2+0)+ ',') -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)+ ',') -text.append(' "backgroundColor": "' + args.backgroundColor+ '",') -text.append(' "avoidShowingBackground": ' + ("true" if args.avoidbackground else "false") + ',') -text.append(' "autoLoad": ' + ("true" if args.autoload else "false") + ',') +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(' "multiRes": {') text.append(' "path": "/%l/%s%y_%x",') diff --git a/utils/video/videojs-pannellum-plugin.js b/utils/video/videojs-pannellum-plugin.js index 0d49c59d..26216574 100644 --- a/utils/video/videojs-pannellum-plugin.js +++ b/utils/video/videojs-pannellum-plugin.js @@ -7,12 +7,14 @@ (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;