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 }; 842