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.
8 * The overlay displaying the image.
10 * @param {HTMLElement} container The container element.
11 * @param {Viewport} viewport The viewport.
12 * @param {MetadataCache} metadataCache The metadataCache.
15 function ImageView(container, viewport, metadataCache) {
16 this.container_ = container;
17 this.viewport_ = viewport;
18 this.document_ = container.ownerDocument;
19 this.contentGeneration_ = 0;
20 this.displayedContentGeneration_ = 0;
21 this.displayedViewportGeneration_ = 0;
23 this.imageLoader_ = new ImageUtil.ImageLoader(this.document_, metadataCache);
24 // We have a separate image loader for prefetch which does not get cancelled
25 // when the selection changes.
26 this.prefetchLoader_ = new ImageUtil.ImageLoader(
27 this.document_, metadataCache);
29 // The content cache is used for prefetching the next image when going
30 // through the images sequentially. The real life photos can be large
31 // (18Mpix = 72Mb pixel array) so we want only the minimum amount of caching.
32 this.contentCache_ = new ImageView.Cache(2);
34 // We reuse previously generated screen-scale images so that going back to
35 // a recently loaded image looks instant even if the image is not in
36 // the content cache any more. Screen-scale images are small (~1Mpix)
37 // so we can afford to cache more of them.
38 this.screenCache_ = new ImageView.Cache(5);
39 this.contentCallbacks_ = [];
42 * The element displaying the current content.
44 * @type {HTMLCanvasElement|HTMLVideoElement}
47 this.screenImage_ = null;
49 this.localImageTransformFetcher_ = function(entry, callback) {
50 metadataCache.getOne(entry, 'fetchedMedia', function(fetchedMedia) {
51 callback(fetchedMedia.imageTransform);
57 * Duration of transition between modes in ms.
59 ImageView.MODE_TRANSITION_DURATION = 350;
62 * If the user flips though images faster than this interval we do not apply
63 * the slide-in/slide-out transition.
65 ImageView.FAST_SCROLL_INTERVAL = 300;
68 * Image load type: full resolution image loaded from cache.
70 ImageView.LOAD_TYPE_CACHED_FULL = 0;
73 * Image load type: screen resolution preview loaded from cache.
75 ImageView.LOAD_TYPE_CACHED_SCREEN = 1;
78 * Image load type: image read from file.
80 ImageView.LOAD_TYPE_IMAGE_FILE = 2;
83 * Image load type: video loaded.
85 ImageView.LOAD_TYPE_VIDEO_FILE = 3;
88 * Image load type: error occurred.
90 ImageView.LOAD_TYPE_ERROR = 4;
93 * Image load type: the file contents is not available offline.
95 ImageView.LOAD_TYPE_OFFLINE = 5;
98 * The total number of load types.
100 ImageView.LOAD_TYPE_TOTAL = 6;
102 ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype};
105 * Draws below overlays with the default zIndex.
106 * @return {number} Z-index.
108 ImageView.prototype.getZIndex = function() { return -1 };
111 * Draws the image on screen.
113 ImageView.prototype.draw = function() {
114 if (!this.contentCanvas_) // Do nothing if the image content is not set.
117 var forceRepaint = false;
119 if (this.displayedViewportGeneration_ !==
120 this.viewport_.getCacheGeneration()) {
121 this.displayedViewportGeneration_ = this.viewport_.getCacheGeneration();
123 this.setupDeviceBuffer(this.screenImage_);
129 this.displayedContentGeneration_ !== this.contentGeneration_) {
130 this.displayedContentGeneration_ = this.contentGeneration_;
132 ImageUtil.trace.resetTimer('paint');
133 this.paintDeviceRect(this.viewport_.getDeviceClipped(),
134 this.contentCanvas_, this.viewport_.getImageClipped());
135 ImageUtil.trace.reportTimer('paint');
140 * @param {number} x X pointer position.
141 * @param {number} y Y pointer position.
142 * @param {boolean} mouseDown True if mouse is down.
143 * @return {string} CSS cursor style.
145 ImageView.prototype.getCursorStyle = function(x, y, mouseDown) {
146 // Indicate that the image is draggable.
147 if (this.viewport_.isClipped() &&
148 this.viewport_.getScreenClipped().inside(x, y))
155 * @param {number} x X pointer position.
156 * @param {number} y Y pointer position.
157 * @return {function} The closure to call on drag.
159 ImageView.prototype.getDragHandler = function(x, y) {
160 var cursor = this.getCursorStyle(x, y);
161 if (cursor === 'move') {
162 // Return the handler that drags the entire image.
163 return this.viewport_.createOffsetSetter(x, y);
170 * @return {number} The cache generation.
172 ImageView.prototype.getCacheGeneration = function() {
173 return this.contentGeneration_;
177 * Invalidates the caches to force redrawing the screen canvas.
179 ImageView.prototype.invalidateCaches = function() {
180 this.contentGeneration_++;
184 * @return {HTMLCanvasElement} The content canvas element.
186 ImageView.prototype.getCanvas = function() { return this.contentCanvas_ };
189 * @return {boolean} True if the a valid image is currently loaded.
191 ImageView.prototype.hasValidImage = function() {
192 return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width;
196 * @return {HTMLVideoElement} The video element.
198 ImageView.prototype.getVideo = function() { return this.videoElement_ };
201 * @return {HTMLCanvasElement} The cached thumbnail image.
203 ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_ };
206 * @return {number} The content revision number.
208 ImageView.prototype.getContentRevision = function() {
209 return this.contentRevision_;
213 * Copies an image fragment from a full resolution canvas to a device resolution
216 * @param {Rect} deviceRect Rectangle in the device coordinates.
217 * @param {HTMLCanvasElement} canvas Full resolution canvas.
218 * @param {Rect} imageRect Rectangle in the full resolution canvas.
220 ImageView.prototype.paintDeviceRect = function(deviceRect, canvas, imageRect) {
221 // Map screen canvas (0,0) to (deviceBounds.left, deviceBounds.top)
222 var deviceBounds = this.viewport_.getDeviceClipped();
223 deviceRect = deviceRect.shift(-deviceBounds.left, -deviceBounds.top);
225 // The source canvas may have different physical size than the image size
226 // set at the viewport. Adjust imageRect accordingly.
227 var bounds = this.viewport_.getImageBounds();
228 var scaleX = canvas.width / bounds.width;
229 var scaleY = canvas.height / bounds.height;
230 imageRect = new Rect(imageRect.left * scaleX, imageRect.top * scaleY,
231 imageRect.width * scaleX, imageRect.height * scaleY);
233 this.screenImage_.getContext('2d'), canvas, deviceRect, imageRect);
237 * Creates an overlay canvas with properties similar to the screen canvas.
238 * Useful for showing quick feedback when editing.
240 * @return {HTMLCanvasElement} Overlay canvas.
242 ImageView.prototype.createOverlayCanvas = function() {
243 var canvas = this.document_.createElement('canvas');
244 canvas.className = 'image';
245 this.container_.appendChild(canvas);
250 * Sets up the canvas as a buffer in the device resolution.
252 * @param {HTMLCanvasElement} canvas The buffer canvas.
254 ImageView.prototype.setupDeviceBuffer = function(canvas) {
255 var deviceRect = this.viewport_.getDeviceClipped();
257 // Set the canvas position and size in device pixels.
258 if (canvas.width !== deviceRect.width)
259 canvas.width = deviceRect.width;
261 if (canvas.height !== deviceRect.height)
262 canvas.height = deviceRect.height;
264 canvas.style.left = deviceRect.left + 'px';
265 canvas.style.top = deviceRect.top + 'px';
267 // Scale the canvas down to screen pixels.
268 this.setTransform(canvas);
272 * @return {ImageData} A new ImageData object with a copy of the content.
274 ImageView.prototype.copyScreenImageData = function() {
275 return this.screenImage_.getContext('2d').getImageData(
276 0, 0, this.screenImage_.width, this.screenImage_.height);
280 * @return {boolean} True if the image is currently being loaded.
282 ImageView.prototype.isLoading = function() {
283 return this.imageLoader_.isBusy();
287 * Cancels the current image loading operation. The callbacks will be ignored.
289 ImageView.prototype.cancelLoad = function() {
290 this.imageLoader_.cancel();
294 * Loads and display a new image.
296 * Loads the thumbnail first, then replaces it with the main image.
297 * Takes into account the image orientation encoded in the metadata.
299 * @param {FileEntry} entry Image entry.
300 * @param {Object} metadata Metadata.
301 * @param {Object} effect Transition effect object.
302 * @param {function(number} displayCallback Called when the image is displayed
303 * (possibly as a prevew).
304 * @param {function(number} loadCallback Called when the image is fully loaded.
305 * The parameter is the load type.
307 ImageView.prototype.load = function(entry, metadata, effect,
308 displayCallback, loadCallback) {
310 // Skip effects when reloading repeatedly very quickly.
311 var time = Date.now();
312 if (this.lastLoadTime_ &&
313 (time - this.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) {
316 this.lastLoadTime_ = time;
319 metadata = metadata || {};
321 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime'));
325 this.contentEntry_ = entry;
326 this.contentRevision_ = -1;
328 var loadingVideo = FileType.getMediaType(entry) === 'video';
330 var video = this.document_.createElement('video');
331 var videoPreview = !!(metadata.thumbnail && metadata.thumbnail.url);
333 var thumbnailLoader = new ThumbnailLoader(
335 ThumbnailLoader.LoaderType.CANVAS,
337 thumbnailLoader.loadDetachedImage(function(success) {
339 var canvas = thumbnailLoader.getImage();
340 video.setAttribute('poster', canvas.toDataURL('image/jpeg'));
341 this.replace(video, effect); // Show the poster immediately.
342 if (displayCallback) displayCallback();
347 var onVideoLoad = function(error) {
348 video.removeEventListener('loadedmetadata', onVideoLoadSuccess);
349 video.removeEventListener('error', onVideoLoadError);
350 displayMainImage(ImageView.LOAD_TYPE_VIDEO_FILE, videoPreview, video,
353 var onVideoLoadError = onVideoLoad.bind(this, 'GALLERY_VIDEO_ERROR');
354 var onVideoLoadSuccess = onVideoLoad.bind(this, null);
356 video.addEventListener('loadedmetadata', onVideoLoadSuccess);
357 video.addEventListener('error', onVideoLoadError);
359 video.src = entry.toURL();
364 // Cache has to be evicted in advance, so the returned cached image is not
365 // evicted later by the prefetched image.
366 this.contentCache_.evictLRU();
368 var cached = this.contentCache_.getItem(this.contentEntry_);
370 displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL,
371 false /* no preview */, cached);
373 var cachedScreen = this.screenCache_.getItem(this.contentEntry_);
374 var imageWidth = metadata.media && metadata.media.width ||
375 metadata.drive && metadata.drive.imageWidth;
376 var imageHeight = metadata.media && metadata.media.height ||
377 metadata.drive && metadata.drive.imageHeight;
379 // We have a cached screen-scale canvas, use it instead of a thumbnail.
380 displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, cachedScreen);
381 // As far as the user can tell the image is loaded. We still need to load
382 // the full res image to make editing possible, but we can report now.
383 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
384 } else if ((effect && effect.constructor.name === 'Slide') &&
385 (metadata.thumbnail && metadata.thumbnail.url)) {
386 // Only show thumbnails if there is no effect or the effect is Slide.
387 // Also no thumbnail if the image is too large to be loaded.
388 var thumbnailLoader = new ThumbnailLoader(
390 ThumbnailLoader.LoaderType.CANVAS,
392 thumbnailLoader.loadDetachedImage(function(success) {
393 displayThumbnail(ImageView.LOAD_TYPE_IMAGE_FILE,
394 success ? thumbnailLoader.getImage() : null);
397 loadMainImage(ImageView.LOAD_TYPE_IMAGE_FILE, entry,
398 false /* no preview*/, 0 /* delay */);
402 function displayThumbnail(loadType, canvas) {
406 if (metadata.media) {
407 width = metadata.media.width;
408 height = metadata.media.height;
410 // If metadata.drive.present is true, the image data is loaded directly
411 // from local cache, whose size may be out of sync with the drive
413 if (metadata.drive && !metadata.drive.present) {
414 width = metadata.drive.imageWidth;
415 height = metadata.drive.imageHeight;
423 if (displayCallback) displayCallback();
425 loadMainImage(loadType, entry, !!canvas,
426 (effect && canvas) ? effect.getSafeInterval() : 0);
429 function loadMainImage(loadType, contentEntry, previewShown, delay) {
430 if (self.prefetchLoader_.isLoading(contentEntry)) {
431 // The image we need is already being prefetched. Initiating another load
432 // would be a waste. Hijack the load instead by overriding the callback.
433 self.prefetchLoader_.setCallback(
434 displayMainImage.bind(null, loadType, previewShown));
436 // Swap the loaders so that the self.isLoading works correctly.
437 var temp = self.prefetchLoader_;
438 self.prefetchLoader_ = self.imageLoader_;
439 self.imageLoader_ = temp;
442 self.prefetchLoader_.cancel(); // The prefetch was doing something useless.
444 self.imageLoader_.load(
446 self.localImageTransformFetcher_,
447 displayMainImage.bind(null, loadType, previewShown),
451 function displayMainImage(loadType, previewShown, content, opt_error) {
453 loadType = ImageView.LOAD_TYPE_ERROR;
455 // If we already displayed the preview we should not replace the content if:
456 // 1. The full content failed to load.
458 // 2. We are loading a video (because the full video is displayed in the
459 // same HTML element as the preview).
460 var animationDuration = 0;
461 if (!(previewShown &&
462 (loadType === ImageView.LOAD_TYPE_ERROR ||
463 loadType === ImageView.LOAD_TYPE_VIDEO_FILE))) {
464 var replaceEffect = previewShown ? null : effect;
465 animationDuration = replaceEffect ? replaceEffect.getSafeInterval() : 0;
466 self.replace(content, replaceEffect);
467 if (!previewShown && displayCallback) displayCallback();
470 if (loadType !== ImageView.LOAD_TYPE_ERROR &&
471 loadType !== ImageView.LOAD_TYPE_CACHED_SCREEN) {
472 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
474 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('LoadMode'),
475 loadType, ImageView.LOAD_TYPE_TOTAL);
477 if (loadType === ImageView.LOAD_TYPE_ERROR &&
478 !navigator.onLine && metadata.streaming) {
479 // |streaming| is set only when the file is not locally cached.
480 loadType = ImageView.LOAD_TYPE_OFFLINE;
482 if (loadCallback) loadCallback(loadType, animationDuration, opt_error);
487 * Prefetches an image.
488 * @param {FileEntry} entry The image entry.
489 * @param {number} delay Image load delay in ms.
491 ImageView.prototype.prefetch = function(entry, delay) {
493 function prefetchDone(canvas) {
495 self.contentCache_.putItem(entry, canvas);
498 var cached = this.contentCache_.getItem(entry);
500 prefetchDone(cached);
501 } else if (FileType.getMediaType(entry) === 'image') {
502 // Evict the LRU item before we allocate the new canvas to avoid unneeded
504 this.contentCache_.evictLRU();
506 this.prefetchLoader_.load(
508 this.localImageTransformFetcher_,
515 * Renames the current image.
516 * @param {FileEntry} newEntry The new image Entry.
518 ImageView.prototype.changeEntry = function(newEntry) {
519 this.contentCache_.renameItem(this.contentEntry_, newEntry);
520 this.screenCache_.renameItem(this.contentEntry_, newEntry);
521 this.contentEntry_ = newEntry;
526 * @param {Rect} zoomToRect Target rectangle for zoom-out-effect.
528 ImageView.prototype.unload = function(zoomToRect) {
529 if (this.unloadTimer_) {
530 clearTimeout(this.unloadTimer_);
531 this.unloadTimer_ = null;
533 if (zoomToRect && this.screenImage_) {
534 var effect = this.createZoomEffect(zoomToRect);
535 this.setTransform(this.screenImage_, effect);
536 this.screenImage_.setAttribute('fade', true);
537 this.unloadTimer_ = setTimeout(function() {
538 this.unloadTimer_ = null;
539 this.unload(null /* force unload */);
541 effect.getSafeInterval());
544 this.container_.textContent = '';
545 this.contentCanvas_ = null;
546 this.screenImage_ = null;
547 this.videoElement_ = null;
551 * @param {HTMLCanvasElement|HTMLVideoElement} content The image element.
552 * @param {number=} opt_width Image width.
553 * @param {number=} opt_height Image height.
554 * @param {boolean=} opt_preview True if the image is a preview (not full res).
557 ImageView.prototype.replaceContent_ = function(
558 content, opt_width, opt_height, opt_preview) {
560 if (this.contentCanvas_ && this.contentCanvas_.parentNode === this.container_)
561 this.container_.removeChild(this.contentCanvas_);
563 if (content.constructor.name === 'HTMLVideoElement') {
564 this.contentCanvas_ = null;
565 this.videoElement_ = content;
566 this.screenImage_ = content;
567 this.screenImage_.className = 'image';
568 this.container_.appendChild(this.screenImage_);
569 this.videoElement_.play();
573 this.screenImage_ = this.document_.createElement('canvas');
574 this.screenImage_.className = 'image';
576 this.videoElement_ = null;
577 this.contentCanvas_ = content;
578 this.invalidateCaches();
579 this.viewport_.setImageSize(
580 opt_width || this.contentCanvas_.width,
581 opt_height || this.contentCanvas_.height);
582 this.viewport_.fitImage();
583 this.viewport_.update();
586 this.container_.appendChild(this.screenImage_);
588 this.preview_ = opt_preview;
589 // If this is not a thumbnail, cache the content and the screen-scale image.
590 if (this.hasValidImage()) {
591 // Insert the full resolution canvas into DOM so that it can be printed.
592 this.container_.appendChild(this.contentCanvas_);
593 this.contentCanvas_.classList.add('fullres');
595 this.contentCache_.putItem(this.contentEntry_, this.contentCanvas_, true);
596 this.screenCache_.putItem(this.contentEntry_, this.screenImage_);
598 // TODO(kaznacheev): It is better to pass screenImage_ as it is usually
599 // much smaller than contentCanvas_ and still contains the entire image.
600 // Once we implement zoom/pan we should pass contentCanvas_ instead.
601 this.updateThumbnail_(this.screenImage_);
603 this.contentRevision_++;
604 for (var i = 0; i !== this.contentCallbacks_.length; i++) {
606 this.contentCallbacks_[i]();
615 * Adds a listener for content changes.
616 * @param {function} callback Callback.
618 ImageView.prototype.addContentCallback = function(callback) {
619 this.contentCallbacks_.push(callback);
623 * Updates the cached thumbnail image.
625 * @param {HTMLCanvasElement} canvas The source canvas.
628 ImageView.prototype.updateThumbnail_ = function(canvas) {
629 ImageUtil.trace.resetTimer('thumb');
630 var pixelCount = 10000;
632 Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount));
634 this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas');
635 this.thumbnailCanvas_.width = Math.round(canvas.width / downScale);
636 this.thumbnailCanvas_.height = Math.round(canvas.height / downScale);
637 Rect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas);
638 ImageUtil.trace.reportTimer('thumb');
642 * Replaces the displayed image, possibly with slide-in animation.
644 * @param {HTMLCanvasElement|HTMLVideoElement} content The image element.
645 * @param {Object=} opt_effect Transition effect object.
646 * @param {number=} opt_width Image width.
647 * @param {number=} opt_height Image height.
648 * @param {boolean=} opt_preview True if the image is a preview (not full res).
650 ImageView.prototype.replace = function(
651 content, opt_effect, opt_width, opt_height, opt_preview) {
652 var oldScreenImage = this.screenImage_;
654 this.replaceContent_(content, opt_width, opt_height, opt_preview);
657 oldScreenImage.parentNode.removeChild(oldScreenImage);
661 var newScreenImage = this.screenImage_;
664 ImageUtil.setAttribute(newScreenImage, 'fade', true);
665 this.setTransform(newScreenImage, opt_effect, 0 /* instant */);
667 setTimeout(function() {
668 this.setTransform(newScreenImage, null,
669 opt_effect && opt_effect.getDuration());
670 if (oldScreenImage) {
671 ImageUtil.setAttribute(newScreenImage, 'fade', false);
672 ImageUtil.setAttribute(oldScreenImage, 'fade', true);
673 console.assert(opt_effect.getReverse, 'Cannot revert an effect.');
674 var reverse = opt_effect.getReverse();
675 this.setTransform(oldScreenImage, reverse);
676 setTimeout(function() {
677 if (oldScreenImage.parentNode)
678 oldScreenImage.parentNode.removeChild(oldScreenImage);
679 }, reverse.getSafeInterval());
685 * @param {HTMLCanvasElement|HTMLVideoElement} element The element to transform.
686 * @param {ImageView.Effect=} opt_effect The effect to apply.
687 * @param {number=} opt_duration Transition duration.
689 ImageView.prototype.setTransform = function(element, opt_effect, opt_duration) {
691 opt_effect = new ImageView.Effect.None();
692 if (typeof opt_duration !== 'number')
693 opt_duration = opt_effect.getDuration();
694 element.style.webkitTransitionDuration = opt_duration + 'ms';
695 element.style.webkitTransitionTimingFunction = opt_effect.getTiming();
696 element.style.webkitTransform = opt_effect.transform(element, this.viewport_);
700 * @param {Rect} screenRect Target rectangle in screen coordinates.
701 * @return {ImageView.Effect.Zoom} Zoom effect object.
703 ImageView.prototype.createZoomEffect = function(screenRect) {
704 return new ImageView.Effect.Zoom(
705 this.viewport_.screenToDeviceRect(screenRect),
706 null /* use viewport */,
707 ImageView.MODE_TRANSITION_DURATION);
711 * Visualizes crop or rotate operation. Hide the old image instantly, animate
712 * the new image to visualize the operation.
714 * @param {HTMLCanvasElement} canvas New content canvas.
715 * @param {Rect} imageCropRect The crop rectangle in image coordinates.
716 * Null for rotation operations.
717 * @param {number} rotate90 Rotation angle in 90 degree increments.
718 * @return {number} Animation duration.
720 ImageView.prototype.replaceAndAnimate = function(
721 canvas, imageCropRect, rotate90) {
722 var oldScale = this.viewport_.getScale();
723 var deviceCropRect = imageCropRect && this.viewport_.screenToDeviceRect(
724 this.viewport_.imageToScreenRect(imageCropRect));
726 var oldScreenImage = this.screenImage_;
727 this.replaceContent_(canvas);
728 var newScreenImage = this.screenImage_;
730 // Display the new canvas, initially transformed.
731 var deviceFullRect = this.viewport_.getDeviceClipped();
733 var effect = rotate90 ?
734 new ImageView.Effect.Rotate(
735 oldScale / this.viewport_.getScale(), -rotate90) :
736 new ImageView.Effect.Zoom(deviceCropRect, deviceFullRect);
738 this.setTransform(newScreenImage, effect, 0 /* instant */);
740 oldScreenImage.parentNode.appendChild(newScreenImage);
741 oldScreenImage.parentNode.removeChild(oldScreenImage);
743 // Let the layout fire, then animate back to non-transformed state.
745 this.setTransform.bind(
746 this, newScreenImage, null, effect.getDuration()),
749 return effect.getSafeInterval();
753 * Visualizes "undo crop". Shrink the current image to the given crop rectangle
754 * while fading in the new image.
756 * @param {HTMLCanvasElement} canvas New content canvas.
757 * @param {Rect} imageCropRect The crop rectangle in image coordinates.
758 * @return {number} Animation duration.
760 ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) {
761 var deviceFullRect = this.viewport_.getDeviceClipped();
762 var oldScale = this.viewport_.getScale();
764 var oldScreenImage = this.screenImage_;
765 this.replaceContent_(canvas);
766 var newScreenImage = this.screenImage_;
768 var deviceCropRect = this.viewport_.screenToDeviceRect(
769 this.viewport_.imageToScreenRect(imageCropRect));
771 var setFade = ImageUtil.setAttribute.bind(null, newScreenImage, 'fade');
773 oldScreenImage.parentNode.insertBefore(newScreenImage, oldScreenImage);
775 var effect = new ImageView.Effect.Zoom(deviceCropRect, deviceFullRect);
776 // Animate to the transformed state.
777 this.setTransform(oldScreenImage, effect);
779 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();
791 * Generic cache with a limited capacity and LRU eviction.
792 * @param {number} capacity Maximum number of cached item.
795 ImageView.Cache = function(capacity) {
796 this.capacity_ = capacity;
802 * Fetches the item from the cache.
803 * @param {FileEntry} entry The entry.
804 * @return {Object} The cached item.
806 ImageView.Cache.prototype.getItem = function(entry) {
807 return this.map_[entry.toURL()];
811 * Puts the item into the cache.
813 * @param {FileEntry} entry The entry.
814 * @param {Object} item The item object.
815 * @param {boolean=} opt_keepLRU True if the LRU order should not be modified.
817 ImageView.Cache.prototype.putItem = function(entry, item, opt_keepLRU) {
818 var pos = this.order_.indexOf(entry.toURL());
820 if ((pos >= 0) !== (entry.toURL() in this.map_))
821 throw new Error('Inconsistent cache state');
823 if (entry.toURL() in this.map_) {
825 // Move to the end (most recently used).
826 this.order_.splice(pos, 1);
827 this.order_.push(entry.toURL());
831 this.order_.push(entry.toURL());
834 if ((pos >= 0) && (item !== this.map_[entry.toURL()]))
835 this.deleteItem_(this.map_[entry.toURL()]);
836 this.map_[entry.toURL()] = item;
838 if (this.order_.length > this.capacity_)
839 throw new Error('Exceeded cache capacity');
843 * Evicts the least recently used items.
845 ImageView.Cache.prototype.evictLRU = function() {
846 if (this.order_.length === this.capacity_) {
847 var url = this.order_.shift();
848 this.deleteItem_(this.map_[url]);
849 delete this.map_[url];
855 * @param {FileEntry} oldEntry The old Entry.
856 * @param {FileEntry} newEntry The new Entry.
858 ImageView.Cache.prototype.renameItem = function(oldEntry, newEntry) {
859 if (util.isSameEntry(oldEntry, newEntry))
860 return; // No need to rename.
862 var pos = this.order_.indexOf(oldEntry.toURL());
864 return; // Not cached.
866 this.order_[pos] = newEntry.toURL();
867 this.map_[newEntry.toURL()] = this.map_[oldEntry.toURL()];
868 delete this.map_[oldEntry.toURL()];
872 * Disposes an object.
874 * @param {Object} item The item object.
877 ImageView.Cache.prototype.deleteItem_ = function(item) {
878 // Trick to reduce memory usage without waiting for gc.
879 if (item instanceof HTMLCanvasElement) {
880 // If the canvas is being used somewhere else (eg. displayed on the screen),
881 // it will be cleared.
887 /* Transition effects */
890 * Base class for effects.
892 * @param {number} duration Duration in ms.
893 * @param {string=} opt_timing CSS transition timing function name.
896 ImageView.Effect = function(duration, opt_timing) {
897 this.duration_ = duration;
898 this.timing_ = opt_timing || 'linear';
904 ImageView.Effect.DEFAULT_DURATION = 180;
909 ImageView.Effect.MARGIN = 100;
912 * @return {number} Effect duration in ms.
914 ImageView.Effect.prototype.getDuration = function() { return this.duration_ };
917 * @return {number} Delay in ms since the beginning of the animation after which
918 * it is safe to perform CPU-heavy operations without disrupting the animation.
920 ImageView.Effect.prototype.getSafeInterval = function() {
921 return this.getDuration() + ImageView.Effect.MARGIN;
925 * @return {string} CSS transition timing function name.
927 ImageView.Effect.prototype.getTiming = function() { return this.timing_ };
930 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
931 * @return {number} Preferred pixel ration to use with this element.
934 ImageView.Effect.getPixelRatio_ = function(element) {
935 if (element.constructor.name === 'HTMLCanvasElement')
936 return Viewport.getDevicePixelRatio();
942 * Default effect. It is not a no-op as it needs to adjust a canvas scale
943 * for devicePixelRatio.
947 ImageView.Effect.None = function() {
948 ImageView.Effect.call(this, 0);
952 * Inherits from ImageView.Effect.
954 ImageView.Effect.None.prototype = { __proto__: ImageView.Effect.prototype };
957 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
958 * @return {string} Transform string.
960 ImageView.Effect.None.prototype.transform = function(element) {
961 var ratio = ImageView.Effect.getPixelRatio_(element);
962 return 'scale(' + (1 / ratio) + ')';
968 * @param {number} direction -1 for left, 1 for right.
969 * @param {boolean=} opt_slow True if slow (as in slideshow).
972 ImageView.Effect.Slide = function Slide(direction, opt_slow) {
973 ImageView.Effect.call(this,
974 opt_slow ? 800 : ImageView.Effect.DEFAULT_DURATION, 'ease-in-out');
975 this.direction_ = direction;
976 this.slow_ = opt_slow;
977 this.shift_ = opt_slow ? 100 : 40;
978 if (this.direction_ < 0) this.shift_ = -this.shift_;
982 * Inherits from ImageView.Effect.
984 ImageView.Effect.Slide.prototype = { __proto__: ImageView.Effect.prototype };
987 * @return {ImageView.Effect.Slide} Reverse Slide effect.
989 ImageView.Effect.Slide.prototype.getReverse = function() {
990 return new ImageView.Effect.Slide(-this.direction_, this.slow_);
994 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
995 * @return {string} Transform string.
997 ImageView.Effect.Slide.prototype.transform = function(element) {
998 var ratio = ImageView.Effect.getPixelRatio_(element);
999 return 'scale(' + (1 / ratio) + ') translate(' + this.shift_ + 'px, 0px)';
1005 * Animates the original rectangle to the target rectangle. Both parameters
1006 * should be given in device coordinates (accounting for devicePixelRatio).
1008 * @param {Rect} deviceTargetRect Target rectangle.
1009 * @param {Rect=} opt_deviceOriginalRect Original rectangle. If omitted,
1010 * the full viewport will be used at the time of |transform| call.
1011 * @param {number=} opt_duration Duration in ms.
1014 ImageView.Effect.Zoom = function(
1015 deviceTargetRect, opt_deviceOriginalRect, opt_duration) {
1016 ImageView.Effect.call(this,
1017 opt_duration || ImageView.Effect.DEFAULT_DURATION);
1018 this.target_ = deviceTargetRect;
1019 this.original_ = opt_deviceOriginalRect;
1023 * Inherits from ImageView.Effect.
1025 ImageView.Effect.Zoom.prototype = { __proto__: ImageView.Effect.prototype };
1028 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
1029 * @param {Viewport} viewport Viewport.
1030 * @return {string} Transform string.
1032 ImageView.Effect.Zoom.prototype.transform = function(element, viewport) {
1033 if (!this.original_)
1034 this.original_ = viewport.getDeviceClipped();
1036 var ratio = ImageView.Effect.getPixelRatio_(element);
1038 var dx = (this.target_.left + this.target_.width / 2) -
1039 (this.original_.left + this.original_.width / 2);
1040 var dy = (this.target_.top + this.target_.height / 2) -
1041 (this.original_.top + this.original_.height / 2);
1043 var scaleX = this.target_.width / this.original_.width;
1044 var scaleY = this.target_.height / this.original_.height;
1046 return 'translate(' + (dx / ratio) + 'px,' + (dy / ratio) + 'px) ' +
1047 'scaleX(' + (scaleX / ratio) + ') scaleY(' + (scaleY / ratio) + ')';
1053 * @param {number} scale Scale.
1054 * @param {number} rotate90 Rotation in 90 degrees increments.
1057 ImageView.Effect.Rotate = function(scale, rotate90) {
1058 ImageView.Effect.call(this, ImageView.Effect.DEFAULT_DURATION);
1059 this.scale_ = scale;
1060 this.rotate90_ = rotate90;
1064 * Inherits from ImageView.Effect.
1066 ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype };
1069 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
1070 * @return {string} Transform string.
1072 ImageView.Effect.Rotate.prototype.transform = function(element) {
1073 var ratio = ImageView.Effect.getPixelRatio_(element);
1074 return 'rotate(' + (this.rotate90_ * 90) + 'deg) ' +
1075 'scale(' + (this.scale_ / ratio) + ')';