image_view.js revision 6e8cce623b6e4fe0c9e4af605d675dd9d0338c38
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 */
15function 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 */
43ImageView.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 */
49ImageView.FAST_SCROLL_INTERVAL = 300;
50
51/**
52 * Image load type: full resolution image loaded from cache.
53 */
54ImageView.LOAD_TYPE_CACHED_FULL = 0;
55
56/**
57 * Image load type: screen resolution preview loaded from cache.
58 */
59ImageView.LOAD_TYPE_CACHED_SCREEN = 1;
60
61/**
62 * Image load type: image read from file.
63 */
64ImageView.LOAD_TYPE_IMAGE_FILE = 2;
65
66/**
67 * Image load type: error occurred.
68 */
69ImageView.LOAD_TYPE_ERROR = 3;
70
71/**
72 * Image load type: the file contents is not available offline.
73 */
74ImageView.LOAD_TYPE_OFFLINE = 4;
75
76/**
77 * The total number of load types.
78 */
79ImageView.LOAD_TYPE_TOTAL = 5;
80
81ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype};
82
83/**
84 * @override
85 */
86ImageView.prototype.getZIndex = function() { return -1; };
87
88/**
89 * @override
90 */
91ImageView.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 */
107ImageView.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 */
120ImageView.prototype.getCacheGeneration = function() {
121  return this.contentGeneration_;
122};
123
124/**
125 * Invalidates the caches to force redrawing the screen canvas.
126 */
127ImageView.prototype.invalidateCaches = function() {
128  this.contentGeneration_++;
129};
130
131/**
132 * @return {HTMLCanvasElement} The content canvas element.
133 */
134ImageView.prototype.getCanvas = function() { return this.contentCanvas_; };
135
136/**
137 * @return {boolean} True if the a valid image is currently loaded.
138 */
139ImageView.prototype.hasValidImage = function() {
140  return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width;
141};
142
143/**
144 * @return {HTMLCanvasElement} The cached thumbnail image.
145 */
146ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_; };
147
148/**
149 * @return {number} The content revision number.
150 */
151ImageView.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 */
163ImageView.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 */
185ImageView.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 */
198ImageView.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 */
226ImageView.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 */
234ImageView.prototype.isLoading = function() {
235  return this.imageLoader_.isBusy();
236};
237
238/**
239 * Cancels the current image loading operation. The callbacks will be ignored.
240 */
241ImageView.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 */
258ImageView.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.drive && metadata.drive.imageWidth;
288    var imageHeight = metadata.media && metadata.media.height ||
289                      metadata.drive && metadata.drive.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.drive.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.drive && !metadata.drive.present) {
326        width = metadata.drive.imageWidth;
327        height = metadata.drive.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.streaming) {
385      // |streaming| is set only when the file is not locally cached.
386      loadType = ImageView.LOAD_TYPE_OFFLINE;
387    }
388    if (loadCallback) loadCallback(loadType, animationDuration, opt_error);
389  }
390};
391
392/**
393 * Prefetches an image.
394 * @param {Gallery.Item} item The image item.
395 * @param {number} delay Image load delay in ms.
396 */
397ImageView.prototype.prefetch = function(item, delay) {
398  if (item.contentImage)
399    return;
400  this.prefetchLoader_.load(item, function(canvas) {
401    if (canvas.width && canvas.height && !item.contentImage)
402      item.contentImage = canvas;
403  }, delay);
404};
405
406/**
407 * Unloads content.
408 * @param {Rect} zoomToRect Target rectangle for zoom-out-effect.
409 */
410ImageView.prototype.unload = function(zoomToRect) {
411  if (this.unloadTimer_) {
412    clearTimeout(this.unloadTimer_);
413    this.unloadTimer_ = null;
414  }
415  if (zoomToRect && this.screenImage_) {
416    var effect = this.createZoomEffect(zoomToRect);
417    this.setTransform_(this.screenImage_, this.viewport_, effect);
418    this.screenImage_.setAttribute('fade', true);
419    this.unloadTimer_ = setTimeout(function() {
420        this.unloadTimer_ = null;
421        this.unload(null /* force unload */);
422      }.bind(this),
423      effect.getSafeInterval());
424    return;
425  }
426  this.container_.textContent = '';
427  this.contentCanvas_ = null;
428  this.screenImage_ = null;
429};
430
431/**
432 * @param {HTMLCanvasElement} content The image element.
433 * @param {number=} opt_width Image width.
434 * @param {number=} opt_height Image height.
435 * @param {boolean=} opt_preview True if the image is a preview (not full res).
436 * @private
437 */
438ImageView.prototype.replaceContent_ = function(
439    content, opt_width, opt_height, opt_preview) {
440
441  if (this.contentCanvas_ && this.contentCanvas_.parentNode === this.container_)
442    this.container_.removeChild(this.contentCanvas_);
443
444  this.screenImage_ = this.document_.createElement('canvas');
445  this.screenImage_.className = 'image';
446
447  this.contentCanvas_ = content;
448  this.invalidateCaches();
449  this.viewport_.setImageSize(
450      opt_width || this.contentCanvas_.width,
451      opt_height || this.contentCanvas_.height);
452  this.draw();
453
454  this.container_.appendChild(this.screenImage_);
455
456  this.preview_ = opt_preview;
457  // If this is not a thumbnail, cache the content and the screen-scale image.
458  if (this.hasValidImage()) {
459    // Insert the full resolution canvas into DOM so that it can be printed.
460    this.container_.appendChild(this.contentCanvas_);
461    this.contentCanvas_.classList.add('fullres');
462
463    this.contentItem_.contentImage = this.contentCanvas_;
464    this.contentItem_.screenImage = this.screenImage_;
465
466    // TODO(kaznacheev): It is better to pass screenImage_ as it is usually
467    // much smaller than contentCanvas_ and still contains the entire image.
468    // Once we implement zoom/pan we should pass contentCanvas_ instead.
469    this.updateThumbnail_(this.screenImage_);
470
471    this.contentRevision_++;
472    for (var i = 0; i !== this.contentCallbacks_.length; i++) {
473      try {
474        this.contentCallbacks_[i]();
475      } catch (e) {
476        console.error(e);
477      }
478    }
479  }
480};
481
482/**
483 * Adds a listener for content changes.
484 * @param {function} callback Callback.
485 */
486ImageView.prototype.addContentCallback = function(callback) {
487  this.contentCallbacks_.push(callback);
488};
489
490/**
491 * Updates the cached thumbnail image.
492 *
493 * @param {HTMLCanvasElement} canvas The source canvas.
494 * @private
495 */
496ImageView.prototype.updateThumbnail_ = function(canvas) {
497  ImageUtil.trace.resetTimer('thumb');
498  var pixelCount = 10000;
499  var downScale =
500      Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount));
501
502  this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas');
503  this.thumbnailCanvas_.width = Math.round(canvas.width / downScale);
504  this.thumbnailCanvas_.height = Math.round(canvas.height / downScale);
505  Rect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas);
506  ImageUtil.trace.reportTimer('thumb');
507};
508
509/**
510 * Replaces the displayed image, possibly with slide-in animation.
511 *
512 * @param {HTMLCanvasElement} content The image element.
513 * @param {Object=} opt_effect Transition effect object.
514 * @param {number=} opt_width Image width.
515 * @param {number=} opt_height Image height.
516 * @param {boolean=} opt_preview True if the image is a preview (not full res).
517 */
518ImageView.prototype.replace = function(
519    content, opt_effect, opt_width, opt_height, opt_preview) {
520  var oldScreenImage = this.screenImage_;
521  var oldViewport = this.viewport_.clone();
522
523  this.replaceContent_(content, opt_width, opt_height, opt_preview);
524  if (!opt_effect) {
525    if (oldScreenImage)
526      oldScreenImage.parentNode.removeChild(oldScreenImage);
527    return;
528  }
529
530  var newScreenImage = this.screenImage_;
531  this.viewport_.resetView();
532
533  if (oldScreenImage)
534    ImageUtil.setAttribute(newScreenImage, 'fade', true);
535  this.setTransform_(
536      newScreenImage, this.viewport_, opt_effect, 0 /* instant */);
537
538  setTimeout(function() {
539    this.setTransform_(
540        newScreenImage,
541        this.viewport_,
542        null,
543        opt_effect && opt_effect.getDuration());
544    if (oldScreenImage) {
545      ImageUtil.setAttribute(newScreenImage, 'fade', false);
546      ImageUtil.setAttribute(oldScreenImage, 'fade', true);
547      console.assert(opt_effect.getReverse, 'Cannot revert an effect.');
548      var reverse = opt_effect.getReverse();
549      this.setTransform_(oldScreenImage, oldViewport, reverse);
550      setTimeout(function() {
551        if (oldScreenImage.parentNode)
552          oldScreenImage.parentNode.removeChild(oldScreenImage);
553      }, reverse.getSafeInterval());
554    }
555  }.bind(this));
556};
557
558/**
559 * @param {HTMLCanvasElement} element The element to transform.
560 * @param {Viewport} viewport Viewport to be used for calculating
561 *     transformation.
562 * @param {ImageView.Effect=} opt_effect The effect to apply.
563 * @param {number=} opt_duration Transition duration.
564 * @private
565 */
566ImageView.prototype.setTransform_ = function(
567    element, viewport, opt_effect, opt_duration) {
568  if (!opt_effect)
569    opt_effect = new ImageView.Effect.None();
570  if (typeof opt_duration !== 'number')
571    opt_duration = opt_effect.getDuration();
572  element.style.webkitTransitionDuration = opt_duration + 'ms';
573  element.style.webkitTransitionTimingFunction = opt_effect.getTiming();
574  element.style.webkitTransform = opt_effect.transform(element, viewport);
575};
576
577/**
578 * @param {Rect} screenRect Target rectangle in screen coordinates.
579 * @return {ImageView.Effect.Zoom} Zoom effect object.
580 */
581ImageView.prototype.createZoomEffect = function(screenRect) {
582  return new ImageView.Effect.ZoomToScreen(
583      screenRect,
584      ImageView.MODE_TRANSITION_DURATION);
585};
586
587/**
588 * Visualizes crop or rotate operation. Hide the old image instantly, animate
589 * the new image to visualize the operation.
590 *
591 * @param {HTMLCanvasElement} canvas New content canvas.
592 * @param {Rect} imageCropRect The crop rectangle in image coordinates.
593 *                             Null for rotation operations.
594 * @param {number} rotate90 Rotation angle in 90 degree increments.
595 * @return {number} Animation duration.
596 */
597ImageView.prototype.replaceAndAnimate = function(
598    canvas, imageCropRect, rotate90) {
599  var oldImageBounds = {
600    width: this.viewport_.getImageBounds().width,
601    height: this.viewport_.getImageBounds().height
602  };
603  var oldScreenImage = this.screenImage_;
604  this.replaceContent_(canvas);
605  var newScreenImage = this.screenImage_;
606  var effect = rotate90 ?
607      new ImageView.Effect.Rotate(rotate90 > 0) :
608      new ImageView.Effect.Zoom(
609          oldImageBounds.width, oldImageBounds.height, imageCropRect);
610
611  this.setTransform_(newScreenImage, this.viewport_, effect, 0 /* instant */);
612
613  oldScreenImage.parentNode.appendChild(newScreenImage);
614  oldScreenImage.parentNode.removeChild(oldScreenImage);
615
616  // Let the layout fire, then animate back to non-transformed state.
617  setTimeout(
618      this.setTransform_.bind(
619          this, newScreenImage, this.viewport_, null, effect.getDuration()),
620      0);
621
622  return effect.getSafeInterval();
623};
624
625/**
626 * Visualizes "undo crop". Shrink the current image to the given crop rectangle
627 * while fading in the new image.
628 *
629 * @param {HTMLCanvasElement} canvas New content canvas.
630 * @param {Rect} imageCropRect The crop rectangle in image coordinates.
631 * @return {number} Animation duration.
632 */
633ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) {
634  var oldScreenImage = this.screenImage_;
635  this.replaceContent_(canvas);
636  var newScreenImage = this.screenImage_;
637  var setFade = ImageUtil.setAttribute.bind(null, newScreenImage, 'fade');
638  setFade(true);
639  oldScreenImage.parentNode.insertBefore(newScreenImage, oldScreenImage);
640  var effect = new ImageView.Effect.Zoom(
641      this.viewport_.getImageBounds().width,
642      this.viewport_.getImageBounds().height,
643      imageCropRect);
644
645  // Animate to the transformed state.
646  this.setTransform_(oldScreenImage, this.viewport_, effect);
647  setTimeout(setFade.bind(null, false), 0);
648  setTimeout(function() {
649    if (oldScreenImage.parentNode)
650      oldScreenImage.parentNode.removeChild(oldScreenImage);
651  }, effect.getSafeInterval());
652
653  return effect.getSafeInterval();
654};
655
656/* Transition effects */
657
658/**
659 * Base class for effects.
660 *
661 * @param {number} duration Duration in ms.
662 * @param {string=} opt_timing CSS transition timing function name.
663 * @constructor
664 */
665ImageView.Effect = function(duration, opt_timing) {
666  this.duration_ = duration;
667  this.timing_ = opt_timing || 'linear';
668};
669
670/**
671 *
672 */
673ImageView.Effect.DEFAULT_DURATION = 180;
674
675/**
676 *
677 */
678ImageView.Effect.MARGIN = 100;
679
680/**
681 * @return {number} Effect duration in ms.
682 */
683ImageView.Effect.prototype.getDuration = function() { return this.duration_; };
684
685/**
686 * @return {number} Delay in ms since the beginning of the animation after which
687 * it is safe to perform CPU-heavy operations without disrupting the animation.
688 */
689ImageView.Effect.prototype.getSafeInterval = function() {
690  return this.getDuration() + ImageView.Effect.MARGIN;
691};
692
693/**
694 * @return {string} CSS transition timing function name.
695 */
696ImageView.Effect.prototype.getTiming = function() { return this.timing_; };
697
698/**
699 * Obtains the CSS transformation string of the effect.
700 * @param {DOMCanvas} element Canvas element to be applied the transformation.
701 * @param {Viewport} viewport Current viewport.
702 * @return CSS transformation description.
703 */
704ImageView.Effect.prototype.transform = function(element, viewport) {
705  throw new Error('Not implemented.');
706};
707
708/**
709 * Default effect.
710 *
711 * @constructor
712 * @extends {ImageView.Effect}
713 */
714ImageView.Effect.None = function() {
715  ImageView.Effect.call(this, 0, 'easy-out');
716};
717
718/**
719 * Inherits from ImageView.Effect.
720 */
721ImageView.Effect.None.prototype = { __proto__: ImageView.Effect.prototype };
722
723/**
724 * @param {HTMLCanvasElement} element Element.
725 * @param {Viewport} viewport Current viewport.
726 * @return {string} Transform string.
727 */
728ImageView.Effect.None.prototype.transform = function(element, viewport) {
729  return viewport.getTransformation();
730};
731
732/**
733 * Slide effect.
734 *
735 * @param {number} direction -1 for left, 1 for right.
736 * @param {boolean=} opt_slow True if slow (as in slideshow).
737 * @constructor
738 * @extends {ImageView.Effect}
739 */
740ImageView.Effect.Slide = function Slide(direction, opt_slow) {
741  ImageView.Effect.call(this,
742      opt_slow ? 800 : ImageView.Effect.DEFAULT_DURATION, 'ease-out');
743  this.direction_ = direction;
744  this.slow_ = opt_slow;
745  this.shift_ = opt_slow ? 100 : 40;
746  if (this.direction_ < 0) this.shift_ = -this.shift_;
747};
748
749ImageView.Effect.Slide.prototype = { __proto__: ImageView.Effect.prototype };
750
751/**
752 * Reverses the slide effect.
753 * @return {ImageView.Effect.Slide} Reversed effect.
754 */
755ImageView.Effect.Slide.prototype.getReverse = function() {
756  return new ImageView.Effect.Slide(-this.direction_, this.slow_);
757};
758
759/**
760 * @override
761 */
762ImageView.Effect.Slide.prototype.transform = function(element, viewport) {
763  return viewport.getShiftTransformation(this.shift_);
764};
765
766/**
767 * Zoom effect.
768 *
769 * Animates the original rectangle to the target rectangle.
770 *
771 * @param {number} previousImageWidth Width of the full resolution image.
772 * @param {number} previousImageHeight Height of the full resolution image.
773 * @param {Rect} imageCropRect Crop rectangle in the full resolution image.
774 * @param {number=} opt_duration Duration of the effect.
775 * @constructor
776 * @extends {ImageView.Effect}
777 */
778ImageView.Effect.Zoom = function(
779    previousImageWidth, previousImageHeight, imageCropRect, opt_duration) {
780  ImageView.Effect.call(this,
781      opt_duration || ImageView.Effect.DEFAULT_DURATION, 'ease-out');
782  this.previousImageWidth_ = previousImageWidth;
783  this.previousImageHeight_ = previousImageHeight;
784  this.imageCropRect_ = imageCropRect;
785};
786
787ImageView.Effect.Zoom.prototype = { __proto__: ImageView.Effect.prototype };
788
789/**
790 * @override
791 */
792ImageView.Effect.Zoom.prototype.transform = function(element, viewport) {
793  return viewport.getInverseTransformForCroppedImage(
794      this.previousImageWidth_, this.previousImageHeight_, this.imageCropRect_);
795};
796
797/**
798 * Effect to zoom to a screen rectangle.
799 *
800 * @param {Rect} screenRect Rectangle in the application window's coordinate.
801 * @param {number=} opt_duration Duration of effect.
802 * @constructor
803 * @extends {ImageView.Effect}
804 */
805ImageView.Effect.ZoomToScreen = function(screenRect, opt_duration) {
806  ImageView.Effect.call(this, opt_duration);
807  this.screenRect_ = screenRect;
808};
809
810ImageView.Effect.ZoomToScreen.prototype = {
811  __proto__: ImageView.Effect.prototype
812};
813
814/**
815 * @override
816 */
817ImageView.Effect.ZoomToScreen.prototype.transform = function(
818    element, viewport) {
819  return viewport.getScreenRectTransformForImage(this.screenRect_);
820};
821
822/**
823 * Rotation effect.
824 *
825 * @param {boolean} orientation Orientation of rotation. True is for clockwise
826 *     and false is for counterclockwise.
827 * @constructor
828 * @extends {ImageView.Effect}
829 */
830ImageView.Effect.Rotate = function(orientation) {
831  ImageView.Effect.call(this, ImageView.Effect.DEFAULT_DURATION);
832  this.orientation_ = orientation;
833};
834
835ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype };
836
837/**
838 * @override
839 */
840ImageView.Effect.Rotate.prototype.transform = function(element, viewport) {
841  return viewport.getInverseTransformForRotatedImage(this.orientation_);
842};
843