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.
13 * @extends {ImageBuffer.Overlay}
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_ = [];
43 * The element displaying the current content.
45 * @type {HTMLCanvasElement}
48 this.screenImage_ = null;
52 * Duration of transition between modes in ms.
54 ImageView.MODE_TRANSITION_DURATION = 350;
57 * If the user flips though images faster than this interval we do not apply
58 * the slide-in/slide-out transition.
60 ImageView.FAST_SCROLL_INTERVAL = 300;
63 * Image load type: full resolution image loaded from cache.
65 ImageView.LOAD_TYPE_CACHED_FULL = 0;
68 * Image load type: screen resolution preview loaded from cache.
70 ImageView.LOAD_TYPE_CACHED_SCREEN = 1;
73 * Image load type: image read from file.
75 ImageView.LOAD_TYPE_IMAGE_FILE = 2;
78 * Image load type: error occurred.
80 ImageView.LOAD_TYPE_ERROR = 3;
83 * Image load type: the file contents is not available offline.
85 ImageView.LOAD_TYPE_OFFLINE = 4;
88 * The total number of load types.
90 ImageView.LOAD_TYPE_TOTAL = 5;
92 ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype};
97 ImageView.prototype.getZIndex = function() { return -1; };
102 ImageView.prototype.draw = function() {
103 if (!this.contentCanvas_) // Do nothing if the image content is not set.
106 var forceRepaint = false;
108 if (this.displayedViewportGeneration_ !==
109 this.viewport_.getCacheGeneration()) {
110 this.displayedViewportGeneration_ = this.viewport_.getCacheGeneration();
112 this.setupDeviceBuffer(this.screenImage_);
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');
128 * Applies the viewport change that does not affect the screen cache size (zoom
129 * change or offset change) with animation.
131 ImageView.prototype.applyViewportChange = function() {
134 new ImageView.Effect.None(),
135 ImageView.Effect.DEFAULT_DURATION);
139 * @return {number} The cache generation.
141 ImageView.prototype.getCacheGeneration = function() {
142 return this.contentGeneration_;
146 * Invalidates the caches to force redrawing the screen canvas.
148 ImageView.prototype.invalidateCaches = function() {
149 this.contentGeneration_++;
153 * @return {HTMLCanvasElement} The content canvas element.
155 ImageView.prototype.getCanvas = function() { return this.contentCanvas_; };
158 * @return {boolean} True if the a valid image is currently loaded.
160 ImageView.prototype.hasValidImage = function() {
161 return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width;
165 * @return {HTMLCanvasElement} The cached thumbnail image.
167 ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_; };
170 * @return {number} The content revision number.
172 ImageView.prototype.getContentRevision = function() {
173 return this.contentRevision_;
177 * Copies an image fragment from a full resolution canvas to a device resolution
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.
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);
193 // Map the rectangle in full resolution image to the rectangle in the device
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);
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.
213 ImageView.prototype.createOverlayCanvas = function() {
214 var canvas = this.document_.createElement('canvas');
215 canvas.className = 'image';
216 this.container_.appendChild(canvas);
221 * Sets up the canvas as a buffer in the device resolution.
223 * @param {HTMLCanvasElement} canvas The buffer canvas.
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;
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.
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.
253 ImageView.prototype.isLoading = function() {
254 return this.imageLoader_.isBusy();
258 * Cancels the current image loading operation. The callbacks will be ignored.
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.
277 ImageView.prototype.load =
278 function(item, effect, displayCallback, loadCallback) {
279 var entry = item.getEntry();
280 var metadata = item.getMetadata() || {};
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) {
289 this.lastLoadTime_ = time;
292 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime'));
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_);
305 displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL,
306 false /* no preview */, cached);
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;
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(
325 ThumbnailLoader.LoaderType.CANVAS,
327 thumbnailLoader.loadDetachedImage(function(success) {
328 displayThumbnail(ImageView.LOAD_TYPE_IMAGE_FILE,
329 success ? thumbnailLoader.getImage() : null);
332 loadMainImage(ImageView.LOAD_TYPE_IMAGE_FILE, entry,
333 false /* no preview*/, 0 /* delay */);
337 function displayThumbnail(loadType, canvas) {
341 if (metadata.media) {
342 width = metadata.media.width;
343 height = metadata.media.height;
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
348 if (metadata.drive && !metadata.drive.present) {
349 width = metadata.drive.imageWidth;
350 height = metadata.drive.imageHeight;
358 if (displayCallback) displayCallback();
360 loadMainImage(loadType, entry, !!canvas,
361 (effect && canvas) ? effect.getSafeInterval() : 0);
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;
377 self.prefetchLoader_.cancel(); // The prefetch was doing something useless.
379 self.imageLoader_.load(
381 displayMainImage.bind(null, loadType, previewShown),
385 function displayMainImage(loadType, previewShown, content, 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();
399 if (loadType !== ImageView.LOAD_TYPE_ERROR &&
400 loadType !== ImageView.LOAD_TYPE_CACHED_SCREEN) {
401 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
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;
411 if (loadCallback) loadCallback(loadType, animationDuration, opt_error);
416 * Prefetches an image.
417 * @param {Gallery.Item} item The image item.
418 * @param {number} delay Image load delay in ms.
420 ImageView.prototype.prefetch = function(item, delay) {
422 var entry = item.getEntry();
423 function prefetchDone(canvas) {
425 self.contentCache_.putItem(entry, canvas);
428 var cached = this.contentCache_.getItem(entry);
430 prefetchDone(cached);
431 } else if (FileType.getMediaType(entry) === 'image') {
432 // Evict the LRU item before we allocate the new canvas to avoid unneeded
434 this.contentCache_.evictLRU();
436 this.prefetchLoader_.load(item, prefetchDone, delay);
441 * Renames the current image.
442 * @param {FileEntry} newEntry The new image Entry.
444 ImageView.prototype.changeEntry = function(newEntry) {
445 this.contentCache_.renameItem(this.contentEntry_, newEntry);
446 this.screenCache_.renameItem(this.contentEntry_, newEntry);
447 this.contentEntry_ = newEntry;
452 * @param {Rect} zoomToRect Target rectangle for zoom-out-effect.
454 ImageView.prototype.unload = function(zoomToRect) {
455 if (this.unloadTimer_) {
456 clearTimeout(this.unloadTimer_);
457 this.unloadTimer_ = null;
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 */);
467 effect.getSafeInterval());
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).
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();
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++) {
520 this.contentCallbacks_[i]();
529 * Adds a listener for content changes.
530 * @param {function} callback Callback.
532 ImageView.prototype.addContentCallback = function(callback) {
533 this.contentCallbacks_.push(callback);
537 * Updates the cached thumbnail image.
539 * @param {HTMLCanvasElement} canvas The source canvas.
542 ImageView.prototype.updateThumbnail_ = function(canvas) {
543 ImageUtil.trace.resetTimer('thumb');
544 var pixelCount = 10000;
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).
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);
571 oldScreenImage.parentNode.removeChild(oldScreenImage);
575 var newScreenImage = this.screenImage_;
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());
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.
603 ImageView.prototype.setTransform = function(element, opt_effect, opt_duration) {
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.
617 ImageView.prototype.createZoomEffect = function(screenRect) {
618 return new ImageView.Effect.ZoomToScreen(
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.
633 ImageView.prototype.replaceAndAnimate = function(
634 canvas, imageCropRect, rotate90) {
635 var oldImageBounds = {
636 width: this.viewport_.getImageBounds().width,
637 height: this.viewport_.getImageBounds().height
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.
654 this.setTransform.bind(
655 this, newScreenImage, null, effect.getDuration()),
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.
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');
675 oldScreenImage.parentNode.insertBefore(newScreenImage, oldScreenImage);
676 var effect = new ImageView.Effect.Zoom(
677 this.viewport_.getImageBounds().width,
678 this.viewport_.getImageBounds().height,
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.
697 ImageView.Cache = function(capacity) {
698 this.capacity_ = capacity;
704 * Fetches the item from the cache.
705 * @param {FileEntry} entry The entry.
706 * @return {Object} The cached item.
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.
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_) {
727 // Move to the end (most recently used).
728 this.order_.splice(pos, 1);
729 this.order_.push(entry.toURL());
733 this.order_.push(entry.toURL());
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.
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];
757 * @param {FileEntry} oldEntry The old Entry.
758 * @param {FileEntry} newEntry The new Entry.
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());
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.
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.
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.
798 ImageView.Effect = function(duration, opt_timing) {
799 this.duration_ = duration;
800 this.timing_ = opt_timing || 'linear';
806 ImageView.Effect.DEFAULT_DURATION = 180;
811 ImageView.Effect.MARGIN = 100;
814 * @return {number} Effect duration in ms.
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.
822 ImageView.Effect.prototype.getSafeInterval = function() {
823 return this.getDuration() + ImageView.Effect.MARGIN;
827 * @return {string} CSS transition timing function name.
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.
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.
846 * @extends {ImageView.Effect}
848 ImageView.Effect.None = function() {
849 ImageView.Effect.call(this, 0);
853 * Inherits from ImageView.Effect.
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.
862 ImageView.Effect.None.prototype.transform = function(element, viewport) {
863 return viewport.getTransformation();
869 * @param {number} direction -1 for left, 1 for right.
870 * @param {boolean=} opt_slow True if slow (as in slideshow).
872 * @extends {ImageView.Effect}
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.
889 ImageView.Effect.Slide.prototype.getReverse = function() {
890 return new ImageView.Effect.Slide(-this.direction_, this.slow_);
896 ImageView.Effect.Slide.prototype.transform = function(element, viewport) {
897 return viewport.getShiftTransformation(this.shift_);
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.
911 * @extends {ImageView.Effect}
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 };
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.
938 * @extends {ImageView.Effect}
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
952 ImageView.Effect.ZoomToScreen.prototype.transform = function(
954 return viewport.getScreenRectTransformForImage(this.screenRect_);
960 * @param {boolean} orientation Orientation of rotation. True is for clockwise
961 * and false is for counterclockwise.
963 * @extends {ImageView.Effect}
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 };
975 ImageView.Effect.Rotate.prototype.transform = function(element, viewport) {
976 return viewport.getInverseTransformForRotatedImage(this.orientation_);