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 |