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
3 // found in the LICENSE file.
6 * Command queue is the only way to modify images.
8 * Command execution is asynchronous (callback-based).
10 * @param {!Document} document Document to create canvases in.
11 * @param {!HTMLCanvasElement} canvas The canvas with the original image.
12 * @param {function(function())} saveFunction Function to save the image.
16 function CommandQueue(document
, canvas
, saveFunction
) {
17 this.document_
= document
;
20 this.subscribers_
= [];
21 this.currentImage_
= canvas
;
23 // Current image may be null or not-null but with width = height = 0.
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);
34 this.baselineImage_
= null;
37 this.previousImage_
= document
.createElement('canvas');
38 this.previousImageAvailable_
= false;
40 this.saveFunction_
= saveFunction
;
46 * Attach the UI elements to the command queue.
47 * Once the UI is attached the results of image manipulations are displayed.
49 * @param {!ImageView} imageView The ImageView object to display the results.
50 * @param {!ImageEditor.Prompt} prompt Prompt to use with this CommandQueue.
51 * @param {function(boolean)} lock Function to enable/disable buttons etc.
53 CommandQueue
.prototype.attachUI = function(imageView
, prompt
, lock
) {
62 * Execute the action when the queue is not busy.
63 * @param {function()} callback Callback.
65 CommandQueue
.prototype.executeWhenReady = function(callback
) {
67 this.subscribers_
.push(callback
);
69 setTimeout(callback
, 0);
73 * @return {boolean} True if the command queue is busy.
75 CommandQueue
.prototype.isBusy = function() { return this.busy_
};
78 * Set the queue state to busy. Lock the UI.
81 CommandQueue
.prototype.setBusy_ = function() {
83 throw new Error('CommandQueue already busy');
87 if (this.UIContext_
.lock
)
88 this.UIContext_
.lock(true);
90 ImageUtil
.trace
.resetTimer('command-busy');
94 * Set the queue state to not busy. Unlock the UI and execute pending actions.
97 CommandQueue
.prototype.clearBusy_ = function() {
99 throw new Error('Inconsistent CommandQueue already not busy');
103 // Execute the actions requested while the queue was busy.
104 while (this.subscribers_
.length
)
105 this.subscribers_
.shift()();
107 if (this.UIContext_
.lock
)
108 this.UIContext_
.lock(false);
110 ImageUtil
.trace
.reportTimer('command-busy');
114 * Commit the image change: save and unlock the UI.
115 * @param {number=} opt_delay Delay in ms (to avoid disrupting the animation).
118 CommandQueue
.prototype.commit_ = function(opt_delay
) {
119 setTimeout(this.saveFunction_
.bind(null, this.clearBusy_
.bind(this)),
124 * Internal function to execute the command in a given context.
126 * @param {!Command} command The command to execute.
127 * @param {!Object} uiContext The UI context.
128 * @param {function(number=)} callback Completion callback.
131 CommandQueue
.prototype.doExecute_ = function(command
, uiContext
, callback
) {
132 if (!this.currentImage_
)
133 throw new Error('Cannot operate on null image');
135 // Remember one previous image so that the first undo is as fast as possible.
136 this.previousImage_
.width
= this.currentImage_
.width
;
137 this.previousImage_
.height
= this.currentImage_
.height
;
138 this.previousImageAvailable_
= true;
139 var context
= this.previousImage_
.getContext('2d');
140 context
.drawImage(this.currentImage_
, 0, 0);
146 * @type {function(HTMLCanvasElement, number=)}
148 (function(result
, opt_delay
) {
149 this.currentImage_
= result
;
156 * Executes the command.
158 * @param {!Command} command Command to execute.
159 * @param {boolean=} opt_keep_redo True if redo stack should not be cleared.
161 CommandQueue
.prototype.execute = function(command
, opt_keep_redo
) {
167 this.undo_
.push(command
);
169 this.doExecute_(command
, this.UIContext_
, this.commit_
.bind(this));
173 * @return {boolean} True if Undo is applicable.
175 CommandQueue
.prototype.canUndo = function() {
176 return this.undo_
.length
!= 0;
180 * Undo the most recent command.
182 CommandQueue
.prototype.undo = function() {
184 throw new Error('Cannot undo');
188 var command
= this.undo_
.pop();
189 this.redo_
.push(command
);
193 function complete() {
194 var delay
= command
.revertView(
195 self
.currentImage_
, self
.UIContext_
.imageView
);
199 if (this.previousImageAvailable_
) {
200 // First undo after an execute call.
201 this.currentImage_
.width
= this.previousImage_
.width
;
202 this.currentImage_
.height
= this.previousImage_
.height
;
203 var context
= this.currentImage_
.getContext('2d');
204 context
.drawImage(this.previousImage_
, 0, 0);
207 this.previousImage_
.width
= 0;
208 this.previousImage_
.height
= 0;
209 this.previousImageAvailable_
= false;
212 // TODO(kaznacheev) Consider recalculating previousImage_ right here
213 // by replaying the commands in the background.
215 this.currentImage_
.width
= this.baselineImage_
.width
;
216 this.currentImage_
.height
= this.baselineImage_
.height
;
217 var context
= this.currentImage_
.getContext('2d');
218 context
.drawImage(this.baselineImage_
, 0, 0);
220 var replay = function(index
) {
221 if (index
< self
.undo_
.length
)
222 self
.doExecute_(self
.undo_
[index
], {}, replay
.bind(null, index
+ 1));
233 * @return {boolean} True if Redo is applicable.
235 CommandQueue
.prototype.canRedo = function() {
236 return this.redo_
.length
!= 0;
240 * Repeat the command that was recently un-done.
242 CommandQueue
.prototype.redo = function() {
244 throw new Error('Cannot redo');
246 this.execute(this.redo_
.pop(), true);
250 * Closes internal buffers. Call to ensure, that internal buffers are freed
251 * as soon as possible.
253 CommandQueue
.prototype.close = function() {
254 // Free memory used by the undo buffer.
255 this.previousImage_
.width
= 0;
256 this.previousImage_
.height
= 0;
257 this.previousImageAvailable_
= false;
259 if (this.baselineImage_
) {
260 this.baselineImage_
.width
= 0;
261 this.baselineImage_
.height
= 0;
266 * Command object encapsulates an operation on an image and a way to visualize
269 * @param {string} name Command name.
273 function Command(name
) {
278 * @return {string} String representation of the command.
280 Command
.prototype.toString = function() {
281 return 'Command ' + this.name_
;
285 * Execute the command and visualize its results.
287 * The two actions are combined into one method because sometimes it is nice
288 * to be able to show partial results for slower operations.
290 * @param {!Document} document Document on which to execute command.
291 * @param {!HTMLCanvasElement} srcCanvas Canvas to execute on.
292 * @param {function(HTMLCanvasElement, number=)} callback Callback to call on
294 * @param {!Object} uiContext Context to work in.
296 Command
.prototype.execute = function(document
, srcCanvas
, callback
, uiContext
) {
297 console
.error('Command.prototype.execute not implemented');
301 * Visualize reversion of the operation.
303 * @param {!HTMLCanvasElement} canvas Image data to use.
304 * @param {!ImageView} imageView ImageView to revert.
305 * @return {number} Animation duration in ms.
307 Command
.prototype.revertView = function(canvas
, imageView
) {
308 imageView
.replace(canvas
);
313 * Creates canvas to render on.
315 * @param {!Document} document Document to create canvas in.
316 * @param {!HTMLCanvasElement} srcCanvas to copy optional dimensions from.
317 * @param {number=} opt_width new canvas width.
318 * @param {number=} opt_height new canvas height.
319 * @return {!HTMLCanvasElement} Newly created canvas.
322 Command
.prototype.createCanvas_ = function(
323 document
, srcCanvas
, opt_width
, opt_height
) {
324 var result
= assertInstanceof(document
.createElement('canvas'),
326 result
.width
= opt_width
|| srcCanvas
.width
;
327 result
.height
= opt_height
|| srcCanvas
.height
;
334 * @param {number} rotate90 Rotation angle in 90 degree increments (signed).
339 Command
.Rotate = function(rotate90
) {
340 Command
.call(this, 'rotate(' + rotate90
* 90 + 'deg)');
341 this.rotate90_
= rotate90
;
344 Command
.Rotate
.prototype = { __proto__
: Command
.prototype };
347 Command
.Rotate
.prototype.execute = function(
348 document
, srcCanvas
, callback
, uiContext
) {
349 var result
= this.createCanvas_(
352 (this.rotate90_
& 1) ? srcCanvas
.height
: srcCanvas
.width
,
353 (this.rotate90_
& 1) ? srcCanvas
.width
: srcCanvas
.height
);
354 ImageUtil
.drawImageTransformed(
355 result
, srcCanvas
, 1, 1, this.rotate90_
* Math
.PI
/ 2);
357 if (uiContext
.imageView
) {
358 delay
= uiContext
.imageView
.replaceAndAnimate(result
, null, this.rotate90_
);
360 setTimeout(callback
, 0, result
, delay
);
364 Command
.Rotate
.prototype.revertView = function(canvas
, imageView
) {
365 return imageView
.replaceAndAnimate(canvas
, null, -this.rotate90_
);
372 * @param {!ImageRect} imageRect Crop rectangle in image coordinates.
377 Command
.Crop = function(imageRect
) {
378 Command
.call(this, 'crop' + imageRect
.toString());
379 this.imageRect_
= imageRect
;
382 Command
.Crop
.prototype = { __proto__
: Command
.prototype };
385 Command
.Crop
.prototype.execute = function(
386 document
, srcCanvas
, callback
, uiContext
) {
387 var result
= this.createCanvas_(
388 document
, srcCanvas
, this.imageRect_
.width
, this.imageRect_
.height
);
389 var ctx
= assertInstanceof(result
.getContext('2d'), CanvasRenderingContext2D
);
390 ImageRect
.drawImage(ctx
, srcCanvas
, null, this.imageRect_
);
392 if (uiContext
.imageView
) {
393 delay
= uiContext
.imageView
.replaceAndAnimate(result
, this.imageRect_
, 0);
395 setTimeout(callback
, 0, result
, delay
);
399 Command
.Crop
.prototype.revertView = function(canvas
, imageView
) {
400 return imageView
.animateAndReplace(canvas
, this.imageRect_
);
407 * @param {string} name Command name.
408 * @param {function(ImageData,ImageData,number,number)} filter Filter function.
409 * @param {?string} message Message to display when done.
414 Command
.Filter = function(name
, filter
, message
) {
415 Command
.call(this, name
);
416 this.filter_
= filter
;
417 this.message_
= message
;
420 Command
.Filter
.prototype = { __proto__
: Command
.prototype };
423 Command
.Filter
.prototype.execute = function(
424 document
, srcCanvas
, callback
, uiContext
) {
425 var result
= this.createCanvas_(document
, srcCanvas
);
429 function onProgressVisible(updatedRow
, rowCount
) {
430 if (updatedRow
== rowCount
) {
431 uiContext
.imageView
.replace(result
);
433 uiContext
.prompt
.show(self
.message_
, 2000);
436 var viewport
= uiContext
.imageView
.viewport_
;
438 var imageStrip
= ImageRect
.createFromBounds(viewport
.getImageBounds());
439 imageStrip
.top
= previousRow
;
440 imageStrip
.height
= updatedRow
- previousRow
;
442 var screenStrip
= ImageRect
.createFromBounds(
443 viewport
.getImageBoundsOnScreen());
444 screenStrip
.top
= Math
.round(viewport
.imageToScreenY(previousRow
));
446 Math
.round(viewport
.imageToScreenY(updatedRow
)) - screenStrip
.top
;
448 uiContext
.imageView
.paintDeviceRect(result
, imageStrip
);
449 previousRow
= updatedRow
;
453 function onProgressInvisible(updatedRow
, rowCount
) {
454 if (updatedRow
== rowCount
) {
459 filter
.applyByStrips(result
, srcCanvas
, this.filter_
,
460 uiContext
.imageView
? onProgressVisible
: onProgressInvisible
);