OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * The overlay displaying the image. | 6 * The overlay displaying the image. |
| 7 * @param {HTMLElement} container The container element. |
| 8 * @param {Viewport} viewport The viewport. |
| 9 * @param {MetadataProvider} metadataProvider The metadataProvider. |
7 */ | 10 */ |
8 function ImageView(container, viewport, metadataProvider) { | 11 function ImageView(container, viewport, metadataProvider) { |
9 this.container_ = container; | 12 this.container_ = container; |
10 this.viewport_ = viewport; | 13 this.viewport_ = viewport; |
11 this.document_ = container.ownerDocument; | 14 this.document_ = container.ownerDocument; |
12 this.contentGeneration_ = 0; | 15 this.contentGeneration_ = 0; |
13 this.displayedContentGeneration_ = 0; | 16 this.displayedContentGeneration_ = 0; |
14 this.displayedViewportGeneration_ = 0; | 17 this.displayedViewportGeneration_ = 0; |
15 | 18 |
16 this.imageLoader_ = new ImageUtil.ImageLoader(this.document_); | 19 this.imageLoader_ = new ImageUtil.ImageLoader(this.document_); |
(...skipping 19 matching lines...) Expand all Loading... |
36 */ | 39 */ |
37 this.screenImage_ = null; | 40 this.screenImage_ = null; |
38 | 41 |
39 this.localImageTransformFetcher_ = function(url, callback) { | 42 this.localImageTransformFetcher_ = function(url, callback) { |
40 metadataProvider.fetchLocal(url, function(metadata) { | 43 metadataProvider.fetchLocal(url, function(metadata) { |
41 callback(metadata.imageTransform); | 44 callback(metadata.imageTransform); |
42 }); | 45 }); |
43 }; | 46 }; |
44 } | 47 } |
45 | 48 |
| 49 /** |
| 50 * Duration of image editing transitions. |
| 51 */ |
46 ImageView.ANIMATION_DURATION = 180; | 52 ImageView.ANIMATION_DURATION = 180; |
| 53 |
| 54 /** |
| 55 * A timeout for use with setTimeout when one wants to wait until the animation |
| 56 * is done. Times 2 is added as a safe margin. |
| 57 */ |
47 ImageView.ANIMATION_WAIT_INTERVAL = ImageView.ANIMATION_DURATION * 2; | 58 ImageView.ANIMATION_WAIT_INTERVAL = ImageView.ANIMATION_DURATION * 2; |
| 59 |
| 60 /** |
| 61 * If the user flips though images faster than this interval we do not apply |
| 62 * the slide-in/slide-out transition. |
| 63 */ |
48 ImageView.FAST_SCROLL_INTERVAL = 300; | 64 ImageView.FAST_SCROLL_INTERVAL = 300; |
49 | 65 |
| 66 /** |
| 67 * Image load type: full resolution image loaded from cache. |
| 68 */ |
50 ImageView.LOAD_TYPE_CACHED_FULL = 0; | 69 ImageView.LOAD_TYPE_CACHED_FULL = 0; |
| 70 |
| 71 /** |
| 72 * Image load type: screeb resolution preview loaded from cache. |
| 73 */ |
51 ImageView.LOAD_TYPE_CACHED_SCREEN = 1; | 74 ImageView.LOAD_TYPE_CACHED_SCREEN = 1; |
| 75 |
| 76 /** |
| 77 * Image load type: image read from file. |
| 78 */ |
52 ImageView.LOAD_TYPE_IMAGE_FILE = 2; | 79 ImageView.LOAD_TYPE_IMAGE_FILE = 2; |
| 80 |
| 81 /** |
| 82 * Image load type: video loaded. |
| 83 */ |
53 ImageView.LOAD_TYPE_VIDEO_FILE = 3; | 84 ImageView.LOAD_TYPE_VIDEO_FILE = 3; |
| 85 |
| 86 /** |
| 87 * Image load type: error occurred. |
| 88 */ |
54 ImageView.LOAD_TYPE_ERROR = 4; | 89 ImageView.LOAD_TYPE_ERROR = 4; |
| 90 |
| 91 /** |
| 92 * The total number of load types. |
| 93 */ |
55 ImageView.LOAD_TYPE_TOTAL = 5; | 94 ImageView.LOAD_TYPE_TOTAL = 5; |
56 | 95 |
57 ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype}; | 96 ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype}; |
58 | 97 |
59 // Draw below overlays with the default zIndex. | 98 /** |
| 99 * Draw below overlays with the default zIndex. |
| 100 * @return {number} Z-index |
| 101 */ |
60 ImageView.prototype.getZIndex = function() { return -1 }; | 102 ImageView.prototype.getZIndex = function() { return -1 }; |
61 | 103 |
| 104 /** |
| 105 * Draw the image on screen. |
| 106 */ |
62 ImageView.prototype.draw = function() { | 107 ImageView.prototype.draw = function() { |
63 if (!this.contentCanvas_) // Do nothing if the image content is not set. | 108 if (!this.contentCanvas_) // Do nothing if the image content is not set. |
64 return; | 109 return; |
65 | 110 |
66 var forceRepaint = false; | 111 var forceRepaint = false; |
67 | 112 |
68 if (this.displayedViewportGeneration_ != | 113 if (this.displayedViewportGeneration_ != |
69 this.viewport_.getCacheGeneration()) { | 114 this.viewport_.getCacheGeneration()) { |
70 this.displayedViewportGeneration_ = this.viewport_.getCacheGeneration(); | 115 this.displayedViewportGeneration_ = this.viewport_.getCacheGeneration(); |
71 | 116 |
72 this.setupDeviceBuffer(this.screenImage_); | 117 this.setupDeviceBuffer(this.screenImage_); |
73 | 118 |
74 forceRepaint = true; | 119 forceRepaint = true; |
75 } | 120 } |
76 | 121 |
77 if (forceRepaint || | 122 if (forceRepaint || |
78 this.displayedContentGeneration_ != this.contentGeneration_) { | 123 this.displayedContentGeneration_ != this.contentGeneration_) { |
79 this.displayedContentGeneration_ = this.contentGeneration_; | 124 this.displayedContentGeneration_ = this.contentGeneration_; |
80 | 125 |
81 ImageUtil.trace.resetTimer('paint'); | 126 ImageUtil.trace.resetTimer('paint'); |
82 this.paintDeviceRect(this.viewport_.getDeviceClipped(), | 127 this.paintDeviceRect(this.viewport_.getDeviceClipped(), |
83 this.contentCanvas_, this.viewport_.getImageClipped()); | 128 this.contentCanvas_, this.viewport_.getImageClipped()); |
84 ImageUtil.trace.reportTimer('paint'); | 129 ImageUtil.trace.reportTimer('paint'); |
85 } | 130 } |
86 }; | 131 }; |
87 | 132 |
88 ImageView.prototype.getCursorStyle = function (x, y, mouseDown) { | 133 /** |
| 134 * @param {number} x X pointer position. |
| 135 * @param {number} y Y pointer position. |
| 136 * @param {boolean} mouseDown True if mouse is down. |
| 137 * @return {string} CSS cursor style. |
| 138 */ |
| 139 ImageView.prototype.getCursorStyle = function(x, y, mouseDown) { |
89 // Indicate that the image is draggable. | 140 // Indicate that the image is draggable. |
90 if (this.viewport_.isClipped() && | 141 if (this.viewport_.isClipped() && |
91 this.viewport_.getScreenClipped().inside(x, y)) | 142 this.viewport_.getScreenClipped().inside(x, y)) |
92 return 'move'; | 143 return 'move'; |
93 | 144 |
94 return null; | 145 return null; |
95 }; | 146 }; |
96 | 147 |
97 ImageView.prototype.getDragHandler = function (x, y) { | 148 /** |
| 149 * @param {number} x X pointer position. |
| 150 * @param {number} y Y pointer position. |
| 151 * @return {function} The closure to call on drag. |
| 152 */ |
| 153 ImageView.prototype.getDragHandler = function(x, y) { |
98 var cursor = this.getCursorStyle(x, y); | 154 var cursor = this.getCursorStyle(x, y); |
99 if (cursor == 'move') { | 155 if (cursor == 'move') { |
100 // Return the handler that drags the entire image. | 156 // Return the handler that drags the entire image. |
101 return this.viewport_.createOffsetSetter(x, y); | 157 return this.viewport_.createOffsetSetter(x, y); |
102 } | 158 } |
103 | 159 |
104 return null; | 160 return null; |
105 }; | 161 }; |
106 | 162 |
| 163 /** |
| 164 * @return {number} The cache generation. |
| 165 */ |
107 ImageView.prototype.getCacheGeneration = function() { | 166 ImageView.prototype.getCacheGeneration = function() { |
108 return this.contentGeneration_; | 167 return this.contentGeneration_; |
109 }; | 168 }; |
110 | 169 |
| 170 /** |
| 171 * Invalidate the caches to force redrawing the screen canvas. |
| 172 */ |
111 ImageView.prototype.invalidateCaches = function() { | 173 ImageView.prototype.invalidateCaches = function() { |
112 this.contentGeneration_++; | 174 this.contentGeneration_++; |
113 }; | 175 }; |
114 | 176 |
| 177 /** |
| 178 * @return {HTMLCanvasElement} The content canvas element. |
| 179 */ |
115 ImageView.prototype.getCanvas = function() { return this.contentCanvas_ }; | 180 ImageView.prototype.getCanvas = function() { return this.contentCanvas_ }; |
116 | 181 |
| 182 /** |
| 183 * @return {boolean} True if the a valid image is currently loaded. |
| 184 */ |
117 ImageView.prototype.hasValidImage = function() { | 185 ImageView.prototype.hasValidImage = function() { |
118 return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width; | 186 return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width; |
119 }; | 187 }; |
120 | 188 |
| 189 /** |
| 190 * @return {HTMLVideoElement} The video element. |
| 191 */ |
121 ImageView.prototype.getVideo = function() { return this.videoElement_ }; | 192 ImageView.prototype.getVideo = function() { return this.videoElement_ }; |
122 | 193 |
| 194 /** |
| 195 * @return {HTMLCanvasElement} The cached thumbnail image. |
| 196 */ |
123 ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_ }; | 197 ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_ }; |
124 | 198 |
| 199 /** |
| 200 * @return {number} The content revision number. |
| 201 */ |
125 ImageView.prototype.getContentRevision = function() { | 202 ImageView.prototype.getContentRevision = function() { |
126 return this.contentRevision_; | 203 return this.contentRevision_; |
127 }; | 204 }; |
128 | 205 |
129 /** | 206 /** |
130 * Copy an image fragment from a full resolution canvas to a device resolution | 207 * Copy an image fragment from a full resolution canvas to a device resolution |
131 * canvas. | 208 * canvas. |
132 * | 209 * |
133 * @param {Rect} deviceRect Rectangle in the device coordinates. | 210 * @param {Rect} deviceRect Rectangle in the device coordinates. |
134 * @param {HTMLCanvasElement} canvas Full resolution canvas. | 211 * @param {HTMLCanvasElement} canvas Full resolution canvas. |
135 * @param {Rect} imageRect Rectangle in the full resolution canvas. | 212 * @param {Rect} imageRect Rectangle in the full resolution canvas. |
136 */ | 213 */ |
137 ImageView.prototype.paintDeviceRect = function (deviceRect, canvas, imageRect) { | 214 ImageView.prototype.paintDeviceRect = function(deviceRect, canvas, imageRect) { |
138 // Map screen canvas (0,0) to (deviceBounds.left, deviceBounds.top) | 215 // Map screen canvas (0,0) to (deviceBounds.left, deviceBounds.top) |
139 var deviceBounds = this.viewport_.getDeviceClipped(); | 216 var deviceBounds = this.viewport_.getDeviceClipped(); |
140 deviceRect = deviceRect.shift(-deviceBounds.left, -deviceBounds.top); | 217 deviceRect = deviceRect.shift(-deviceBounds.left, -deviceBounds.top); |
141 | 218 |
142 // The source canvas may have different physical size than the image size | 219 // The source canvas may have different physical size than the image size |
143 // set at the viewport. Adjust imageRect accordingly. | 220 // set at the viewport. Adjust imageRect accordingly. |
144 var bounds = this.viewport_.getImageBounds(); | 221 var bounds = this.viewport_.getImageBounds(); |
145 var scaleX = canvas.width / bounds.width; | 222 var scaleX = canvas.width / bounds.width; |
146 var scaleY = canvas.height / bounds.height; | 223 var scaleY = canvas.height / bounds.height; |
147 imageRect = new Rect(imageRect.left * scaleX, imageRect.top * scaleY, | 224 imageRect = new Rect(imageRect.left * scaleX, imageRect.top * scaleY, |
148 imageRect.width * scaleX, imageRect.height * scaleY); | 225 imageRect.width * scaleX, imageRect.height * scaleY); |
149 Rect.drawImage( | 226 Rect.drawImage( |
150 this.screenImage_.getContext("2d"), canvas, deviceRect, imageRect); | 227 this.screenImage_.getContext('2d'), canvas, deviceRect, imageRect); |
151 }; | 228 }; |
152 | 229 |
153 /** | 230 /** |
154 * Create an overlay canvas with properties similar to the screen canvas. | 231 * Create an overlay canvas with properties similar to the screen canvas. |
155 * Useful for showing quick feedback when editing. | 232 * Useful for showing quick feedback when editing. |
156 * | 233 * |
157 * @return {HTMLCanvasElement} Overlay canvas | 234 * @return {HTMLCanvasElement} Overlay canvas |
158 */ | 235 */ |
159 ImageView.prototype.createOverlayCanvas = function() { | 236 ImageView.prototype.createOverlayCanvas = function() { |
160 var canvas = this.document_.createElement('canvas'); | 237 var canvas = this.document_.createElement('canvas'); |
(...skipping 20 matching lines...) Expand all Loading... |
181 canvas.style.left = deviceRect.left + 'px'; | 258 canvas.style.left = deviceRect.left + 'px'; |
182 canvas.style.top = deviceRect.top + 'px'; | 259 canvas.style.top = deviceRect.top + 'px'; |
183 | 260 |
184 // Scale the canvas down down to screen pixels. | 261 // Scale the canvas down down to screen pixels. |
185 this.setTransform(canvas); | 262 this.setTransform(canvas); |
186 }; | 263 }; |
187 | 264 |
188 /** | 265 /** |
189 * @return {ImageData} A new ImageData object with a copy of the content. | 266 * @return {ImageData} A new ImageData object with a copy of the content. |
190 */ | 267 */ |
191 ImageView.prototype.copyScreenImageData = function () { | 268 ImageView.prototype.copyScreenImageData = function() { |
192 return this.screenImage_.getContext("2d").getImageData( | 269 return this.screenImage_.getContext('2d').getImageData( |
193 0, 0, this.screenImage_.width, this.screenImage_.height); | 270 0, 0, this.screenImage_.width, this.screenImage_.height); |
194 }; | 271 }; |
195 | 272 |
| 273 /** |
| 274 * @return {boolean} True if the image is currently being loaded. |
| 275 */ |
196 ImageView.prototype.isLoading = function() { | 276 ImageView.prototype.isLoading = function() { |
197 return this.imageLoader_.isBusy(); | 277 return this.imageLoader_.isBusy(); |
198 }; | 278 }; |
199 | 279 |
| 280 /** |
| 281 * Cancel the current image loading operation. The callbacks will be ignored. |
| 282 */ |
200 ImageView.prototype.cancelLoad = function() { | 283 ImageView.prototype.cancelLoad = function() { |
201 this.imageLoader_.cancel(); | 284 this.imageLoader_.cancel(); |
202 }; | 285 }; |
203 | 286 |
204 /** | 287 /** |
205 * Load and display a new image. | 288 * Load and display a new image. |
206 * | 289 * |
207 * Loads the thumbnail first, then replaces it with the main image. | 290 * Loads the thumbnail first, then replaces it with the main image. |
208 * Takes into account the image orientation encoded in the metadata. | 291 * Takes into account the image orientation encoded in the metadata. |
209 * | 292 * |
210 * @param {number} id Unique image id for caching purposes | 293 * @param {number} id Unique image id for caching purposes. |
211 * @param {string|HTMLCanvasElement} source | 294 * @param {string} url Image url. |
212 * @param {Object} metadata | 295 * @param {Object} metadata Metadata. |
213 * @param {Object} slide Slide-in animation direction. | 296 * @param {Object} slide Slide-in animation direction. |
214 * @param {function(number} opt_callback The parameter is the load type. | 297 * @param {function(number} opt_callback The parameter is the load type. |
215 */ | 298 */ |
216 ImageView.prototype.load = function( | 299 ImageView.prototype.load = function( |
217 id, source, metadata, slide, opt_callback) { | 300 id, url, metadata, slide, opt_callback) { |
218 | 301 |
219 metadata = metadata|| {}; | 302 metadata = metadata || {}; |
220 | 303 |
221 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime')); | 304 ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime')); |
222 | 305 |
223 var self = this; | 306 var self = this; |
224 | 307 |
225 this.contentID_ = id; | 308 this.contentID_ = id; |
226 this.contentRevision_ = -1; | 309 this.contentRevision_ = -1; |
227 | 310 |
228 var loadingVideo = FileType.getMediaType(source) == 'video'; | 311 var loadingVideo = FileType.getMediaType(url) == 'video'; |
229 if (loadingVideo) { | 312 if (loadingVideo) { |
230 var video = this.document_.createElement('video'); | 313 var video = this.document_.createElement('video'); |
231 if (metadata.thumbnailURL) { | 314 if (metadata.thumbnailURL) { |
232 video.setAttribute('poster', metadata.thumbnailURL); | 315 video.setAttribute('poster', metadata.thumbnailURL); |
233 this.replace(video, slide); // Show the poster immediately. | 316 this.replace(video, slide); // Show the poster immediately. |
234 } | 317 } |
235 video.addEventListener('loadedmetadata', onVideoLoad); | 318 video.addEventListener('loadedmetadata', onVideoLoad); |
236 video.addEventListener('error', onVideoLoad); | 319 video.addEventListener('error', onVideoLoad); |
237 | 320 |
238 // Do not try no stream when offline. | 321 // Do not try no stream when offline. |
239 video.src = (navigator.onLine && metadata.streamingURL) || source; | 322 video.src = (navigator.onLine && metadata.streamingURL) || url; |
240 video.load(); | 323 video.load(); |
241 | 324 |
242 function onVideoLoad() { | 325 function onVideoLoad() { |
243 video.removeEventListener('loadedmetadata', onVideoLoad); | 326 video.removeEventListener('loadedmetadata', onVideoLoad); |
244 video.removeEventListener('error', onVideoLoad); | 327 video.removeEventListener('error', onVideoLoad); |
245 displayMainImage(ImageView.LOAD_TYPE_VIDEO_FILE, slide, | 328 displayMainImage(ImageView.LOAD_TYPE_VIDEO_FILE, slide, |
246 !!metadata.thumbnailURL /* preview shown */, video); | 329 !!metadata.thumbnailURL /* preview shown */, video); |
247 } | 330 } |
248 return; | 331 return; |
249 } | 332 } |
250 var readyContent = this.getReadyContent(id, source); | 333 var cached = this.contentCache_.getItem(id); |
251 if (readyContent) { | 334 if (cached) { |
252 displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL, slide, | 335 displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL, slide, |
253 false /* no preview */, readyContent); | 336 false /* no preview */, cached); |
254 } else { | 337 } else { |
255 var cachedScreen = this.screenCache_.getItem(id); | 338 var cachedScreen = this.screenCache_.getItem(id); |
256 if (cachedScreen) { | 339 if (cachedScreen) { |
257 // We have a cached screen-scale canvas, use it instead of a thumbnail. | 340 // We have a cached screen-scale canvas, use it instead of a thumbnail. |
258 displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, slide, cachedScreen); | 341 displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, slide, cachedScreen); |
259 // As far as the user can tell the image is loaded. We still need to load | 342 // As far as the user can tell the image is loaded. We still need to load |
260 // the full res image to make editing possible, but we can report now. | 343 // the full res image to make editing possible, but we can report now. |
261 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime')); | 344 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime')); |
262 } else if (metadata.thumbnailURL) { | 345 } else if (metadata.thumbnailURL) { |
263 this.imageLoader_.load( | 346 this.imageLoader_.load( |
264 metadata.thumbnailURL, | 347 metadata.thumbnailURL, |
265 function(url, callback) { callback(metadata.thumbnailTransform); }, | 348 function(url, callback) { callback(metadata.thumbnailTransform); }, |
266 displayThumbnail.bind(null, ImageView.LOAD_TYPE_IMAGE_FILE, slide)); | 349 displayThumbnail.bind(null, ImageView.LOAD_TYPE_IMAGE_FILE, slide)); |
267 } else { | 350 } else { |
268 loadMainImage(ImageView.LOAD_TYPE_IMAGE_FILE, slide, source, | 351 loadMainImage(ImageView.LOAD_TYPE_IMAGE_FILE, slide, url, |
269 false /* no preview*/, 0 /* delay */); | 352 false /* no preview*/, 0 /* delay */); |
270 } | 353 } |
271 } | 354 } |
272 | 355 |
273 function displayThumbnail(loadType, slide, canvas) { | 356 function displayThumbnail(loadType, slide, canvas) { |
274 // The thumbnail may have different aspect ratio than the main image. | 357 // The thumbnail may have different aspect ratio than the main image. |
275 // Force the main image proportions to avoid flicker. | 358 // Force the main image proportions to avoid flicker. |
276 var time = Date.now(); | 359 var time = Date.now(); |
277 | 360 |
278 var mainImageLoadDelay = ImageView.ANIMATION_WAIT_INTERVAL; | 361 var mainImageLoadDelay = ImageView.ANIMATION_WAIT_INTERVAL; |
279 var mainImageSlide = slide; | 362 var mainImageSlide = slide; |
280 | 363 |
281 // Do not do slide-in animation when scrolling very fast. | 364 // Do not do slide-in animation when scrolling very fast. |
282 if (self.lastLoadTime_ && | 365 if (self.lastLoadTime_ && |
283 (time - self.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) { | 366 (time - self.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) { |
284 mainImageSlide = 0; | 367 mainImageSlide = 0; |
285 } | 368 } |
286 self.lastLoadTime_ = time; | 369 self.lastLoadTime_ = time; |
287 | 370 |
288 if (canvas.width) { | 371 if (canvas.width) { |
289 if (metadata.remote) { | 372 if (metadata.remote) { |
290 // We do not know the main image size, but chances are that it is large | 373 // We do not know the main image size, but chances are that it is large |
291 // enough. Show the thumbnail at the maximum possible scale. | 374 // enough. Show the thumbnail at the maximum possible scale. |
292 var bounds = self.viewport_.getScreenBounds(); | 375 var bounds = self.viewport_.getScreenBounds(); |
293 var scale = Math.min (bounds.width / canvas.width, | 376 var scale = Math.min(bounds.width / canvas.width, |
294 bounds.height / canvas.height); | 377 bounds.height / canvas.height); |
295 self.replace(canvas, slide, | 378 self.replace(canvas, slide, |
296 canvas.width * scale, canvas.height * scale, true /* preview */); | 379 canvas.width * scale, canvas.height * scale, true /* preview */); |
297 } else { | 380 } else { |
298 self.replace(canvas, slide, | 381 self.replace(canvas, slide, |
299 metadata.width, metadata.height, true /* preview */); | 382 metadata.width, metadata.height, true /* preview */); |
300 } | 383 } |
301 if (!mainImageSlide) mainImageLoadDelay = 0; | 384 if (!mainImageSlide) mainImageLoadDelay = 0; |
302 mainImageSlide = 0; | 385 mainImageSlide = 0; |
303 } else { | 386 } else { |
304 // Thumbnail image load failed, loading the main image immediately. | 387 // Thumbnail image load failed, loading the main image immediately. |
305 mainImageLoadDelay = 0; | 388 mainImageLoadDelay = 0; |
306 } | 389 } |
307 loadMainImage(loadType, mainImageSlide, source, | 390 loadMainImage(loadType, mainImageSlide, url, |
308 canvas.width != 0, mainImageLoadDelay); | 391 canvas.width != 0, mainImageLoadDelay); |
309 } | 392 } |
310 | 393 |
311 function loadMainImage(loadType, slide, contentURL, previewShown, delay) { | 394 function loadMainImage(loadType, slide, contentURL, previewShown, delay) { |
312 if (self.prefetchLoader_.isLoading(contentURL)) { | 395 if (self.prefetchLoader_.isLoading(contentURL)) { |
313 // The image we need is already being prefetched. Initiating another load | 396 // The image we need is already being prefetched. Initiating another load |
314 // would be a waste. Hijack the load instead by overriding the callback. | 397 // would be a waste. Hijack the load instead by overriding the callback. |
315 self.prefetchLoader_.setCallback( | 398 self.prefetchLoader_.setCallback( |
316 displayMainImage.bind(null, loadType, slide, previewShown)); | 399 displayMainImage.bind(null, loadType, slide, previewShown)); |
317 | 400 |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
351 loadType != ImageView.LOAD_TYPE_CACHED_SCREEN) { | 434 loadType != ImageView.LOAD_TYPE_CACHED_SCREEN) { |
352 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime')); | 435 ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime')); |
353 } | 436 } |
354 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('LoadMode'), | 437 ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('LoadMode'), |
355 loadType, ImageView.LOAD_TYPE_TOTAL); | 438 loadType, ImageView.LOAD_TYPE_TOTAL); |
356 if (opt_callback) opt_callback(loadType); | 439 if (opt_callback) opt_callback(loadType); |
357 } | 440 } |
358 }; | 441 }; |
359 | 442 |
360 /** | 443 /** |
361 * Try to get the canvas from the content cache or from the source. | |
362 * | |
363 * @param {number} id Unique image id for caching purposes | |
364 * @param {string|HTMLCanvasElement} source | |
365 */ | |
366 ImageView.prototype.getReadyContent = function(id, source) { | |
367 if (source.constructor.name == 'HTMLCanvasElement') | |
368 return source; | |
369 | |
370 return this.contentCache_.getItem(id); | |
371 }; | |
372 | |
373 /** | |
374 * Prefetch an image. | 444 * Prefetch an image. |
375 * | 445 * |
376 * @param {number} id Unique image id for caching purposes | 446 * @param {number} id Unique image id for caching purposes. |
377 * @param {string|HTMLCanvasElement} source | 447 * @param {string} url The image url. |
378 */ | 448 */ |
379 ImageView.prototype.prefetch = function(id, source) { | 449 ImageView.prototype.prefetch = function(id, url) { |
380 var self = this; | 450 var self = this; |
381 function prefetchDone(canvas) { | 451 function prefetchDone(canvas) { |
382 if (canvas.width) | 452 if (canvas.width) |
383 self.contentCache_.putItem(id, canvas); | 453 self.contentCache_.putItem(id, canvas); |
384 } | 454 } |
385 | 455 |
386 var cached = this.getReadyContent(id, source); | 456 var cached = this.contentCache_.getItem(id); |
387 if (cached) { | 457 if (cached) { |
388 prefetchDone(cached); | 458 prefetchDone(cached); |
389 } else if (FileType.getMediaType(source) == 'image') { | 459 } else if (FileType.getMediaType(url) == 'image') { |
390 // Evict the LRU item before we allocate the new canvas to avoid unneeded | 460 // Evict the LRU item before we allocate the new canvas to avoid unneeded |
391 // strain on memory. | 461 // strain on memory. |
392 this.contentCache_.evictLRU(); | 462 this.contentCache_.evictLRU(); |
393 | 463 |
394 this.prefetchLoader_.load( | 464 this.prefetchLoader_.load( |
395 source, | 465 url, |
396 this.localImageTransformFetcher_, | 466 this.localImageTransformFetcher_, |
397 prefetchDone, | 467 prefetchDone, |
398 ImageView.ANIMATION_WAIT_INTERVAL); | 468 ImageView.ANIMATION_WAIT_INTERVAL); |
399 } | 469 } |
400 }; | 470 }; |
401 | 471 |
| 472 /** |
| 473 * |
| 474 * @param {HTMLCanvasElement|HTMLVideoElement} content The image element. |
| 475 * @param {boolean} opt_reuseScreenCanvas True if it is OK to reuse the screen |
| 476 * resolution canvas. |
| 477 * @param {number} opt_width Image width. |
| 478 * @param {number} opt_height Image height. |
| 479 * @param {boolean} opt_preview True if the image is a preview (not full res). |
| 480 * @private |
| 481 */ |
402 ImageView.prototype.replaceContent_ = function( | 482 ImageView.prototype.replaceContent_ = function( |
403 content, opt_reuseScreenCanvas, opt_width, opt_height, opt_preview) { | 483 content, opt_reuseScreenCanvas, opt_width, opt_height, opt_preview) { |
404 | 484 |
405 if (content.constructor.name == 'HTMLVideoElement') { | 485 if (content.constructor.name == 'HTMLVideoElement') { |
406 this.contentCanvas_ = null; | 486 this.contentCanvas_ = null; |
407 this.videoElement_ = content; | 487 this.videoElement_ = content; |
408 this.screenImage_ = content; | 488 this.screenImage_ = content; |
409 this.screenImage_.className = 'image'; | 489 this.screenImage_.className = 'image'; |
410 return; | 490 return; |
411 } | 491 } |
(...skipping 26 matching lines...) Expand all Loading... |
438 | 518 |
439 // TODO(kaznacheev): It is better to pass screenImage_ as it is usually | 519 // TODO(kaznacheev): It is better to pass screenImage_ as it is usually |
440 // much smaller than contentCanvas_ and still contains the entire image. | 520 // much smaller than contentCanvas_ and still contains the entire image. |
441 // Once we implement zoom/pan we should pass contentCanvas_ instead. | 521 // Once we implement zoom/pan we should pass contentCanvas_ instead. |
442 this.updateThumbnail_(this.screenImage_); | 522 this.updateThumbnail_(this.screenImage_); |
443 | 523 |
444 this.contentRevision_++; | 524 this.contentRevision_++; |
445 for (var i = 0; i != this.contentCallbacks_.length; i++) { | 525 for (var i = 0; i != this.contentCallbacks_.length; i++) { |
446 try { | 526 try { |
447 this.contentCallbacks_[i](); | 527 this.contentCallbacks_[i](); |
448 } catch(e) { | 528 } catch (e) { |
449 console.error(e); | 529 console.error(e); |
450 } | 530 } |
451 } | 531 } |
452 } | 532 } |
453 }; | 533 }; |
454 | 534 |
| 535 /** |
| 536 * Add a listener for content changes. |
| 537 * @param {function} callback Callback. |
| 538 */ |
455 ImageView.prototype.addContentCallback = function(callback) { | 539 ImageView.prototype.addContentCallback = function(callback) { |
456 this.contentCallbacks_.push(callback); | 540 this.contentCallbacks_.push(callback); |
457 }; | 541 }; |
458 | 542 |
| 543 /** |
| 544 * Update the cached thumbnail image. |
| 545 * |
| 546 * @param {HTMLCanvasElement} canvas The source canvas. |
| 547 * @private |
| 548 */ |
459 ImageView.prototype.updateThumbnail_ = function(canvas) { | 549 ImageView.prototype.updateThumbnail_ = function(canvas) { |
460 ImageUtil.trace.resetTimer('thumb'); | 550 ImageUtil.trace.resetTimer('thumb'); |
461 var pixelCount = 10000; | 551 var pixelCount = 10000; |
462 var downScale = | 552 var downScale = |
463 Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount)); | 553 Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount)); |
464 | 554 |
465 this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas'); | 555 this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas'); |
466 this.thumbnailCanvas_.width = Math.round(canvas.width / downScale); | 556 this.thumbnailCanvas_.width = Math.round(canvas.width / downScale); |
467 this.thumbnailCanvas_.height = Math.round(canvas.height / downScale); | 557 this.thumbnailCanvas_.height = Math.round(canvas.height / downScale); |
468 Rect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas); | 558 Rect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas); |
469 ImageUtil.trace.reportTimer('thumb'); | 559 ImageUtil.trace.reportTimer('thumb'); |
470 }; | 560 }; |
471 | 561 |
472 /** | 562 /** |
473 * Replace the displayed image, possibly with slide-in animation. | 563 * Replace the displayed image, possibly with slide-in animation. |
474 * | 564 * |
475 * @param {HTMLCanvasElement|HTMLVideoElement} content | 565 * @param {HTMLCanvasElement|HTMLVideoElement} content The image element. |
476 * @param {number} opt_slide Slide-in animation direction. | 566 * @param {number} opt_slide Slide-in animation direction. |
477 * <0 for right-to-left, > 0 for left-to-right, 0 for no animation. | 567 * <0 for right-to-left, > 0 for left-to-right, 0 for no animation. |
| 568 * @param {number} opt_width Image width. |
| 569 * @param {number} opt_height Image height. |
| 570 * @param {boolean} opt_preview True if the image is a preview (not full res). |
478 */ | 571 */ |
479 ImageView.prototype.replace = function( | 572 ImageView.prototype.replace = function( |
480 content, opt_slide, opt_width, opt_height, opt_preview) { | 573 content, opt_slide, opt_width, opt_height, opt_preview) { |
481 var oldScreenImage = this.screenImage_; | 574 var oldScreenImage = this.screenImage_; |
482 | 575 |
483 this.replaceContent_(content, !opt_slide, opt_width, opt_height, opt_preview); | 576 this.replaceContent_(content, !opt_slide, opt_width, opt_height, opt_preview); |
484 | 577 |
485 opt_slide = opt_slide || 0; | 578 opt_slide = opt_slide || 0; |
486 // TODO(kaznacheev): The line below is too obscure. | 579 // TODO(kaznacheev): The line below is too obscure. |
487 // Refactor the whole 'slide' thing for clarity. | 580 // Refactor the whole 'slide' thing for clarity. |
(...skipping 138 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
626 0); | 719 0); |
627 | 720 |
628 setTimeout(function() { newScreenImage.removeAttribute('fade') }, 0); | 721 setTimeout(function() { newScreenImage.removeAttribute('fade') }, 0); |
629 | 722 |
630 setTimeout(function() { | 723 setTimeout(function() { |
631 oldScreenImage.parentNode.removeChild(oldScreenImage); | 724 oldScreenImage.parentNode.removeChild(oldScreenImage); |
632 }, ImageView.ANIMATION_WAIT_INTERVAL); | 725 }, ImageView.ANIMATION_WAIT_INTERVAL); |
633 }; | 726 }; |
634 | 727 |
635 | 728 |
| 729 /** |
| 730 * Generic cache with a limited capacity and LRU eviction. |
| 731 * |
| 732 * @param {number} capacity Maximum number of cached item. |
| 733 */ |
636 ImageView.Cache = function(capacity) { | 734 ImageView.Cache = function(capacity) { |
637 this.capacity_ = capacity; | 735 this.capacity_ = capacity; |
638 this.map_ = {}; | 736 this.map_ = {}; |
639 this.order_ = []; | 737 this.order_ = []; |
640 }; | 738 }; |
641 | 739 |
| 740 /** |
| 741 * Fetch the item from the cache. |
| 742 * |
| 743 * @param {string} id The item ID. |
| 744 * @return {object} The cached item. |
| 745 */ |
642 ImageView.Cache.prototype.getItem = function(id) { return this.map_[id] }; | 746 ImageView.Cache.prototype.getItem = function(id) { return this.map_[id] }; |
643 | 747 |
| 748 /** |
| 749 * Put the item into the cache. |
| 750 * @param {string} id The item ID. |
| 751 * @param {object} item The item object. |
| 752 * @param {boolean} opt_keepLRU True if the LRU order should not be modified. |
| 753 */ |
644 ImageView.Cache.prototype.putItem = function(id, item, opt_keepLRU) { | 754 ImageView.Cache.prototype.putItem = function(id, item, opt_keepLRU) { |
645 var pos = this.order_.indexOf(id); | 755 var pos = this.order_.indexOf(id); |
646 | 756 |
647 if ((pos >= 0) != (id in this.map_)) | 757 if ((pos >= 0) != (id in this.map_)) |
648 throw new Error('Inconsistent cache state'); | 758 throw new Error('Inconsistent cache state'); |
649 | 759 |
650 if (id in this.map_) { | 760 if (id in this.map_) { |
651 if (!opt_keepLRU) { | 761 if (!opt_keepLRU) { |
652 // Move to the end (most recently used). | 762 // Move to the end (most recently used). |
653 this.order_.splice(pos, 1); | 763 this.order_.splice(pos, 1); |
654 this.order_.push(id); | 764 this.order_.push(id); |
655 } | 765 } |
656 } else { | 766 } else { |
657 this.evictLRU(); | 767 this.evictLRU(); |
658 this.order_.push(id); | 768 this.order_.push(id); |
659 } | 769 } |
660 | 770 |
661 this.map_[id] = item; | 771 this.map_[id] = item; |
662 | 772 |
663 if (this.order_.length > this.capacity_) | 773 if (this.order_.length > this.capacity_) |
664 throw new Error('Exceeded cache capacity'); | 774 throw new Error('Exceeded cache capacity'); |
665 }; | 775 }; |
666 | 776 |
| 777 /** |
| 778 * Evict the least recently used items. |
| 779 */ |
667 ImageView.Cache.prototype.evictLRU = function() { | 780 ImageView.Cache.prototype.evictLRU = function() { |
668 if (this.order_.length == this.capacity_) { | 781 if (this.order_.length == this.capacity_) { |
669 var id = this.order_.shift(); | 782 var id = this.order_.shift(); |
670 delete this.map_[id]; | 783 delete this.map_[id]; |
671 } | 784 } |
672 }; | 785 }; |
OLD | NEW |