@@ -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 | |||
@@ -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`. | |||
@@ -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'); | |||
@@ -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; | |||
} | |||