From 8b5e36e9b008fd058aab568bfa9cc9ae89dd49da Mon Sep 17 00:00:00 2001 From: Matthew Petroff Date: Wed, 3 Oct 2018 20:23:53 -0400 Subject: [PATCH 1/6] When setting view via API, reset timer for auto rotate (fixes #664). --- src/js/pannellum.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/js/pannellum.js b/src/js/pannellum.js index f233249..895a9da 100644 --- a/src/js/pannellum.js +++ b/src/js/pannellum.js @@ -2399,6 +2399,7 @@ this.setPitch = function(pitch, animated, callback, callbackArgs) { } else { config.pitch = pitch; } + latestInteraction = Date.now(); animateInit(); return this; }; @@ -2467,6 +2468,7 @@ this.setYaw = function(yaw, animated, callback, callbackArgs) { } else { config.yaw = yaw; } + latestInteraction = Date.now(); animateInit(); return this; }; @@ -2528,6 +2530,7 @@ this.setHfov = function(hfov, animated, callback, callbackArgs) { } else { setHfov(hfov); } + latestInteraction = Date.now(); animateInit(); return this; }; From a40b906ebdbc6aec89041658f206d5b8a0228917 Mon Sep 17 00:00:00 2001 From: Will Calderbank Date: Thu, 27 Sep 2018 15:10:44 +0100 Subject: [PATCH 2/6] Add animateFinished event fired on animations finishing --- src/js/pannellum.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/pannellum.js b/src/js/pannellum.js index 895a9da..08cd810 100644 --- a/src/js/pannellum.js +++ b/src/js/pannellum.js @@ -1372,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 - From 6d8a19991d6a8bcd33656fafa9bd7a1448423c58 Mon Sep 17 00:00:00 2001 From: Matthew Petroff Date: Wed, 3 Oct 2018 21:00:30 -0400 Subject: [PATCH 3/6] Add documentation for new `animatefinished` event. --- doc/events.md | 6 ++++++ src/js/pannellum.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/events.md b/doc/events.md index 6b70488..940c02a 100644 --- a/doc/events.md +++ b/doc/events.md @@ -27,6 +27,12 @@ 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/src/js/pannellum.js b/src/js/pannellum.js index 08cd810..52918e3 100644 --- a/src/js/pannellum.js +++ b/src/js/pannellum.js @@ -1372,7 +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()}); + fireEvent('animatefinished', {pitch: _this.getPitch(), yaw: _this.getYaw(), hfov: _this.getHfov()}); animating = false; prevTime = undefined; var autoRotateStartTime = config.autoRotateInactivityDelay - From 3151cafb1c0c6d9ab997e660e3b36ab5950de7cb Mon Sep 17 00:00:00 2001 From: Matthew Petroff Date: Tue, 9 Oct 2018 10:38:03 -0400 Subject: [PATCH 4/6] Split equirectangular images into two textures if too big. This allows for equirectangular images on a given device to be twice as big as they were previously, so (almost) all devices now support equirectangular images up to 8192px across. This technique could be extended to double the maximum size again (using eight textures), but images bigger than 8192px should be converted to the multi-resolution format anyway, so there's little point in implementing this extension. --- src/js/libpannellum.js | 77 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/src/js/libpannellum.js b/src/js/libpannellum.js index 662fb3c..128073a 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'); From d12baa08d4f15f4f09dd2d78b193b8f47a9697d2 Mon Sep 17 00:00:00 2001 From: Matthew Petroff Date: Wed, 10 Oct 2018 12:23:37 -0400 Subject: [PATCH 5/6] Fix mouse position for iOS (fixes #668). --- src/js/pannellum.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/js/pannellum.js b/src/js/pannellum.js index 52918e3..c9958c8 100644 --- a/src/js/pannellum.js +++ b/src/js/pannellum.js @@ -658,8 +658,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; } From 5c36101744431e87c5ad548cb63ef7dd89690610 Mon Sep 17 00:00:00 2001 From: Matthew Petroff Date: Wed, 10 Oct 2018 18:10:30 -0400 Subject: [PATCH 6/6] Restructure how animated move callbacks are handled to fix possible race condition (see #658). --- src/js/pannellum.js | 54 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/js/pannellum.js b/src/js/pannellum.js index c9958c8..f1e669b 100644 --- a/src/js/pannellum.js +++ b/src/js/pannellum.js @@ -67,6 +67,7 @@ var config, externalEventListeners = {}, specifiedPhotoSphereExcludes = [], update = false, // Should we update when still to render dynamic content + eps = 1e-6, hotspotsCreated = false; var defaultConfig = { @@ -1301,11 +1302,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; } @@ -2388,20 +2385,25 @@ 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; } - latestInteraction = Date.now(); animateInit(); return this; }; @@ -2450,6 +2452,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) { @@ -2463,14 +2471,13 @@ 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; } - latestInteraction = Date.now(); animateInit(); return this; }; @@ -2519,20 +2526,25 @@ 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); } - latestInteraction = Date.now(); animateInit(); return this; }; @@ -2575,16 +2587,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; }