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 |