Gallery: Random fixes for the Viewport class.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / image_view.js
blob312fe726e42d58fd0e187dc959cdf7049855edcb
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 'use strict';
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);
18   this.container_ = container;
19   this.viewport_ = viewport;
20   this.document_ = container.ownerDocument;
21   this.contentGeneration_ = 0;
22   this.displayedContentGeneration_ = 0;
23   this.displayedViewportGeneration_ = 0;
25   this.imageLoader_ = new ImageUtil.ImageLoader(this.document_);
26   // We have a separate image loader for prefetch which does not get cancelled
27   // when the selection changes.
28   this.prefetchLoader_ = new ImageUtil.ImageLoader(this.document_);
30   // The content cache is used for prefetching the next image when going
31   // through the images sequentially. The real life photos can be large
32   // (18Mpix = 72Mb pixel array) so we want only the minimum amount of caching.
33   this.contentCache_ = new ImageView.Cache(2);
35   // We reuse previously generated screen-scale images so that going back to
36   // a recently loaded image looks instant even if the image is not in
37   // the content cache any more. Screen-scale images are small (~1Mpix)
38   // so we can afford to cache more of them.
39   this.screenCache_ = new ImageView.Cache(5);
40   this.contentCallbacks_ = [];
42   /**
43    * The element displaying the current content.
44    *
45    * @type {HTMLCanvasElement}
46    * @private
47    */
48   this.screenImage_ = null;
51 /**
52  * Duration of transition between modes in ms.
53  */
54 ImageView.MODE_TRANSITION_DURATION = 350;
56 /**
57  * If the user flips though images faster than this interval we do not apply
58  * the slide-in/slide-out transition.
59  */
60 ImageView.FAST_SCROLL_INTERVAL = 300;
62 /**
63  * Image load type: full resolution image loaded from cache.
64  */
65 ImageView.LOAD_TYPE_CACHED_FULL = 0;
67 /**
68  * Image load type: screen resolution preview loaded from cache.
69  */
70 ImageView.LOAD_TYPE_CACHED_SCREEN = 1;
72 /**
73  * Image load type: image read from file.
74  */
75 ImageView.LOAD_TYPE_IMAGE_FILE = 2;
77 /**
78  * Image load type: error occurred.
79  */
80 ImageView.LOAD_TYPE_ERROR = 3;
82 /**
83  * Image load type: the file contents is not available offline.
84  */
85 ImageView.LOAD_TYPE_OFFLINE = 4;
87 /**
88  * The total number of load types.
89  */
90 ImageView.LOAD_TYPE_TOTAL = 5;
92 ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype};
94 /**
95  * @override
96  */
97 ImageView.prototype.getZIndex = function() { return -1; };
99 /**
100  * @override
101  */
102 ImageView.prototype.draw = function() {
103   if (!this.contentCanvas_)  // Do nothing if the image content is not set.
104     return;
106   var forceRepaint = false;
108   if (this.displayedViewportGeneration_ !==
109       this.viewport_.getCacheGeneration()) {
110     this.displayedViewportGeneration_ = this.viewport_.getCacheGeneration();
112     this.setupDeviceBuffer(this.screenImage_);
114     forceRepaint = true;
115   }
117   if (forceRepaint ||
118       this.displayedContentGeneration_ !== this.contentGeneration_) {
119     this.displayedContentGeneration_ = this.contentGeneration_;
121     ImageUtil.trace.resetTimer('paint');
122     this.paintDeviceRect(this.contentCanvas_, new Rect(this.contentCanvas_));
123     ImageUtil.trace.reportTimer('paint');
124   }
128  * Applies the viewport change that does not affect the screen cache size (zoom
129  * change or offset change) with animation.
130  */
131 ImageView.prototype.applyViewportChange = function() {
132   this.setTransform(
133       this.screenImage_,
134       new ImageView.Effect.None(),
135       ImageView.Effect.DEFAULT_DURATION);
139  * @return {number} The cache generation.
140  */
141 ImageView.prototype.getCacheGeneration = function() {
142   return this.contentGeneration_;
146  * Invalidates the caches to force redrawing the screen canvas.
147  */
148 ImageView.prototype.invalidateCaches = function() {
149   this.contentGeneration_++;
153  * @return {HTMLCanvasElement} The content canvas element.
154  */
155 ImageView.prototype.getCanvas = function() { return this.contentCanvas_; };
158  * @return {boolean} True if the a valid image is currently loaded.
159  */
160 ImageView.prototype.hasValidImage = function() {
161   return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width;
165  * @return {HTMLCanvasElement} The cached thumbnail image.
166  */
167 ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_; };
170  * @return {number} The content revision number.
171  */
172 ImageView.prototype.getContentRevision = function() {
173   return this.contentRevision_;
177  * Copies an image fragment from a full resolution canvas to a device resolution
178  * canvas.
180  * @param {HTMLCanvasElement} canvas Canvas containing whole image. The canvas
181  *     may not be full resolution (scaled).
182  * @param {Rect} imageRect Rectangle region of the canvas to be rendered.
183  */
184 ImageView.prototype.paintDeviceRect = function(canvas, imageRect) {
185   // Check canvas size.
186   var deviceBounds = this.viewport_.getDeviceBounds();
187   if (this.screenImage_.width != deviceBounds.width ||
188       this.screenImage_.height != deviceBounds.height) {
189     console.error('The size of canvas is invalid.', (new Error).stack);
190     return;
191   }
193   // Map the rectangle in full resolution image to the rectangle in the device
194   // canvas.
195   var scaleX = deviceBounds.width / canvas.width;
196   var scaleY = deviceBounds.height / canvas.height;
197   var deviceRect = new Rect(
198       imageRect.left * scaleX,
199       imageRect.top * scaleY,
200       imageRect.width * scaleX,
201       imageRect.height * scaleY);
203   Rect.drawImage(
204       this.screenImage_.getContext('2d'), canvas, deviceRect, imageRect);
208  * Creates an overlay canvas with properties similar to the screen canvas.
209  * Useful for showing quick feedback when editing.
211  * @return {HTMLCanvasElement} Overlay canvas.
212  */
213 ImageView.prototype.createOverlayCanvas = function() {
214   var canvas = this.document_.createElement('canvas');
215   canvas.className = 'image';
216   this.container_.appendChild(canvas);
217   return canvas;
221  * Sets up the canvas as a buffer in the device resolution.
223  * @param {HTMLCanvasElement} canvas The buffer canvas.
224  */
225 ImageView.prototype.setupDeviceBuffer = function(canvas) {
226   // Set the canvas position and size in device pixels.
227   var deviceRect = this.viewport_.getDeviceBounds();
228   if (canvas.width !== deviceRect.width)
229     canvas.width = deviceRect.width;
230   if (canvas.height !== deviceRect.height)
231     canvas.height = deviceRect.height;
233   // Center the image.
234   var imageBoudns = this.viewport_.getImageElementBoundsOnScreen();
235   canvas.style.left = imageBoudns.left + 'px';
236   canvas.style.top = imageBoudns.top + 'px';
238   // Scale the canvas down to screen pixels.
239   this.setTransform(canvas);
243  * @return {ImageData} A new ImageData object with a copy of the content.
244  */
245 ImageView.prototype.copyScreenImageData = function() {
246   return this.screenImage_.getContext('2d').getImageData(
247       0, 0, this.screenImage_.width, this.screenImage_.height);
251  * @return {boolean} True if the image is currently being loaded.
252  */
253 ImageView.prototype.isLoading = function() {
254   return this.imageLoader_.isBusy();
258  * Cancels the current image loading operation. The callbacks will be ignored.
259  */
260 ImageView.prototype.cancelLoad = function() {
261   this.imageLoader_.cancel();
265  * Loads and display a new image.
267  * Loads the thumbnail first, then replaces it with the main image.
268  * Takes into account the image orientation encoded in the metadata.
270  * @param {Gallery.Item} item Gallery item to be loaded.
271  * @param {Object} effect Transition effect object.
272  * @param {function(number} displayCallback Called when the image is displayed
273  *   (possibly as a prevew).
274  * @param {function(number} loadCallback Called when the image is fully loaded.
275  *   The parameter is the load type.
276  */
277 ImageView.prototype.load =
278     function(item, effect, displayCallback, loadCallback) {
279   var entry = item.getEntry();
280   var metadata = item.getMetadata() || {};
282   if (effect) {
283     // Skip effects when reloading repeatedly very quickly.
284     var time = Date.now();
285     if (this.lastLoadTime_ &&
286        (time - this.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) {
287       effect = null;
288     }
289     this.lastLoadTime_ = time;
290   }
292   ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime'));
294   var self = this;
296   this.contentEntry_ = entry;
297   this.contentRevision_ = -1;
299   // Cache has to be evicted in advance, so the returned cached image is not
300   // evicted later by the prefetched image.
301   this.contentCache_.evictLRU();
303   var cached = this.contentCache_.getItem(this.contentEntry_);
304   if (cached) {
305     displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL,
306         false /* no preview */, cached);
307   } else {
308     var cachedScreen = this.screenCache_.getItem(this.contentEntry_);
309     var imageWidth = metadata.media && metadata.media.width ||
310                      metadata.drive && metadata.drive.imageWidth;
311     var imageHeight = metadata.media && metadata.media.height ||
312                       metadata.drive && metadata.drive.imageHeight;
313     if (cachedScreen) {
314       // We have a cached screen-scale canvas, use it instead of a thumbnail.
315       displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, cachedScreen);
316       // As far as the user can tell the image is loaded. We still need to load
317       // the full res image to make editing possible, but we can report now.
318       ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
319     } else if ((effect && effect.constructor.name === 'Slide') &&
320                (metadata.thumbnail && metadata.thumbnail.url)) {
321       // Only show thumbnails if there is no effect or the effect is Slide.
322       // Also no thumbnail if the image is too large to be loaded.
323       var thumbnailLoader = new ThumbnailLoader(
324           entry,
325           ThumbnailLoader.LoaderType.CANVAS,
326           metadata);
327       thumbnailLoader.loadDetachedImage(function(success) {
328         displayThumbnail(ImageView.LOAD_TYPE_IMAGE_FILE,
329                          success ? thumbnailLoader.getImage() : null);
330       });
331     } else {
332       loadMainImage(ImageView.LOAD_TYPE_IMAGE_FILE, entry,
333           false /* no preview*/, 0 /* delay */);
334     }
335   }
337   function displayThumbnail(loadType, canvas) {
338     if (canvas) {
339       var width = null;
340       var height = null;
341       if (metadata.media) {
342         width = metadata.media.width;
343         height = metadata.media.height;
344       }
345       // If metadata.drive.present is true, the image data is loaded directly
346       // from local cache, whose size may be out of sync with the drive
347       // metadata.
348       if (metadata.drive && !metadata.drive.present) {
349         width = metadata.drive.imageWidth;
350         height = metadata.drive.imageHeight;
351       }
352       self.replace(
353           canvas,
354           effect,
355           width,
356           height,
357           true /* preview */);
358       if (displayCallback) displayCallback();
359     }
360     loadMainImage(loadType, entry, !!canvas,
361         (effect && canvas) ? effect.getSafeInterval() : 0);
362   }
364   function loadMainImage(loadType, contentEntry, previewShown, delay) {
365     if (self.prefetchLoader_.isLoading(contentEntry)) {
366       // The image we need is already being prefetched. Initiating another load
367       // would be a waste. Hijack the load instead by overriding the callback.
368       self.prefetchLoader_.setCallback(
369           displayMainImage.bind(null, loadType, previewShown));
371       // Swap the loaders so that the self.isLoading works correctly.
372       var temp = self.prefetchLoader_;
373       self.prefetchLoader_ = self.imageLoader_;
374       self.imageLoader_ = temp;
375       return;
376     }
377     self.prefetchLoader_.cancel();  // The prefetch was doing something useless.
379     self.imageLoader_.load(
380         item,
381         displayMainImage.bind(null, loadType, previewShown),
382         delay);
383   }
385   function displayMainImage(loadType, previewShown, content, opt_error) {
386     if (opt_error)
387       loadType = ImageView.LOAD_TYPE_ERROR;
389     // If we already displayed the preview we should not replace the content if
390     // the full content failed to load.
391     var animationDuration = 0;
392     if (!(previewShown && loadType === ImageView.LOAD_TYPE_ERROR)) {
393       var replaceEffect = previewShown ? null : effect;
394       animationDuration = replaceEffect ? replaceEffect.getSafeInterval() : 0;
395       self.replace(content, replaceEffect);
396       if (!previewShown && displayCallback) displayCallback();
397     }
399     if (loadType !== ImageView.LOAD_TYPE_ERROR &&
400         loadType !== ImageView.LOAD_TYPE_CACHED_SCREEN) {
401       ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
402     }
403     ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('LoadMode'),
404         loadType, ImageView.LOAD_TYPE_TOTAL);
406     if (loadType === ImageView.LOAD_TYPE_ERROR &&
407         !navigator.onLine && metadata.streaming) {
408       // |streaming| is set only when the file is not locally cached.
409       loadType = ImageView.LOAD_TYPE_OFFLINE;
410     }
411     if (loadCallback) loadCallback(loadType, animationDuration, opt_error);
412   }
416  * Prefetches an image.
417  * @param {Gallery.Item} item The image item.
418  * @param {number} delay Image load delay in ms.
419  */
420 ImageView.prototype.prefetch = function(item, delay) {
421   var self = this;
422   var entry = item.getEntry();
423   function prefetchDone(canvas) {
424     if (canvas.width)
425       self.contentCache_.putItem(entry, canvas);
426   }
428   var cached = this.contentCache_.getItem(entry);
429   if (cached) {
430     prefetchDone(cached);
431   } else if (FileType.getMediaType(entry) === 'image') {
432     // Evict the LRU item before we allocate the new canvas to avoid unneeded
433     // strain on memory.
434     this.contentCache_.evictLRU();
436     this.prefetchLoader_.load(item, prefetchDone, delay);
437   }
441  * Renames the current image.
442  * @param {FileEntry} newEntry The new image Entry.
443  */
444 ImageView.prototype.changeEntry = function(newEntry) {
445   this.contentCache_.renameItem(this.contentEntry_, newEntry);
446   this.screenCache_.renameItem(this.contentEntry_, newEntry);
447   this.contentEntry_ = newEntry;
451  * Unloads content.
452  * @param {Rect} zoomToRect Target rectangle for zoom-out-effect.
453  */
454 ImageView.prototype.unload = function(zoomToRect) {
455   if (this.unloadTimer_) {
456     clearTimeout(this.unloadTimer_);
457     this.unloadTimer_ = null;
458   }
459   if (zoomToRect && this.screenImage_) {
460     var effect = this.createZoomEffect(zoomToRect);
461     this.setTransform(this.screenImage_, effect);
462     this.screenImage_.setAttribute('fade', true);
463     this.unloadTimer_ = setTimeout(function() {
464         this.unloadTimer_ = null;
465         this.unload(null /* force unload */);
466       }.bind(this),
467       effect.getSafeInterval());
468     return;
469   }
470   this.container_.textContent = '';
471   this.contentCanvas_ = null;
472   this.screenImage_ = null;
476  * @param {HTMLCanvasElement} content The image element.
477  * @param {number=} opt_width Image width.
478  * @param {number=} opt_height Image height.
479  * @param {boolean=} opt_preview True if the image is a preview (not full res).
480  * @private
481  */
482 ImageView.prototype.replaceContent_ = function(
483     content, opt_width, opt_height, opt_preview) {
485   if (this.contentCanvas_ && this.contentCanvas_.parentNode === this.container_)
486     this.container_.removeChild(this.contentCanvas_);
488   this.screenImage_ = this.document_.createElement('canvas');
489   this.screenImage_.className = 'image';
491   this.contentCanvas_ = content;
492   this.invalidateCaches();
493   this.viewport_.setImageSize(
494       opt_width || this.contentCanvas_.width,
495       opt_height || this.contentCanvas_.height);
496   this.viewport_.fitImage();
497   this.viewport_.update();
498   this.draw();
500   this.container_.appendChild(this.screenImage_);
502   this.preview_ = opt_preview;
503   // If this is not a thumbnail, cache the content and the screen-scale image.
504   if (this.hasValidImage()) {
505     // Insert the full resolution canvas into DOM so that it can be printed.
506     this.container_.appendChild(this.contentCanvas_);
507     this.contentCanvas_.classList.add('fullres');
509     this.contentCache_.putItem(this.contentEntry_, this.contentCanvas_, true);
510     this.screenCache_.putItem(this.contentEntry_, this.screenImage_);
512     // TODO(kaznacheev): It is better to pass screenImage_ as it is usually
513     // much smaller than contentCanvas_ and still contains the entire image.
514     // Once we implement zoom/pan we should pass contentCanvas_ instead.
515     this.updateThumbnail_(this.screenImage_);
517     this.contentRevision_++;
518     for (var i = 0; i !== this.contentCallbacks_.length; i++) {
519       try {
520         this.contentCallbacks_[i]();
521       } catch (e) {
522         console.error(e);
523       }
524     }
525   }
529  * Adds a listener for content changes.
530  * @param {function} callback Callback.
531  */
532 ImageView.prototype.addContentCallback = function(callback) {
533   this.contentCallbacks_.push(callback);
537  * Updates the cached thumbnail image.
539  * @param {HTMLCanvasElement} canvas The source canvas.
540  * @private
541  */
542 ImageView.prototype.updateThumbnail_ = function(canvas) {
543   ImageUtil.trace.resetTimer('thumb');
544   var pixelCount = 10000;
545   var downScale =
546       Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount));
548   this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas');
549   this.thumbnailCanvas_.width = Math.round(canvas.width / downScale);
550   this.thumbnailCanvas_.height = Math.round(canvas.height / downScale);
551   Rect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas);
552   ImageUtil.trace.reportTimer('thumb');
556  * Replaces the displayed image, possibly with slide-in animation.
558  * @param {HTMLCanvasElement} content The image element.
559  * @param {Object=} opt_effect Transition effect object.
560  * @param {number=} opt_width Image width.
561  * @param {number=} opt_height Image height.
562  * @param {boolean=} opt_preview True if the image is a preview (not full res).
563  */
564 ImageView.prototype.replace = function(
565     content, opt_effect, opt_width, opt_height, opt_preview) {
566   var oldScreenImage = this.screenImage_;
568   this.replaceContent_(content, opt_width, opt_height, opt_preview);
569   if (!opt_effect) {
570     if (oldScreenImage)
571       oldScreenImage.parentNode.removeChild(oldScreenImage);
572     return;
573   }
575   var newScreenImage = this.screenImage_;
577   if (oldScreenImage)
578     ImageUtil.setAttribute(newScreenImage, 'fade', true);
579   this.setTransform(newScreenImage, opt_effect, 0 /* instant */);
581   setTimeout(function() {
582     this.setTransform(newScreenImage, null,
583         opt_effect && opt_effect.getDuration());
584     if (oldScreenImage) {
585       ImageUtil.setAttribute(newScreenImage, 'fade', false);
586       ImageUtil.setAttribute(oldScreenImage, 'fade', true);
587       console.assert(opt_effect.getReverse, 'Cannot revert an effect.');
588       var reverse = opt_effect.getReverse();
589       this.setTransform(oldScreenImage, reverse);
590       setTimeout(function() {
591         if (oldScreenImage.parentNode)
592           oldScreenImage.parentNode.removeChild(oldScreenImage);
593       }, reverse.getSafeInterval());
594     }
595   }.bind(this), 0);
599  * @param {HTMLCanvasElement} element The element to transform.
600  * @param {ImageView.Effect=} opt_effect The effect to apply.
601  * @param {number=} opt_duration Transition duration.
602  */
603 ImageView.prototype.setTransform = function(element, opt_effect, opt_duration) {
604   if (!opt_effect)
605     opt_effect = new ImageView.Effect.None();
606   if (typeof opt_duration !== 'number')
607     opt_duration = opt_effect.getDuration();
608   element.style.webkitTransitionDuration = opt_duration + 'ms';
609   element.style.webkitTransitionTimingFunction = opt_effect.getTiming();
610   element.style.webkitTransform = opt_effect.transform(element, this.viewport_);
614  * @param {Rect} screenRect Target rectangle in screen coordinates.
615  * @return {ImageView.Effect.Zoom} Zoom effect object.
616  */
617 ImageView.prototype.createZoomEffect = function(screenRect) {
618   return new ImageView.Effect.ZoomToScreen(
619       screenRect,
620       ImageView.MODE_TRANSITION_DURATION);
624  * Visualizes crop or rotate operation. Hide the old image instantly, animate
625  * the new image to visualize the operation.
627  * @param {HTMLCanvasElement} canvas New content canvas.
628  * @param {Rect} imageCropRect The crop rectangle in image coordinates.
629  *                             Null for rotation operations.
630  * @param {number} rotate90 Rotation angle in 90 degree increments.
631  * @return {number} Animation duration.
632  */
633 ImageView.prototype.replaceAndAnimate = function(
634     canvas, imageCropRect, rotate90) {
635   var oldImageBounds = {
636     width: this.viewport_.getImageBounds().width,
637     height: this.viewport_.getImageBounds().height
638   };
639   var oldScreenImage = this.screenImage_;
640   this.replaceContent_(canvas);
641   var newScreenImage = this.screenImage_;
642   var effect = rotate90 ?
643       new ImageView.Effect.Rotate(rotate90 > 0) :
644       new ImageView.Effect.Zoom(
645           oldImageBounds.width, oldImageBounds.height, imageCropRect);
647   this.setTransform(newScreenImage, effect, 0 /* instant */);
649   oldScreenImage.parentNode.appendChild(newScreenImage);
650   oldScreenImage.parentNode.removeChild(oldScreenImage);
652   // Let the layout fire, then animate back to non-transformed state.
653   setTimeout(
654       this.setTransform.bind(
655           this, newScreenImage, null, effect.getDuration()),
656       0);
658   return effect.getSafeInterval();
662  * Visualizes "undo crop". Shrink the current image to the given crop rectangle
663  * while fading in the new image.
665  * @param {HTMLCanvasElement} canvas New content canvas.
666  * @param {Rect} imageCropRect The crop rectangle in image coordinates.
667  * @return {number} Animation duration.
668  */
669 ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) {
670   var oldScreenImage = this.screenImage_;
671   this.replaceContent_(canvas);
672   var newScreenImage = this.screenImage_;
673   var setFade = ImageUtil.setAttribute.bind(null, newScreenImage, 'fade');
674   setFade(true);
675   oldScreenImage.parentNode.insertBefore(newScreenImage, oldScreenImage);
676   var effect = new ImageView.Effect.Zoom(
677       this.viewport_.getImageBounds().width,
678       this.viewport_.getImageBounds().height,
679       imageCropRect);
681   // Animate to the transformed state.
682   this.setTransform(oldScreenImage, effect);
683   setTimeout(setFade.bind(null, false), 0);
684   setTimeout(function() {
685     if (oldScreenImage.parentNode)
686       oldScreenImage.parentNode.removeChild(oldScreenImage);
687   }, effect.getSafeInterval());
689   return effect.getSafeInterval();
693  * Generic cache with a limited capacity and LRU eviction.
694  * @param {number} capacity Maximum number of cached item.
695  * @constructor
696  */
697 ImageView.Cache = function(capacity) {
698   this.capacity_ = capacity;
699   this.map_ = {};
700   this.order_ = [];
704  * Fetches the item from the cache.
705  * @param {FileEntry} entry The entry.
706  * @return {Object} The cached item.
707  */
708 ImageView.Cache.prototype.getItem = function(entry) {
709   return this.map_[entry.toURL()];
713  * Puts the item into the cache.
715  * @param {FileEntry} entry The entry.
716  * @param {Object} item The item object.
717  * @param {boolean=} opt_keepLRU True if the LRU order should not be modified.
718  */
719 ImageView.Cache.prototype.putItem = function(entry, item, opt_keepLRU) {
720   var pos = this.order_.indexOf(entry.toURL());
722   if ((pos >= 0) !== (entry.toURL() in this.map_))
723     throw new Error('Inconsistent cache state');
725   if (entry.toURL() in this.map_) {
726     if (!opt_keepLRU) {
727       // Move to the end (most recently used).
728       this.order_.splice(pos, 1);
729       this.order_.push(entry.toURL());
730     }
731   } else {
732     this.evictLRU();
733     this.order_.push(entry.toURL());
734   }
736   if ((pos >= 0) && (item !== this.map_[entry.toURL()]))
737     this.deleteItem_(this.map_[entry.toURL()]);
738   this.map_[entry.toURL()] = item;
740   if (this.order_.length > this.capacity_)
741     throw new Error('Exceeded cache capacity');
745  * Evicts the least recently used items.
746  */
747 ImageView.Cache.prototype.evictLRU = function() {
748   if (this.order_.length === this.capacity_) {
749     var url = this.order_.shift();
750     this.deleteItem_(this.map_[url]);
751     delete this.map_[url];
752   }
756  * Changes the Entry.
757  * @param {FileEntry} oldEntry The old Entry.
758  * @param {FileEntry} newEntry The new Entry.
759  */
760 ImageView.Cache.prototype.renameItem = function(oldEntry, newEntry) {
761   if (util.isSameEntry(oldEntry, newEntry))
762     return;  // No need to rename.
764   var pos = this.order_.indexOf(oldEntry.toURL());
765   if (pos < 0)
766     return;  // Not cached.
768   this.order_[pos] = newEntry.toURL();
769   this.map_[newEntry.toURL()] = this.map_[oldEntry.toURL()];
770   delete this.map_[oldEntry.toURL()];
774  * Disposes an object.
776  * @param {Object} item The item object.
777  * @private
778  */
779 ImageView.Cache.prototype.deleteItem_ = function(item) {
780   // Trick to reduce memory usage without waiting for gc.
781   if (item instanceof HTMLCanvasElement) {
782     // If the canvas is being used somewhere else (eg. displayed on the screen),
783     // it will be cleared.
784     item.width = 0;
785     item.height = 0;
786   }
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  */
798 ImageView.Effect = function(duration, opt_timing) {
799   this.duration_ = duration;
800   this.timing_ = opt_timing || 'linear';
805  */
806 ImageView.Effect.DEFAULT_DURATION = 180;
810  */
811 ImageView.Effect.MARGIN = 100;
814  * @return {number} Effect duration in ms.
815  */
816 ImageView.Effect.prototype.getDuration = function() { return this.duration_; };
819  * @return {number} Delay in ms since the beginning of the animation after which
820  * it is safe to perform CPU-heavy operations without disrupting the animation.
821  */
822 ImageView.Effect.prototype.getSafeInterval = function() {
823   return this.getDuration() + ImageView.Effect.MARGIN;
827  * @return {string} CSS transition timing function name.
828  */
829 ImageView.Effect.prototype.getTiming = function() { return this.timing_; };
832  * Obtains the CSS transformation string of the effect.
833  * @param {DOMCanvas} element Canvas element to be applied the transforamtion.
834  * @param {Viewport} viewport Current viewport.
835  * @return CSS transformation description.
836  */
837 ImageView.Effect.prototype.transform = function(element, viewport) {
838   throw new Error('Not implemented.');
842  * Default effect. It is not a no-op as it needs to adjust a canvas scale
843  * for devicePixelRatio.
845  * @constructor
846  * @extends {ImageView.Effect}
847  */
848 ImageView.Effect.None = function() {
849   ImageView.Effect.call(this, 0);
853  * Inherits from ImageView.Effect.
854  */
855 ImageView.Effect.None.prototype = { __proto__: ImageView.Effect.prototype };
858  * @param {HTMLCanvasElement} element Element.
859  * @param {Viewport} viewport Current viewport.
860  * @return {string} Transform string.
861  */
862 ImageView.Effect.None.prototype.transform = function(element, viewport) {
863   return viewport.getTransformation();
867  * Slide effect.
869  * @param {number} direction -1 for left, 1 for right.
870  * @param {boolean=} opt_slow True if slow (as in slideshow).
871  * @constructor
872  * @extends {ImageView.Effect}
873  */
874 ImageView.Effect.Slide = function Slide(direction, opt_slow) {
875   ImageView.Effect.call(this,
876       opt_slow ? 800 : ImageView.Effect.DEFAULT_DURATION, 'ease-in-out');
877   this.direction_ = direction;
878   this.slow_ = opt_slow;
879   this.shift_ = opt_slow ? 100 : 40;
880   if (this.direction_ < 0) this.shift_ = -this.shift_;
883 ImageView.Effect.Slide.prototype = { __proto__: ImageView.Effect.prototype };
886  * Reverses the slide effect.
887  * @return {ImageView.Effect.Slide} Reversed effect.
888  */
889 ImageView.Effect.Slide.prototype.getReverse = function() {
890   return new ImageView.Effect.Slide(-this.direction_, this.slow_);
894  * @override
895  */
896 ImageView.Effect.Slide.prototype.transform = function(element, viewport) {
897   return viewport.getShiftTransformation(this.shift_);
901  * Zoom effect.
903  * Animates the original rectangle to the target rectangle. Both parameters
904  * should be given in device coordinates (accounting for devicePixelRatio).
906  * @param {number} previousImageWidth Width of the full resolution image.
907  * @param {number} previousImageHeight Hieght of the full resolution image.
908  * @param {Rect} imageCropRect Crop rectangle in the full resolution image.
909  * @param {number=} opt_duration Duration of the effect.
910  * @constructor
911  * @extends {ImageView.Effect}
912  */
913 ImageView.Effect.Zoom = function(
914     previousImageWidth, previousImageHeight, imageCropRect, opt_duration) {
915   ImageView.Effect.call(this,
916       opt_duration || ImageView.Effect.DEFAULT_DURATION);
917   this.previousImageWidth_ = previousImageWidth;
918   this.previousImageHeight_ = previousImageHeight;
919   this.imageCropRect_ = imageCropRect;
922 ImageView.Effect.Zoom.prototype = { __proto__: ImageView.Effect.prototype };
925  * @override
926  */
927 ImageView.Effect.Zoom.prototype.transform = function(element, viewport) {
928   return viewport.getInverseTransformForCroppedImage(
929       this.previousImageWidth_, this.previousImageHeight_, this.imageCropRect_);
933  * Effect to zoom to a screen rectangle.
935  * @param {Rect} screenRect Rectangle in the application window's coordinate.
936  * @param {number=} opt_duration Duration of effect.
937  * @constructor
938  * @extends {ImageView.Effect}
939  */
940 ImageView.Effect.ZoomToScreen = function(screenRect, opt_duration) {
941   ImageView.Effect.call(this, opt_duration);
942   this.screenRect_ = screenRect;
945 ImageView.Effect.ZoomToScreen.prototype = {
946   __proto__: ImageView.Effect.prototype
950  * @override
951  */
952 ImageView.Effect.ZoomToScreen.prototype.transform = function(
953     element, viewport) {
954   return viewport.getScreenRectTransformForImage(this.screenRect_);
958  * Rotation effect.
960  * @param {boolean} orientation Orientation of rotation. True is for clockwise
961  *     and false is for counterclockwise.
962  * @constructor
963  * @extends {ImageView.Effect}
964  */
965 ImageView.Effect.Rotate = function(orientation) {
966   ImageView.Effect.call(this, ImageView.Effect.DEFAULT_DURATION);
967   this.orientation_ = orientation;
970 ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype };
973  * @override
974  */
975 ImageView.Effect.Rotate.prototype.transform = function(element, viewport) {
976   return viewport.getInverseTransformForRotatedImage(this.orientation_);