Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / image_view.js
blobcd8fb1ccbc15b89f338556152d7a7ef269b48ee0
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.
5 /**
6  * The overlay displaying the image.
7  *
8  * @param {!HTMLElement} container The container element.
9  * @param {!Viewport} viewport The viewport.
10  * @param {!MetadataModel} metadataModel
11  * @constructor
12  * @extends {ImageBuffer.Overlay}
13  * @struct
14  */
15 function ImageView(container, viewport, metadataModel) {
16   ImageBuffer.Overlay.call(this);
18   this.container_ = container;
20   /**
21    * The viewport.
22    * @type {!Viewport}
23    * @private
24    */
25   this.viewport_ = viewport;
27   this.document_ = assertInstanceof(container.ownerDocument, HTMLDocument);
28   this.contentGeneration_ = 0;
29   this.displayedContentGeneration_ = 0;
31   this.imageLoader_ =
32       new ImageUtil.ImageLoader(this.document_, metadataModel);
33   // We have a separate image loader for prefetch which does not get cancelled
34   // when the selection changes.
35   this.prefetchLoader_ =
36       new ImageUtil.ImageLoader(this.document_, metadataModel);
38   this.contentCallbacks_ = [];
40   /**
41    * The element displaying the current content.
42    * @type {HTMLCanvasElement}
43    * @private
44    */
45   this.screenImage_ = null;
47   /**
48    * The content canvas element.
49    * @type {(HTMLCanvasElement|HTMLImageElement)}
50    * @private
51    */
52   this.contentCanvas_ = null;
54   /**
55    * True if the image is a preview (not full res).
56    * @type {boolean}
57    * @private
58    */
59   this.preview_ = false;
61   /**
62    * Cached thumbnail image.
63    * @type {HTMLCanvasElement}
64    * @private
65    */
66   this.thumbnailCanvas_ = null;
68   /**
69    * The content revision number.
70    * @type {number}
71    * @private
72    */
73   this.contentRevision_ = -1;
75   /**
76    * The last load time.
77    * @type {?number}
78    * @private
79    */
80   this.lastLoadTime_ = null;
82   /**
83    * Gallery item which is loaded.
84    * @type {Gallery.Item}
85    * @private
86    */
87   this.contentItem_ = null;
89   /**
90    * Timer to unload.
91    * @type {?number}
92    * @private
93    */
94   this.unloadTimer_ = null;
97 /**
98  * Duration of transition between modes in ms.
99  * @type {number}
100  * @const
101  */
102 ImageView.MODE_TRANSITION_DURATION = 350;
105  * If the user flips though images faster than this interval we do not apply
106  * the slide-in/slide-out transition.
107  * @type {number}
108  * @const
109  */
110 ImageView.FAST_SCROLL_INTERVAL = 300;
114  * Image load types.
115  * @enum {number}
116  */
117 ImageView.LoadType = {
118   // Full resolution image loaded from cache.
119   CACHED_FULL: 0,
120   // Screen resolution preview loaded from cache.
121   CACHED_SCREEN: 1,
122   // Image read from file.
123   IMAGE_FILE: 2,
124   // Error occurred.
125   ERROR: 3,
126   // The file contents is not available offline.
127   OFFLINE: 4
131  * Target of image load.
132  * @enum {string}
133  */
134 ImageView.LoadTarget = {
135   CACHED_MAIN_IMAGE: 'cachedMainImage',
136   CACHED_THUMBNAIL: 'cachedThumbnail',
137   THUMBNAIL: 'thumbnail',
138   MAIN_IMAGE: 'mainImage'
142  * Obtains prefered load type from GalleryItem.
144  * @param {!Gallery.Item} item
145  * @param {!ImageView.Effect} effect
146  * @return {ImageView.LoadTarget} Load target.
147  */
148 ImageView.getLoadTarget = function(item, effect) {
149   if (item.contentImage)
150     return ImageView.LoadTarget.CACHED_MAIN_IMAGE;
151   if (item.screenImage)
152     return ImageView.LoadTarget.CACHED_THUMBNAIL;
154   // Only show thumbnails if there is no effect or the effect is Slide.
155   var thumbnailLoader = new ThumbnailLoader(
156       item.getEntry(),
157       ThumbnailLoader.LoaderType.CANVAS,
158       item.getThumbnailMetadataItem());
159   if ((effect instanceof ImageView.Effect.None ||
160        effect instanceof ImageView.Effect.Slide) &&
161       thumbnailLoader.getLoadTarget() !==
162       ThumbnailLoader.LoadTarget.FILE_ENTRY) {
163     return ImageView.LoadTarget.THUMBNAIL;
164   }
166   return ImageView.LoadTarget.MAIN_IMAGE;
169 ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype};
172  * @override
173  */
174 ImageView.prototype.getZIndex = function() { return -1; };
177  * @override
178  */
179 ImageView.prototype.draw = function() {
180   if (!this.contentCanvas_)  // Do nothing if the image content is not set.
181     return;
182   this.setTransform_(
183       this.contentCanvas_,
184       this.viewport_,
185       new ImageView.Effect.None(),
186       ImageView.Effect.DEFAULT_DURATION);
187   if ((this.screenImage_ && this.setupDeviceBuffer(this.screenImage_)) ||
188       this.displayedContentGeneration_ !== this.contentGeneration_) {
189     this.displayedContentGeneration_ = this.contentGeneration_;
190     ImageUtil.trace.resetTimer('paint');
191     this.paintDeviceRect(
192         this.contentCanvas_, ImageRect.createFromImage(this.contentCanvas_));
193     ImageUtil.trace.reportTimer('paint');
194   }
198  * Applies the viewport change that does not affect the screen cache size (zoom
199  * change or offset change) with animation.
200  */
201 ImageView.prototype.applyViewportChange = function() {
202   var zooming = this.viewport_.getZoom() > 1;
203   if (this.contentCanvas_) {
204     // Show full resolution image only for zooming.
205     this.contentCanvas_.style.opacity = zooming ? '1' : '0';
206     this.setTransform_(
207         this.contentCanvas_,
208         this.viewport_,
209         new ImageView.Effect.None(),
210         ImageView.Effect.DEFAULT_DURATION);
211   }
212   if (this.screenImage_) {
213       this.setTransform_(
214           this.screenImage_,
215           this.viewport_,
216           new ImageView.Effect.None(),
217           ImageView.Effect.DEFAULT_DURATION);
218   }
222  * @return {number} The cache generation.
223  */
224 ImageView.prototype.getCacheGeneration = function() {
225   return this.contentGeneration_;
229  * Invalidates the caches to force redrawing the screen canvas.
230  */
231 ImageView.prototype.invalidateCaches = function() {
232   this.contentGeneration_++;
236  * @return {HTMLCanvasElement} The content canvas element.
237  */
238 ImageView.prototype.getCanvas = function() { return this.contentCanvas_; };
241  * @return {boolean} True if the a valid image is currently loaded.
242  */
243 ImageView.prototype.hasValidImage = function() {
244   return !!(!this.preview_ && this.contentCanvas_ && this.contentCanvas_.width);
248  * @return {!HTMLCanvasElement} The cached thumbnail image.
249  */
250 ImageView.prototype.getThumbnail = function() {
251   assert(this.thumbnailCanvas_);
252   return this.thumbnailCanvas_;
256  * @return {number} The content revision number.
257  */
258 ImageView.prototype.getContentRevision = function() {
259   return this.contentRevision_;
263  * Copies an image fragment from a full resolution canvas to a device resolution
264  * canvas.
266  * @param {!HTMLCanvasElement} canvas Canvas containing whole image. The canvas
267  *     may not be full resolution (scaled).
268  * @param {!ImageRect} imageRect Rectangle region of the canvas to be rendered.
269  */
270 ImageView.prototype.paintDeviceRect = function(canvas, imageRect) {
271   // Map the rectangle in full resolution image to the rectangle in the device
272   // canvas.
273   var deviceBounds = this.viewport_.getDeviceBounds();
274   var scaleX = deviceBounds.width / canvas.width;
275   var scaleY = deviceBounds.height / canvas.height;
276   var deviceRect = new ImageRect(
277       imageRect.left * scaleX,
278       imageRect.top * scaleY,
279       imageRect.width * scaleX,
280       imageRect.height * scaleY);
282   ImageRect.drawImage(
283       this.screenImage_.getContext('2d'), canvas, deviceRect, imageRect);
287  * Creates an overlay canvas with properties similar to the screen canvas.
288  * Useful for showing quick feedback when editing.
290  * @return {!HTMLCanvasElement} Overlay canvas.
291  */
292 ImageView.prototype.createOverlayCanvas = function() {
293   var canvas = assertInstanceof(this.document_.createElement('canvas'),
294       HTMLCanvasElement);
295   canvas.className = 'image';
296   this.container_.appendChild(canvas);
297   return canvas;
301  * Sets up the canvas as a buffer in the device resolution.
303  * @param {!HTMLCanvasElement} canvas The buffer canvas.
304  * @return {boolean} True if the canvas needs to be rendered.
305  */
306 ImageView.prototype.setupDeviceBuffer = function(canvas) {
307   // Set the canvas position and size in device pixels.
308   var deviceRect = this.viewport_.getDeviceBounds();
309   var needRepaint = false;
310   if (canvas.width !== deviceRect.width) {
311     canvas.width = deviceRect.width;
312     needRepaint = true;
313   }
314   if (canvas.height !== deviceRect.height) {
315     canvas.height = deviceRect.height;
316     needRepaint = true;
317   }
318   this.setTransform_(canvas, this.viewport_);
319   return needRepaint;
323  * @return {!ImageData} A new ImageData object with a copy of the content.
324  */
325 ImageView.prototype.copyScreenImageData = function() {
326   return this.screenImage_.getContext('2d').getImageData(
327       0, 0, this.screenImage_.width, this.screenImage_.height);
331  * @return {boolean} True if the image is currently being loaded.
332  */
333 ImageView.prototype.isLoading = function() {
334   return this.imageLoader_.isBusy();
338  * Cancels the current image loading operation. The callbacks will be ignored.
339  */
340 ImageView.prototype.cancelLoad = function() {
341   this.imageLoader_.cancel();
345  * Loads and display a new image.
347  * Loads the thumbnail first, then replaces it with the main image.
348  * Takes into account the image orientation encoded in the metadata.
350  * @param {!Gallery.Item} item Gallery item to be loaded.
351  * @param {!ImageView.Effect} effect Transition effect object.
352  * @param {function()} displayCallback Called when the image is displayed
353  *     (possibly as a preview).
354  * @param {function(!ImageView.LoadType, number, *=)} loadCallback Called when
355  *     the image is fully loaded. The first parameter is the load type.
356  */
357 ImageView.prototype.load =
358     function(item, effect, displayCallback, loadCallback) {
359   var entry = item.getEntry();
361   if (!(effect instanceof ImageView.Effect.None)) {
362     // Skip effects when reloading repeatedly very quickly.
363     var time = Date.now();
364     if (this.lastLoadTime_ &&
365         (time - this.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) {
366       effect = new ImageView.Effect.None();
367     }
368     this.lastLoadTime_ = time;
369   }
371   ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime'));
373   var self = this;
375   this.contentItem_ = item;
376   this.contentRevision_ = -1;
378   switch (ImageView.getLoadTarget(item, effect)) {
379     case ImageView.LoadTarget.CACHED_MAIN_IMAGE:
380       displayMainImage(
381           ImageView.LoadType.CACHED_FULL,
382           false /* no preview */,
383           assert(item.contentImage));
384       break;
386     case ImageView.LoadTarget.CACHED_THUMBNAIL:
387       // We have a cached screen-scale canvas, use it instead of a thumbnail.
388       displayThumbnail(ImageView.LoadType.CACHED_SCREEN, item.screenImage);
389       // As far as the user can tell the image is loaded. We still need to load
390       // the full res image to make editing possible, but we can report now.
391       ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
392       break;
394     case ImageView.LoadTarget.THUMBNAIL:
395       var thumbnailLoader = new ThumbnailLoader(
396           entry,
397           ThumbnailLoader.LoaderType.CANVAS,
398           item.getThumbnailMetadataItem());
399       thumbnailLoader.loadDetachedImage(function(success) {
400         displayThumbnail(
401             ImageView.LoadType.IMAGE_FILE,
402             success ? thumbnailLoader.getImage() : null);
403       });
404       break;
406     case ImageView.LoadTarget.MAIN_IMAGE:
407       loadMainImage(
408           ImageView.LoadType.IMAGE_FILE,
409           entry,
410           false /* no preview*/,
411           0 /* delay */);
412       break;
414     default:
415       assertNotReached();
416   }
418   /**
419    * @param {!ImageView.LoadType} loadType A load type.
420    * @param {(HTMLCanvasElement|HTMLImageElement)} canvas A canvas.
421    */
422   function displayThumbnail(loadType, canvas) {
423     if (canvas) {
424       var width = item.getMetadataItem().imageWidth;
425       var height = item.getMetadataItem().imageHeight;
426       self.replace(
427           canvas,
428           effect,
429           width,
430           height,
431           true /* preview */);
432       if (displayCallback)
433         displayCallback();
434     }
435     loadMainImage(loadType, entry, !!canvas,
436         (effect && canvas) ? effect.getSafeInterval() : 0);
437   }
439   /**
440    * @param {!ImageView.LoadType} loadType Load type.
441    * @param {Entry} contentEntry A content entry.
442    * @param {boolean} previewShown A preview is shown or not.
443    * @param {number} delay Load delay.
444    */
445   function loadMainImage(loadType, contentEntry, previewShown, delay) {
446     if (self.prefetchLoader_.isLoading(contentEntry)) {
447       // The image we need is already being prefetched. Initiating another load
448       // would be a waste. Hijack the load instead by overriding the callback.
449       self.prefetchLoader_.setCallback(
450           displayMainImage.bind(null, loadType, previewShown));
452       // Swap the loaders so that the self.isLoading works correctly.
453       var temp = self.prefetchLoader_;
454       self.prefetchLoader_ = self.imageLoader_;
455       self.imageLoader_ = temp;
456       return;
457     }
458     self.prefetchLoader_.cancel();  // The prefetch was doing something useless.
460     self.imageLoader_.load(
461         item,
462         displayMainImage.bind(null, loadType, previewShown),
463         delay);
464   }
466   /**
467    * @param {!ImageView.LoadType} loadType Load type.
468    * @param {boolean} previewShown A preview is shown or not.
469    * @param {!(HTMLCanvasElement|HTMLImageElement)} content A content.
470    * @param {string=} opt_error Error message.
471    */
472   function displayMainImage(loadType, previewShown, content, opt_error) {
473     if (opt_error)
474       loadType = ImageView.LoadType.ERROR;
476     // If we already displayed the preview we should not replace the content if
477     // the full content failed to load.
478     var animationDuration = 0;
479     if (!(previewShown && loadType === ImageView.LoadType.ERROR)) {
480       var replaceEffect = previewShown ? null : effect;
481       animationDuration = replaceEffect ? replaceEffect.getSafeInterval() : 0;
482       self.replace(content, replaceEffect);
483       if (!previewShown && displayCallback) displayCallback();
484     }
486     if (loadType !== ImageView.LoadType.ERROR &&
487         loadType !== ImageView.LoadType.CACHED_SCREEN) {
488       ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
489     }
490     ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('LoadMode'),
491         loadType, Object.keys(ImageView.LoadType).length);
493     if (loadType === ImageView.LoadType.ERROR &&
494         !navigator.onLine && !item.getMetadataItem().present) {
495       loadType = ImageView.LoadType.OFFLINE;
496     }
497     if (loadCallback) loadCallback(loadType, animationDuration, opt_error);
498   }
502  * Prefetches an image.
503  * @param {!Gallery.Item} item The image item.
504  * @param {number=} opt_delay Image load delay in ms.
505  */
506 ImageView.prototype.prefetch = function(item, opt_delay) {
507   if (item.contentImage || this.prefetchLoader_.isLoading(item.getEntry()))
508     return;
509   this.prefetchLoader_.load(item, function(canvas) {
510     if (canvas.width && canvas.height && !item.contentImage)
511       item.contentImage = canvas;
512   }, opt_delay);
516  * Unloads content.
517  * @param {ImageRect=} opt_zoomToRect Target rectangle for zoom-out-effect.
518  */
519 ImageView.prototype.unload = function(opt_zoomToRect) {
520   if (this.unloadTimer_) {
521     clearTimeout(this.unloadTimer_);
522     this.unloadTimer_ = null;
523   }
524   if (opt_zoomToRect && this.screenImage_) {
525     var effect = this.createZoomEffect(opt_zoomToRect);
526     this.setTransform_(this.screenImage_, this.viewport_, effect);
527     this.screenImage_.setAttribute('fade', true);
528     this.unloadTimer_ = setTimeout(function() {
529       this.unloadTimer_ = null;
530       this.unload(null /* force unload */);
531     }.bind(this), effect.getSafeInterval());
532     return;
533   }
534   this.container_.textContent = '';
535   this.contentCanvas_ = null;
536   this.screenImage_ = null;
540  * @param {!(HTMLCanvasElement|HTMLImageElement)} content The image element.
541  * @param {number=} opt_width Image width.
542  * @param {number=} opt_height Image height.
543  * @param {boolean=} opt_preview True if the image is a preview (not full res).
544  * @private
545  */
546 ImageView.prototype.replaceContent_ = function(
547     content, opt_width, opt_height, opt_preview) {
549   if (this.contentCanvas_ && this.contentCanvas_.parentNode === this.container_)
550     this.container_.removeChild(this.contentCanvas_);
552   this.screenImage_ = assertInstanceof(this.document_.createElement('canvas'),
553       HTMLCanvasElement);
554   this.screenImage_.className = 'image';
556   this.contentCanvas_ = content;
557   this.invalidateCaches();
558   this.viewport_.setImageSize(
559       opt_width || this.contentCanvas_.width,
560       opt_height || this.contentCanvas_.height);
561   this.draw();
563   this.container_.appendChild(this.screenImage_);
565   this.preview_ = opt_preview || false;
566   // If this is not a thumbnail, cache the content and the screen-scale image.
567   if (this.hasValidImage()) {
568     // Insert the full resolution canvas into DOM so that it can be printed.
569     this.container_.appendChild(this.contentCanvas_);
570     this.contentCanvas_.classList.add('fullres');
571     this.setTransform_(
572         this.contentCanvas_, this.viewport_, null, 0);
574     this.contentItem_.contentImage = this.contentCanvas_;
575     this.contentItem_.screenImage = this.screenImage_;
577     // TODO(kaznacheev): It is better to pass screenImage_ as it is usually
578     // much smaller than contentCanvas_ and still contains the entire image.
579     // Once we implement zoom/pan we should pass contentCanvas_ instead.
580     this.updateThumbnail_(this.screenImage_);
582     this.contentRevision_++;
583     for (var i = 0; i !== this.contentCallbacks_.length; i++) {
584       try {
585         this.contentCallbacks_[i]();
586       } catch (e) {
587         console.error(e);
588       }
589     }
590   }
594  * Adds a listener for content changes.
595  * @param {function()} callback Callback.
596  */
597 ImageView.prototype.addContentCallback = function(callback) {
598   this.contentCallbacks_.push(callback);
602  * Updates the cached thumbnail image.
604  * @param {!HTMLCanvasElement} canvas The source canvas.
605  * @private
606  */
607 ImageView.prototype.updateThumbnail_ = function(canvas) {
608   ImageUtil.trace.resetTimer('thumb');
609   var pixelCount = 10000;
610   var downScale =
611       Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount));
613   this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas');
614   this.thumbnailCanvas_.width = Math.round(canvas.width / downScale);
615   this.thumbnailCanvas_.height = Math.round(canvas.height / downScale);
616   ImageRect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas);
617   ImageUtil.trace.reportTimer('thumb');
621  * Replaces the displayed image, possibly with slide-in animation.
623  * @param {!(HTMLCanvasElement|HTMLImageElement)} content The image element.
624  * @param {ImageView.Effect=} opt_effect Transition effect object.
625  * @param {number=} opt_width Image width.
626  * @param {number=} opt_height Image height.
627  * @param {boolean=} opt_preview True if the image is a preview (not full res).
628  */
629 ImageView.prototype.replace = function(
630     content, opt_effect, opt_width, opt_height, opt_preview) {
631   var oldScreenImage = this.screenImage_;
632   var oldViewport = this.viewport_.clone();
633   this.replaceContent_(content, opt_width, opt_height, opt_preview);
634   if (!opt_effect) {
635     if (oldScreenImage)
636       oldScreenImage.parentNode.removeChild(oldScreenImage);
637     return;
638   }
640   assert(this.screenImage_);
641   var newScreenImage = this.screenImage_;
642   this.viewport_.resetView();
644   if (oldScreenImage)
645     ImageUtil.setAttribute(newScreenImage, 'fade', true);
646   this.setTransform_(
647       newScreenImage, this.viewport_, opt_effect, 0 /* instant */);
648   this.setTransform_(
649       content, this.viewport_, opt_effect, 0 /* instant */);
651   // We need to call requestAnimationFrame twice here. The first call is for
652   // commiting the styles of beggining of transition that are assigned above.
653   // The second call is for assigning and commiting the styles of end of
654   // transition, which triggers transition animation.
655   requestAnimationFrame(function() {
656     requestAnimationFrame(function() {
657       this.setTransform_(
658           newScreenImage,
659           this.viewport_,
660           null,
661           opt_effect ? opt_effect.getDuration() : undefined);
662       this.setTransform_(
663           content,
664           this.viewport_,
665           null,
666           opt_effect ? opt_effect.getDuration() : undefined);
667       if (oldScreenImage) {
668         ImageUtil.setAttribute(newScreenImage, 'fade', false);
669         ImageUtil.setAttribute(oldScreenImage, 'fade', true);
670         var reverse = opt_effect.getReverse();
671         if (reverse) {
672           this.setTransform_(oldScreenImage, oldViewport, reverse);
673           setTimeout(function() {
674             if (oldScreenImage.parentNode)
675               oldScreenImage.parentNode.removeChild(oldScreenImage);
676           }, reverse.getSafeInterval());
677         } else {
678             if (oldScreenImage.parentNode)
679               oldScreenImage.parentNode.removeChild(oldScreenImage);
680         }
681       }
682     }.bind(this));
683   }.bind(this));
687  * @param {!HTMLCanvasElement|!HTMLImageElement} element The element to
688  *     transform.
689  * @param {!Viewport} viewport Viewport to be used for calculating
690  *     transformation.
691  * @param {ImageView.Effect=} opt_effect The effect to apply.
692  * @param {number=} opt_duration Transition duration.
693  * @private
694  */
695 ImageView.prototype.setTransform_ = function(
696     element, viewport, opt_effect, opt_duration) {
697   if (!opt_effect)
698     opt_effect = new ImageView.Effect.None();
699   if (typeof opt_duration !== 'number')
700     opt_duration = opt_effect.getDuration();
701   element.style.transitionDuration = opt_duration + 'ms';
702   element.style.transitionTimingFunction = opt_effect.getTiming();
703   element.style.transform = opt_effect.transform(element, viewport);
707  * Creates zoom effect object.
708  * @param {!ImageRect} screenRect Target rectangle in screen coordinates.
709  * @return {!ImageView.Effect} Zoom effect object.
710  */
711 ImageView.prototype.createZoomEffect = function(screenRect) {
712   return new ImageView.Effect.ZoomToScreen(
713       screenRect,
714       ImageView.MODE_TRANSITION_DURATION);
718  * Visualizes crop or rotate operation. Hide the old image instantly, animate
719  * the new image to visualize the operation.
721  * @param {!HTMLCanvasElement} canvas New content canvas.
722  * @param {ImageRect} imageCropRect The crop rectangle in image coordinates.
723  *     Null for rotation operations.
724  * @param {number} rotate90 Rotation angle in 90 degree increments.
725  * @return {number} Animation duration.
726  */
727 ImageView.prototype.replaceAndAnimate = function(
728     canvas, imageCropRect, rotate90) {
729   assert(this.screenImage_);
731   var oldImageBounds = {
732     width: this.viewport_.getImageBounds().width,
733     height: this.viewport_.getImageBounds().height
734   };
735   var oldScreenImage = this.screenImage_;
736   this.replaceContent_(canvas);
737   var newScreenImage = this.screenImage_;
738   var effect = rotate90 ?
739       new ImageView.Effect.Rotate(rotate90 > 0) :
740       new ImageView.Effect.Zoom(
741           oldImageBounds.width, oldImageBounds.height, assert(imageCropRect));
743   this.setTransform_(newScreenImage, this.viewport_, effect, 0 /* instant */);
745   oldScreenImage.parentNode.appendChild(newScreenImage);
746   oldScreenImage.parentNode.removeChild(oldScreenImage);
748   // Let the layout fire, then animate back to non-transformed state.
749   setTimeout(
750       this.setTransform_.bind(
751           this, newScreenImage, this.viewport_, null, effect.getDuration()),
752       0);
754   return effect.getSafeInterval();
758  * Visualizes "undo crop". Shrink the current image to the given crop rectangle
759  * while fading in the new image.
761  * @param {!HTMLCanvasElement} canvas New content canvas.
762  * @param {!ImageRect} imageCropRect The crop rectangle in image coordinates.
763  * @return {number} Animation duration.
764  */
765 ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) {
766   var oldScreenImage = this.screenImage_;
767   this.replaceContent_(canvas);
768   var newScreenImage = this.screenImage_;
769   var setFade = ImageUtil.setAttribute.bind(null, assert(newScreenImage),
770       'fade');
771   setFade(true);
772   oldScreenImage.parentNode.insertBefore(newScreenImage, oldScreenImage);
773   var effect = new ImageView.Effect.Zoom(
774       this.viewport_.getImageBounds().width,
775       this.viewport_.getImageBounds().height,
776       imageCropRect);
778   // Animate to the transformed state.
779   this.setTransform_(oldScreenImage, this.viewport_, effect);
780   setTimeout(setFade.bind(null, false), 0);
781   setTimeout(function() {
782     if (oldScreenImage.parentNode)
783       oldScreenImage.parentNode.removeChild(oldScreenImage);
784   }, effect.getSafeInterval());
786   return effect.getSafeInterval();
789 /* Transition effects */
792  * Base class for effects.
794  * @param {number} duration Duration in ms.
795  * @param {string=} opt_timing CSS transition timing function name.
796  * @constructor
797  * @struct
798  */
799 ImageView.Effect = function(duration, opt_timing) {
800   this.duration_ = duration;
801   this.timing_ = opt_timing || 'linear';
805  * Default duration of an effect.
806  * @type {number}
807  * @const
808  */
809 ImageView.Effect.DEFAULT_DURATION = 180;
812  * Effect margin.
813  * @type {number}
814  * @const
815  */
816 ImageView.Effect.MARGIN = 100;
819  * @return {number} Effect duration in ms.
820  */
821 ImageView.Effect.prototype.getDuration = function() { return this.duration_; };
824  * @return {number} Delay in ms since the beginning of the animation after which
825  * it is safe to perform CPU-heavy operations without disrupting the animation.
826  */
827 ImageView.Effect.prototype.getSafeInterval = function() {
828   return this.getDuration() + ImageView.Effect.MARGIN;
832  * Reverses the effect.
833  * @return {ImageView.Effect} Reversed effect. Null is returned if this
834  *     is not supported in the effect.
835  */
836 ImageView.Effect.prototype.getReverse = function() {
837   return null;
841  * @return {string} CSS transition timing function name.
842  */
843 ImageView.Effect.prototype.getTiming = function() { return this.timing_; };
846  * Obtains the CSS transformation string of the effect.
847  * @param {!HTMLCanvasElement|!HTMLImageElement} element Canvas element to be
848  *     applied the transformation.
849  * @param {!Viewport} viewport Current viewport.
850  * @return {string} CSS transformation description.
851  */
852 ImageView.Effect.prototype.transform = function(element, viewport) {
853   throw new Error('Not implemented.');
857  * Default effect.
859  * @constructor
860  * @extends {ImageView.Effect}
861  * @struct
862  */
863 ImageView.Effect.None = function() {
864   ImageView.Effect.call(this, 0, 'easy-out');
868  * Inherits from ImageView.Effect.
869  */
870 ImageView.Effect.None.prototype = { __proto__: ImageView.Effect.prototype };
873  * @override
874  */
875 ImageView.Effect.None.prototype.transform = function(element, viewport) {
876   return viewport.getTransformation(element.width, element.height);
880  * Slide effect.
882  * @param {number} direction -1 for left, 1 for right.
883  * @param {boolean=} opt_slow True if slow (as in slideshow).
884  * @constructor
885  * @extends {ImageView.Effect}
886  * @struct
887  */
888 ImageView.Effect.Slide = function Slide(direction, opt_slow) {
889   ImageView.Effect.call(this,
890       opt_slow ? 800 : ImageView.Effect.DEFAULT_DURATION, 'ease-out');
891   this.direction_ = direction;
892   this.slow_ = opt_slow;
893   this.shift_ = opt_slow ? 100 : 40;
894   if (this.direction_ < 0) this.shift_ = -this.shift_;
897 ImageView.Effect.Slide.prototype = { __proto__: ImageView.Effect.prototype };
900  * @override
901  */
902 ImageView.Effect.Slide.prototype.getReverse = function() {
903   return new ImageView.Effect.Slide(-this.direction_, this.slow_);
907  * @override
908  */
909 ImageView.Effect.Slide.prototype.transform = function(element, viewport) {
910   return viewport.getTransformation(
911       element.width, element.height, this.shift_);
915  * Zoom effect.
917  * Animates the original rectangle to the target rectangle.
919  * @param {number} previousImageWidth Width of the full resolution image.
920  * @param {number} previousImageHeight Height of the full resolution image.
921  * @param {!ImageRect} imageCropRect Crop rectangle in the full resolution
922  *     image.
923  * @param {number=} opt_duration Duration of the effect.
924  * @constructor
925  * @extends {ImageView.Effect}
926  * @struct
927  */
928 ImageView.Effect.Zoom = function(
929     previousImageWidth, previousImageHeight, imageCropRect, opt_duration) {
930   ImageView.Effect.call(this,
931       opt_duration || ImageView.Effect.DEFAULT_DURATION, 'ease-out');
932   this.previousImageWidth_ = previousImageWidth;
933   this.previousImageHeight_ = previousImageHeight;
934   this.imageCropRect_ = imageCropRect;
937 ImageView.Effect.Zoom.prototype = { __proto__: ImageView.Effect.prototype };
940  * @override
941  */
942 ImageView.Effect.Zoom.prototype.transform = function(element, viewport) {
943   return viewport.getCroppingTransformation(
944       element.width,
945       element.height,
946       this.previousImageWidth_,
947       this.previousImageHeight_,
948       this.imageCropRect_);
952  * Effect to zoom to a screen rectangle.
954  * @param {!ImageRect} screenRect Rectangle in the application window's
955  *     coordinate.
956  * @param {number=} opt_duration Duration of effect.
957  * @constructor
958  * @extends {ImageView.Effect}
959  * @struct
960  */
961 ImageView.Effect.ZoomToScreen = function(screenRect, opt_duration) {
962   ImageView.Effect.call(this, opt_duration ||
963       ImageView.Effect.DEFAULT_DURATION);
964   this.screenRect_ = screenRect;
967 ImageView.Effect.ZoomToScreen.prototype = {
968   __proto__: ImageView.Effect.prototype
972  * @override
973  */
974 ImageView.Effect.ZoomToScreen.prototype.transform = function(
975     element, viewport) {
976   return viewport.getScreenRectTransformation(
977       element.width,
978       element.height,
979       this.screenRect_);
983  * Rotation effect.
985  * @param {boolean} orientation Orientation of rotation. True is for clockwise
986  *     and false is for counterclockwise.
987  * @constructor
988  * @extends {ImageView.Effect}
989  * @struct
990  */
991 ImageView.Effect.Rotate = function(orientation) {
992   ImageView.Effect.call(this, ImageView.Effect.DEFAULT_DURATION);
993   this.orientation_ = orientation;
996 ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype };
999  * @override
1000  */
1001 ImageView.Effect.Rotate.prototype.transform = function(element, viewport) {
1002   return viewport.getRotatingTransformation(
1003       element.width, element.height, this.orientation_ ? -1 : 1);