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