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