| OLD | NEW |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 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 ImageBuffer object holds an offscreen canvas object and | 6 * The ImageBuffer object holds an offscreen canvas object and |
| 7 * draws its content on the screen canvas applying scale and offset. | 7 * draws its content on the screen canvas applying scale and offset. |
| 8 * Supports pluggable overlays that modify the image appearance and behavior. | 8 * Supports pluggable overlays that modify the image appearance and behavior. |
| 9 * @constructor | 9 * @constructor |
| 10 */ | 10 */ |
| 11 function ImageBuffer(screenCanvas) { | 11 function ImageBuffer(screenCanvas) { |
| 12 this.screenCanvas_ = screenCanvas; | 12 this.screenCanvas_ = screenCanvas; |
| 13 this.screenContext_ = this.screenCanvas_.getContext("2d"); | |
| 14 | 13 |
| 15 this.scale_ = 1; | 14 this.viewport_ = new Viewport(this.repaint.bind(this)); |
| 16 this.offsetX_ = 0; | 15 this.viewport_.setScreenSize(screenCanvas.width, screenCanvas.height); |
| 17 this.offsetY_ = 0; | 16 |
| 17 this.content_ = new ImageBuffer.Content( |
| 18 this.viewport_, screenCanvas.ownerDocument); |
| 18 | 19 |
| 19 this.overlays_ = []; | 20 this.overlays_ = []; |
| 20 | 21 this.addOverlay(new ImageBuffer.Margin(this.viewport_)); |
| 21 this.setImageCanvas(this.createBlankCanvas(0, 0)); | 22 this.addOverlay(this.content_); |
| 23 this.addOverlay(new ImageBuffer.Overview(this.viewport_, this.content_)); |
| 22 } | 24 } |
| 23 | 25 |
| 24 /* | 26 ImageBuffer.prototype.getViewport = function() { return this.viewport_ }; |
| 25 * Viewport manipulation. | |
| 26 */ | |
| 27 | 27 |
| 28 ImageBuffer.prototype.setScaleControl = function(scaleControl) { | 28 ImageBuffer.prototype.getContent = function() { return this.content_ }; |
| 29 this.scaleControl_ = scaleControl; | |
| 30 }; | |
| 31 | |
| 32 ImageBuffer.prototype.getScale = function () { return this.scale_ }; | |
| 33 | |
| 34 ImageBuffer.prototype.setScale = function (scale, notify) { | |
| 35 if (this.scale_ == scale) return; | |
| 36 this.scale_ = scale; | |
| 37 if (notify && this.scaleControl_) this.scaleControl_.displayScale(scale); | |
| 38 }; | |
| 39 | |
| 40 ImageBuffer.prototype.getFittingScale = function() { | |
| 41 var scaleX = this.screenCanvas_.width / this.imageCanvas_.width; | |
| 42 var scaleY = this.screenCanvas_.height / this.imageCanvas_.height; | |
| 43 return Math.min(scaleX, scaleY) * 0.85; | |
| 44 }; | |
| 45 | |
| 46 ImageBuffer.prototype.fitImage = function() { | |
| 47 var scale = this.getFittingScale(); | |
| 48 if (this.scaleControl_) this.scaleControl_.setMinScale(scale); | |
| 49 this.setScale(scale, true); | |
| 50 }; | |
| 51 | |
| 52 ImageBuffer.prototype.resizeScreen = function(width, height, keepFitting) { | |
| 53 var wasFitting = this.getScale() == this.getFittingScale(); | |
| 54 | |
| 55 this.screenCanvas_.width = width; | |
| 56 this.screenCanvas_.height = height; | |
| 57 | |
| 58 var minScale = this.getFittingScale(); | |
| 59 if (this.scaleControl_) this.scaleControl_.setMinScale(minScale); | |
| 60 if ((wasFitting && keepFitting) || this.getScale() < minScale) { | |
| 61 this.setScale(minScale, true); | |
| 62 } | |
| 63 this.repaint(); | |
| 64 }; | |
| 65 | |
| 66 ImageBuffer.prototype.getOffsetX = function () { return this.offsetX_; }; | |
| 67 | |
| 68 ImageBuffer.prototype.getOffsetY = function () { return this.offsetY_; }; | |
| 69 | |
| 70 ImageBuffer.prototype.setOffset = function(x, y, ignoreClipping) { | |
| 71 if (!ignoreClipping) { | |
| 72 var limitX = Math.max(0, -this.marginX_ / this.getScale()); | |
| 73 var limitY = Math.max(0, -this.marginY_ / this.getScale()); | |
| 74 x = ImageUtil.clip(-limitX, x, limitX); | |
| 75 y = ImageUtil.clip(-limitY, y, limitY); | |
| 76 } | |
| 77 if (this.offsetX_ == x && this.offsetY_ == y) return; | |
| 78 this.offsetX_ = x; | |
| 79 this.offsetY_ = y; | |
| 80 }; | |
| 81 | |
| 82 ImageBuffer.prototype.setCenter = function(x, y, ignoreClipping) { | |
| 83 this.setOffset( | |
| 84 this.imageWhole_.width / 2 - x, | |
| 85 this.imageWhole_.height / 2 - y, | |
| 86 ignoreClipping); | |
| 87 }; | |
| 88 | |
| 89 /** | |
| 90 * @return {Rect} The visible part of the image, in image coordinates. | |
| 91 */ | |
| 92 ImageBuffer.prototype.getClippedImage = function() { | |
| 93 return this.imageVisible_; | |
| 94 }; | |
| 95 | |
| 96 /** | |
| 97 * @return {Rect} The visible part of the image, in screen coordinates. | |
| 98 */ | |
| 99 ImageBuffer.prototype.getClippedScreen = function() { | |
| 100 return this.screenVisible_; | |
| 101 }; | |
| 102 | |
| 103 /** | |
| 104 * Returns a closure that can be called to pan the image. | |
| 105 * Useful for implementing non-trivial variants of panning (overview etc). | |
| 106 * @param {Number} originalX The x coordinate on the screen canvas that | |
| 107 * corresponds to zero change to offsetX. | |
| 108 * @param {Number} originalY The y coordinate on the screen canvas that | |
| 109 * corresponds to zero change to offsetY. | |
| 110 * @param {Function} scaleFunc returns the current image to screen scale. | |
| 111 * @param {Function} hitFunc returns true if (x,y) is in the valid region. | |
| 112 */ | |
| 113 ImageBuffer.prototype.createOffsetSetter_ = function ( | |
| 114 originalX, originalY, scaleFunc, hitFunc) { | |
| 115 var self = this; | |
| 116 var originalOffsetX = this.offsetX_; | |
| 117 var originalOffsetY = this.offsetY_; | |
| 118 if (!hitFunc) hitFunc = function() { return true; } | |
| 119 if (!scaleFunc) scaleFunc = this.getScale.bind(this); | |
| 120 return function(x, y) { | |
| 121 if (hitFunc(x, y)) { | |
| 122 var scale = scaleFunc(); | |
| 123 self.setOffset( | |
| 124 originalOffsetX + (x - originalX) / scale, | |
| 125 originalOffsetY + (y - originalY) / scale); | |
| 126 self.repaint(); | |
| 127 } | |
| 128 }; | |
| 129 }; | |
| 130 | |
| 131 /** | |
| 132 * @return {Boolean} True if the entire image is visible on the screen canvas. | |
| 133 */ | |
| 134 ImageBuffer.prototype.isFullyVisible = function () { | |
| 135 return this.marginX_ >= 0 && this.marginY_ >= 0; | |
| 136 }; | |
| 137 | |
| 138 ImageBuffer.prototype.updateViewPort = function () { | |
| 139 var scale = this.getScale(); | |
| 140 | |
| 141 this.screenWhole_ = new Rect(this.screenCanvas_); | |
| 142 this.imageWhole_ = new Rect(this.imageCanvas_); | |
| 143 | |
| 144 // Image bounds in screen coordinates. | |
| 145 this.imageOnScreen_ = {}; | |
| 146 | |
| 147 this.imageOnScreen_.width = Math.floor(this.imageWhole_.width * scale); | |
| 148 this.imageOnScreen_.height = Math.floor(this.imageWhole_.height * scale); | |
| 149 | |
| 150 this.marginX_ = Math.floor( | |
| 151 (this.screenCanvas_.width - this.imageOnScreen_.width) / 2); | |
| 152 this.marginY_ = Math.floor( | |
| 153 (this.screenCanvas_.height - this.imageOnScreen_.height) / 2); | |
| 154 | |
| 155 this.imageOnScreen_.left = this.marginX_; | |
| 156 this.imageOnScreen_.top = this.marginY_; | |
| 157 | |
| 158 this.imageVisible_ = new Rect(this.imageWhole_); | |
| 159 this.screenVisible_ = new Rect(this.screenWhole_); | |
| 160 | |
| 161 if (this.marginX_ < 0) { | |
| 162 this.imageOnScreen_.left += | |
| 163 ImageUtil.clip(this.marginX_, this.offsetX_ * scale, -this.marginX_); | |
| 164 this.imageVisible_.left = -this.imageOnScreen_.left / scale; | |
| 165 this.imageVisible_.width = this.screenCanvas_.width / scale; | |
| 166 } else { | |
| 167 this.screenVisible_.left = this.imageOnScreen_.left; | |
| 168 this.screenVisible_.width = this.imageOnScreen_.width; | |
| 169 } | |
| 170 | |
| 171 if (this.marginY_ < 0) { | |
| 172 this.imageOnScreen_.top += | |
| 173 ImageUtil.clip(this.marginY_, this.offsetY_ * scale, -this.marginY_); | |
| 174 this.imageVisible_.top = -this.imageOnScreen_.top / scale; | |
| 175 this.imageVisible_.height = this.screenCanvas_.height / scale; | |
| 176 } else { | |
| 177 this.screenVisible_.top = this.imageOnScreen_.top; | |
| 178 this.screenVisible_.height = this.imageOnScreen_.height; | |
| 179 } | |
| 180 | |
| 181 this.updateOverlays(); | |
| 182 }; | |
| 183 | |
| 184 /* | |
| 185 * Coordinate conversion between the screen canvas and the image. | |
| 186 */ | |
| 187 | |
| 188 ImageBuffer.prototype.screenToImageSize = function(size) { | |
| 189 return size / this.getScale(); | |
| 190 }; | |
| 191 | |
| 192 ImageBuffer.prototype.screenToImageX = function(x) { | |
| 193 return Math.round((x - this.imageOnScreen_.left) / this.getScale()); | |
| 194 }; | |
| 195 | |
| 196 ImageBuffer.prototype.screenToImageY = function(y) { | |
| 197 return Math.round((y - this.imageOnScreen_.top) / this.getScale()); | |
| 198 }; | |
| 199 | |
| 200 ImageBuffer.prototype.screenToImageRect = function(rect) { | |
| 201 return new Rect( | |
| 202 this.screenToImageX(rect.left), | |
| 203 this.screenToImageY(rect.top), | |
| 204 this.screenToImageSize(rect.width), | |
| 205 this.screenToImageSize(rect.height)); | |
| 206 }; | |
| 207 | |
| 208 ImageBuffer.prototype.imageToScreenSize = function(size) { | |
| 209 return size * this.getScale(); | |
| 210 }; | |
| 211 | |
| 212 ImageBuffer.prototype.imageToScreenX = function(x) { | |
| 213 return Math.round(this.imageOnScreen_.left + x * this.getScale()); | |
| 214 }; | |
| 215 | |
| 216 ImageBuffer.prototype.imageToScreenY = function(y) { | |
| 217 return Math.round(this.imageOnScreen_.top + y * this.getScale()); | |
| 218 }; | |
| 219 | |
| 220 ImageBuffer.prototype.imageToScreenRect = function(rect) { | |
| 221 return new Rect( | |
| 222 this.imageToScreenX(rect.left), | |
| 223 this.imageToScreenY(rect.top), | |
| 224 this.imageToScreenSize(rect.width), | |
| 225 this.imageToScreenSize(rect.height)); | |
| 226 }; | |
| 227 | |
| 228 /* | |
| 229 * Content manipulation. | |
| 230 */ | |
| 231 | 29 |
| 232 /** | 30 /** |
| 233 * Loads the new content. | 31 * Loads the new content. |
| 234 * A string parameter is treated as an image url. | 32 * A string parameter is treated as an image url. |
| 235 * @param {String|HTMLImageElement|HTMLCanvasElement} source | 33 * @param {String|HTMLImageElement|HTMLCanvasElement} source |
| 236 */ | 34 */ |
| 237 ImageBuffer.prototype.load = function(source) { | 35 ImageBuffer.prototype.load = function(source) { |
| 238 if (typeof source == 'string') { | 36 if (typeof source == 'string') { |
| 239 var self = this; | 37 var self = this; |
| 240 var image = new Image(); | 38 var image = new Image(); |
| 241 image.onload = function(e) { self.load(e.target); }; | 39 image.onload = function(e) { self.load(e.target); }; |
| 242 image.src = source; | 40 image.src = source; |
| 243 } else { | 41 } else { |
| 244 this.imageCanvas_.width = source.width, | 42 this.content_.load(source); |
| 245 this.imageCanvas_.height = source.height; | |
| 246 this.drawImage(source); | |
| 247 | |
| 248 if (this.scaleControl_) | |
| 249 this.scaleControl_.displayImageSize( | |
| 250 this.imageCanvas_.width, this.imageCanvas_.height); | |
| 251 this.fitImage(); | |
| 252 this.repaint(); | 43 this.repaint(); |
| 253 } | 44 } |
| 254 }; | 45 }; |
| 255 | 46 |
| 256 ImageBuffer.prototype.getImageCanvas = function() { return this.imageCanvas_; }; | 47 ImageBuffer.prototype.resizeScreen = function(width, height, keepFitting) { |
| 48 this.screenCanvas_.width = width; |
| 49 this.screenCanvas_.height = height; |
| 257 | 50 |
| 258 /** | 51 var wasFitting = |
| 259 * Replaces the off-screen canvas. | 52 this.viewport_.getScale() == this.viewport_.getFittingScale(); |
| 260 * To be used when the editor modifies the image dimensions. | |
| 261 * @param {HTMLCanvasElement} canvas | |
| 262 */ | |
| 263 ImageBuffer.prototype.setImageCanvas = function(canvas) { | |
| 264 this.imageCanvas_ = canvas; | |
| 265 this.imageContext_ = canvas.getContext("2d"); | |
| 266 if (this.scaleControl_) | |
| 267 this.scaleControl_.displayImageSize( | |
| 268 this.imageCanvas_.width, this.imageCanvas_.height); | |
| 269 }; | |
| 270 | 53 |
| 271 /** | 54 this.viewport_.setScreenSize(width, height); |
| 272 * @return {HTMLCanvasElement} A new blank canvas of the required size. | |
| 273 */ | |
| 274 ImageBuffer.prototype.createBlankCanvas = function (width, height) { | |
| 275 var canvas = this.screenCanvas_.ownerDocument.createElement('canvas'); | |
| 276 canvas.width = width; | |
| 277 canvas.height = height; | |
| 278 return canvas; | |
| 279 }; | |
| 280 | 55 |
| 281 /** | 56 var minScale = this.viewport_.getFittingScale(); |
| 282 * @return {HTMLCanvasElement} A new canvas with a copy of the content. | 57 if ((wasFitting && keepFitting) || this.viewport_.getScale() < minScale) { |
| 283 */ | 58 this.viewport_.setScale(minScale, true); |
| 284 ImageBuffer.prototype.copyImageCanvas = function () { | 59 } |
| 285 var canvas = this.createBlankCanvas( | 60 this.repaint(); |
| 286 this.imageCanvas_.width, this.imageCanvas_.height); | |
| 287 canvas.getContext('2d').drawImage(this.imageCanvas_, 0, 0); | |
| 288 return canvas; | |
| 289 }; | |
| 290 | |
| 291 /** | |
| 292 * @return {ImageData} A new ImageData object with a copy of the content. | |
| 293 */ | |
| 294 ImageBuffer.prototype.copyImageData = function () { | |
| 295 return this.imageContext_.getImageData( | |
| 296 0, 0, this.imageCanvas_.width, this.imageCanvas_.height); | |
| 297 }; | |
| 298 | |
| 299 /** | |
| 300 * @param {HTMLImageElement|HTMLCanvasElement} image | |
| 301 */ | |
| 302 ImageBuffer.prototype.drawImage = function(image) { | |
| 303 ImageUtil.trace.resetTimer('drawImage'); | |
| 304 this.imageContext_.drawImage(image, 0, 0); | |
| 305 ImageUtil.trace.reportTimer('drawImage'); | |
| 306 }; | |
| 307 | |
| 308 /** | |
| 309 * @param {ImageData} imageData | |
| 310 */ | |
| 311 ImageBuffer.prototype.drawImageData = function (imageData) { | |
| 312 ImageUtil.trace.resetTimer('putImageData'); | |
| 313 this.imageContext_.putImageData(imageData, 0, 0); | |
| 314 ImageUtil.trace.reportTimer('putImageData'); | |
| 315 }; | 61 }; |
| 316 | 62 |
| 317 /** | 63 /** |
| 318 * Paints the content on the screen canvas taking the current scale and offset | 64 * Paints the content on the screen canvas taking the current scale and offset |
| 319 * into account. | 65 * into account. |
| 320 */ | 66 */ |
| 321 ImageBuffer.prototype.repaint = function () { | 67 ImageBuffer.prototype.repaint = function (opt_fromOverlay) { |
| 322 ImageUtil.trace.resetTimer('repaint'); | 68 this.viewport_.update(); |
| 69 this.drawOverlays(this.screenCanvas_.getContext("2d"), opt_fromOverlay); |
| 70 }; |
| 323 | 71 |
| 324 this.updateViewPort(); | 72 ImageBuffer.prototype.repaintScreenRect = function (screenRect, imageRect) { |
| 325 | 73 Rect.drawImage( |
| 326 this.screenContext_.save(); | 74 this.screenCanvas_.getContext('2d'), |
| 327 | 75 this.getContent().getCanvas(), |
| 328 this.screenContext_.fillStyle = '#F0F0F0'; | 76 screenRect || this.getViewport().imageToScreenRect(screenRect), |
| 329 this.screenContext_.strokeStyle = '#000000'; | 77 imageRect || this.getViewport().screenToImageRect(screenRect)); |
| 330 | |
| 331 Rect.drawImage(this.screenContext_, this.imageCanvas_, | |
| 332 this.imageOnScreen_, this.imageWhole_); | |
| 333 Rect.fillBetween(this.screenContext_, this.imageOnScreen_, | |
| 334 this.screenWhole_); | |
| 335 Rect.stroke(this.screenContext_, this.imageOnScreen_); | |
| 336 | |
| 337 this.screenContext_.restore(); | |
| 338 | |
| 339 this.drawOverlays(this.screenContext_); | |
| 340 | |
| 341 ImageUtil.trace.reportTimer('repaint'); | |
| 342 }; | 78 }; |
| 343 | 79 |
| 344 /** | 80 /** |
| 345 * ImageBuffer.Overlay is a pluggable extension that modifies the outlook | |
| 346 * and the behavior of the ImageBuffer instance. | |
| 347 */ | |
| 348 ImageBuffer.Overlay = function() {}; | |
| 349 | |
| 350 ImageBuffer.Overlay.prototype.getZIndex = function() { return 0 }; | |
| 351 | |
| 352 ImageBuffer.Overlay.prototype.updateViewPort = function() {} | |
| 353 | |
| 354 ImageBuffer.Overlay.prototype.draw = function() {} | |
| 355 | |
| 356 ImageBuffer.Overlay.prototype.getCursorStyle = function() { return null }; | |
| 357 | |
| 358 ImageBuffer.Overlay.prototype.onClick = function() { return false }; | |
| 359 | |
| 360 ImageBuffer.Overlay.prototype.getDragHandler = function() { return null }; | |
| 361 | |
| 362 /** | |
| 363 * @param {ImageBuffer.Overlay} overlay | 81 * @param {ImageBuffer.Overlay} overlay |
| 364 */ | 82 */ |
| 365 ImageBuffer.prototype.addOverlay = function (overlay) { | 83 ImageBuffer.prototype.addOverlay = function (overlay) { |
| 366 var zIndex = overlay.getZIndex(); | 84 var zIndex = overlay.getZIndex(); |
| 367 // Store the overlays in the ascending Z-order. | 85 // Store the overlays in the ascending Z-order. |
| 368 var i; | 86 var i; |
| 369 for (i = 0; i != this.overlays_.length; i++) { | 87 for (i = 0; i != this.overlays_.length; i++) { |
| 370 if (zIndex < this.overlays_[i].getZIndex()) break; | 88 if (zIndex < this.overlays_[i].getZIndex()) break; |
| 371 } | 89 } |
| 372 this.overlays_.splice(i, 0, overlay); | 90 this.overlays_.splice(i, 0, overlay); |
| 373 }; | 91 }; |
| 374 | 92 |
| 375 /** | 93 /** |
| 376 * @param {ImageBuffer.Overlay} overlay | 94 * @param {ImageBuffer.Overlay} overlay |
| 377 */ | 95 */ |
| 378 ImageBuffer.prototype.removeOverlay = function (overlay) { | 96 ImageBuffer.prototype.removeOverlay = function (overlay) { |
| 379 for (var i = 0; i != this.overlays_.length; i++) { | 97 for (var i = 0; i != this.overlays_.length; i++) { |
| 380 if (this.overlays_[i] == overlay) { | 98 if (this.overlays_[i] == overlay) { |
| 381 this.overlays_.splice(i, 1); | 99 this.overlays_.splice(i, 1); |
| 382 return; | 100 return; |
| 383 } | 101 } |
| 384 } | 102 } |
| 385 throw new Error('Cannot remove overlay ' + overlay); | 103 throw new Error('Cannot remove overlay ' + overlay); |
| 386 }; | 104 }; |
| 387 | 105 |
| 388 /** | 106 /** |
| 389 * Updates viewport configuration on all overlays. | 107 * Draws overlays in the ascending Z-order. |
| 108 * Skips overlays below opt_startFrom. |
| 390 */ | 109 */ |
| 391 ImageBuffer.prototype.updateOverlays = function (context) { | 110 ImageBuffer.prototype.drawOverlays = function (context, opt_fromOverlay) { |
| 111 var skip = true; |
| 392 for (var i = 0; i != this.overlays_.length; i++) { | 112 for (var i = 0; i != this.overlays_.length; i++) { |
| 393 this.overlays_[i].updateViewPort(this); | 113 var overlay = this.overlays_[i]; |
| 394 } | 114 if (!opt_fromOverlay || opt_fromOverlay == overlay) skip = false; |
| 395 } | 115 if (skip) continue; |
| 396 | 116 |
| 397 /** | |
| 398 * Draws overlays in the ascending Z-order. | |
| 399 */ | |
| 400 ImageBuffer.prototype.drawOverlays = function (context) { | |
| 401 for (var i = 0; i != this.overlays_.length; i++) { | |
| 402 context.save(); | 117 context.save(); |
| 403 this.overlays_[i].draw(context); | 118 overlay.draw(context); |
| 404 context.restore(); | 119 context.restore(); |
| 405 } | 120 } |
| 406 }; | 121 }; |
| 407 | 122 |
| 408 /** | 123 /** |
| 409 * Searches for a cursor style in the descending Z-order. | 124 * Searches for a cursor style in the descending Z-order. |
| 410 * @return {String} A value for style.cursor CSS property. | 125 * @return {String} A value for style.cursor CSS property. |
| 411 */ | 126 */ |
| 412 ImageBuffer.prototype.getCursorStyle = function (x, y, mouseDown) { | 127 ImageBuffer.prototype.getCursorStyle = function (x, y, mouseDown) { |
| 413 for (var i = this.overlays_.length - 1; i >= 0; i--) { | 128 for (var i = this.overlays_.length - 1; i >= 0; i--) { |
| 414 var style = this.overlays_[i].getCursorStyle(x, y, mouseDown); | 129 var style = this.overlays_[i].getCursorStyle(x, y, mouseDown); |
| 415 if (style) return style; | 130 if (style) return style; |
| 416 } | 131 } |
| 417 | |
| 418 // Indicate that the image is draggable. | |
| 419 if (!this.isFullyVisible() && this.screenVisible_.inside(x, y)) | |
| 420 return 'move'; | |
| 421 | |
| 422 return 'default'; | 132 return 'default'; |
| 423 }; | 133 }; |
| 424 | 134 |
| 425 /** | 135 /** |
| 426 * Searches for a click handler in the descending Z-order. | 136 * Searches for a click handler in the descending Z-order. |
| 427 * @return {Boolean} True if handled. | 137 * @return {Boolean} True if handled. |
| 428 */ | 138 */ |
| 429 ImageBuffer.prototype.onClick = function (x, y) { | 139 ImageBuffer.prototype.onClick = function (x, y) { |
| 430 for (var i = this.overlays_.length - 1; i >= 0; i--) { | 140 for (var i = this.overlays_.length - 1; i >= 0; i--) { |
| 431 if (this.overlays_[i].onClick(x, y)) return true; | 141 if (this.overlays_[i].onClick(x, y)) return true; |
| 432 } | 142 } |
| 433 return false; | 143 return false; |
| 434 }; | 144 }; |
| 435 | 145 |
| 436 /** | 146 /** |
| 437 * Searches for a drag handler in the descending Z-order. | 147 * Searches for a drag handler in the descending Z-order. |
| 438 * @return {Function} A closure to be called on mouse drag. | 148 * @return {Function} A closure to be called on mouse drag. |
| 439 */ | 149 */ |
| 440 ImageBuffer.prototype.getDragHandler = function (x, y) { | 150 ImageBuffer.prototype.getDragHandler = function (x, y) { |
| 441 for (var i = this.overlays_.length - 1; i >= 0; i--) { | 151 for (var i = this.overlays_.length - 1; i >= 0; i--) { |
| 442 var handler = this.overlays_[i].getDragHandler(x, y); | 152 var handler = this.overlays_[i].getDragHandler(x, y); |
| 443 if (handler) return handler; | 153 if (handler) return handler; |
| 444 } | 154 } |
| 445 | 155 return null; |
| 446 if (!this.isFullyVisible() && this.screenVisible_.inside(x, y)) { | 156 }; |
| 157 |
| 158 /** |
| 159 * ImageBuffer.Overlay is a pluggable extension that modifies the outlook |
| 160 * and the behavior of the ImageBuffer instance. |
| 161 */ |
| 162 ImageBuffer.Overlay = function() {}; |
| 163 |
| 164 ImageBuffer.Overlay.prototype.getZIndex = function() { return 0 }; |
| 165 |
| 166 ImageBuffer.Overlay.prototype.draw = function() {}; |
| 167 |
| 168 ImageBuffer.Overlay.prototype.getCursorStyle = function() { return null }; |
| 169 |
| 170 ImageBuffer.Overlay.prototype.onClick = function() { return false }; |
| 171 |
| 172 ImageBuffer.Overlay.prototype.getDragHandler = function() { return null }; |
| 173 |
| 174 |
| 175 /** |
| 176 * The margin overlay draws the image outline and paints the margins. |
| 177 */ |
| 178 ImageBuffer.Margin = function(viewport) { |
| 179 this.viewport_ = viewport; |
| 180 }; |
| 181 |
| 182 ImageBuffer.Margin.prototype = {__proto__: ImageBuffer.Overlay.prototype}; |
| 183 |
| 184 // Draw below everything including the content. |
| 185 ImageBuffer.Margin.prototype.getZIndex = function() { return -2 }; |
| 186 |
| 187 ImageBuffer.Margin.prototype.draw = function(context) { |
| 188 context.save(); |
| 189 context.fillStyle = '#F0F0F0'; |
| 190 context.strokeStyle = '#000000'; |
| 191 Rect.fillBetween(context, |
| 192 this.viewport_.getImageBoundsOnScreen(), |
| 193 this.viewport_.getScreenBounds()); |
| 194 Rect.stroke(context, this.viewport_.getImageBoundsOnScreen()); |
| 195 context.restore(); |
| 196 }; |
| 197 |
| 198 /** |
| 199 * The overlay containing the image. |
| 200 */ |
| 201 ImageBuffer.Content = function(viewport, document) { |
| 202 this.viewport_ = viewport; |
| 203 this.document_ = document; |
| 204 |
| 205 this.contentGeneration_ = 0; |
| 206 |
| 207 this.setCanvas(this.createBlankCanvas(0, 0)); |
| 208 }; |
| 209 |
| 210 ImageBuffer.Content.prototype = {__proto__: ImageBuffer.Overlay.prototype}; |
| 211 |
| 212 // Draw below overlays with the default zIndex. |
| 213 ImageBuffer.Content.prototype.getZIndex = function() { return -1 }; |
| 214 |
| 215 ImageBuffer.Content.prototype.draw = function(context) { |
| 216 Rect.drawImage( |
| 217 context, this.canvas_, this.viewport_.getImageBoundsOnScreen()); |
| 218 }; |
| 219 |
| 220 ImageBuffer.Content.prototype.getCursorStyle = function (x, y, mouseDown) { |
| 221 // Indicate that the image is draggable. |
| 222 if (this.viewport_.isClipped() && |
| 223 this.viewport_.getScreenClipped().inside(x, y)) |
| 224 return 'move'; |
| 225 |
| 226 return null; |
| 227 }; |
| 228 |
| 229 ImageBuffer.Content.prototype.getDragHandler = function (x, y) { |
| 230 var cursor = this.getCursorStyle(x, y); |
| 231 if (cursor == 'move') { |
| 447 // Return the handler that drags the entire image. | 232 // Return the handler that drags the entire image. |
| 448 return this.createOffsetSetter_(x, y, this.getScale.bind(this)); | 233 return this.viewport_.createOffsetSetter(x, y); |
| 449 } | 234 } |
| 450 | 235 |
| 451 return null; | 236 return null; |
| 452 }; | 237 }; |
| 453 | 238 |
| 454 /** | 239 ImageBuffer.Content.prototype.getCacheGeneration = function() { |
| 455 * Overview overlay draws the image thumbnail in the bottom right corner. | 240 return this.generation_; |
| 456 * Indicates the currently visible part. | 241 }; |
| 457 * Supports panning by dragging. | 242 |
| 458 */ | 243 ImageBuffer.Content.prototype.invalidateCaches = function() { |
| 459 | 244 this.generation_++; |
| 460 ImageBuffer.Overview = function() {}; | 245 }; |
| 246 |
| 247 ImageBuffer.Content.prototype.getCanvas = function() { return this.canvas_ }; |
| 248 |
| 249 /** |
| 250 * Replaces the off-screen canvas. |
| 251 * To be used when the editor modifies the image dimensions. |
| 252 * If the logical width/height are supplied they override the canvas dimensions |
| 253 * and the canvas contents is scaled when displayed. |
| 254 * @param {HTMLCanvasElement} canvas |
| 255 * @param {Number} opt_width Logical width (=canvas.width by default) |
| 256 * @param {Number} opt_height Logical height (=canvas.height by default) |
| 257 */ |
| 258 ImageBuffer.Content.prototype.setCanvas = function( |
| 259 canvas, opt_width, opt_height) { |
| 260 this.canvas_ = canvas; |
| 261 this.viewport_.setImageSize(opt_width || canvas.width, |
| 262 opt_height || canvas.height); |
| 263 |
| 264 this.invalidateCaches(); |
| 265 }; |
| 266 |
| 267 /** |
| 268 * @return {HTMLCanvasElement} A new blank canvas of the required size. |
| 269 */ |
| 270 ImageBuffer.Content.prototype.createBlankCanvas = function (width, height) { |
| 271 var canvas = this.document_.createElement('canvas'); |
| 272 canvas.width = width; |
| 273 canvas.height = height; |
| 274 return canvas; |
| 275 }; |
| 276 |
| 277 /** |
| 278 * @param {Number} opt_width Width of the copy, original width by default. |
| 279 * @param {Number} opt_height Height of the copy, original height by default. |
| 280 * @return {HTMLCanvasElement} A new canvas with a copy of the content. |
| 281 */ |
| 282 ImageBuffer.Content.prototype.copyCanvas = function (opt_width, opt_height) { |
| 283 var canvas = this.createBlankCanvas(opt_width || this.canvas_.width, |
| 284 opt_height || this.canvas_.height); |
| 285 Rect.drawImage(canvas.getContext('2d'), this.canvas_); |
| 286 return canvas; |
| 287 }; |
| 288 |
| 289 /** |
| 290 * @return {ImageData} A new ImageData object with a copy of the content. |
| 291 */ |
| 292 ImageBuffer.Content.prototype.copyImageData = function (opt_width, opt_height) { |
| 293 return this.canvas_.getContext("2d").getImageData( |
| 294 0, 0, opt_width || this.canvas_.width, opt_height || this.canvas_.height); |
| 295 }; |
| 296 |
| 297 /** |
| 298 * @param {HTMLImageElement|HTMLCanvasElement} image |
| 299 */ |
| 300 ImageBuffer.Content.prototype.load = function(image) { |
| 301 this.canvas_.width = image.width; |
| 302 this.canvas_.height = image.height; |
| 303 |
| 304 Rect.drawImage(this.canvas_.getContext("2d"), image); |
| 305 this.invalidateCaches(); |
| 306 |
| 307 this.viewport_.setImageSize(image.width, image.height); |
| 308 this.viewport_.fitImage(); |
| 309 }; |
| 310 |
| 311 /** |
| 312 * @param {ImageData} imageData |
| 313 */ |
| 314 ImageBuffer.Content.prototype.drawImageData = function (imageData, x, y) { |
| 315 this.canvas_.getContext("2d").putImageData(imageData, x, y); |
| 316 this.invalidateCaches(); |
| 317 }; |
| 318 |
| 319 /** |
| 320 * The overview overlay draws the image thumbnail in the bottom right corner. |
| 321 * Indicates the currently visible part. Supports panning by dragging. |
| 322 */ |
| 323 ImageBuffer.Overview = function(viewport, content) { |
| 324 this.viewport_ = viewport; |
| 325 this.content_ = content; |
| 326 this.contentGeneration_ = 0; |
| 327 }; |
| 328 |
| 329 ImageBuffer.Overview.prototype = {__proto__: ImageBuffer.Overlay.prototype}; |
| 330 |
| 331 // Draw above everything. |
| 332 ImageBuffer.Overview.prototype.getZIndex = function() { return 100 }; |
| 461 | 333 |
| 462 ImageBuffer.Overview.MAX_SIZE = 150; | 334 ImageBuffer.Overview.MAX_SIZE = 150; |
| 463 ImageBuffer.Overview.RIGHT = 7; | 335 ImageBuffer.Overview.RIGHT = 7; |
| 464 ImageBuffer.Overview.BOTTOM = 50; | 336 ImageBuffer.Overview.BOTTOM = 50; |
| 465 | 337 |
| 466 ImageBuffer.Overview.prototype = {__proto__: ImageBuffer.Overlay.prototype}; | 338 ImageBuffer.Overview.prototype.update = function() { |
| 467 | 339 var imageBounds = this.viewport_.getImageBounds(); |
| 468 ImageBuffer.Overview.prototype.getZIndex = function() { return 100; } | 340 |
| 469 | 341 if (this.contentGeneration_ != this.content_.getCacheGeneration()) { |
| 470 ImageBuffer.Overview.prototype.updateViewPort = function(buffer) { | 342 this.contentGeneration_ = this.content_.getCacheGeneration(); |
| 471 this.buffer_ = buffer; | 343 |
| 472 | 344 var aspect = imageBounds.width / imageBounds.height; |
| 473 this.whole_ = null; | 345 if (aspect > 1) { |
| 474 this.visible_ = null; | 346 this.bounds_ = new Rect(ImageBuffer.Overview.MAX_SIZE, |
| 475 | 347 ImageBuffer.Overview.MAX_SIZE / aspect); |
| 476 if (this.buffer_.isFullyVisible()) return; | 348 } else { |
| 477 | 349 this.bounds_ = new Rect(ImageBuffer.Overview.MAX_SIZE * aspect, |
| 478 var screenWhole = this.buffer_.screenWhole_; | 350 ImageBuffer.Overview.MAX_SIZE); |
| 479 var imageWhole = this.buffer_.imageWhole_; | 351 } |
| 480 var imageVisible = this.buffer_.imageVisible_; | 352 |
| 481 | 353 this.canvas_ = |
| 482 var aspect = imageWhole.width / imageWhole.height; | 354 this.content_.copyCanvas(this.bounds_.width, this.bounds_.height); |
| 483 if (aspect > 1) { | 355 } |
| 484 this.whole_ = new Rect(ImageBuffer.Overview.MAX_SIZE, | 356 |
| 485 ImageBuffer.Overview.MAX_SIZE / aspect); | 357 this.clipped_ = null; |
| 486 } else { | 358 |
| 487 this.whole_ = new Rect(ImageBuffer.Overview.MAX_SIZE * aspect, | 359 if (this.viewport_.isClipped()) { |
| 488 ImageBuffer.Overview.MAX_SIZE); | 360 var screenBounds = this.viewport_.getScreenBounds(); |
| 489 } | 361 |
| 490 | 362 this.bounds_ = this.bounds_.moveTo( |
| 491 this.whole_ = this.whole_.moveTo( | 363 screenBounds.width - ImageBuffer.Overview.RIGHT - this.bounds_.width, |
| 492 screenWhole.width - ImageBuffer.Overview.RIGHT - this.whole_.width, | 364 screenBounds.height - ImageBuffer.Overview.BOTTOM - |
| 493 screenWhole.height - ImageBuffer.Overview.BOTTOM - this.whole_.height); | 365 this.bounds_.height); |
| 494 | 366 |
| 495 this.scale_ = this.whole_.width / imageWhole.width; | 367 this.scale_ = this.bounds_.width / imageBounds.width; |
| 496 | 368 |
| 497 this.visible_ = imageVisible. | 369 this.clipped_ = this.viewport_.getImageClipped(). |
| 498 scale(this.scale_). | 370 scale(this.scale_). |
| 499 shift(this.whole_.left, this.whole_.top); | 371 shift(this.bounds_.left, this.bounds_.top); |
| 372 } |
| 500 }; | 373 }; |
| 501 | 374 |
| 502 ImageBuffer.Overview.prototype.draw = function(context) { | 375 ImageBuffer.Overview.prototype.draw = function(context) { |
| 503 if (!this.visible_) return; | 376 this.update(); |
| 377 |
| 378 if (!this.clipped_) return; |
| 504 | 379 |
| 505 // Draw the thumbnail. | 380 // Draw the thumbnail. |
| 506 Rect.drawImage(context, this.buffer_.imageCanvas_, | 381 Rect.drawImage(context, this.canvas_, this.bounds_); |
| 507 this.whole_, this.buffer_.imageWhole_); | |
| 508 | 382 |
| 509 // Draw the thumbnail border. | 383 // Draw the thumbnail border. |
| 510 context.strokeStyle = '#000000'; | 384 context.strokeStyle = '#000000'; |
| 511 Rect.stroke(context, this.whole_); | 385 Rect.stroke(context, this.bounds_); |
| 512 | 386 |
| 513 // Draw the shadow over the off-screen part of the thumbnail. | 387 // Draw the shadow over the off-screen part of the thumbnail. |
| 514 context.globalAlpha = 0.3; | 388 context.globalAlpha = 0.3; |
| 515 context.fillStyle = '#000000'; | 389 context.fillStyle = '#000000'; |
| 516 Rect.fillBetween(context, this.visible_, this.whole_); | 390 Rect.fillBetween(context, this.clipped_, this.bounds_); |
| 517 | 391 |
| 518 // Outline the on-screen part of the thumbnail. | 392 // Outline the on-screen part of the thumbnail. |
| 519 context.strokeStyle = '#FFFFFF'; | 393 context.strokeStyle = '#FFFFFF'; |
| 520 Rect.stroke(context, this.visible_); | 394 Rect.stroke(context, this.clipped_); |
| 521 }; | 395 }; |
| 522 | 396 |
| 523 ImageBuffer.Overview.prototype.getCursorStyle = function(x, y) { | 397 ImageBuffer.Overview.prototype.getCursorStyle = function(x, y) { |
| 524 if (!this.whole_ || !this.whole_.inside(x, y)) return null; | 398 if (!this.bounds_ || !this.bounds_.inside(x, y)) return null; |
| 525 | 399 |
| 526 // Indicate that the on-screen part is draggable. | 400 // Indicate that the on-screen part is draggable. |
| 527 if (this.visible_.inside(x, y)) return 'move'; | 401 if (this.clipped_.inside(x, y)) return 'move'; |
| 528 | 402 |
| 529 // Indicathe that the rest of the thumbnail is clickable. | 403 // Indicate that the rest of the thumbnail is clickable. |
| 530 return 'crosshair'; | 404 return 'crosshair'; |
| 531 }; | 405 }; |
| 532 | 406 |
| 533 ImageBuffer.Overview.prototype.onClick = function(x, y) { | 407 ImageBuffer.Overview.prototype.onClick = function(x, y) { |
| 534 if (!this.whole_ || !this.whole_.inside(x, y)) return false; | 408 if (this.getCursorStyle(x, y) != 'crosshair') return false; |
| 535 | 409 this.viewport_.setCenter( |
| 536 if (this.visible_.inside(x, y)) return false; | 410 (x - this.bounds_.left) / this.scale_, |
| 537 | 411 (y - this.bounds_.top) / this.scale_); |
| 538 this.buffer_.setCenter( | 412 this.viewport_.repaint(); |
| 539 (x - this.whole_.left) / this.scale_, | |
| 540 (y - this.whole_.top) / this.scale_); | |
| 541 this.buffer_.repaint(); | |
| 542 return true; | 413 return true; |
| 543 }; | 414 }; |
| 544 | 415 |
| 545 ImageBuffer.Overview.prototype.getDragHandler = function(x, y) { | 416 ImageBuffer.Overview.prototype.getDragHandler = function(x, y) { |
| 546 if (!this.whole_ || !this.whole_.inside(x, y)) return null; | 417 var cursor = this.getCursorStyle(x, y); |
| 547 | 418 |
| 548 if (this.visible_.inside(x, y)) { | 419 if (cursor == 'move') { |
| 549 var self = this; | 420 var self = this; |
| 550 function scale() { return -self.scale_;} | 421 function scale() { return -self.scale_;} |
| 551 function hit(x, y) { return self.whole_ && self.whole_.inside(x, y); } | 422 function hit(x, y) { return self.bounds_ && self.bounds_.inside(x, y); } |
| 552 return this.buffer_.createOffsetSetter_(x, y, scale, hit); | 423 return this.viewport_.createOffsetSetter(x, y, scale, hit); |
| 553 } else { | 424 } else if (cursor == 'crosshair') { |
| 554 // Force non-draggable behavior. | 425 // Force non-draggable behavior. |
| 555 return function() {}; | 426 return function() {}; |
| 556 } | 427 } else { |
| 557 }; | 428 return null; |
| 429 } |
| 430 }; |
| OLD | NEW |