diff options
author | Daniel Narvaez <dwnarvaez@gmail.com> | 2013-02-06 14:30:36 (GMT) |
---|---|---|
committer | Daniel Narvaez <dwnarvaez@gmail.com> | 2013-02-06 14:30:36 (GMT) |
commit | f5bac2f1e1a51b83d215a07f9d5d87db337873f3 (patch) | |
tree | 15c5f594b1e00c6272552cc5544a1bc757713e34 /shared/js/media/media_frame.js | |
parent | c301ead6bb9c201801400964427b517dafb6a03c (diff) |
Get the build to work
Diffstat (limited to 'shared/js/media/media_frame.js')
-rw-r--r-- | shared/js/media/media_frame.js | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/shared/js/media/media_frame.js b/shared/js/media/media_frame.js new file mode 100644 index 0000000..aaf8fbe --- /dev/null +++ b/shared/js/media/media_frame.js @@ -0,0 +1,537 @@ +/* + * media_frame.js: + * + * A MediaFrame displays a photo or a video. The gallery app uses + * three side by side to support smooth panning from one item to the + * next. The Camera app uses one for image and video preview. The + * Gallery app's open activity uses one of these to display the opened + * item. + * + * MediaFrames have different behavior depending on whether they display + * images or videos. Photo frames allow the user to zoom and pan on the photo. + * Video frames allow the user to play and pause but don't allow zooming. + * + * When a frame is displaying a video, it handles mouse events. + * When display a picture, however, it expects the client to handle events + * and call the pan() and zoom() methods. + * + * The pan() method is a little unusual. It "uses" as much of the pan + * event as it can, and returns a number indicating how much of the + * horizontal motion it did not use. The gallery uses this returned + * value for transitioning between frames. If a frame displays a + * photo that is not zoomed in at all, then it can't use any of the + * pan, and returns the full amount which the gallery app turns into a + * panning motion between frames. But if the photo is zoomed in, then + * the MediaFrame will just move the photo within itself, if it can, and + * return 0. + * + * Much of the code in this file used to be part of the PhotoState class. + */ +function MediaFrame(container, includeVideo) { + if (typeof container === 'string') + container = document.getElementById(container); + this.container = container; + this.image = document.createElement('img'); + this.container.appendChild(this.image); + this.image.style.display = 'none'; + if (includeVideo !== false) { + this.video = new VideoPlayer(container); + this.video.hide(); + } + this.displayingVideo = false; + this.displayingImage = false; + this.blob = null; + this.url = null; +} + +MediaFrame.prototype.displayImage = function displayImage(blob, width, height, + preview) +{ + this.clear(); // Reset everything + + // Remember what we're displaying + this.blob = blob; + this.fullsizeWidth = width; + this.fullsizeHeight = height; + this.preview = preview; + + // Keep track of what kind of content we have + this.displayingImage = true; + + // Make the image element visible + this.image.style.display = 'block'; + + // If the preview is at least as big as the screen, display that. + // Otherwise, display the full-size image. + if (preview && + (preview.width >= window.innerWidth || + preview.height >= window.innerHeight)) { + this.displayingPreview = true; + this._displayImage(blob.slice(preview.start, preview.end, 'image/jpeg'), + preview.width, preview.height); + } + else { + this._displayImage(blob, width, height); + } +}; + +// A utility function we use to display the full-size image or the +// preview The last two arguments are optimizations used by +// switchToFullSizeImage() to make the transition from preview to +// fullscreen smooth. If waitForPaint is true, then this function will +// keep the old image on the screen until the new image is painted +// over it so we (hopefully) don't end up with a blank screen or +// flash. And if callback is specified, it will call the callback +// when thew new images is visible on the screen. If either of those +// arguments are specified, the width and height must be specified. +MediaFrame.prototype._displayImage = function _displayImage(blob, width, height, + waitForPaint, + callback) +{ + var self = this; + var oldImage; + + // Create a URL for the blob (or preview blob) + if (this.url) + URL.revokeObjectURL(this.url); + this.url = URL.createObjectURL(blob); + + // If we don't know the width or the height yet, then set up an event + // handler to set the image size and position once it is loaded. + // This happens for the open activity. + if (!width || !height) { + this.image.src = this.url; + this.image.addEventListener('load', function onload() { + this.removeEventListener('load', onload); + self.itemWidth = this.naturalWidth; + self.itemHeight = this.naturalHeight; + self.computeFit(); + self.setPosition(); + }); + return; + } + + // Otherwise, we have a width and height, and we may also have to handle + // the waitForPaint and callback arguments + + // If waitForPaint is set, then keep the old image around and displayed + // until the new image is loaded. + if (waitForPaint) { + // Remember the old image + oldImage = this.image; + + // Create a new element to load the new image into. + // Insert it into the frame, but don't remove the old image yet + this.image = document.createElement('img'); + this.container.appendChild(this.image); + + // Change the old image slightly to give the user some immediate + // feedback that something is happening + oldImage.classList.add('swapping'); + } + + // Start loading the new image + this.image.src = this.url; + // Update image size and position + this.itemWidth = width; + this.itemHeight = height; + this.computeFit(); + this.setPosition(); + + // If waitForPaint is set, or if there is a callback, then we need to + // run some code when the new image has loaded and been painted. + if (waitForPaint || callback) { + whenLoadedAndVisible(this.image, 1000, function() { + if (waitForPaint) { + // Remove the old image now that the new one is visible + self.container.removeChild(oldImage); + oldImage.src = null; + } + + if (callback) { + // Let the caller know that the new image is ready, but + // wait for an animation frame before doing it. The point of + // using mozRequestAnimationFrame here is that it gives the + // removeChild() call above a chance to take effect. + mozRequestAnimationFrame(function() { + callback(); + }); + } + }); + } + + // Wait until the load event on the image fires, and then wait for a + // MozAfterPaint event after that, and then, finally, invoke the + // callback. Don't wait more than the timeout, though: we need to + // ensure that we always call the callback even if the image does not + // load or if we don't get a MozAfterPaint event. + function whenLoadedAndVisible(image, timeout, callback) { + var called = false; + var timer = setTimeout(function() + { + called = true; + callback(); + }, + timeout || 1000); + + image.addEventListener('load', function onload() { + image.removeEventListener('load', onload); + window.addEventListener('MozAfterPaint', function onpaint() { + window.removeEventListener('MozAfterPaint', onpaint); + clearTimeout(timer); + if (!called) { + callback(); + } + }); + }); + } +}; + +MediaFrame.prototype._switchToFullSizeImage = function _switchToFull(cb) { + if (this.displayingImage && this.displayingPreview) { + this.displayingPreview = false; + this._displayImage(this.blob, this.fullsizeWidth, this.fullsizeHeight, + true, cb); + } +}; + +MediaFrame.prototype._switchToPreviewImage = function _switchToPreview() { + if (this.displayingImage && !this.displayingPreview) { + this.displayingPreview = true; + this._displayImage(this.blob.slice(this.preview.start, + this.preview.end, + 'image/jpeg'), + this.preview.width, + this.preview.height); + } +}; + +MediaFrame.prototype.displayVideo = function displayVideo(blob, width, height, + rotation) +{ + if (!this.video) + return; + + this.clear(); // reset everything + + // Keep track of what kind of content we have + this.displayingVideo = true; + + // Show the video player and hide the image + this.video.show(); + + // Remember the blob + this.blob = blob; + + // Get a new URL for this blob + this.url = URL.createObjectURL(blob); + + // Display it in the video element. + // The VideoPlayer class takes care of positioning itself, so we + // don't have to do anything here with computeFit() or setPosition() + this.video.load(this.url, rotation || 0); +}; + +// Reset the frame state, release any urls and and hide everything +MediaFrame.prototype.clear = function clear() { + // Reset the saved state + this.displayingImage = false; + this.displayingPreview = false; + this.displayingVideo = false; + this.itemWidth = this.itemHeight = null; + this.blob = null; + this.fullsizeWidth = this.fullsizeHeight = null; + this.preview = null; + this.fit = null; + if (this.url) { + URL.revokeObjectURL(this.url); + this.url = null; + } + + // Hide the image + this.image.style.display = 'none'; + this.image.src = null; // XXX: use about:blank or '' here? + + // Hide the video player + if (this.video) { + this.video.hide(); + + // If the video player has its src set, clear it and release resources + // We do this in a roundabout way to avoid getting a warning in the console + if (this.video.player.src) { + this.video.player.removeAttribute('src'); + this.video.player.load(); + } + } +}; + +// Set the item's position based on this.fit +// The VideoPlayer object fits itself to its container, and it +// can't be zoomed or panned, so we only need to do this for images +MediaFrame.prototype.setPosition = function setPosition() { + if (!this.fit || !this.displayingImage) + return; + + this.image.style.transform = + 'translate(' + this.fit.left + 'px,' + this.fit.top + 'px) ' + + 'scale(' + this.fit.scale + ')'; +}; + +MediaFrame.prototype.computeFit = function computeFit() { + if (!this.displayingImage) + return; + this.viewportWidth = this.container.offsetWidth; + this.viewportHeight = this.container.offsetHeight; + + var scalex = this.viewportWidth / this.itemWidth; + var scaley = this.viewportHeight / this.itemHeight; + var scale = Math.min(Math.min(scalex, scaley), 1); + + // Set the image size and position + var width = Math.floor(this.itemWidth * scale); + var height = Math.floor(this.itemHeight * scale); + + this.fit = { + width: width, + height: height, + left: Math.floor((this.viewportWidth - width) / 2), + top: Math.floor((this.viewportHeight - height) / 2), + scale: scale, + baseScale: scale + }; +}; + +MediaFrame.prototype.reset = function reset() { + // If we're not displaying the preview image, but we have one, + // and it is the right size, then switch to it + if (this.displayingImage && !this.displayingPreview && this.preview && + (this.preview.width >= window.innerWidth || + this.preview.height >= window.innerHeight)) { + this._switchToPreviewImage(); // resets image size and position + return; + } + + // Otherwise, if we are displaying the preview image but it is no + // longer big enough for the screen (such as after a resize event) + // then switch to full size. This case should be rare. + if (this.displayingImage && this.displayingPreview && + this.preview.width < window.innerWidth && + this.preview.height < window.innerHeight) { + this._switchToFullSizeImage(); // resets image size and position + return; + } + + // Otherwise, just resize and position the item we're already displaying + this.computeFit(); + this.setPosition(); +}; + +// We call this from the resize handler when the user rotates the +// screen or when going into or out of fullscreen mode. If the user +// has not zoomed in, then we just fit the image to the new size (same +// as reset). But if the user has zoomed in (and we need to stay +// zoomed for the new size) then we adjust the fit properties so that +// the pixel that was at the center of the screen before remains at +// the center now, or as close as possible +MediaFrame.prototype.resize = function resize() { + var oldWidth = this.viewportWidth; + var oldHeight = this.viewportHeight; + var newWidth = this.container.offsetWidth; + var newHeight = this.container.offsetHeight; + + var oldfit = this.fit; // The current image fit + + // If this is triggered by a resize event before the frame has computed + // its size, then there is nothing we can do yet. + if (!oldfit) + return; + + // Compute the new fit. + // This updates the the viewportWidth, viewportHeight and fit properties + this.computeFit(); + + // This is how the image would fit at the new screen size + var newfit = this.fit; + + // If no zooming has been done, then a resize is just a reset. + // The same is true if the new fit base scale is greater than the + // old scale. + if (oldfit.scale === oldfit.baseScale || newfit.baseScale > oldfit.scale) { + this.reset(); + return; + } + + // Otherwise, just adjust the old fit as needed and use that so we + // retain the zoom factor. + oldfit.left += (newWidth - oldWidth) / 2; + oldfit.top += (newHeight - oldHeight) / 2; + oldfit.baseScale = newfit.baseScale; + this.fit = oldfit; + + // Reposition this image without resetting the zoom + this.setPosition(); +}; + +// Zoom in by the specified factor, adjusting the pan amount so that +// the image pixels at (centerX, centerY) remain at that position. +// Assume that zoom gestures can't be done in the middle of swipes, so +// if we're calling zoom, then the swipe property will be 0. +// If time is specified and non-zero, then we set a CSS transition +// to animate the zoom. +MediaFrame.prototype.zoom = function zoom(scale, centerX, centerY, time) { + // Ignore zooms if we're not displaying an image + if (!this.displayingImage) + return; + + // If we were displaying the preview, switch to the full-size image + if (this.displayingPreview) { + // If we want to to animate the zoom, then switch images, wait + // for the new one to load, and call this function again to process + // the zoom and animation. But if we're not animating, then just + // switch images and continue. + if (time) { // if animating + var self = this; + this._switchToFullSizeImage(function() { + self.zoom(scale, centerX, centerY, time); + }); + return; + } + else { + this.switching = true; + var self = this; + this._switchToFullSizeImage(function() { self.switching = false; }); + } + } + + // Never zoom in farther than the native resolution of the image + if (this.fit.scale * scale > 1) { + scale = 1 / (this.fit.scale); + } + // And never zoom out to make the image smaller than it would normally be + else if (this.fit.scale * scale < this.fit.baseScale) { + scale = this.fit.baseScale / this.fit.scale; + } + + this.fit.scale = this.fit.scale * scale; + + // Change the size of the photo + this.fit.width = Math.floor(this.itemWidth * this.fit.scale); + this.fit.height = Math.floor(this.itemHeight * this.fit.scale); + + // centerX and centerY are in viewport coordinates. + // These are the photo coordinates displayed at that point in the viewport + var photoX = centerX - this.fit.left; + var photoY = centerY - this.fit.top; + + // After zooming, these are the new photo coordinates. + // Note we just use the relative scale amount here, not this.fit.scale + var photoX = Math.floor(photoX * scale); + var photoY = Math.floor(photoY * scale); + + // To keep that point still, here are the new left and top values we need + this.fit.left = centerX - photoX; + this.fit.top = centerY - photoY; + + // Now make sure we didn't pan too much: If the image fits on the + // screen, center it. If the image is bigger than the screen, then + // make sure we haven't gone past any edges + if (this.fit.width <= this.viewportWidth) { + this.fit.left = (this.viewportWidth - this.fit.width) / 2; + } + else { + // Don't let the left of the photo be past the left edge of the screen + if (this.fit.left > 0) + this.fit.left = 0; + + // Right of photo shouldn't be to the left of the right edge + if (this.fit.left + this.fit.width < this.viewportWidth) { + this.fit.left = this.viewportWidth - this.fit.width; + } + } + + if (this.fit.height <= this.viewportHeight) { + this.fit.top = (this.viewportHeight - this.fit.height) / 2; + } + else { + // Don't let the top of the photo be below the top of the screen + if (this.fit.top > 0) + this.fit.top = 0; + + // bottom of photo shouldn't be above the bottom of screen + if (this.fit.top + this.fit.height < this.viewportHeight) { + this.fit.top = this.viewportHeight - this.fit.height; + } + } + + if (this.switching) + return; + + // If a time was specified, set up a transition so that the + // call to setPosition() below is animated + if (time) { + // If a time was specfied, animate the transformation + this.image.style.transition = 'transform ' + time + 'ms ease'; + var self = this; + this.image.addEventListener('transitionend', function done(e) { + self.image.removeEventListener('transitionend', done); + self.image.style.transition = null; + }); + } + + this.setPosition(); +}; + +// If the item being displayed is larger than the continer, pan it by +// the specified amounts. Return the "unused" dx amount for the gallery app +// to use for sideways swiping +MediaFrame.prototype.pan = function(dx, dy) { + // We can only pan images, so return the entire dx amount + if (!this.displayingImage) { + return dx; + } + + // Handle panning in the y direction first, since it is easier. + // Don't pan in the y direction if we already fit on the screen + if (this.fit.height > this.viewportHeight) { + this.fit.top += dy; + + // Don't let the top of the photo be below the top of the screen + if (this.fit.top > 0) + this.fit.top = 0; + + // bottom of photo shouldn't be above the bottom of screen + if (this.fit.top + this.fit.height < this.viewportHeight) + this.fit.top = this.viewportHeight - this.fit.height; + } + + // Now handle the X dimension. If we've already panned as far as we can + // within the image (or if it isn't zoomed in) then return the "extra" + // unused dx amount to the caller so that the caller can use them to + // shift the frame left or right. + var extra = 0; + + if (this.fit.width <= this.viewportWidth) { + // In this case, the photo isn't zoomed in, so it is all extra + extra = dx; + } + else { + this.fit.left += dx; + + // If this would take the left edge of the photo past the + // left edge of the screen, then some of the motion is extra + if (this.fit.left > 0) { + extra = this.fit.left; + this.fit.left = 0; + } + + // Or, if this would take the right edge of the photo past the + // right edge of the screen, then we've got extra. + if (this.fit.left + this.fit.width < this.viewportWidth) { + extra = this.fit.left + this.fit.width - this.viewportWidth; + this.fit.left = this.viewportWidth - this.fit.width; + } + } + + this.setPosition(); + return extra; +}; |