Home | History | Annotate | Download | only in image_editor
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * The overlay displaying the image.
      9  *
     10  * @param {HTMLElement} container The container element.
     11  * @param {Viewport} viewport The viewport.
     12  * @constructor
     13  * @extends {ImageBuffer.Overlay}
     14  */
     15 function ImageView(container, viewport) {
     16   ImageBuffer.Overlay.call(this);
     17 
     18   this.container_ = container;
     19   this.viewport_ = viewport;
     20   this.document_ = container.ownerDocument;
     21   this.contentGeneration_ = 0;
     22   this.displayedContentGeneration_ = 0;
     23 
     24   this.imageLoader_ = new ImageUtil.ImageLoader(this.document_);
     25   // We have a separate image loader for prefetch which does not get cancelled
     26   // when the selection changes.
     27   this.prefetchLoader_ = new ImageUtil.ImageLoader(this.document_);
     28 
     29   this.contentCallbacks_ = [];
     30 
     31   /**
     32    * The element displaying the current content.
     33    *
     34    * @type {HTMLCanvasElement}
     35    * @private
     36    */
     37   this.screenImage_ = null;
     38 }
     39 
     40 /**
     41  * Duration of transition between modes in ms.
     42  */
     43 ImageView.MODE_TRANSITION_DURATION = 350;
     44 
     45 /**
     46  * If the user flips though images faster than this interval we do not apply
     47  * the slide-in/slide-out transition.
     48  */
     49 ImageView.FAST_SCROLL_INTERVAL = 300;
     50 
     51 /**
     52  * Image load type: full resolution image loaded from cache.
     53  */
     54 ImageView.LOAD_TYPE_CACHED_FULL = 0;
     55 
     56 /**
     57  * Image load type: screen resolution preview loaded from cache.
     58  */
     59 ImageView.LOAD_TYPE_CACHED_SCREEN = 1;
     60 
     61 /**
     62  * Image load type: image read from file.
     63  */
     64 ImageView.LOAD_TYPE_IMAGE_FILE = 2;
     65 
     66 /**
     67  * Image load type: error occurred.
     68  */
     69 ImageView.LOAD_TYPE_ERROR = 3;
     70 
     71 /**
     72  * Image load type: the file contents is not available offline.
     73  */
     74 ImageView.LOAD_TYPE_OFFLINE = 4;
     75 
     76 /**
     77  * The total number of load types.
     78  */
     79 ImageView.LOAD_TYPE_TOTAL = 5;
     80 
     81 ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype};
     82 
     83 /**
     84  * @override
     85  */
     86 ImageView.prototype.getZIndex = function() { return -1; };
     87 
     88 /**
     89  * @override
     90  */
     91 ImageView.prototype.draw = function() {
     92   if (!this.contentCanvas_)  // Do nothing if the image content is not set.
     93     return;
     94   if (this.setupDeviceBuffer(this.screenImage_) ||
     95       this.displayedContentGeneration_ !== this.contentGeneration_) {
     96     this.displayedContentGeneration_ = this.contentGeneration_;
     97     ImageUtil.trace.resetTimer('paint');
     98     this.paintDeviceRect(this.contentCanvas_, new Rect(this.contentCanvas_));
     99     ImageUtil.trace.reportTimer('paint');
    100   }
    101 };
    102 
    103 /**
    104  * Applies the viewport change that does not affect the screen cache size (zoom
    105  * change or offset change) with animation.
    106  */
    107 ImageView.prototype.applyViewportChange = function() {
    108   if (this.screenImage_) {
    109     this.setTransform_(
    110         this.screenImage_,
    111         this.viewport_,
    112         new ImageView.Effect.None(),
    113         ImageView.Effect.DEFAULT_DURATION);
    114   }
    115 };
    116 
    117 /**
    118  * @return {number} The cache generation.
    119  */
    120 ImageView.prototype.getCacheGeneration = function() {
    121   return this.contentGeneration_;
    122 };
    123 
    124 /**
    125  * Invalidates the caches to force redrawing the screen canvas.
    126  */
    127 ImageView.prototype.invalidateCaches = function() {
    128   this.contentGeneration_++;
    129 };
    130 
    131 /**
    132  * @return {HTMLCanvasElement} The content canvas element.
    133  */
    134 ImageView.prototype.getCanvas = function() { return this.contentCanvas_; };
    135 
    136 /**
    137  * @return {boolean} True if the a valid image is currently loaded.
    138  */
    139 ImageView.prototype.hasValidImage = function() {
    140   return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width;
    141 };
    142 
    143 /**
    144  * @return {HTMLCanvasElement} The cached thumbnail image.
    145  */
    146 ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_; };
    147 
    148 /**
    149  * @return {number} The content revision number.
    150  */
    151 ImageView.prototype.getContentRevision = function() {
    152   return this.contentRevision_;
    153 };
    154 
    155 /**
    156  * Copies an image fragment from a full resolution canvas to a device resolution
    157  * canvas.
    158  *
    159  * @param {HTMLCanvasElement} canvas Canvas containing whole image. The canvas
    160  *     may not be full resolution (scaled).
    161  * @param {Rect} imageRect Rectangle region of the canvas to be rendered.
    162  */
    163 ImageView.prototype.paintDeviceRect = function(canvas, imageRect) {
    164   // Map the rectangle in full resolution image to the rectangle in the device
    165   // canvas.
    166   var deviceBounds = this.viewport_.getDeviceBounds();
    167   var scaleX = deviceBounds.width / canvas.width;
    168   var scaleY = deviceBounds.height / canvas.height;
    169   var deviceRect = new Rect(
    170       imageRect.left * scaleX,
    171       imageRect.top * scaleY,
    172       imageRect.width * scaleX,
    173       imageRect.height * scaleY);
    174 
    175   Rect.drawImage(
    176       this.screenImage_.getContext('2d'), canvas, deviceRect, imageRect);
    177 };
    178 
    179 /**
    180  * Creates an overlay canvas with properties similar to the screen canvas.
    181  * Useful for showing quick feedback when editing.
    182  *
    183  * @return {HTMLCanvasElement} Overlay canvas.
    184  */
    185 ImageView.prototype.createOverlayCanvas = function() {
    186   var canvas = this.document_.createElement('canvas');
    187   canvas.className = 'image';
    188   this.container_.appendChild(canvas);
    189   return canvas;
    190 };
    191 
    192 /**
    193  * Sets up the canvas as a buffer in the device resolution.
    194  *
    195  * @param {HTMLCanvasElement} canvas The buffer canvas.
    196  * @return {boolean} True if the canvas needs to be rendered.
    197  */
    198 ImageView.prototype.setupDeviceBuffer = function(canvas) {
    199   // Set the canvas position and size in device pixels.
    200   var deviceRect = this.viewport_.getDeviceBounds();
    201   var needRepaint = false;
    202   if (canvas.width !== deviceRect.width) {
    203     canvas.width = deviceRect.width;
    204     needRepaint = true;
    205   }
    206   if (canvas.height !== deviceRect.height) {
    207     canvas.height = deviceRect.height;
    208     needRepaint = true;
    209   }
    210 
    211   // Center the image.
    212   var imageBounds = this.viewport_.getImageElementBoundsOnScreen();
    213   canvas.style.left = imageBounds.left + 'px';
    214   canvas.style.top = imageBounds.top + 'px';
    215   canvas.style.width = imageBounds.width + 'px';
    216   canvas.style.height = imageBounds.height + 'px';
    217 
    218   this.setTransform_(canvas, this.viewport_);
    219 
    220   return needRepaint;
    221 };
    222 
    223 /**
    224  * @return {ImageData} A new ImageData object with a copy of the content.
    225  */
    226 ImageView.prototype.copyScreenImageData = function() {
    227   return this.screenImage_.getContext('2d').getImageData(
    228       0, 0, this.screenImage_.width, this.screenImage_.height);
    229 };
    230 
    231 /**
    232  * @return {boolean} True if the image is currently being loaded.
    233  */
    234 ImageView.prototype.isLoading = function() {
    235   return this.imageLoader_.isBusy();
    236 };
    237 
    238 /**
    239  * Cancels the current image loading operation. The callbacks will be ignored.
    240  */
    241 ImageView.prototype.cancelLoad = function() {
    242   this.imageLoader_.cancel();
    243 };
    244 
    245 /**
    246  * Loads and display a new image.
    247  *
    248  * Loads the thumbnail first, then replaces it with the main image.
    249  * Takes into account the image orientation encoded in the metadata.
    250  *
    251  * @param {Gallery.Item} item Gallery item to be loaded.
    252  * @param {Object} effect Transition effect object.
    253  * @param {function(number} displayCallback Called when the image is displayed
    254  *   (possibly as a preview).
    255  * @param {function(number} loadCallback Called when the image is fully loaded.
    256  *   The parameter is the load type.
    257  */
    258 ImageView.prototype.load =
    259     function(item, effect, displayCallback, loadCallback) {
    260   var entry = item.getEntry();
    261   var metadata = item.getMetadata() || {};
    262 
    263   if (effect) {
    264     // Skip effects when reloading repeatedly very quickly.
    265     var time = Date.now();
    266     if (this.lastLoadTime_ &&
    267         (time - this.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) {
    268       effect = null;
    269     }
    270     this.lastLoadTime_ = time;
    271   }
    272 
    273   ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime'));
    274 
    275   var self = this;
    276 
    277   this.contentItem_ = item;
    278   this.contentRevision_ = -1;
    279 
    280   var cached = item.contentImage;
    281   if (cached) {
    282     displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL,
    283         false /* no preview */, cached);
    284   } else {
    285     var cachedScreen = item.screenImage;
    286     var imageWidth = metadata.media && metadata.media.width ||
    287                      metadata.external && metadata.external.imageWidth;
    288     var imageHeight = metadata.media && metadata.media.height ||
    289                       metadata.external && metadata.external.imageHeight;
    290     if (cachedScreen) {
    291       // We have a cached screen-scale canvas, use it instead of a thumbnail.
    292       displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, cachedScreen);
    293       // As far as the user can tell the image is loaded. We still need to load
    294       // the full res image to make editing possible, but we can report now.
    295       ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
    296     } else if ((effect && effect.constructor.name === 'Slide') &&
    297                (metadata.thumbnail && metadata.thumbnail.url)) {
    298       // Only show thumbnails if there is no effect or the effect is Slide.
    299       // Also no thumbnail if the image is too large to be loaded.
    300       var thumbnailLoader = new ThumbnailLoader(
    301           entry,
    302           ThumbnailLoader.LoaderType.CANVAS,
    303           metadata);
    304       thumbnailLoader.loadDetachedImage(function(success) {
    305         displayThumbnail(ImageView.LOAD_TYPE_IMAGE_FILE,
    306                          success ? thumbnailLoader.getImage() : null);
    307       });
    308     } else {
    309       loadMainImage(ImageView.LOAD_TYPE_IMAGE_FILE, entry,
    310           false /* no preview*/, 0 /* delay */);
    311     }
    312   }
    313 
    314   function displayThumbnail(loadType, canvas) {
    315     if (canvas) {
    316       var width = null;
    317       var height = null;
    318       if (metadata.media) {
    319         width = metadata.media.width;
    320         height = metadata.media.height;
    321       }
    322       // If metadata.external.present is true, the image data is loaded directly
    323       // from local cache, whose size may be out of sync with the drive
    324       // metadata.
    325       if (metadata.external && !metadata.external.present) {
    326         width = metadata.external.imageWidth;
    327         height = metadata.external.imageHeight;
    328       }
    329       self.replace(
    330           canvas,
    331           effect,
    332           width,
    333           height,
    334           true /* preview */);
    335       if (displayCallback) displayCallback();
    336     }
    337     loadMainImage(loadType, entry, !!canvas,
    338         (effect && canvas) ? effect.getSafeInterval() : 0);
    339   }
    340 
    341   function loadMainImage(loadType, contentEntry, previewShown, delay) {
    342     if (self.prefetchLoader_.isLoading(contentEntry)) {
    343       // The image we need is already being prefetched. Initiating another load
    344       // would be a waste. Hijack the load instead by overriding the callback.
    345       self.prefetchLoader_.setCallback(
    346           displayMainImage.bind(null, loadType, previewShown));
    347 
    348       // Swap the loaders so that the self.isLoading works correctly.
    349       var temp = self.prefetchLoader_;
    350       self.prefetchLoader_ = self.imageLoader_;
    351       self.imageLoader_ = temp;
    352       return;
    353     }
    354     self.prefetchLoader_.cancel();  // The prefetch was doing something useless.
    355 
    356     self.imageLoader_.load(
    357         item,
    358         displayMainImage.bind(null, loadType, previewShown),
    359         delay);
    360   }
    361 
    362   function displayMainImage(loadType, previewShown, content, opt_error) {
    363     if (opt_error)
    364       loadType = ImageView.LOAD_TYPE_ERROR;
    365 
    366     // If we already displayed the preview we should not replace the content if
    367     // the full content failed to load.
    368     var animationDuration = 0;
    369     if (!(previewShown && loadType === ImageView.LOAD_TYPE_ERROR)) {
    370       var replaceEffect = previewShown ? null : effect;
    371       animationDuration = replaceEffect ? replaceEffect.getSafeInterval() : 0;
    372       self.replace(content, replaceEffect);
    373       if (!previewShown && displayCallback) displayCallback();
    374     }
    375 
    376     if (loadType !== ImageView.LOAD_TYPE_ERROR &&
    377         loadType !== ImageView.LOAD_TYPE_CACHED_SCREEN) {
    378       ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
    379     }
    380     ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('LoadMode'),
    381         loadType, ImageView.LOAD_TYPE_TOTAL);
    382 
    383     if (loadType === ImageView.LOAD_TYPE_ERROR &&
    384         !navigator.onLine && !metadata.external.present) {
    385       loadType = ImageView.LOAD_TYPE_OFFLINE;
    386     }
    387     if (loadCallback) loadCallback(loadType, animationDuration, opt_error);
    388   }
    389 };
    390 
    391 /**
    392  * Prefetches an image.
    393  * @param {Gallery.Item} item The image item.
    394  * @param {number} delay Image load delay in ms.
    395  */
    396 ImageView.prototype.prefetch = function(item, delay) {
    397   if (item.contentImage)
    398     return;
    399   this.prefetchLoader_.load(item, function(canvas) {
    400     if (canvas.width && canvas.height && !item.contentImage)
    401       item.contentImage = canvas;
    402   }, delay);
    403 };
    404 
    405 /**
    406  * Unloads content.
    407  * @param {Rect} zoomToRect Target rectangle for zoom-out-effect.
    408  */
    409 ImageView.prototype.unload = function(zoomToRect) {
    410   if (this.unloadTimer_) {
    411     clearTimeout(this.unloadTimer_);
    412     this.unloadTimer_ = null;
    413   }
    414   if (zoomToRect && this.screenImage_) {
    415     var effect = this.createZoomEffect(zoomToRect);
    416     this.setTransform_(this.screenImage_, this.viewport_, effect);
    417     this.screenImage_.setAttribute('fade', true);
    418     this.unloadTimer_ = setTimeout(function() {
    419       this.unloadTimer_ = null;
    420       this.unload(null /* force unload */);
    421     }.bind(this), effect.getSafeInterval());
    422     return;
    423   }
    424   this.container_.textContent = '';
    425   this.contentCanvas_ = null;
    426   this.screenImage_ = null;
    427 };
    428 
    429 /**
    430  * @param {HTMLCanvasElement} content The image element.
    431  * @param {number=} opt_width Image width.
    432  * @param {number=} opt_height Image height.
    433  * @param {boolean=} opt_preview True if the image is a preview (not full res).
    434  * @private
    435  */
    436 ImageView.prototype.replaceContent_ = function(
    437     content, opt_width, opt_height, opt_preview) {
    438 
    439   if (this.contentCanvas_ && this.contentCanvas_.parentNode === this.container_)
    440     this.container_.removeChild(this.contentCanvas_);
    441 
    442   this.screenImage_ = this.document_.createElement('canvas');
    443   this.screenImage_.className = 'image';
    444 
    445   this.contentCanvas_ = content;
    446   this.invalidateCaches();
    447   this.viewport_.setImageSize(
    448       opt_width || this.contentCanvas_.width,
    449       opt_height || this.contentCanvas_.height);
    450   this.draw();
    451 
    452   this.container_.appendChild(this.screenImage_);
    453 
    454   this.preview_ = opt_preview;
    455   // If this is not a thumbnail, cache the content and the screen-scale image.
    456   if (this.hasValidImage()) {
    457     // Insert the full resolution canvas into DOM so that it can be printed.
    458     this.container_.appendChild(this.contentCanvas_);
    459     this.contentCanvas_.classList.add('fullres');
    460 
    461     this.contentItem_.contentImage = this.contentCanvas_;
    462     this.contentItem_.screenImage = this.screenImage_;
    463 
    464     // TODO(kaznacheev): It is better to pass screenImage_ as it is usually
    465     // much smaller than contentCanvas_ and still contains the entire image.
    466     // Once we implement zoom/pan we should pass contentCanvas_ instead.
    467     this.updateThumbnail_(this.screenImage_);
    468 
    469     this.contentRevision_++;
    470     for (var i = 0; i !== this.contentCallbacks_.length; i++) {
    471       try {
    472         this.contentCallbacks_[i]();
    473       } catch (e) {
    474         console.error(e);
    475       }
    476     }
    477   }
    478 };
    479 
    480 /**
    481  * Adds a listener for content changes.
    482  * @param {function} callback Callback.
    483  */
    484 ImageView.prototype.addContentCallback = function(callback) {
    485   this.contentCallbacks_.push(callback);
    486 };
    487 
    488 /**
    489  * Updates the cached thumbnail image.
    490  *
    491  * @param {HTMLCanvasElement} canvas The source canvas.
    492  * @private
    493  */
    494 ImageView.prototype.updateThumbnail_ = function(canvas) {
    495   ImageUtil.trace.resetTimer('thumb');
    496   var pixelCount = 10000;
    497   var downScale =
    498       Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount));
    499 
    500   this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas');
    501   this.thumbnailCanvas_.width = Math.round(canvas.width / downScale);
    502   this.thumbnailCanvas_.height = Math.round(canvas.height / downScale);
    503   Rect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas);
    504   ImageUtil.trace.reportTimer('thumb');
    505 };
    506 
    507 /**
    508  * Replaces the displayed image, possibly with slide-in animation.
    509  *
    510  * @param {HTMLCanvasElement} content The image element.
    511  * @param {Object=} opt_effect Transition effect object.
    512  * @param {number=} opt_width Image width.
    513  * @param {number=} opt_height Image height.
    514  * @param {boolean=} opt_preview True if the image is a preview (not full res).
    515  */
    516 ImageView.prototype.replace = function(
    517     content, opt_effect, opt_width, opt_height, opt_preview) {
    518   var oldScreenImage = this.screenImage_;
    519   var oldViewport = this.viewport_.clone();
    520 
    521   this.replaceContent_(content, opt_width, opt_height, opt_preview);
    522   if (!opt_effect) {
    523     if (oldScreenImage)
    524       oldScreenImage.parentNode.removeChild(oldScreenImage);
    525     return;
    526   }
    527 
    528   var newScreenImage = this.screenImage_;
    529   this.viewport_.resetView();
    530 
    531   if (oldScreenImage)
    532     ImageUtil.setAttribute(newScreenImage, 'fade', true);
    533   this.setTransform_(
    534       newScreenImage, this.viewport_, opt_effect, 0 /* instant */);
    535 
    536   setTimeout(function() {
    537     this.setTransform_(
    538         newScreenImage,
    539         this.viewport_,
    540         null,
    541         opt_effect && opt_effect.getDuration());
    542     if (oldScreenImage) {
    543       ImageUtil.setAttribute(newScreenImage, 'fade', false);
    544       ImageUtil.setAttribute(oldScreenImage, 'fade', true);
    545       console.assert(opt_effect.getReverse, 'Cannot revert an effect.');
    546       var reverse = opt_effect.getReverse();
    547       this.setTransform_(oldScreenImage, oldViewport, reverse);
    548       setTimeout(function() {
    549         if (oldScreenImage.parentNode)
    550           oldScreenImage.parentNode.removeChild(oldScreenImage);
    551       }, reverse.getSafeInterval());
    552     }
    553   }.bind(this));
    554 };
    555 
    556 /**
    557  * @param {HTMLCanvasElement} element The element to transform.
    558  * @param {Viewport} viewport Viewport to be used for calculating
    559  *     transformation.
    560  * @param {ImageView.Effect=} opt_effect The effect to apply.
    561  * @param {number=} opt_duration Transition duration.
    562  * @private
    563  */
    564 ImageView.prototype.setTransform_ = function(
    565     element, viewport, opt_effect, opt_duration) {
    566   if (!opt_effect)
    567     opt_effect = new ImageView.Effect.None();
    568   if (typeof opt_duration !== 'number')
    569     opt_duration = opt_effect.getDuration();
    570   element.style.webkitTransitionDuration = opt_duration + 'ms';
    571   element.style.webkitTransitionTimingFunction = opt_effect.getTiming();
    572   element.style.webkitTransform = opt_effect.transform(element, viewport);
    573 };
    574 
    575 /**
    576  * @param {Rect} screenRect Target rectangle in screen coordinates.
    577  * @return {ImageView.Effect.Zoom} Zoom effect object.
    578  */
    579 ImageView.prototype.createZoomEffect = function(screenRect) {
    580   return new ImageView.Effect.ZoomToScreen(
    581       screenRect,
    582       ImageView.MODE_TRANSITION_DURATION);
    583 };
    584 
    585 /**
    586  * Visualizes crop or rotate operation. Hide the old image instantly, animate
    587  * the new image to visualize the operation.
    588  *
    589  * @param {HTMLCanvasElement} canvas New content canvas.
    590  * @param {Rect} imageCropRect The crop rectangle in image coordinates.
    591  *                             Null for rotation operations.
    592  * @param {number} rotate90 Rotation angle in 90 degree increments.
    593  * @return {number} Animation duration.
    594  */
    595 ImageView.prototype.replaceAndAnimate = function(
    596     canvas, imageCropRect, rotate90) {
    597   var oldImageBounds = {
    598     width: this.viewport_.getImageBounds().width,
    599     height: this.viewport_.getImageBounds().height
    600   };
    601   var oldScreenImage = this.screenImage_;
    602   this.replaceContent_(canvas);
    603   var newScreenImage = this.screenImage_;
    604   var effect = rotate90 ?
    605       new ImageView.Effect.Rotate(rotate90 > 0) :
    606       new ImageView.Effect.Zoom(
    607           oldImageBounds.width, oldImageBounds.height, imageCropRect);
    608 
    609   this.setTransform_(newScreenImage, this.viewport_, effect, 0 /* instant */);
    610 
    611   oldScreenImage.parentNode.appendChild(newScreenImage);
    612   oldScreenImage.parentNode.removeChild(oldScreenImage);
    613 
    614   // Let the layout fire, then animate back to non-transformed state.
    615   setTimeout(
    616       this.setTransform_.bind(
    617           this, newScreenImage, this.viewport_, null, effect.getDuration()),
    618       0);
    619 
    620   return effect.getSafeInterval();
    621 };
    622 
    623 /**
    624  * Visualizes "undo crop". Shrink the current image to the given crop rectangle
    625  * while fading in the new image.
    626  *
    627  * @param {HTMLCanvasElement} canvas New content canvas.
    628  * @param {Rect} imageCropRect The crop rectangle in image coordinates.
    629  * @return {number} Animation duration.
    630  */
    631 ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) {
    632   var oldScreenImage = this.screenImage_;
    633   this.replaceContent_(canvas);
    634   var newScreenImage = this.screenImage_;
    635   var setFade = ImageUtil.setAttribute.bind(null, newScreenImage, 'fade');
    636   setFade(true);
    637   oldScreenImage.parentNode.insertBefore(newScreenImage, oldScreenImage);
    638   var effect = new ImageView.Effect.Zoom(
    639       this.viewport_.getImageBounds().width,
    640       this.viewport_.getImageBounds().height,
    641       imageCropRect);
    642 
    643   // Animate to the transformed state.
    644   this.setTransform_(oldScreenImage, this.viewport_, effect);
    645   setTimeout(setFade.bind(null, false), 0);
    646   setTimeout(function() {
    647     if (oldScreenImage.parentNode)
    648       oldScreenImage.parentNode.removeChild(oldScreenImage);
    649   }, effect.getSafeInterval());
    650 
    651   return effect.getSafeInterval();
    652 };
    653 
    654 /* Transition effects */
    655 
    656 /**
    657  * Base class for effects.
    658  *
    659  * @param {number} duration Duration in ms.
    660  * @param {string=} opt_timing CSS transition timing function name.
    661  * @constructor
    662  */
    663 ImageView.Effect = function(duration, opt_timing) {
    664   this.duration_ = duration;
    665   this.timing_ = opt_timing || 'linear';
    666 };
    667 
    668 /**
    669  *
    670  */
    671 ImageView.Effect.DEFAULT_DURATION = 180;
    672 
    673 /**
    674  *
    675  */
    676 ImageView.Effect.MARGIN = 100;
    677 
    678 /**
    679  * @return {number} Effect duration in ms.
    680  */
    681 ImageView.Effect.prototype.getDuration = function() { return this.duration_; };
    682 
    683 /**
    684  * @return {number} Delay in ms since the beginning of the animation after which
    685  * it is safe to perform CPU-heavy operations without disrupting the animation.
    686  */
    687 ImageView.Effect.prototype.getSafeInterval = function() {
    688   return this.getDuration() + ImageView.Effect.MARGIN;
    689 };
    690 
    691 /**
    692  * @return {string} CSS transition timing function name.
    693  */
    694 ImageView.Effect.prototype.getTiming = function() { return this.timing_; };
    695 
    696 /**
    697  * Obtains the CSS transformation string of the effect.
    698  * @param {DOMCanvas} element Canvas element to be applied the transformation.
    699  * @param {Viewport} viewport Current viewport.
    700  * @return {string} CSS transformation description.
    701  */
    702 ImageView.Effect.prototype.transform = function(element, viewport) {
    703   throw new Error('Not implemented.');
    704   return '';
    705 };
    706 
    707 /**
    708  * Default effect.
    709  *
    710  * @constructor
    711  * @extends {ImageView.Effect}
    712  */
    713 ImageView.Effect.None = function() {
    714   ImageView.Effect.call(this, 0, 'easy-out');
    715 };
    716 
    717 /**
    718  * Inherits from ImageView.Effect.
    719  */
    720 ImageView.Effect.None.prototype = { __proto__: ImageView.Effect.prototype };
    721 
    722 /**
    723  * @param {HTMLCanvasElement} element Element.
    724  * @param {Viewport} viewport Current viewport.
    725  * @return {string} Transform string.
    726  */
    727 ImageView.Effect.None.prototype.transform = function(element, viewport) {
    728   return viewport.getTransformation();
    729 };
    730 
    731 /**
    732  * Slide effect.
    733  *
    734  * @param {number} direction -1 for left, 1 for right.
    735  * @param {boolean=} opt_slow True if slow (as in slideshow).
    736  * @constructor
    737  * @extends {ImageView.Effect}
    738  */
    739 ImageView.Effect.Slide = function Slide(direction, opt_slow) {
    740   ImageView.Effect.call(this,
    741       opt_slow ? 800 : ImageView.Effect.DEFAULT_DURATION, 'ease-out');
    742   this.direction_ = direction;
    743   this.slow_ = opt_slow;
    744   this.shift_ = opt_slow ? 100 : 40;
    745   if (this.direction_ < 0) this.shift_ = -this.shift_;
    746 };
    747 
    748 ImageView.Effect.Slide.prototype = { __proto__: ImageView.Effect.prototype };
    749 
    750 /**
    751  * Reverses the slide effect.
    752  * @return {ImageView.Effect.Slide} Reversed effect.
    753  */
    754 ImageView.Effect.Slide.prototype.getReverse = function() {
    755   return new ImageView.Effect.Slide(-this.direction_, this.slow_);
    756 };
    757 
    758 /**
    759  * @override
    760  */
    761 ImageView.Effect.Slide.prototype.transform = function(element, viewport) {
    762   return viewport.getShiftTransformation(this.shift_);
    763 };
    764 
    765 /**
    766  * Zoom effect.
    767  *
    768  * Animates the original rectangle to the target rectangle.
    769  *
    770  * @param {number} previousImageWidth Width of the full resolution image.
    771  * @param {number} previousImageHeight Height of the full resolution image.
    772  * @param {Rect} imageCropRect Crop rectangle in the full resolution image.
    773  * @param {number=} opt_duration Duration of the effect.
    774  * @constructor
    775  * @extends {ImageView.Effect}
    776  */
    777 ImageView.Effect.Zoom = function(
    778     previousImageWidth, previousImageHeight, imageCropRect, opt_duration) {
    779   ImageView.Effect.call(this,
    780       opt_duration || ImageView.Effect.DEFAULT_DURATION, 'ease-out');
    781   this.previousImageWidth_ = previousImageWidth;
    782   this.previousImageHeight_ = previousImageHeight;
    783   this.imageCropRect_ = imageCropRect;
    784 };
    785 
    786 ImageView.Effect.Zoom.prototype = { __proto__: ImageView.Effect.prototype };
    787 
    788 /**
    789  * @override
    790  */
    791 ImageView.Effect.Zoom.prototype.transform = function(element, viewport) {
    792   return viewport.getInverseTransformForCroppedImage(
    793       this.previousImageWidth_, this.previousImageHeight_, this.imageCropRect_);
    794 };
    795 
    796 /**
    797  * Effect to zoom to a screen rectangle.
    798  *
    799  * @param {Rect} screenRect Rectangle in the application window's coordinate.
    800  * @param {number=} opt_duration Duration of effect.
    801  * @constructor
    802  * @extends {ImageView.Effect}
    803  */
    804 ImageView.Effect.ZoomToScreen = function(screenRect, opt_duration) {
    805   ImageView.Effect.call(this, opt_duration);
    806   this.screenRect_ = screenRect;
    807 };
    808 
    809 ImageView.Effect.ZoomToScreen.prototype = {
    810   __proto__: ImageView.Effect.prototype
    811 };
    812 
    813 /**
    814  * @override
    815  */
    816 ImageView.Effect.ZoomToScreen.prototype.transform = function(
    817     element, viewport) {
    818   return viewport.getScreenRectTransformForImage(this.screenRect_);
    819 };
    820 
    821 /**
    822  * Rotation effect.
    823  *
    824  * @param {boolean} orientation Orientation of rotation. True is for clockwise
    825  *     and false is for counterclockwise.
    826  * @constructor
    827  * @extends {ImageView.Effect}
    828  */
    829 ImageView.Effect.Rotate = function(orientation) {
    830   ImageView.Effect.call(this, ImageView.Effect.DEFAULT_DURATION);
    831   this.orientation_ = orientation;
    832 };
    833 
    834 ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype };
    835 
    836 /**
    837  * @override
    838  */
    839 ImageView.Effect.Rotate.prototype.transform = function(element, viewport) {
    840   return viewport.getInverseTransformForRotatedImage(this.orientation_);
    841 };
    842