| OLD | NEW |
| (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 * Command queue is the only way to modify images. | |
| 9 * Supports undo/redo. | |
| 10 * Command execution is asynchronous (callback-based). | |
| 11 * | |
| 12 * @param {Document} document Document to create canvases in. | |
| 13 * @param {HTMLCanvasElement} canvas The canvas with the original image. | |
| 14 * @param {function(callback)} saveFunction Function to save the image. | |
| 15 * @constructor | |
| 16 */ | |
| 17 function CommandQueue(document, canvas, saveFunction) { | |
| 18 this.document_ = document; | |
| 19 this.undo_ = []; | |
| 20 this.redo_ = []; | |
| 21 this.subscribers_ = []; | |
| 22 this.currentImage_ = canvas; | |
| 23 | |
| 24 // Current image may be null or not-null but with width = height = 0. | |
| 25 // Copying an image with zero dimensions causes js errors. | |
| 26 if (this.currentImage_) { | |
| 27 this.baselineImage_ = document.createElement('canvas'); | |
| 28 this.baselineImage_.width = this.currentImage_.width; | |
| 29 this.baselineImage_.height = this.currentImage_.height; | |
| 30 if (this.currentImage_.width > 0 && this.currentImage_.height > 0) { | |
| 31 var context = this.baselineImage_.getContext('2d'); | |
| 32 context.drawImage(this.currentImage_, 0, 0); | |
| 33 } | |
| 34 } else { | |
| 35 this.baselineImage_ = null; | |
| 36 } | |
| 37 | |
| 38 this.previousImage_ = document.createElement('canvas'); | |
| 39 this.previousImageAvailable_ = false; | |
| 40 | |
| 41 this.saveFunction_ = saveFunction; | |
| 42 this.busy_ = false; | |
| 43 this.UIContext_ = {}; | |
| 44 } | |
| 45 | |
| 46 /** | |
| 47 * Attach the UI elements to the command queue. | |
| 48 * Once the UI is attached the results of image manipulations are displayed. | |
| 49 * | |
| 50 * @param {ImageView} imageView The ImageView object to display the results. | |
| 51 * @param {ImageEditor.Prompt} prompt Prompt to use with this CommandQueue. | |
| 52 * @param {function(boolean)} lock Function to enable/disable buttons etc. | |
| 53 */ | |
| 54 CommandQueue.prototype.attachUI = function(imageView, prompt, lock) { | |
| 55 this.UIContext_ = { | |
| 56 imageView: imageView, | |
| 57 prompt: prompt, | |
| 58 lock: lock | |
| 59 }; | |
| 60 }; | |
| 61 | |
| 62 /** | |
| 63 * Execute the action when the queue is not busy. | |
| 64 * @param {function} callback Callback. | |
| 65 */ | |
| 66 CommandQueue.prototype.executeWhenReady = function(callback) { | |
| 67 if (this.isBusy()) | |
| 68 this.subscribers_.push(callback); | |
| 69 else | |
| 70 setTimeout(callback, 0); | |
| 71 }; | |
| 72 | |
| 73 /** | |
| 74 * @return {boolean} True if the command queue is busy. | |
| 75 */ | |
| 76 CommandQueue.prototype.isBusy = function() { return this.busy_ }; | |
| 77 | |
| 78 /** | |
| 79 * Set the queue state to busy. Lock the UI. | |
| 80 * @private | |
| 81 */ | |
| 82 CommandQueue.prototype.setBusy_ = function() { | |
| 83 if (this.busy_) | |
| 84 throw new Error('CommandQueue already busy'); | |
| 85 | |
| 86 this.busy_ = true; | |
| 87 | |
| 88 if (this.UIContext_.lock) | |
| 89 this.UIContext_.lock(true); | |
| 90 | |
| 91 ImageUtil.trace.resetTimer('command-busy'); | |
| 92 }; | |
| 93 | |
| 94 /** | |
| 95 * Set the queue state to not busy. Unlock the UI and execute pending actions. | |
| 96 * @private | |
| 97 */ | |
| 98 CommandQueue.prototype.clearBusy_ = function() { | |
| 99 if (!this.busy_) | |
| 100 throw new Error('Inconsistent CommandQueue already not busy'); | |
| 101 | |
| 102 this.busy_ = false; | |
| 103 | |
| 104 // Execute the actions requested while the queue was busy. | |
| 105 while (this.subscribers_.length) | |
| 106 this.subscribers_.shift()(); | |
| 107 | |
| 108 if (this.UIContext_.lock) | |
| 109 this.UIContext_.lock(false); | |
| 110 | |
| 111 ImageUtil.trace.reportTimer('command-busy'); | |
| 112 }; | |
| 113 | |
| 114 /** | |
| 115 * Commit the image change: save and unlock the UI. | |
| 116 * @param {number=} opt_delay Delay in ms (to avoid disrupting the animation). | |
| 117 * @private | |
| 118 */ | |
| 119 CommandQueue.prototype.commit_ = function(opt_delay) { | |
| 120 setTimeout(this.saveFunction_.bind(null, this.clearBusy_.bind(this)), | |
| 121 opt_delay || 0); | |
| 122 }; | |
| 123 | |
| 124 /** | |
| 125 * Internal function to execute the command in a given context. | |
| 126 * | |
| 127 * @param {Command} command The command to execute. | |
| 128 * @param {Object} uiContext The UI context. | |
| 129 * @param {function} callback Completion callback. | |
| 130 * @private | |
| 131 */ | |
| 132 CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) { | |
| 133 if (!this.currentImage_) | |
| 134 throw new Error('Cannot operate on null image'); | |
| 135 | |
| 136 // Remember one previous image so that the first undo is as fast as possible. | |
| 137 this.previousImage_.width = this.currentImage_.width; | |
| 138 this.previousImage_.height = this.currentImage_.height; | |
| 139 this.previousImageAvailable_ = true; | |
| 140 var context = this.previousImage_.getContext('2d'); | |
| 141 context.drawImage(this.currentImage_, 0, 0); | |
| 142 | |
| 143 command.execute( | |
| 144 this.document_, | |
| 145 this.currentImage_, | |
| 146 function(result, opt_delay) { | |
| 147 this.currentImage_ = result; | |
| 148 callback(opt_delay); | |
| 149 }.bind(this), | |
| 150 uiContext); | |
| 151 }; | |
| 152 | |
| 153 /** | |
| 154 * Executes the command. | |
| 155 * | |
| 156 * @param {Command} command Command to execute. | |
| 157 * @param {boolean=} opt_keep_redo True if redo stack should not be cleared. | |
| 158 */ | |
| 159 CommandQueue.prototype.execute = function(command, opt_keep_redo) { | |
| 160 this.setBusy_(); | |
| 161 | |
| 162 if (!opt_keep_redo) | |
| 163 this.redo_ = []; | |
| 164 | |
| 165 this.undo_.push(command); | |
| 166 | |
| 167 this.doExecute_(command, this.UIContext_, this.commit_.bind(this)); | |
| 168 }; | |
| 169 | |
| 170 /** | |
| 171 * @return {boolean} True if Undo is applicable. | |
| 172 */ | |
| 173 CommandQueue.prototype.canUndo = function() { | |
| 174 return this.undo_.length != 0; | |
| 175 }; | |
| 176 | |
| 177 /** | |
| 178 * Undo the most recent command. | |
| 179 */ | |
| 180 CommandQueue.prototype.undo = function() { | |
| 181 if (!this.canUndo()) | |
| 182 throw new Error('Cannot undo'); | |
| 183 | |
| 184 this.setBusy_(); | |
| 185 | |
| 186 var command = this.undo_.pop(); | |
| 187 this.redo_.push(command); | |
| 188 | |
| 189 var self = this; | |
| 190 | |
| 191 function complete() { | |
| 192 var delay = command.revertView( | |
| 193 self.currentImage_, self.UIContext_.imageView); | |
| 194 self.commit_(delay); | |
| 195 } | |
| 196 | |
| 197 if (this.previousImageAvailable_) { | |
| 198 // First undo after an execute call. | |
| 199 this.currentImage_.width = this.previousImage_.width; | |
| 200 this.currentImage_.height = this.previousImage_.height; | |
| 201 var context = this.currentImage_.getContext('2d'); | |
| 202 context.drawImage(this.previousImage_, 0, 0); | |
| 203 | |
| 204 // Free memory. | |
| 205 this.previousImage_.width = 0; | |
| 206 this.previousImage_.height = 0; | |
| 207 this.previousImageAvailable_ = false; | |
| 208 | |
| 209 complete(); | |
| 210 // TODO(kaznacheev) Consider recalculating previousImage_ right here | |
| 211 // by replaying the commands in the background. | |
| 212 } else { | |
| 213 this.currentImage_.width = this.baselineImage_.width; | |
| 214 this.currentImage_.height = this.baselineImage_.height; | |
| 215 var context = this.currentImage_.getContext('2d'); | |
| 216 context.drawImage(this.baselineImage_, 0, 0); | |
| 217 | |
| 218 var replay = function(index) { | |
| 219 if (index < self.undo_.length) | |
| 220 self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1)); | |
| 221 else { | |
| 222 complete(); | |
| 223 } | |
| 224 }; | |
| 225 | |
| 226 replay(0); | |
| 227 } | |
| 228 }; | |
| 229 | |
| 230 /** | |
| 231 * @return {boolean} True if Redo is applicable. | |
| 232 */ | |
| 233 CommandQueue.prototype.canRedo = function() { | |
| 234 return this.redo_.length != 0; | |
| 235 }; | |
| 236 | |
| 237 /** | |
| 238 * Repeat the command that was recently un-done. | |
| 239 */ | |
| 240 CommandQueue.prototype.redo = function() { | |
| 241 if (!this.canRedo()) | |
| 242 throw new Error('Cannot redo'); | |
| 243 | |
| 244 this.execute(this.redo_.pop(), true); | |
| 245 }; | |
| 246 | |
| 247 /** | |
| 248 * Closes internal buffers. Call to ensure, that internal buffers are freed | |
| 249 * as soon as possible. | |
| 250 */ | |
| 251 CommandQueue.prototype.close = function() { | |
| 252 // Free memory used by the undo buffer. | |
| 253 this.previousImage_.width = 0; | |
| 254 this.previousImage_.height = 0; | |
| 255 this.previousImageAvailable_ = false; | |
| 256 | |
| 257 if (this.baselineImage_) { | |
| 258 this.baselineImage_.width = 0; | |
| 259 this.baselineImage_.height = 0; | |
| 260 } | |
| 261 }; | |
| 262 | |
| 263 /** | |
| 264 * Command object encapsulates an operation on an image and a way to visualize | |
| 265 * its result. | |
| 266 * | |
| 267 * @param {string} name Command name. | |
| 268 * @constructor | |
| 269 */ | |
| 270 function Command(name) { | |
| 271 this.name_ = name; | |
| 272 } | |
| 273 | |
| 274 /** | |
| 275 * @return {string} String representation of the command. | |
| 276 */ | |
| 277 Command.prototype.toString = function() { | |
| 278 return 'Command ' + this.name_; | |
| 279 }; | |
| 280 | |
| 281 /** | |
| 282 * Execute the command and visualize its results. | |
| 283 * | |
| 284 * The two actions are combined into one method because sometimes it is nice | |
| 285 * to be able to show partial results for slower operations. | |
| 286 * | |
| 287 * @param {Document} document Document on which to execute command. | |
| 288 * @param {HTMLCanvasElement} srcCanvas Canvas to execute on. | |
| 289 * @param {function(HTMLCanvasElement, number)} callback Callback to call on | |
| 290 * completion. | |
| 291 * @param {Object} uiContext Context to work in. | |
| 292 */ | |
| 293 Command.prototype.execute = function(document, srcCanvas, callback, uiContext) { | |
| 294 console.error('Command.prototype.execute not implemented'); | |
| 295 }; | |
| 296 | |
| 297 /** | |
| 298 * Visualize reversion of the operation. | |
| 299 * | |
| 300 * @param {HTMLCanvasElement} canvas Image data to use. | |
| 301 * @param {ImageView} imageView ImageView to revert. | |
| 302 * @return {number} Animation duration in ms. | |
| 303 */ | |
| 304 Command.prototype.revertView = function(canvas, imageView) { | |
| 305 imageView.replace(canvas); | |
| 306 return 0; | |
| 307 }; | |
| 308 | |
| 309 /** | |
| 310 * Creates canvas to render on. | |
| 311 * | |
| 312 * @param {Document} document Document to create canvas in. | |
| 313 * @param {HTMLCanvasElement} srcCanvas to copy optional dimensions from. | |
| 314 * @param {number=} opt_width new canvas width. | |
| 315 * @param {number=} opt_height new canvas height. | |
| 316 * @return {HTMLCanvasElement} Newly created canvas. | |
| 317 * @private | |
| 318 */ | |
| 319 Command.prototype.createCanvas_ = function( | |
| 320 document, srcCanvas, opt_width, opt_height) { | |
| 321 var result = document.createElement('canvas'); | |
| 322 result.width = opt_width || srcCanvas.width; | |
| 323 result.height = opt_height || srcCanvas.height; | |
| 324 return result; | |
| 325 }; | |
| 326 | |
| 327 | |
| 328 /** | |
| 329 * Rotate command | |
| 330 * @param {number} rotate90 Rotation angle in 90 degree increments (signed). | |
| 331 * @constructor | |
| 332 * @extends {Command} | |
| 333 */ | |
| 334 Command.Rotate = function(rotate90) { | |
| 335 Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)'); | |
| 336 this.rotate90_ = rotate90; | |
| 337 }; | |
| 338 | |
| 339 Command.Rotate.prototype = { __proto__: Command.prototype }; | |
| 340 | |
| 341 /** @override */ | |
| 342 Command.Rotate.prototype.execute = function( | |
| 343 document, srcCanvas, callback, uiContext) { | |
| 344 var result = this.createCanvas_( | |
| 345 document, | |
| 346 srcCanvas, | |
| 347 (this.rotate90_ & 1) ? srcCanvas.height : srcCanvas.width, | |
| 348 (this.rotate90_ & 1) ? srcCanvas.width : srcCanvas.height); | |
| 349 ImageUtil.drawImageTransformed( | |
| 350 result, srcCanvas, 1, 1, this.rotate90_ * Math.PI / 2); | |
| 351 var delay; | |
| 352 if (uiContext.imageView) { | |
| 353 delay = uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_); | |
| 354 } | |
| 355 setTimeout(callback, 0, result, delay); | |
| 356 }; | |
| 357 | |
| 358 /** @override */ | |
| 359 Command.Rotate.prototype.revertView = function(canvas, imageView) { | |
| 360 return imageView.replaceAndAnimate(canvas, null, -this.rotate90_); | |
| 361 }; | |
| 362 | |
| 363 | |
| 364 /** | |
| 365 * Crop command. | |
| 366 * | |
| 367 * @param {Rect} imageRect Crop rectangle in image coordinates. | |
| 368 * @constructor | |
| 369 * @extends {Command} | |
| 370 */ | |
| 371 Command.Crop = function(imageRect) { | |
| 372 Command.call(this, 'crop' + imageRect.toString()); | |
| 373 this.imageRect_ = imageRect; | |
| 374 }; | |
| 375 | |
| 376 Command.Crop.prototype = { __proto__: Command.prototype }; | |
| 377 | |
| 378 /** @override */ | |
| 379 Command.Crop.prototype.execute = function( | |
| 380 document, srcCanvas, callback, uiContext) { | |
| 381 var result = this.createCanvas_( | |
| 382 document, srcCanvas, this.imageRect_.width, this.imageRect_.height); | |
| 383 Rect.drawImage(result.getContext('2d'), srcCanvas, null, this.imageRect_); | |
| 384 var delay; | |
| 385 if (uiContext.imageView) { | |
| 386 delay = uiContext.imageView.replaceAndAnimate(result, this.imageRect_, 0); | |
| 387 } | |
| 388 setTimeout(callback, 0, result, delay); | |
| 389 }; | |
| 390 | |
| 391 /** @override */ | |
| 392 Command.Crop.prototype.revertView = function(canvas, imageView) { | |
| 393 return imageView.animateAndReplace(canvas, this.imageRect_); | |
| 394 }; | |
| 395 | |
| 396 | |
| 397 /** | |
| 398 * Filter command. | |
| 399 * | |
| 400 * @param {string} name Command name. | |
| 401 * @param {function(ImageData,ImageData,number,number)} filter Filter function. | |
| 402 * @param {string} message Message to display when done. | |
| 403 * @constructor | |
| 404 * @extends {Command} | |
| 405 */ | |
| 406 Command.Filter = function(name, filter, message) { | |
| 407 Command.call(this, name); | |
| 408 this.filter_ = filter; | |
| 409 this.message_ = message; | |
| 410 }; | |
| 411 | |
| 412 Command.Filter.prototype = { __proto__: Command.prototype }; | |
| 413 | |
| 414 /** @override */ | |
| 415 Command.Filter.prototype.execute = function( | |
| 416 document, srcCanvas, callback, uiContext) { | |
| 417 var result = this.createCanvas_(document, srcCanvas); | |
| 418 | |
| 419 var self = this; | |
| 420 | |
| 421 var previousRow = 0; | |
| 422 | |
| 423 function onProgressVisible(updatedRow, rowCount) { | |
| 424 if (updatedRow == rowCount) { | |
| 425 uiContext.imageView.replace(result); | |
| 426 if (self.message_) | |
| 427 uiContext.prompt.show(self.message_, 2000); | |
| 428 callback(result); | |
| 429 } else { | |
| 430 var viewport = uiContext.imageView.viewport_; | |
| 431 | |
| 432 var imageStrip = new Rect(viewport.getImageBounds()); | |
| 433 imageStrip.top = previousRow; | |
| 434 imageStrip.height = updatedRow - previousRow; | |
| 435 | |
| 436 var screenStrip = new Rect(viewport.getImageBoundsOnScreen()); | |
| 437 screenStrip.top = Math.round(viewport.imageToScreenY(previousRow)); | |
| 438 screenStrip.height = | |
| 439 Math.round(viewport.imageToScreenY(updatedRow)) - screenStrip.top; | |
| 440 | |
| 441 uiContext.imageView.paintDeviceRect( | |
| 442 viewport.screenToDeviceRect(screenStrip), result, imageStrip); | |
| 443 previousRow = updatedRow; | |
| 444 } | |
| 445 } | |
| 446 | |
| 447 function onProgressInvisible(updatedRow, rowCount) { | |
| 448 if (updatedRow == rowCount) { | |
| 449 callback(result); | |
| 450 } | |
| 451 } | |
| 452 | |
| 453 filter.applyByStrips(result, srcCanvas, this.filter_, | |
| 454 uiContext.imageView ? onProgressVisible : onProgressInvisible); | |
| 455 }; | |
| OLD | NEW |