Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,879 @@
'use strict';
/* global Downsample, VideoPlayer */
/*
* 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 displaying 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.
*
* MediaFrame uses the #-moz-samplesize media fragment (via the downsample.js
* module) to downsample large jpeg images while decoding them when necessary.
* You can specify a maximum image decode size (in megapixels) when invoking
* the constructor. The MediaFrame code also includes a runtime check for
* the amount of RAM available on the device, and may limit the image decode
* size on low-memory devices.
*/
function MediaFrame(container, includeVideo, maxImageSize) {
this.clear(); // Set all the properties we'll use to default values
if (typeof container === 'string') {
container = document.getElementById(container);
}
this.container = container;
this.maximumImageSize = maxImageSize || 0;
// Create an <img> element to display the image
this.image = new Image();
this.image.className = 'image-view';
this.image.style.opacity = 0; // Start off hidden.
this.image.onload = function () { // When image loads...
this.style.opacity = 1; // make it visible
};
this.image.style.transformOrigin = 'center center'; // for zooming
this.image.style.backgroundImage = 'none';
this.image.style.backgroundSize = 'contain';
this.image.style.backgroundRepeat = 'no-repeat';
this.image.style.backgroundColor = '#222'; // should be overridable somehow
this.container.appendChild(this.image);
// Create the video player element unless we know we'll never need it
if (includeVideo !== false) {
this.video = new VideoPlayer(container);
this.video.hide();
}
// Add a class to the container so we could find it later and use it as
// a key in the instance weakmap.
container.classList.add('media-frame-container');
MediaFrame.instancesToLocalize.set(container, this);
}
// WeakMap with the container nodes as keys and MediaFrame instances as values.
MediaFrame.instancesToLocalize = new WeakMap();
function localize() {
// Retrieve MediaFrame instances by searching for container nodes.
for (var container of document.querySelectorAll('.media-frame-container')) {
var instance = MediaFrame.instancesToLocalize.get(container);
if (instance) {
instance.localize();
}
}
}
document.addEventListener('DOMRetranslated', localize);
MediaFrame.computeMaxImageDecodeSize = function(mem) {
if (!mem) {
return 0;
}
else if (mem < 256) { // This is a Tarako-class device ultra low-end device.
return 2 * 1024 * 1024; // 2 megapixels
}
else if (mem < 512) { // This is a low-end 256mb device
// Normally we can handle 3mp images on devices like this, but if
// this device has a big screen and low memory (like a memory
// throttled Flame) then it needs something smaller than 3mp.
var screensize = screen.width * window.devicePixelRatio *
screen.height * window.devicePixelRatio;
if (mem < 325 && screensize > 480 * 800) {
return 2.5 * 1024 * 1024; // 2.5mp megapixels for throttled Flame
}
return 3 * 1024 * 1024; // 3 megapixels otherwise
}
else if (mem < 1024) { // A mid-range 512mb device
return 5 * 1024 * 1024; // 5 megapixels
}
else { // A high-end device with 1 gigabyte or more of memory
// Allow 10 megapixels of image decode size per gigabyte of memory
return (mem / 1024) * 10 * 1024 * 1024;
}
};
//
// Find out how much memory this device has because we may need to limit image
// decode size on low-end devices. Note that navigator.getFeature requires
// the "feature-detection" permission (at least for now) so we only run this
// code if the client app has that permission.
//
if (navigator.getFeature) {
MediaFrame.pendingPromise = navigator.getFeature('hardware.memory');
MediaFrame.pendingPromise.then(
function resolve(mem) {
delete MediaFrame.pendingPromise;
MediaFrame.maxImageDecodeSize = MediaFrame.computeMaxImageDecodeSize(mem);
},
function reject(err) {
// This should never happen!
delete MediaFrame.pendingPromise;
MediaFrame.maxImageDecodeSize = 0;
}
);
}
MediaFrame.prototype.displayImage = function displayImage(blob,
width,
height,
preview,
rotation,
mirrored)
{
var self = this;
// If we are still querying the device memory, wait for that query to
// complete and then try again.
if (MediaFrame.pendingPromise) {
MediaFrame.pendingPromise.then(function resolve() {
self.displayImage(blob, width, height, preview, rotation, mirrored);
});
return;
}
this.clear(); // Reset everything
// Remember what we're displaying. This doesn't really need to be public
// but the Gallery app uses it.
this.imageblob = blob;
// Figure out if we are going to downsample the image before displaying it
// We expose fullSampleSize as part of the public api only for testing.
this.fullSampleSize = computeFullSampleSize(blob, width, height);
this.fullsizeWidth = this.fullSampleSize.scale(width);
this.fullsizeHeight = this.fullSampleSize.scale(height);
// Valid small EXIF preview used for setting css background image
this.tinyPreviewURL = null;
// Create a blob URL for the image and combine it with the media fragment.
// imageurl is what we'll call revokeObjectURL on. fullImageURL may
// have a media fragment appended, so we need to track both properties.
this.imageurl = URL.createObjectURL(blob);
this.fullImageURL = this.imageurl + this.fullSampleSize;
// Note: There is a default value for orientation/mirrored since some
// images don't have EXIF data to retrieve this information.
this.rotation = rotation || 0;
this.mirrored = mirrored || false;
// Keep track of what kind of content we have
this.displayingImage = true;
this.localize();
function isValidPreview(preview) {
// If no preview at all, we can't use it.
if (!preview) {
return false;
}
// If we don't know the preview size, we can't use it.
if (!preview.width || !preview.height) {
return false;
}
// If there isn't a preview offset or file, we can't use it
if (!preview.start && !preview.filename) {
return false;
}
// If the aspect ratio does not match, we can't use it
if (Math.abs(width / height - preview.width / preview.height) > 0.01) {
return false;
}
return true;
}
function isBigEnoughPreview(preview) {
// Check if we have preview before continuing with size check
if (!preview) {
return false;
}
// If setMinimumPreviewSize has been called, then a preview is big
// enough if it is at least that big.
if (self.minimumPreviewWidth && self.minimumPreviewHeight) {
return Math.max(preview.width, preview.height) >=
Math.max(self.minimumPreviewWidth, self.minimumPreviewHeight) &&
Math.min(preview.width, preview.height) >=
Math.min(self.minimumPreviewWidth, self.minimumPreviewHeight);
}
// Otherwise a preview is big enough if at least one dimension is >= the
// screen size in both portrait and landscape mode.
var screenWidth = window.innerWidth * window.devicePixelRatio;
var screenHeight = window.innerHeight * window.devicePixelRatio;
return ((preview.width >= screenWidth ||
preview.height >= screenHeight) && // portrait
(preview.width >= screenHeight ||
preview.height >= screenWidth)); // landscape
}
// To save memory, we want to avoid displaying the image at full size
// whenever we can display a smaller preview of it. In general, we only
// want to decode the full-size image if the user zooms in on it.
// This code determines whether we have a usable preview image (or whether
// we can downsample the full-size image) and if so, displays that image
var isValid = isValidPreview(preview);
var isBigEnough = isBigEnoughPreview(preview);
if (isValid && isBigEnough) {
if (preview.start) {
gotPreview(blob.slice(preview.start, preview.end, 'image/jpeg'),
preview.width, preview.height);
}
else {
var storage = navigator.getDeviceStorage('pictures');
var getreq = storage.get(preview.filename);
getreq.onsuccess = function() {
gotPreview(getreq.result, preview.width, preview.height);
};
getreq.onerror = function() {
noPreview();
};
}
}
else if (isValid) {
// If we have valid small EXIF preview use it for css background image
// This will help to load large downsampled preview images without flash
// See bug 1188286
// Create a blob URL for the tiny preview
this.tinyPreviewURL = URL.createObjectURL(
blob.slice(preview.start, preview.end, 'image/jpeg'));
noPreview(this.tinyPreviewURL);
}
else {
noPreview();
}
// If we've got a usable preview blob from EXIF or an external file,
// this is what we do with it.
function gotPreview(previewblob, previewWidth, previewHeight) {
// Create a blob URL for the preview
self.previewurl = URL.createObjectURL(previewblob);
// In this case previewImageURL is the same as previewurl. In some other
// cases, previewImageURL may have a media fragment appended, so we need
// two distinct properties, however.
self.previewImageURL = self.previewurl;
// Remember the preview size
self.previewWidth = previewWidth;
self.previewHeight = previewHeight;
// Start off with the preview image displayed
self.displayingPreview = true;
self._displayImage(self.previewImageURL,
self.previewWidth, self.previewHeight);
}
// If we don't have a preview image we can use this is what we do.
function noPreview(bgURL) {
self.previewurl = null;
// Figure out whether we can downsample the fullsize image for
// use as a preview
var previewSampleSize = computePreviewSampleSize(blob, width, height);
// If we can create a preview by downsampling...
if (previewSampleSize !== Downsample.NONE) {
// Combine the full image url with the downsample media fragment
// to create a url for the downsampled preview.
self.previewImageURL = self.imageurl + previewSampleSize;
// Compute the preview size based on the downsample amount.
self.previewWidth = previewSampleSize.scale(width);
self.previewHeight = previewSampleSize.scale(height);
// Now start off with the downsampled image displayed
self.displayingPreview = true;
self._displayImage(self.previewImageURL,
self.previewWidth, self.previewHeight, bgURL);
}
else {
// If we can't (or don't need to) downsample the full image then note
// that we don't have a preview and display the image at full size.
self.previewImageURL = null;
self.displayingPreview = false;
self._displayImage(self.fullImageURL,
self.fullsizeWidth, self.fullsizeHeight);
}
}
// If the blob is a JPEG then we can use #-moz-samplesize to downsample
// it while decoding. If this is a particularly large image then to avoid
// OOMs, we may not want to allow it to ever be decoded at full size
function computeFullSampleSize(blob, width, height) {
if (blob.type !== 'image/jpeg') {
// We're not using #-moz-samplesize at all
return Downsample.NONE;
}
// Determine the maximum size we will decode the image at, based on
// device memory and the maximum size passed to the constructor.
var max = MediaFrame.maxImageDecodeSize || 0;
if (self.maximumImageSize && (max === 0 || self.maximumImageSize < max)) {
max = self.maximumImageSize;
}
if (!max || width * height <= max) {
return Downsample.NONE;
}
return Downsample.areaAtLeast(max / (width * height));
}
function computePreviewSampleSize(blob, width, height) {
// If the image is not a JPEG we can't use a samplesize
if (blob.type !== 'image/jpeg') {
return Downsample.NONE;
}
//
// Determine how much we can scale the image down and still have it
// big enough to fill the screen in at least one dimension.
//
// For example, suppose we have a 1600x1200 photo and a 320x480 screen
//
// portraitScale = Math.min(.2, .4) = 0.2
// landscapeScale = Math.min(.3, .266) = 0.266
// scale = 0.266
//
var screenWidth = window.innerWidth * window.devicePixelRatio;
var screenHeight = window.innerHeight * window.devicePixelRatio;
// To display the image in portrait orientation, this is how much we
// have to scale it down to ensure that both dimensions fit
var portraitScale = Math.min(screenWidth / width, screenHeight / height);
// To display the image in landscape, this is we need to scale it
// this much
var landscapeScale = Math.min(screenHeight / width, screenWidth / height);
// We need an image that is big enough in either orientation
var scale = Math.max(portraitScale, landscapeScale);
// Return the largest samplesize that still produces a big enough preview
return Downsample.sizeNoMoreThan(scale);
}
};
// An internal method to set the url and size of the img element and to
// reposition the image appropriately. We use this when first displaying an
// image and when switching from the preview image to the full image and back.
// The url argument is used as the img.src. The bg argument, if specified, is
// a URL used for the CSS background-image property. This is useful for
// switching from a preview image to a full-size image (when the user zooms
// in) without a flash while the full-size image is decoded.
MediaFrame.prototype._displayImage = function(url, width, height, bg) {
// Set the size of the image
this.image.style.width = width + 'px';
this.image.style.height = height + 'px';
// If a background was specfied, use it. If this is the preview image URL
// and it is already decoded, it gives us something to display while the
// full-size image is decoding.
if (bg) {
this.image.style.backgroundImage = 'url(' + bg + ')';
// Wait one frame before loading the main image in this case
// to ensure that this background image actually gets displayed
requestAnimationFrame(() => { this.image.src = url; });
}
else {
this.image.style.backgroundImage = 'none';
// Start loading and decoding the main image
this.image.src = url;
}
// Remember the width and height, but swap them for rotated images.
if (this.rotation === 0 || this.rotation === 180) {
this.itemWidth = width;
this.itemHeight = height;
} else {
this.itemWidth = height;
this.itemHeight = width;
}
// The image div has a new size, so we have to change its transform
this.computeFit();
this.setPosition();
// Query the position of the image in order to flush the changes
// made by setPosition() above. This prevents us from accidentally
// animating those changes when the user double taps to zoom.
var temp = this.image.clientLeft; // jshint ignore:line
};
// This function adds a label for accessibility to the image frame.
// Videos are localized within the video player, so this is only for images.
MediaFrame.prototype.localize = function localize() {
if (!this.displayingImage) {
return;
}
var portrait = this.fullsizeWidth < this.fullsizeHeight;
if (this.rotation == 90 || this.rotation == 270) {
// If rotated sideways, the width and height are swapped.
portrait = !portrait;
}
var timestamp = this.imageblob.lastModifiedDate;
var orientationL10nId = portrait ? 'orientationPortrait' :
'orientationLandscape';
document.l10n.formatValue(orientationL10nId).then((orientationText) => {
if (timestamp) {
if (!this.dtf) {
// XXX: add localized/timeformatchange event to reset
this.dtf = Intl.DateTimeFormat(navigator.languages, {
hour12: navigator.mozHour12,
hour: 'numeric',
minute: 'numeric',
day: 'numeric',
month: 'numeric',
year: 'numeric'
});
}
var ts = this.dtf.format(new Date(timestamp));
document.l10n.setAttributes(
this.image,
'imageDescription',
{
orientation: orientationText,
timestamp: ts
}
);
} else {
document.l10n.setAttributes(
this.image,
'imageDescriptionNoTimestamp',
{ orientation: orientationText }
);
}
});
};
MediaFrame.prototype._switchToFullSizeImage = function _switchToFull() {
if (!this.displayingImage || !this.displayingPreview) {
return;
}
this.displayingPreview = false;
this._displayImage(this.fullImageURL,
this.fullsizeWidth, this.fullsizeHeight,
this.previewImageURL);
};
MediaFrame.prototype._switchToPreviewImage = function _switchToPreview() {
// If we're not displaying an image or already displaying preview
// or don't have a preview to display then there is nothing to do.
if (!this.displayingImage || this.displayingPreview ||
!this.previewImageURL) {
return;
}
this.displayingPreview = true;
this._displayImage(this.previewImageURL,
this.previewWidth, this.previewHeight);
};
// The video and poster image can have different sizes, but they must
// have the same aspect ratio and rotation. The aspect ratio is the raw
// media width divided by the raw media height before any rotation is applied.
MediaFrame.prototype.displayVideo = function displayVideo(videoblob, posterblob,
aspectRatio, rotation)
{
if (!this.video) {
return;
}
this.clear(); // reset everything
// Keep track of what kind of content we have
this.displayingVideo = true;
// Remember the blobs
this.videoblob = videoblob;
this.posterblob = posterblob;
// Get new URLs for the blobs
this.videourl = URL.createObjectURL(videoblob);
this.posterurl = URL.createObjectURL(posterblob);
// Display them 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.videourl, this.posterurl, aspectRatio, rotation || 0,
videoblob.lastModifiedDate);
// Show the player controls
this.video.show();
};
// 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.imageblob = null;
this.videoblob = null;
this.posterblob = null;
this.fullSampleSize = null;
this.fullImageURL = null;
this.previewImageURL = null;
this.fullsizeWidth = this.fullsizeHeight = null;
this.previewWidth = this.previewHeight = null;
this.fit = null;
if (this.imageurl) {
URL.revokeObjectURL(this.imageurl);
}
this.imageurl = null;
if (this.previewurl) {
URL.revokeObjectURL(this.previewurl);
}
this.previewurl = null;
if (this.tinyPreviewURL) {
URL.revokeObjectURL(this.tinyPreviewURL);
}
this.tinyPreviewURL = null;
// hide the image and release anything it was displaying
if (this.image) {
this.image.style.opacity = 0;
this.image.style.backgroundImage = 'none';
this.image.src = '';
this.image.removeAttribute('aria-label');
}
// Hide the video player
if (this.video) {
this.video.reset();
this.video.hide();
if (this.videourl) {
URL.revokeObjectURL(this.videourl);
}
this.videourl = null;
if (this.posterurl) {
URL.revokeObjectURL(this.posterurl);
}
this.posterurl = null;
}
};
// 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;
}
var dx = this.fit.left, dy = this.fit.top;
// We have to adjust the translation to account for the fact that the
// scaling is being done around the middle of the image, rather than the
// upper-left corner. And we have to make this adjustment differently
// for different rotations.
switch (this.rotation) {
case 0:
case 180:
dx += (this.fit.width - this.itemWidth) / 2;
dy += (this.fit.height - this.itemHeight) / 2;
break;
case 90:
case 270:
dx += (this.fit.width - this.itemHeight) / 2;
dy += (this.fit.height - this.itemWidth) / 2;
break;
}
var sx = this.mirrored ? -this.fit.scale : this.fit.scale;
var sy = this.fit.scale;
var transform =
'translate(' + dx + 'px, ' + dy + 'px) ' +
'scale(' + sx + ',' + sy + ')' +
'rotate(' + this.rotation + 'deg) ';
this.image.style.transform = transform;
};
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.previewImageURL) {
this._switchToPreviewImage(); // resets image size and position
return;
}
// Otherwise, just resize and position the item we're already displaying
this.computeFit();
this.setPosition();
// If frame is resized, the video's size also need to reset.
if (this.displayingVideo) {
this.video.setPlayerSize();
}
};
// 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 (or almost no zooming), then a resize is just
// a reset. The same is true if the new fit base scale is greater than the
// old scale.
// The scale is calculated with division, the value may not be accurate
// because of IEEE 754. We use abs difference to do the equality checking.
if (Math.abs(oldfit.scale - oldfit.baseScale) < 0.01 ||
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 (fixedX, fixedY) 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, fixedX, fixedY, 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) {
this._switchToFullSizeImage();
}
// 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);
// fixedX and fixedY are in viewport coordinates.
// These are the photo coordinates displayed at that point in the viewport
var photoX = fixedX - this.fit.left;
var photoY = fixedY - this.fit.top;
// After zooming, these are the new photo coordinates.
// Note we just use the relative scale amount here, not this.fit.scale
photoX = Math.floor(photoX * scale);
photoY = Math.floor(photoY * scale);
// To keep that point still, here are the new left and top values we need
this.fit.left = fixedX - photoX;
this.fit.top = fixedY - photoY;
// Now make sure we didn't pan too much: If the image fits on the
// screen, fixed 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 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
var transition = 'transform ' + time + 'ms ease';
this.image.style.transition = transition;
var self = this;
this.image.addEventListener('transitionend', function done() {
self.image.removeEventListener('transitionend', done);
self.image.style.transition = '';
});
}
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;
};
MediaFrame.prototype.setMinimumPreviewSize = function(w, h) {
this.minimumPreviewWidth = w;
this.minimumPreviewHeight = h;
};