Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(285)

Side by Side Diff: chrome/browser/resources/file_manager/js/image_editor/image_view.js

Issue 39123003: [Files.app] Split the JavaScript files into subdirectories: common, background, and foreground (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fixed test failure. Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2012 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 * @param {MetadataCache} metadataCache The metadataCache.
13 * @constructor
14 */
15 function ImageView(container, viewport, metadataCache) {
16 this.container_ = container;
17 this.viewport_ = viewport;
18 this.document_ = container.ownerDocument;
19 this.contentGeneration_ = 0;
20 this.displayedContentGeneration_ = 0;
21 this.displayedViewportGeneration_ = 0;
22
23 this.imageLoader_ = new ImageUtil.ImageLoader(this.document_, metadataCache);
24 // We have a separate image loader for prefetch which does not get cancelled
25 // when the selection changes.
26 this.prefetchLoader_ = new ImageUtil.ImageLoader(
27 this.document_, metadataCache);
28
29 // The content cache is used for prefetching the next image when going
30 // through the images sequentially. The real life photos can be large
31 // (18Mpix = 72Mb pixel array) so we want only the minimum amount of caching.
32 this.contentCache_ = new ImageView.Cache(2);
33
34 // We reuse previously generated screen-scale images so that going back to
35 // a recently loaded image looks instant even if the image is not in
36 // the content cache any more. Screen-scale images are small (~1Mpix)
37 // so we can afford to cache more of them.
38 this.screenCache_ = new ImageView.Cache(5);
39 this.contentCallbacks_ = [];
40
41 /**
42 * The element displaying the current content.
43 *
44 * @type {HTMLCanvasElement|HTMLVideoElement}
45 * @private
46 */
47 this.screenImage_ = null;
48
49 this.localImageTransformFetcher_ = function(url, callback) {
50 metadataCache.get(url, 'fetchedMedia', function(fetchedMedia) {
51 callback(fetchedMedia.imageTransform);
52 });
53 };
54 }
55
56 /**
57 * Duration of transition between modes in ms.
58 */
59 ImageView.MODE_TRANSITION_DURATION = 350;
60
61 /**
62 * If the user flips though images faster than this interval we do not apply
63 * the slide-in/slide-out transition.
64 */
65 ImageView.FAST_SCROLL_INTERVAL = 300;
66
67 /**
68 * Image load type: full resolution image loaded from cache.
69 */
70 ImageView.LOAD_TYPE_CACHED_FULL = 0;
71
72 /**
73 * Image load type: screen resolution preview loaded from cache.
74 */
75 ImageView.LOAD_TYPE_CACHED_SCREEN = 1;
76
77 /**
78 * Image load type: image read from file.
79 */
80 ImageView.LOAD_TYPE_IMAGE_FILE = 2;
81
82 /**
83 * Image load type: video loaded.
84 */
85 ImageView.LOAD_TYPE_VIDEO_FILE = 3;
86
87 /**
88 * Image load type: error occurred.
89 */
90 ImageView.LOAD_TYPE_ERROR = 4;
91
92 /**
93 * Image load type: the file contents is not available offline.
94 */
95 ImageView.LOAD_TYPE_OFFLINE = 5;
96
97 /**
98 * The total number of load types.
99 */
100 ImageView.LOAD_TYPE_TOTAL = 6;
101
102 ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype};
103
104 /**
105 * Draw below overlays with the default zIndex.
106 * @return {number} Z-index.
107 */
108 ImageView.prototype.getZIndex = function() { return -1 };
109
110 /**
111 * Draw the image on screen.
112 */
113 ImageView.prototype.draw = function() {
114 if (!this.contentCanvas_) // Do nothing if the image content is not set.
115 return;
116
117 var forceRepaint = false;
118
119 if (this.displayedViewportGeneration_ !=
120 this.viewport_.getCacheGeneration()) {
121 this.displayedViewportGeneration_ = this.viewport_.getCacheGeneration();
122
123 this.setupDeviceBuffer(this.screenImage_);
124
125 forceRepaint = true;
126 }
127
128 if (forceRepaint ||
129 this.displayedContentGeneration_ != this.contentGeneration_) {
130 this.displayedContentGeneration_ = this.contentGeneration_;
131
132 ImageUtil.trace.resetTimer('paint');
133 this.paintDeviceRect(this.viewport_.getDeviceClipped(),
134 this.contentCanvas_, this.viewport_.getImageClipped());
135 ImageUtil.trace.reportTimer('paint');
136 }
137 };
138
139 /**
140 * @param {number} x X pointer position.
141 * @param {number} y Y pointer position.
142 * @param {boolean} mouseDown True if mouse is down.
143 * @return {string} CSS cursor style.
144 */
145 ImageView.prototype.getCursorStyle = function(x, y, mouseDown) {
146 // Indicate that the image is draggable.
147 if (this.viewport_.isClipped() &&
148 this.viewport_.getScreenClipped().inside(x, y))
149 return 'move';
150
151 return null;
152 };
153
154 /**
155 * @param {number} x X pointer position.
156 * @param {number} y Y pointer position.
157 * @return {function} The closure to call on drag.
158 */
159 ImageView.prototype.getDragHandler = function(x, y) {
160 var cursor = this.getCursorStyle(x, y);
161 if (cursor == 'move') {
162 // Return the handler that drags the entire image.
163 return this.viewport_.createOffsetSetter(x, y);
164 }
165
166 return null;
167 };
168
169 /**
170 * @return {number} The cache generation.
171 */
172 ImageView.prototype.getCacheGeneration = function() {
173 return this.contentGeneration_;
174 };
175
176 /**
177 * Invalidate the caches to force redrawing the screen canvas.
178 */
179 ImageView.prototype.invalidateCaches = function() {
180 this.contentGeneration_++;
181 };
182
183 /**
184 * @return {HTMLCanvasElement} The content canvas element.
185 */
186 ImageView.prototype.getCanvas = function() { return this.contentCanvas_ };
187
188 /**
189 * @return {boolean} True if the a valid image is currently loaded.
190 */
191 ImageView.prototype.hasValidImage = function() {
192 return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width;
193 };
194
195 /**
196 * @return {HTMLVideoElement} The video element.
197 */
198 ImageView.prototype.getVideo = function() { return this.videoElement_ };
199
200 /**
201 * @return {HTMLCanvasElement} The cached thumbnail image.
202 */
203 ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_ };
204
205 /**
206 * @return {number} The content revision number.
207 */
208 ImageView.prototype.getContentRevision = function() {
209 return this.contentRevision_;
210 };
211
212 /**
213 * Copy an image fragment from a full resolution canvas to a device resolution
214 * canvas.
215 *
216 * @param {Rect} deviceRect Rectangle in the device coordinates.
217 * @param {HTMLCanvasElement} canvas Full resolution canvas.
218 * @param {Rect} imageRect Rectangle in the full resolution canvas.
219 */
220 ImageView.prototype.paintDeviceRect = function(deviceRect, canvas, imageRect) {
221 // Map screen canvas (0,0) to (deviceBounds.left, deviceBounds.top)
222 var deviceBounds = this.viewport_.getDeviceClipped();
223 deviceRect = deviceRect.shift(-deviceBounds.left, -deviceBounds.top);
224
225 // The source canvas may have different physical size than the image size
226 // set at the viewport. Adjust imageRect accordingly.
227 var bounds = this.viewport_.getImageBounds();
228 var scaleX = canvas.width / bounds.width;
229 var scaleY = canvas.height / bounds.height;
230 imageRect = new Rect(imageRect.left * scaleX, imageRect.top * scaleY,
231 imageRect.width * scaleX, imageRect.height * scaleY);
232 Rect.drawImage(
233 this.screenImage_.getContext('2d'), canvas, deviceRect, imageRect);
234 };
235
236 /**
237 * Create an overlay canvas with properties similar to the screen canvas.
238 * Useful for showing quick feedback when editing.
239 *
240 * @return {HTMLCanvasElement} Overlay canvas.
241 */
242 ImageView.prototype.createOverlayCanvas = function() {
243 var canvas = this.document_.createElement('canvas');
244 canvas.className = 'image';
245 this.container_.appendChild(canvas);
246 return canvas;
247 };
248
249 /**
250 * Sets up the canvas as a buffer in the device resolution.
251 *
252 * @param {HTMLCanvasElement} canvas The buffer canvas.
253 */
254 ImageView.prototype.setupDeviceBuffer = function(canvas) {
255 var deviceRect = this.viewport_.getDeviceClipped();
256
257 // Set the canvas position and size in device pixels.
258 if (canvas.width != deviceRect.width)
259 canvas.width = deviceRect.width;
260
261 if (canvas.height != deviceRect.height)
262 canvas.height = deviceRect.height;
263
264 canvas.style.left = deviceRect.left + 'px';
265 canvas.style.top = deviceRect.top + 'px';
266
267 // Scale the canvas down to screen pixels.
268 this.setTransform(canvas);
269 };
270
271 /**
272 * @return {ImageData} A new ImageData object with a copy of the content.
273 */
274 ImageView.prototype.copyScreenImageData = function() {
275 return this.screenImage_.getContext('2d').getImageData(
276 0, 0, this.screenImage_.width, this.screenImage_.height);
277 };
278
279 /**
280 * @return {boolean} True if the image is currently being loaded.
281 */
282 ImageView.prototype.isLoading = function() {
283 return this.imageLoader_.isBusy();
284 };
285
286 /**
287 * Cancel the current image loading operation. The callbacks will be ignored.
288 */
289 ImageView.prototype.cancelLoad = function() {
290 this.imageLoader_.cancel();
291 };
292
293 /**
294 * Load and display a new image.
295 *
296 * Loads the thumbnail first, then replaces it with the main image.
297 * Takes into account the image orientation encoded in the metadata.
298 *
299 * @param {string} url Image url.
300 * @param {Object} metadata Metadata.
301 * @param {Object} effect Transition effect object.
302 * @param {function(number} displayCallback Called when the image is displayed
303 * (possibly as a prevew).
304 * @param {function(number} loadCallback Called when the image is fully loaded.
305 * The parameter is the load type.
306 */
307 ImageView.prototype.load = function(url, metadata, effect,
308 displayCallback, loadCallback) {
309 if (effect) {
310 // Skip effects when reloading repeatedly very quickly.
311 var time = Date.now();
312 if (this.lastLoadTime_ &&
313 (time - this.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) {
314 effect = null;
315 }
316 this.lastLoadTime_ = time;
317 }
318
319 metadata = metadata || {};
320
321 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime'));
322
323 var self = this;
324
325 this.contentID_ = url;
326 this.contentRevision_ = -1;
327
328 var loadingVideo = FileType.getMediaType(url) == 'video';
329 if (loadingVideo) {
330 var video = this.document_.createElement('video');
331 var videoPreview = !!(metadata.thumbnail && metadata.thumbnail.url);
332 if (videoPreview) {
333 var thumbnailLoader = new ThumbnailLoader(
334 metadata.thumbnail.url,
335 ThumbnailLoader.LoaderType.CANVAS,
336 metadata);
337 thumbnailLoader.loadDetachedImage(function(success) {
338 if (success) {
339 var canvas = thumbnailLoader.getImage();
340 video.setAttribute('poster', canvas.toDataURL('image/jpeg'));
341 this.replace(video, effect); // Show the poster immediately.
342 if (displayCallback) displayCallback();
343 }
344 }.bind(this));
345 }
346
347 var onVideoLoad = function(error) {
348 video.removeEventListener('loadedmetadata', onVideoLoadSuccess);
349 video.removeEventListener('error', onVideoLoadError);
350 displayMainImage(ImageView.LOAD_TYPE_VIDEO_FILE, videoPreview, video,
351 error);
352 };
353 var onVideoLoadError = onVideoLoad.bind(this, 'VIDEO_ERROR');
354 var onVideoLoadSuccess = onVideoLoad.bind(this, null);
355
356 video.addEventListener('loadedmetadata', onVideoLoadSuccess);
357 video.addEventListener('error', onVideoLoadError);
358
359 video.src = url;
360 video.load();
361 return;
362 }
363
364 // Cache has to be evicted in advance, so the returned cached image is not
365 // evicted later by the prefetched image.
366 this.contentCache_.evictLRU();
367
368 var cached = this.contentCache_.getItem(this.contentID_);
369 if (cached) {
370 displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL,
371 false /* no preview */, cached);
372 } else {
373 var cachedScreen = this.screenCache_.getItem(this.contentID_);
374 var imageWidth = metadata.media && metadata.media.width ||
375 metadata.drive && metadata.drive.imageWidth;
376 var imageHeight = metadata.media && metadata.media.height ||
377 metadata.drive && metadata.drive.imageHeight;
378 if (cachedScreen) {
379 // We have a cached screen-scale canvas, use it instead of a thumbnail.
380 displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, cachedScreen);
381 // As far as the user can tell the image is loaded. We still need to load
382 // the full res image to make editing possible, but we can report now.
383 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
384 } else if ((!effect || (effect.constructor.name == 'Slide')) &&
385 metadata.thumbnail && metadata.thumbnail.url &&
386 !(imageWidth && imageHeight &&
387 ImageUtil.ImageLoader.isTooLarge(imageWidth, imageHeight))) {
388 // Only show thumbnails if there is no effect or the effect is Slide.
389 // Also no thumbnail if the image is too large to be loaded.
390 var thumbnailLoader = new ThumbnailLoader(
391 metadata.thumbnail.url,
392 ThumbnailLoader.LoaderType.CANVAS,
393 metadata);
394 thumbnailLoader.loadDetachedImage(function(success) {
395 displayThumbnail(ImageView.LOAD_TYPE_IMAGE_FILE,
396 success ? thumbnailLoader.getImage() : null);
397 });
398 } else {
399 loadMainImage(ImageView.LOAD_TYPE_IMAGE_FILE, url,
400 false /* no preview*/, 0 /* delay */);
401 }
402 }
403
404 function displayThumbnail(loadType, canvas) {
405 if (canvas) {
406 self.replace(
407 canvas,
408 effect,
409 metadata.media.width || metadata.drive.imageWidth,
410 metadata.media.height || metadata.drive.imageHeight,
411 true /* preview */);
412 if (displayCallback) displayCallback();
413 }
414 loadMainImage(loadType, url, !!canvas,
415 (effect && canvas) ? effect.getSafeInterval() : 0);
416 }
417
418 function loadMainImage(loadType, contentURL, previewShown, delay) {
419 if (self.prefetchLoader_.isLoading(contentURL)) {
420 // The image we need is already being prefetched. Initiating another load
421 // would be a waste. Hijack the load instead by overriding the callback.
422 self.prefetchLoader_.setCallback(
423 displayMainImage.bind(null, loadType, previewShown));
424
425 // Swap the loaders so that the self.isLoading works correctly.
426 var temp = self.prefetchLoader_;
427 self.prefetchLoader_ = self.imageLoader_;
428 self.imageLoader_ = temp;
429 return;
430 }
431 self.prefetchLoader_.cancel(); // The prefetch was doing something useless.
432
433 self.imageLoader_.load(
434 contentURL,
435 self.localImageTransformFetcher_,
436 displayMainImage.bind(null, loadType, previewShown),
437 delay);
438 }
439
440 function displayMainImage(loadType, previewShown, content, opt_error) {
441 if (opt_error)
442 loadType = ImageView.LOAD_TYPE_ERROR;
443
444 // If we already displayed the preview we should not replace the content if:
445 // 1. The full content failed to load.
446 // or
447 // 2. We are loading a video (because the full video is displayed in the
448 // same HTML element as the preview).
449 var animationDuration = 0;
450 if (!(previewShown &&
451 (loadType == ImageView.LOAD_TYPE_ERROR ||
452 loadType == ImageView.LOAD_TYPE_VIDEO_FILE))) {
453 var replaceEffect = previewShown ? null : effect;
454 animationDuration = replaceEffect ? replaceEffect.getSafeInterval() : 0;
455 self.replace(content, replaceEffect);
456 if (!previewShown && displayCallback) displayCallback();
457 }
458
459 if (loadType != ImageView.LOAD_TYPE_ERROR &&
460 loadType != ImageView.LOAD_TYPE_CACHED_SCREEN) {
461 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
462 }
463 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('LoadMode'),
464 loadType, ImageView.LOAD_TYPE_TOTAL);
465
466 if (loadType == ImageView.LOAD_TYPE_ERROR &&
467 !navigator.onLine && metadata.streaming) {
468 // |streaming| is set only when the file is not locally cached.
469 loadType = ImageView.LOAD_TYPE_OFFLINE;
470 }
471 if (loadCallback) loadCallback(loadType, animationDuration, opt_error);
472 }
473 };
474
475 /**
476 * Prefetch an image.
477 *
478 * @param {string} url The image url.
479 * @param {number} delay Image load delay in ms.
480 */
481 ImageView.prototype.prefetch = function(url, delay) {
482 var self = this;
483 function prefetchDone(canvas) {
484 if (canvas.width)
485 self.contentCache_.putItem(url, canvas);
486 }
487
488 var cached = this.contentCache_.getItem(url);
489 if (cached) {
490 prefetchDone(cached);
491 } else if (FileType.getMediaType(url) == 'image') {
492 // Evict the LRU item before we allocate the new canvas to avoid unneeded
493 // strain on memory.
494 this.contentCache_.evictLRU();
495
496 this.prefetchLoader_.load(
497 url,
498 this.localImageTransformFetcher_,
499 prefetchDone,
500 delay);
501 }
502 };
503
504 /**
505 * Rename the current image.
506 *
507 * @param {string} newUrl The new image url.
508 */
509 ImageView.prototype.changeUrl = function(newUrl) {
510 this.contentCache_.renameItem(this.contentID_, newUrl);
511 this.screenCache_.renameItem(this.contentID_, newUrl);
512 this.contentID_ = newUrl;
513 };
514
515 /**
516 * Unload content.
517 *
518 * @param {Rect} zoomToRect Target rectangle for zoom-out-effect.
519 */
520 ImageView.prototype.unload = function(zoomToRect) {
521 if (this.unloadTimer_) {
522 clearTimeout(this.unloadTimer_);
523 this.unloadTimer_ = null;
524 }
525 if (zoomToRect && this.screenImage_) {
526 var effect = this.createZoomEffect(zoomToRect);
527 this.setTransform(this.screenImage_, effect);
528 this.screenImage_.setAttribute('fade', true);
529 this.unloadTimer_ = setTimeout(function() {
530 this.unloadTimer_ = null;
531 this.unload(null /* force unload */);
532 }.bind(this),
533 effect.getSafeInterval());
534 return;
535 }
536 this.container_.textContent = '';
537 this.contentCanvas_ = null;
538 this.screenImage_ = null;
539 this.videoElement_ = null;
540 };
541
542 /**
543 *
544 * @param {HTMLCanvasElement|HTMLVideoElement} content The image element.
545 * @param {number=} opt_width Image width.
546 * @param {number=} opt_height Image height.
547 * @param {boolean=} opt_preview True if the image is a preview (not full res).
548 * @private
549 */
550 ImageView.prototype.replaceContent_ = function(
551 content, opt_width, opt_height, opt_preview) {
552
553 if (this.contentCanvas_ && this.contentCanvas_.parentNode == this.container_)
554 this.container_.removeChild(this.contentCanvas_);
555
556 if (content.constructor.name == 'HTMLVideoElement') {
557 this.contentCanvas_ = null;
558 this.videoElement_ = content;
559 this.screenImage_ = content;
560 this.screenImage_.className = 'image';
561 this.container_.appendChild(this.screenImage_);
562 this.videoElement_.play();
563 return;
564 }
565
566 this.screenImage_ = this.document_.createElement('canvas');
567 this.screenImage_.className = 'image';
568
569 this.videoElement_ = null;
570 this.contentCanvas_ = content;
571 this.invalidateCaches();
572 this.viewport_.setImageSize(
573 opt_width || this.contentCanvas_.width,
574 opt_height || this.contentCanvas_.height);
575 this.viewport_.fitImage();
576 this.viewport_.update();
577 this.draw();
578
579 this.container_.appendChild(this.screenImage_);
580
581 this.preview_ = opt_preview;
582 // If this is not a thumbnail, cache the content and the screen-scale image.
583 if (this.hasValidImage()) {
584 // Insert the full resolution canvas into DOM so that it can be printed.
585 this.container_.appendChild(this.contentCanvas_);
586 this.contentCanvas_.classList.add('fullres');
587
588 this.contentCache_.putItem(this.contentID_, this.contentCanvas_, true);
589 this.screenCache_.putItem(this.contentID_, this.screenImage_);
590
591 // TODO(kaznacheev): It is better to pass screenImage_ as it is usually
592 // much smaller than contentCanvas_ and still contains the entire image.
593 // Once we implement zoom/pan we should pass contentCanvas_ instead.
594 this.updateThumbnail_(this.screenImage_);
595
596 this.contentRevision_++;
597 for (var i = 0; i != this.contentCallbacks_.length; i++) {
598 try {
599 this.contentCallbacks_[i]();
600 } catch (e) {
601 console.error(e);
602 }
603 }
604 }
605 };
606
607 /**
608 * Add a listener for content changes.
609 * @param {function} callback Callback.
610 */
611 ImageView.prototype.addContentCallback = function(callback) {
612 this.contentCallbacks_.push(callback);
613 };
614
615 /**
616 * Update the cached thumbnail image.
617 *
618 * @param {HTMLCanvasElement} canvas The source canvas.
619 * @private
620 */
621 ImageView.prototype.updateThumbnail_ = function(canvas) {
622 ImageUtil.trace.resetTimer('thumb');
623 var pixelCount = 10000;
624 var downScale =
625 Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount));
626
627 this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas');
628 this.thumbnailCanvas_.width = Math.round(canvas.width / downScale);
629 this.thumbnailCanvas_.height = Math.round(canvas.height / downScale);
630 Rect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas);
631 ImageUtil.trace.reportTimer('thumb');
632 };
633
634 /**
635 * Replace the displayed image, possibly with slide-in animation.
636 *
637 * @param {HTMLCanvasElement|HTMLVideoElement} content The image element.
638 * @param {Object=} opt_effect Transition effect object.
639 * @param {number=} opt_width Image width.
640 * @param {number=} opt_height Image height.
641 * @param {boolean=} opt_preview True if the image is a preview (not full res).
642 */
643 ImageView.prototype.replace = function(
644 content, opt_effect, opt_width, opt_height, opt_preview) {
645 var oldScreenImage = this.screenImage_;
646
647 this.replaceContent_(content, opt_width, opt_height, opt_preview);
648 if (!opt_effect) {
649 if (oldScreenImage)
650 oldScreenImage.parentNode.removeChild(oldScreenImage);
651 return;
652 }
653
654 var newScreenImage = this.screenImage_;
655
656 if (oldScreenImage)
657 ImageUtil.setAttribute(newScreenImage, 'fade', true);
658 this.setTransform(newScreenImage, opt_effect, 0 /* instant */);
659
660 setTimeout(function() {
661 this.setTransform(newScreenImage, null,
662 opt_effect && opt_effect.getDuration());
663 if (oldScreenImage) {
664 ImageUtil.setAttribute(newScreenImage, 'fade', false);
665 ImageUtil.setAttribute(oldScreenImage, 'fade', true);
666 console.assert(opt_effect.getReverse, 'Cannot revert an effect.');
667 var reverse = opt_effect.getReverse();
668 this.setTransform(oldScreenImage, reverse);
669 setTimeout(function() {
670 if (oldScreenImage.parentNode)
671 oldScreenImage.parentNode.removeChild(oldScreenImage);
672 }, reverse.getSafeInterval());
673 }
674 }.bind(this), 0);
675 };
676
677 /**
678 * @param {HTMLCanvasElement|HTMLVideoElement} element The element to transform.
679 * @param {ImageView.Effect=} opt_effect The effect to apply.
680 * @param {number=} opt_duration Transition duration.
681 */
682 ImageView.prototype.setTransform = function(element, opt_effect, opt_duration) {
683 if (!opt_effect)
684 opt_effect = new ImageView.Effect.None();
685 if (typeof opt_duration != 'number')
686 opt_duration = opt_effect.getDuration();
687 element.style.webkitTransitionDuration = opt_duration + 'ms';
688 element.style.webkitTransitionTimingFunction = opt_effect.getTiming();
689 element.style.webkitTransform = opt_effect.transform(element, this.viewport_);
690 };
691
692 /**
693 * @param {Rect} screenRect Target rectangle in screen coordinates.
694 * @return {ImageView.Effect.Zoom} Zoom effect object.
695 */
696 ImageView.prototype.createZoomEffect = function(screenRect) {
697 return new ImageView.Effect.Zoom(
698 this.viewport_.screenToDeviceRect(screenRect),
699 null /* use viewport */,
700 ImageView.MODE_TRANSITION_DURATION);
701 };
702
703 /**
704 * Visualize crop or rotate operation. Hide the old image instantly, animate
705 * the new image to visualize the operation.
706 *
707 * @param {HTMLCanvasElement} canvas New content canvas.
708 * @param {Rect} imageCropRect The crop rectangle in image coordinates.
709 * Null for rotation operations.
710 * @param {number} rotate90 Rotation angle in 90 degree increments.
711 * @return {number} Animation duration.
712 */
713 ImageView.prototype.replaceAndAnimate = function(
714 canvas, imageCropRect, rotate90) {
715 var oldScale = this.viewport_.getScale();
716 var deviceCropRect = imageCropRect && this.viewport_.screenToDeviceRect(
717 this.viewport_.imageToScreenRect(imageCropRect));
718
719 var oldScreenImage = this.screenImage_;
720 this.replaceContent_(canvas);
721 var newScreenImage = this.screenImage_;
722
723 // Display the new canvas, initially transformed.
724 var deviceFullRect = this.viewport_.getDeviceClipped();
725
726 var effect = rotate90 ?
727 new ImageView.Effect.Rotate(
728 oldScale / this.viewport_.getScale(), -rotate90) :
729 new ImageView.Effect.Zoom(deviceCropRect, deviceFullRect);
730
731 this.setTransform(newScreenImage, effect, 0 /* instant */);
732
733 oldScreenImage.parentNode.appendChild(newScreenImage);
734 oldScreenImage.parentNode.removeChild(oldScreenImage);
735
736 // Let the layout fire, then animate back to non-transformed state.
737 setTimeout(
738 this.setTransform.bind(
739 this, newScreenImage, null, effect.getDuration()),
740 0);
741
742 return effect.getSafeInterval();
743 };
744
745 /**
746 * Visualize "undo crop". Shrink the current image to the given crop rectangle
747 * while fading in the new image.
748 *
749 * @param {HTMLCanvasElement} canvas New content canvas.
750 * @param {Rect} imageCropRect The crop rectangle in image coordinates.
751 * @return {number} Animation duration.
752 */
753 ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) {
754 var deviceFullRect = this.viewport_.getDeviceClipped();
755 var oldScale = this.viewport_.getScale();
756
757 var oldScreenImage = this.screenImage_;
758 this.replaceContent_(canvas);
759 var newScreenImage = this.screenImage_;
760
761 var deviceCropRect = this.viewport_.screenToDeviceRect(
762 this.viewport_.imageToScreenRect(imageCropRect));
763
764 var setFade = ImageUtil.setAttribute.bind(null, newScreenImage, 'fade');
765 setFade(true);
766 oldScreenImage.parentNode.insertBefore(newScreenImage, oldScreenImage);
767
768 var effect = new ImageView.Effect.Zoom(deviceCropRect, deviceFullRect);
769 // Animate to the transformed state.
770 this.setTransform(oldScreenImage, effect);
771
772 setTimeout(setFade.bind(null, false), 0);
773
774 setTimeout(function() {
775 if (oldScreenImage.parentNode)
776 oldScreenImage.parentNode.removeChild(oldScreenImage);
777 }, effect.getSafeInterval());
778
779 return effect.getSafeInterval();
780 };
781
782
783 /**
784 * Generic cache with a limited capacity and LRU eviction.
785 *
786 * @param {number} capacity Maximum number of cached item.
787 * @constructor
788 */
789 ImageView.Cache = function(capacity) {
790 this.capacity_ = capacity;
791 this.map_ = {};
792 this.order_ = [];
793 };
794
795 /**
796 * Fetch the item from the cache.
797 *
798 * @param {string} id The item ID.
799 * @return {Object} The cached item.
800 */
801 ImageView.Cache.prototype.getItem = function(id) { return this.map_[id] };
802
803 /**
804 * Put the item into the cache.
805 * @param {string} id The item ID.
806 * @param {Object} item The item object.
807 * @param {boolean=} opt_keepLRU True if the LRU order should not be modified.
808 */
809 ImageView.Cache.prototype.putItem = function(id, item, opt_keepLRU) {
810 var pos = this.order_.indexOf(id);
811
812 if ((pos >= 0) != (id in this.map_))
813 throw new Error('Inconsistent cache state');
814
815 if (id in this.map_) {
816 if (!opt_keepLRU) {
817 // Move to the end (most recently used).
818 this.order_.splice(pos, 1);
819 this.order_.push(id);
820 }
821 } else {
822 this.evictLRU();
823 this.order_.push(id);
824 }
825
826 if ((pos >= 0) && (item != this.map_[id]))
827 this.deleteItem_(this.map_[id]);
828 this.map_[id] = item;
829
830 if (this.order_.length > this.capacity_)
831 throw new Error('Exceeded cache capacity');
832 };
833
834 /**
835 * Evict the least recently used items.
836 */
837 ImageView.Cache.prototype.evictLRU = function() {
838 if (this.order_.length == this.capacity_) {
839 var id = this.order_.shift();
840 this.deleteItem_(this.map_[id]);
841 delete this.map_[id];
842 }
843 };
844
845 /**
846 * Change the id of an entry.
847 * @param {string} oldId The old ID.
848 * @param {string} newId The new ID.
849 */
850 ImageView.Cache.prototype.renameItem = function(oldId, newId) {
851 if (oldId == newId)
852 return; // No need to rename.
853
854 var pos = this.order_.indexOf(oldId);
855 if (pos < 0)
856 return; // Not cached.
857
858 this.order_[pos] = newId;
859 this.map_[newId] = this.map_[oldId];
860 delete this.map_[oldId];
861 };
862
863 /**
864 * Disposes an object.
865 *
866 * @param {Object} item The item object.
867 * @private
868 */
869 ImageView.Cache.prototype.deleteItem_ = function(item) {
870 // Trick to reduce memory usage without waiting for gc.
871 if (item instanceof HTMLCanvasElement) {
872 // If the canvas is being used somewhere else (eg. displayed on the screen),
873 // it will be cleared.
874 item.width = 0;
875 item.height = 0;
876 }
877 };
878
879 /* Transition effects */
880
881 /**
882 * Base class for effects.
883 *
884 * @param {number} duration Duration in ms.
885 * @param {string=} opt_timing CSS transition timing function name.
886 * @constructor
887 */
888 ImageView.Effect = function(duration, opt_timing) {
889 this.duration_ = duration;
890 this.timing_ = opt_timing || 'linear';
891 };
892
893 /**
894 *
895 */
896 ImageView.Effect.DEFAULT_DURATION = 180;
897
898 /**
899 *
900 */
901 ImageView.Effect.MARGIN = 100;
902
903 /**
904 * @return {number} Effect duration in ms.
905 */
906 ImageView.Effect.prototype.getDuration = function() { return this.duration_ };
907
908 /**
909 * @return {number} Delay in ms since the beginning of the animation after which
910 * it is safe to perform CPU-heavy operations without disrupting the animation.
911 */
912 ImageView.Effect.prototype.getSafeInterval = function() {
913 return this.getDuration() + ImageView.Effect.MARGIN;
914 };
915
916 /**
917 * @return {string} CSS transition timing function name.
918 */
919 ImageView.Effect.prototype.getTiming = function() { return this.timing_ };
920
921 /**
922 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
923 * @return {number} Preferred pixel ration to use with this element.
924 * @private
925 */
926 ImageView.Effect.getPixelRatio_ = function(element) {
927 if (element.constructor.name == 'HTMLCanvasElement')
928 return Viewport.getDevicePixelRatio();
929 else
930 return 1;
931 };
932
933 /**
934 * Default effect. It is not a no-op as it needs to adjust a canvas scale
935 * for devicePixelRatio.
936 *
937 * @constructor
938 */
939 ImageView.Effect.None = function() {
940 ImageView.Effect.call(this, 0);
941 };
942
943 /**
944 * Inherits from ImageView.Effect.
945 */
946 ImageView.Effect.None.prototype = { __proto__: ImageView.Effect.prototype };
947
948 /**
949 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
950 * @return {string} Transform string.
951 */
952 ImageView.Effect.None.prototype.transform = function(element) {
953 var ratio = ImageView.Effect.getPixelRatio_(element);
954 return 'scale(' + (1 / ratio) + ')';
955 };
956
957 /**
958 * Slide effect.
959 *
960 * @param {number} direction -1 for left, 1 for right.
961 * @param {boolean=} opt_slow True if slow (as in slideshow).
962 * @constructor
963 */
964 ImageView.Effect.Slide = function Slide(direction, opt_slow) {
965 ImageView.Effect.call(this,
966 opt_slow ? 800 : ImageView.Effect.DEFAULT_DURATION, 'ease-in-out');
967 this.direction_ = direction;
968 this.slow_ = opt_slow;
969 this.shift_ = opt_slow ? 100 : 40;
970 if (this.direction_ < 0) this.shift_ = -this.shift_;
971 };
972
973 /**
974 * Inherits from ImageView.Effect.
975 */
976 ImageView.Effect.Slide.prototype = { __proto__: ImageView.Effect.prototype };
977
978 /**
979 * @return {ImageView.Effect.Slide} Reverse Slide effect.
980 */
981 ImageView.Effect.Slide.prototype.getReverse = function() {
982 return new ImageView.Effect.Slide(-this.direction_, this.slow_);
983 };
984
985 /**
986 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
987 * @return {string} Transform string.
988 */
989 ImageView.Effect.Slide.prototype.transform = function(element) {
990 var ratio = ImageView.Effect.getPixelRatio_(element);
991 return 'scale(' + (1 / ratio) + ') translate(' + this.shift_ + 'px, 0px)';
992 };
993
994 /**
995 * Zoom effect.
996 *
997 * Animates the original rectangle to the target rectangle. Both parameters
998 * should be given in device coordinates (accounting for devicePixelRatio).
999 *
1000 * @param {Rect} deviceTargetRect Target rectangle.
1001 * @param {Rect=} opt_deviceOriginalRect Original rectangle. If omitted,
1002 * the full viewport will be used at the time of |transform| call.
1003 * @param {number=} opt_duration Duration in ms.
1004 * @constructor
1005 */
1006 ImageView.Effect.Zoom = function(
1007 deviceTargetRect, opt_deviceOriginalRect, opt_duration) {
1008 ImageView.Effect.call(this,
1009 opt_duration || ImageView.Effect.DEFAULT_DURATION);
1010 this.target_ = deviceTargetRect;
1011 this.original_ = opt_deviceOriginalRect;
1012 };
1013
1014 /**
1015 * Inherits from ImageView.Effect.
1016 */
1017 ImageView.Effect.Zoom.prototype = { __proto__: ImageView.Effect.prototype };
1018
1019 /**
1020 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
1021 * @param {Viewport} viewport Viewport.
1022 * @return {string} Transform string.
1023 */
1024 ImageView.Effect.Zoom.prototype.transform = function(element, viewport) {
1025 if (!this.original_)
1026 this.original_ = viewport.getDeviceClipped();
1027
1028 var ratio = ImageView.Effect.getPixelRatio_(element);
1029
1030 var dx = (this.target_.left + this.target_.width / 2) -
1031 (this.original_.left + this.original_.width / 2);
1032 var dy = (this.target_.top + this.target_.height / 2) -
1033 (this.original_.top + this.original_.height / 2);
1034
1035 var scaleX = this.target_.width / this.original_.width;
1036 var scaleY = this.target_.height / this.original_.height;
1037
1038 return 'translate(' + (dx / ratio) + 'px,' + (dy / ratio) + 'px) ' +
1039 'scaleX(' + (scaleX / ratio) + ') scaleY(' + (scaleY / ratio) + ')';
1040 };
1041
1042 /**
1043 * Rotate effect.
1044 *
1045 * @param {number} scale Scale.
1046 * @param {number} rotate90 Rotation in 90 degrees increments.
1047 * @constructor
1048 */
1049 ImageView.Effect.Rotate = function(scale, rotate90) {
1050 ImageView.Effect.call(this, ImageView.Effect.DEFAULT_DURATION);
1051 this.scale_ = scale;
1052 this.rotate90_ = rotate90;
1053 };
1054
1055 /**
1056 * Inherits from ImageView.Effect.
1057 */
1058 ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype };
1059
1060 /**
1061 * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
1062 * @return {string} Transform string.
1063 */
1064 ImageView.Effect.Rotate.prototype.transform = function(element) {
1065 var ratio = ImageView.Effect.getPixelRatio_(element);
1066 return 'rotate(' + (this.rotate90_ * 90) + 'deg) ' +
1067 'scale(' + (this.scale_ / ratio) + ')';
1068 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698