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/doc/json-config-parameters.md b/doc/json-config-parameters.md index 7d86bfb..9cddd9d 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`. 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'); diff --git a/src/js/pannellum.js b/src/js/pannellum.js index 1caf37c..82d2536 100644 --- a/src/js/pannellum.js +++ b/src/js/pannellum.js @@ -68,6 +68,7 @@ var config, externalEventListeners = {}, specifiedPhotoSphereExcludes = [], update = false, // Should we update when still to render dynamic content + eps = 1e-6, hotspotsCreated = false; var defaultConfig = { @@ -110,6 +111,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 @@ -676,8 +678,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; } @@ -1283,19 +1286,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); } } @@ -1338,11 +1341,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; } @@ -1410,6 +1409,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 - @@ -2510,16 +2510,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; } @@ -2571,6 +2577,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) { @@ -2584,10 +2596,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; } @@ -2639,16 +2651,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); } @@ -2748,16 +2766,20 @@ this.setVfovBounds = 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; }