Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 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 * Command queue is the only way to modify images. | 6 * Command queue is the only way to modify images. |
| 7 * Supports undo/redo. | 7 * Supports undo/redo. |
| 8 * Command execution is asynchronous (callback-based). | 8 * Command execution is asynchronous (callback-based). |
| 9 * | 9 * |
| 10 * @param {!Document} document Document to create canvases in. | 10 * @param {!Document} document Document to create canvases in. |
| 11 * @param {!HTMLCanvasElement} canvas The canvas with the original image. | 11 * @param {!HTMLCanvasElement|!HTMLImageElement} image The canvas with the |
| 12 * original image. | |
| 12 * @param {function(function())} saveFunction Function to save the image. | 13 * @param {function(function())} saveFunction Function to save the image. |
| 13 * @constructor | 14 * @constructor |
| 14 * @struct | 15 * @struct |
| 15 */ | 16 */ |
| 16 function CommandQueue(document, canvas, saveFunction) { | 17 function CommandQueue(document, image, saveFunction) { |
| 17 this.document_ = document; | 18 this.document_ = document; |
| 18 this.undo_ = []; | 19 this.undo_ = []; |
| 19 this.redo_ = []; | 20 this.redo_ = []; |
| 20 this.subscribers_ = []; | 21 this.subscribers_ = []; |
| 21 this.currentImage_ = canvas; | 22 /** |
| 23 * @type {HTMLCanvasElement|HTMLImageElement} | |
| 24 * @private | |
| 25 */ | |
| 22 | 26 |
|
yawano
2016/01/21 05:14:38
nit: remove blank line from line 26. And insert a
ryoh
2016/01/21 07:28:53
Done.
| |
| 23 // Current image may be null or not-null but with width = height = 0. | 27 this.currentImage_ = image; |
| 24 // Copying an image with zero dimensions causes js errors. | |
| 25 if (this.currentImage_) { | |
| 26 this.baselineImage_ = document.createElement('canvas'); | |
| 27 this.baselineImage_.width = this.currentImage_.width; | |
| 28 this.baselineImage_.height = this.currentImage_.height; | |
| 29 if (this.currentImage_.width > 0 && this.currentImage_.height > 0) { | |
| 30 var context = this.baselineImage_.getContext('2d'); | |
| 31 context.drawImage(this.currentImage_, 0, 0); | |
| 32 } | |
| 33 } else { | |
| 34 this.baselineImage_ = null; | |
| 35 } | |
| 36 | 28 |
| 37 this.previousImage_ = document.createElement('canvas'); | 29 /** |
| 38 this.previousImageAvailable_ = false; | 30 * @type {HTMLCanvasElement|HTMLImageElement} |
| 31 * @private | |
| 32 */ | |
| 33 this.baselineImage_ = image; | |
| 34 | |
| 35 /** | |
| 36 * @type {HTMLCanvasElement|HTMLImageElement} | |
| 37 * @private | |
| 38 */ | |
| 39 this.previousImage_ = null; | |
| 39 | 40 |
| 40 this.saveFunction_ = saveFunction; | 41 this.saveFunction_ = saveFunction; |
| 41 this.busy_ = false; | 42 this.busy_ = false; |
| 42 this.UIContext_ = {}; | 43 this.UIContext_ = {}; |
| 43 } | 44 } |
| 44 | 45 |
| 45 /** | 46 /** |
| 46 * Attach the UI elements to the command queue. | 47 * Attach the UI elements to the command queue. |
| 47 * Once the UI is attached the results of image manipulations are displayed. | 48 * Once the UI is attached the results of image manipulations are displayed. |
| 48 * | 49 * |
| (...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 132 * | 133 * |
| 133 * @param {!Command} command The command to execute. | 134 * @param {!Command} command The command to execute. |
| 134 * @param {!Object} uiContext The UI context. | 135 * @param {!Object} uiContext The UI context. |
| 135 * @param {function(number=)} callback Completion callback. | 136 * @param {function(number=)} callback Completion callback. |
| 136 * @private | 137 * @private |
| 137 */ | 138 */ |
| 138 CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) { | 139 CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) { |
| 139 if (!this.currentImage_) | 140 if (!this.currentImage_) |
| 140 throw new Error('Cannot operate on null image'); | 141 throw new Error('Cannot operate on null image'); |
| 141 | 142 |
| 142 // Remember one previous image so that the first undo is as fast as possible. | |
| 143 this.previousImage_.width = this.currentImage_.width; | |
| 144 this.previousImage_.height = this.currentImage_.height; | |
| 145 this.previousImageAvailable_ = true; | |
| 146 var context = this.previousImage_.getContext('2d'); | |
| 147 context.drawImage(this.currentImage_, 0, 0); | |
| 148 | |
| 149 command.execute( | 143 command.execute( |
| 150 this.document_, | 144 this.document_, |
| 151 this.currentImage_, | 145 this.currentImage_, |
| 152 /** | 146 /** |
| 153 * @type {function(HTMLCanvasElement, number=)} | 147 * @type {function(HTMLCanvasElement, number=)} |
| 154 */ | 148 */ |
| 155 (function(result, opt_delay) { | 149 (function(result, opt_delay) { |
| 150 this.previousImage_ = this.currentImage_; | |
| 156 this.currentImage_ = result; | 151 this.currentImage_ = result; |
| 157 callback(opt_delay); | 152 callback(opt_delay); |
| 158 }.bind(this)), | 153 }.bind(this)), |
| 159 uiContext); | 154 uiContext); |
| 160 }; | 155 }; |
| 161 | 156 |
| 162 /** | 157 /** |
| 163 * Executes the command. | 158 * Executes the command. |
| 164 * | 159 * |
| 165 * @param {!Command} command Command to execute. | 160 * @param {!Command} command Command to execute. |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 197 this.redo_.push(command); | 192 this.redo_.push(command); |
| 198 | 193 |
| 199 var self = this; | 194 var self = this; |
| 200 | 195 |
| 201 function complete() { | 196 function complete() { |
| 202 var delay = command.revertView( | 197 var delay = command.revertView( |
| 203 self.currentImage_, self.UIContext_.imageView); | 198 self.currentImage_, self.UIContext_.imageView); |
| 204 self.commit_(false /* Do not show undo action */, delay); | 199 self.commit_(false /* Do not show undo action */, delay); |
| 205 } | 200 } |
| 206 | 201 |
| 207 if (this.previousImageAvailable_) { | 202 if (this.previousImage_) { |
| 208 // First undo after an execute call. | 203 // First undo after an execute call. |
| 209 this.currentImage_.width = this.previousImage_.width; | 204 this.currentImage_ = this.previousImage_; |
| 210 this.currentImage_.height = this.previousImage_.height; | 205 this.previousImage_ = null; |
| 211 var context = this.currentImage_.getContext('2d'); | |
| 212 context.drawImage(this.previousImage_, 0, 0); | |
| 213 | |
| 214 // Free memory. | |
| 215 this.previousImage_.width = 0; | |
| 216 this.previousImage_.height = 0; | |
| 217 this.previousImageAvailable_ = false; | |
| 218 | 206 |
| 219 complete(); | 207 complete(); |
| 220 // TODO(kaznacheev) Consider recalculating previousImage_ right here | 208 // TODO(kaznacheev) Consider recalculating previousImage_ right here |
| 221 // by replaying the commands in the background. | 209 // by replaying the commands in the background. |
| 222 } else { | 210 } else { |
| 223 this.currentImage_.width = this.baselineImage_.width; | 211 this.currentImage_ = this.baselineImage_; |
| 224 this.currentImage_.height = this.baselineImage_.height; | |
| 225 var context = this.currentImage_.getContext('2d'); | |
| 226 context.drawImage(this.baselineImage_, 0, 0); | |
| 227 | 212 |
| 228 var replay = function(index) { | 213 var replay = function(index) { |
| 229 if (index < self.undo_.length) | 214 if (index < self.undo_.length) |
| 230 self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1)); | 215 self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1)); |
| 231 else { | 216 else { |
| 232 complete(); | 217 complete(); |
| 233 } | 218 } |
| 234 }; | 219 }; |
| 235 | 220 |
| 236 replay(0); | 221 replay(0); |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 253 | 238 |
| 254 this.execute(this.redo_.pop(), true); | 239 this.execute(this.redo_.pop(), true); |
| 255 }; | 240 }; |
| 256 | 241 |
| 257 /** | 242 /** |
| 258 * Closes internal buffers. Call to ensure, that internal buffers are freed | 243 * Closes internal buffers. Call to ensure, that internal buffers are freed |
| 259 * as soon as possible. | 244 * as soon as possible. |
| 260 */ | 245 */ |
| 261 CommandQueue.prototype.close = function() { | 246 CommandQueue.prototype.close = function() { |
| 262 // Free memory used by the undo buffer. | 247 // Free memory used by the undo buffer. |
| 263 this.previousImage_.width = 0; | 248 this.currentImage_ = null; |
| 264 this.previousImage_.height = 0; | 249 this.previousImage_ = null; |
| 265 this.previousImageAvailable_ = false; | 250 this.baselineImage_ = null; |
| 266 | |
| 267 if (this.baselineImage_) { | |
| 268 this.baselineImage_.width = 0; | |
| 269 this.baselineImage_.height = 0; | |
| 270 } | |
| 271 }; | 251 }; |
| 272 | 252 |
| 273 /** | 253 /** |
| 274 * Command object encapsulates an operation on an image and a way to visualize | 254 * Command object encapsulates an operation on an image and a way to visualize |
| 275 * its result. | 255 * its result. |
| 276 * | 256 * |
| 277 * @param {string} name Command name. | 257 * @param {string} name Command name. |
| 278 * @constructor | 258 * @constructor |
| 279 * @struct | 259 * @struct |
| 280 */ | 260 */ |
| 281 function Command(name) { | 261 function Command(name) { |
| 282 this.name_ = name; | 262 this.name_ = name; |
| 283 } | 263 } |
| 284 | 264 |
| 285 /** | 265 /** |
| 286 * @return {string} String representation of the command. | 266 * @return {string} String representation of the command. |
| 287 */ | 267 */ |
| 288 Command.prototype.toString = function() { | 268 Command.prototype.toString = function() { |
| 289 return 'Command ' + this.name_; | 269 return 'Command ' + this.name_; |
| 290 }; | 270 }; |
| 291 | 271 |
| 292 /** | 272 /** |
| 293 * Execute the command and visualize its results. | 273 * Execute the command and visualize its results. |
| 294 * | 274 * |
| 295 * The two actions are combined into one method because sometimes it is nice | 275 * The two actions are combined into one method because sometimes it is nice |
| 296 * to be able to show partial results for slower operations. | 276 * to be able to show partial results for slower operations. |
| 297 * | 277 * |
| 298 * @param {!Document} document Document on which to execute command. | 278 * @param {!Document} document Document on which to execute command. |
| 299 * @param {!HTMLCanvasElement} srcCanvas Canvas to execute on. | 279 * @param {!HTMLCanvasElement|!HTMLImageElement} srcImage Image to execute on. |
| 280 * Do NOT modify this object. | |
| 300 * @param {function(HTMLCanvasElement, number=)} callback Callback to call on | 281 * @param {function(HTMLCanvasElement, number=)} callback Callback to call on |
| 301 * completion. | 282 * completion. |
| 302 * @param {!Object} uiContext Context to work in. | 283 * @param {!Object} uiContext Context to work in. |
| 303 */ | 284 */ |
| 304 Command.prototype.execute = function(document, srcCanvas, callback, uiContext) { | 285 Command.prototype.execute = function(document, srcImage, callback, uiContext) { |
| 305 console.error('Command.prototype.execute not implemented'); | 286 console.error('Command.prototype.execute not implemented'); |
| 306 }; | 287 }; |
| 307 | 288 |
| 308 /** | 289 /** |
| 309 * Visualize reversion of the operation. | 290 * Visualize reversion of the operation. |
| 310 * | 291 * |
| 311 * @param {!HTMLCanvasElement} canvas Image data to use. | 292 * @param {!HTMLCanvasElement|!HTMLCanvasElement} image previous image. |
| 312 * @param {!ImageView} imageView ImageView to revert. | 293 * @param {!ImageView} imageView ImageView to revert. |
| 313 * @return {number} Animation duration in ms. | 294 * @return {number} Animation duration in ms. |
| 314 */ | 295 */ |
| 315 Command.prototype.revertView = function(canvas, imageView) { | 296 Command.prototype.revertView = function(image, imageView) { |
| 316 imageView.replace(canvas); | 297 imageView.replace(image); |
| 317 return 0; | 298 return 0; |
| 318 }; | 299 }; |
| 319 | 300 |
| 320 /** | 301 /** |
| 321 * Creates canvas to render on. | 302 * Creates canvas to render on. |
| 322 * | 303 * |
| 323 * @param {!Document} document Document to create canvas in. | 304 * @param {!Document} document Document to create canvas in. |
| 324 * @param {!HTMLCanvasElement} srcCanvas to copy optional dimensions from. | 305 * @param {!HTMLCanvasElement|!HTMLImageElement} srcImage to copy optional |
| 306 * dimensions from. | |
| 325 * @param {number=} opt_width new canvas width. | 307 * @param {number=} opt_width new canvas width. |
| 326 * @param {number=} opt_height new canvas height. | 308 * @param {number=} opt_height new canvas height. |
| 327 * @return {!HTMLCanvasElement} Newly created canvas. | 309 * @return {!HTMLCanvasElement} Newly created canvas. |
| 328 * @private | 310 * @private |
| 329 */ | 311 */ |
| 330 Command.prototype.createCanvas_ = function( | 312 Command.prototype.createCanvas_ = function( |
| 331 document, srcCanvas, opt_width, opt_height) { | 313 document, srcImage, opt_width, opt_height) { |
| 332 var result = assertInstanceof(document.createElement('canvas'), | 314 var result = assertInstanceof(document.createElement('canvas'), |
| 333 HTMLCanvasElement); | 315 HTMLCanvasElement); |
| 334 result.width = opt_width || srcCanvas.width; | 316 result.width = opt_width || srcImage.width; |
| 335 result.height = opt_height || srcCanvas.height; | 317 result.height = opt_height || srcImage.height; |
| 336 return result; | 318 return result; |
| 337 }; | 319 }; |
| 338 | 320 |
| 339 | 321 |
| 340 /** | 322 /** |
| 341 * Rotate command | 323 * Rotate command |
| 342 * @param {number} rotate90 Rotation angle in 90 degree increments (signed). | 324 * @param {number} rotate90 Rotation angle in 90 degree increments (signed). |
| 343 * @constructor | 325 * @constructor |
| 344 * @extends {Command} | 326 * @extends {Command} |
| 345 * @struct | 327 * @struct |
| 346 */ | 328 */ |
| 347 Command.Rotate = function(rotate90) { | 329 Command.Rotate = function(rotate90) { |
| 348 Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)'); | 330 Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)'); |
| 349 this.rotate90_ = rotate90; | 331 this.rotate90_ = rotate90; |
| 350 }; | 332 }; |
| 351 | 333 |
| 352 Command.Rotate.prototype = { __proto__: Command.prototype }; | 334 Command.Rotate.prototype = { __proto__: Command.prototype }; |
| 353 | 335 |
| 354 /** @override */ | 336 /** @override */ |
| 355 Command.Rotate.prototype.execute = function( | 337 Command.Rotate.prototype.execute = function( |
| 356 document, srcCanvas, callback, uiContext) { | 338 document, srcImage, callback, uiContext) { |
| 357 var result = this.createCanvas_( | 339 var result = this.createCanvas_( |
| 358 document, | 340 document, |
| 359 srcCanvas, | 341 srcImage, |
| 360 (this.rotate90_ & 1) ? srcCanvas.height : srcCanvas.width, | 342 (this.rotate90_ & 1) ? srcImage.height : srcImage.width, |
| 361 (this.rotate90_ & 1) ? srcCanvas.width : srcCanvas.height); | 343 (this.rotate90_ & 1) ? srcImage.width : srcImage.height); |
| 362 ImageUtil.drawImageTransformed( | 344 ImageUtil.drawImageTransformed( |
| 363 result, srcCanvas, 1, 1, this.rotate90_ * Math.PI / 2); | 345 result, srcImage, 1, 1, this.rotate90_ * Math.PI / 2); |
| 364 var delay; | 346 var delay; |
| 365 if (uiContext.imageView) { | 347 if (uiContext.imageView) { |
| 366 delay = uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_); | 348 delay = uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_); |
| 367 } | 349 } |
| 368 setTimeout(callback, 0, result, delay); | 350 setTimeout(callback, 0, result, delay); |
| 369 }; | 351 }; |
| 370 | 352 |
| 371 /** @override */ | 353 /** @override */ |
| 372 Command.Rotate.prototype.revertView = function(canvas, imageView) { | 354 Command.Rotate.prototype.revertView = function(image, imageView) { |
| 373 return imageView.replaceAndAnimate(canvas, null, -this.rotate90_); | 355 return imageView.replaceAndAnimate(image, null, -this.rotate90_); |
| 374 }; | 356 }; |
| 375 | 357 |
| 376 | 358 |
| 377 /** | 359 /** |
| 378 * Crop command. | 360 * Crop command. |
| 379 * | 361 * |
| 380 * @param {!ImageRect} imageRect Crop rectangle in image coordinates. | 362 * @param {!ImageRect} imageRect Crop rectangle in image coordinates. |
| 381 * @constructor | 363 * @constructor |
| 382 * @extends {Command} | 364 * @extends {Command} |
| 383 * @struct | 365 * @struct |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 397 var ctx = assertInstanceof(result.getContext('2d'), CanvasRenderingContext2D); | 379 var ctx = assertInstanceof(result.getContext('2d'), CanvasRenderingContext2D); |
| 398 ImageRect.drawImage(ctx, srcCanvas, null, this.imageRect_); | 380 ImageRect.drawImage(ctx, srcCanvas, null, this.imageRect_); |
| 399 var delay; | 381 var delay; |
| 400 if (uiContext.imageView) { | 382 if (uiContext.imageView) { |
| 401 delay = uiContext.imageView.replaceAndAnimate(result, this.imageRect_, 0); | 383 delay = uiContext.imageView.replaceAndAnimate(result, this.imageRect_, 0); |
| 402 } | 384 } |
| 403 setTimeout(callback, 0, result, delay); | 385 setTimeout(callback, 0, result, delay); |
| 404 }; | 386 }; |
| 405 | 387 |
| 406 /** @override */ | 388 /** @override */ |
| 407 Command.Crop.prototype.revertView = function(canvas, imageView) { | 389 Command.Crop.prototype.revertView = function(image, imageView) { |
| 408 return imageView.animateAndReplace(canvas, this.imageRect_); | 390 return imageView.animateAndReplace(image, this.imageRect_); |
| 409 }; | 391 }; |
| 410 | 392 |
| 411 | 393 |
| 412 /** | 394 /** |
| 413 * Filter command. | 395 * Filter command. |
| 414 * | 396 * |
| 415 * @param {string} name Command name. | 397 * @param {string} name Command name. |
| 416 * @param {function(!ImageData,!ImageData,number,number)} filter Filter | 398 * @param {function(!ImageData,!ImageData,number,number)} filter Filter |
| 417 * function. | 399 * function. |
| 418 * @param {?string} message Message to display when done. | 400 * @param {?string} message Message to display when done. |
| 419 * @constructor | 401 * @constructor |
| 420 * @extends {Command} | 402 * @extends {Command} |
| 421 * @struct | 403 * @struct |
| 422 */ | 404 */ |
| 423 Command.Filter = function(name, filter, message) { | 405 Command.Filter = function(name, filter, message) { |
| 424 Command.call(this, name); | 406 Command.call(this, name); |
| 425 this.filter_ = filter; | 407 this.filter_ = filter; |
| 426 this.message_ = message; | 408 this.message_ = message; |
| 427 }; | 409 }; |
| 428 | 410 |
| 429 Command.Filter.prototype = { __proto__: Command.prototype }; | 411 Command.Filter.prototype = { __proto__: Command.prototype }; |
| 430 | 412 |
| 431 /** @override */ | 413 /** @override */ |
| 432 Command.Filter.prototype.execute = function( | 414 Command.Filter.prototype.execute = function( |
| 433 document, srcCanvas, callback, uiContext) { | 415 document, srcImage, callback, uiContext) { |
| 434 var result = this.createCanvas_(document, srcCanvas); | 416 var result = this.createCanvas_(document, srcImage); |
| 435 var self = this; | 417 var self = this; |
| 436 var previousRow = 0; | 418 var previousRow = 0; |
| 437 | 419 |
| 438 function onProgressVisible(updatedRow, rowCount) { | 420 function onProgressVisible(updatedRow, rowCount) { |
| 439 if (updatedRow == rowCount) { | 421 if (updatedRow == rowCount) { |
| 440 uiContext.imageView.replace(result); | 422 uiContext.imageView.replace(result); |
| 441 if (self.message_) | 423 if (self.message_) |
| 442 uiContext.prompt.show(self.message_, 2000); | 424 uiContext.prompt.show(self.message_, 2000); |
| 443 callback(result); | 425 callback(result); |
| 444 } else { | 426 } else { |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 458 previousRow = updatedRow; | 440 previousRow = updatedRow; |
| 459 } | 441 } |
| 460 } | 442 } |
| 461 | 443 |
| 462 function onProgressInvisible(updatedRow, rowCount) { | 444 function onProgressInvisible(updatedRow, rowCount) { |
| 463 if (updatedRow == rowCount) { | 445 if (updatedRow == rowCount) { |
| 464 callback(result); | 446 callback(result); |
| 465 } | 447 } |
| 466 } | 448 } |
| 467 | 449 |
| 468 filter.applyByStrips(result, srcCanvas, this.filter_, | 450 filter.applyByStrips(result, srcImage, this.filter_, |
| 469 uiContext.imageView ? onProgressVisible : onProgressInvisible); | 451 uiContext.imageView ? onProgressVisible : onProgressInvisible); |
| 470 }; | 452 }; |
| OLD | NEW |