1 /**
  2  * The MIT License (MIT)
  3  *
  4  * Copyright (c) 2016 DeNA Co., Ltd.
  5  *
  6  * Permission is hereby granted, free of charge, to any person obtaining a copy
  7  * of this software and associated documentation files (the "Software"), to deal
  8  * in the Software without restriction, including without limitation the rights
  9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10  * copies of the Software, and to permit persons to whom the Software is
 11  * furnished to do so, subject to the following conditions:
 12  *
 13  * The above copyright notice and this permission notice shall be included in
 14  * all copies or substantial portions of the Software.
 15  *
 16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 22  * SOFTWARE.
 23  */
 24 
 25 /// <reference path="base.js"/>
 26 /// <reference path="object.js"/>
 27 /// <reference path="color.js"/>
 28 /// <reference path="renderer.js"/>
 29 /// <reference path="user_agent.js"/>
 30 
 31 /**
 32  * A class that composes an image.
 33  * @constructor
 34  */
 35 createjs.Composer = function() {
 36 };
 37 
 38 /**
 39  * An inner class that encapsulates a composed image.
 40  * @param {number} width
 41  * @param {number} height
 42  * @constructor
 43  */
 44 createjs.Composer.Renderer = function(width, height) {
 45   /// <param type="number" name="width"/>
 46   /// <param type="number" name="height"/>
 47   var canvas = createjs.createCanvas();
 48   canvas.width = width;
 49   canvas.height = height;
 50 
 51   /**
 52    * The HTMLCanvasElement object representing the composed image.
 53    * @type {HTMLCanvasElement}
 54    * @private
 55    */
 56   this.canvas_ = canvas;
 57 
 58   /**
 59    * The 2D rendering context attached to the HTMLCanvasElement object.
 60    * @type {CanvasRenderingContext2D}
 61    * @private
 62    */
 63   this.context_ = createjs.getRenderingContext2D(canvas);
 64 };
 65 
 66 /**
 67  * Returns the HTMLCanvasElement object associated with this renderer.
 68  * @param {number} width
 69  * @param {number} height
 70  * @const
 71  */
 72 createjs.Composer.Renderer.prototype.reset = function(width, height) {
 73   /// <param type="number" name="width"/>
 74   /// <param type="number" name="height"/>
 75   this.canvas_.width = width;
 76   this.canvas_.height = height;
 77 };
 78 
 79 /**
 80  * Deletes all resources associated with this renderer.
 81  * @const
 82  */
 83 createjs.Composer.Renderer.prototype.destroy = function() {
 84   this.context_ = null;
 85   if (this.canvas_) {
 86     this.canvas_.width = 0;
 87     this.canvas_ = null;
 88   }
 89 };
 90 
 91 /**
 92  * Returns the HTMLCanvasElement object associated with this renderer.
 93  * @return {HTMLCanvasElement}
 94  * @const
 95  */
 96 createjs.Composer.Renderer.prototype.getCanvas = function() {
 97   /// <returns type="HTMLCanvasElement"/>
 98   return this.canvas_;
 99 };
100 
101 /**
102  * Sets the alpha value used by this renderer.
103  * @param {number} alpha
104  * @const
105  */
106 createjs.Composer.Renderer.prototype.setAlpha = function(alpha) {
107   /// <param type="number" name="alpha"/>
108   this.context_.globalAlpha = alpha;
109 };
110 
111 /**
112  * Sets the composition operation used by this renderer.
113  * @param {number} operation
114  * @const
115  */
116 createjs.Composer.Renderer.prototype.setComposition = function(operation) {
117   this.context_.globalCompositeOperation =
118       createjs.Renderer.getCompositionName(operation);
119 };
120 
121 /**
122  * Sets the fill color used by the 'fillRect()' method.
123  * @param {string} color
124  * @const
125  */
126 createjs.Composer.Renderer.prototype.setFillColor = function(color) {
127   this.context_.fillStyle = color;
128 };
129 
130 /**
131  * Fills the specified region of this renderer with the color specified with a
132  * 'setFillColor()' call.
133  * @param {number} width
134  * @param {number} height
135  * @const
136  */
137 createjs.Composer.Renderer.prototype.fillRect = function(width, height) {
138   /// <param type="number" name="width"/>
139   /// <param type="number" name="height"/>
140   this.context_.fillRect(0, 0, width, height);
141 };
142 
143 /**
144  * Draws an image.
145  * @param {HTMLImageElement|HTMLCanvasElement} image
146  * @const
147  */
148 createjs.Composer.Renderer.prototype.drawImage = function(image) {
149   /// <signature>
150   ///   <param type="HTMLImageElement" name="image"/>
151   /// </signature>
152   /// <signature>
153   ///   <param type="HTMLCanvasElement" name="canvas"/>
154   /// </signature>
155   this.context_.drawImage(image, 0, 0);
156 };
157 
158 /**
159  * Draws an alpha mask. This method copies the specified image and applies the
160  * following color-matrix filter to the copy.
161  *       | 1 0 0 0 0 |
162  *       | 0 1 0 0 0 |
163  *   A = | 0 0 1 0 0 |
164  *       | 1 0 0 0 0 |
165  *       | 0 0 0 0 0 |
166  * @param {HTMLImageElement|HTMLCanvasElement} alpha
167  * @param {number} width
168  * @param {number} height
169  * @const
170  */
171 createjs.Composer.Renderer.prototype.drawAlphaMask =
172     function(alpha, width, height) {
173   /// <signature>
174   ///   <param type="HTMLImageElement" name="alpha"/>
175   ///   <param type="number" name="width"/>
176   ///   <param type="number" name="height"/>
177   /// </signature>
178   /// <signature>
179   ///   <param type="HTMLCanvasElement" name="alpha"/>
180   ///   <param type="number" name="width"/>
181   ///   <param type="number" name="height"/>
182   /// </signature>
183   this.context_.drawImage(alpha, 0, 0);
184   var image = this.context_.getImageData(0, 0, width, height);
185   var data = image.data;
186   var length = data.length;
187   for (var i = 0; i < length; i += 4) {
188     data[i + 3] = data[i];
189   }
190   this.context_.putImageData(image, 0, 0);
191 };
192 
193 /**
194  * Multiplies the specified color to this renderer. This method applies the
195  * following color-matrix filter to the HTMLCanvasElement object associated with
196  * this renderer.
197  *       | 1 0 0 0 red   |
198  *       | 0 1 0 0 green |
199  *   A = | 0 0 1 0 blue  |
200  *       | 0 0 0 1 0     |
201  *       | 0 0 0 0 0     |
202  * @param {number} red
203  * @param {number} green
204  * @param {number} blue
205  * @param {number} width
206  * @param {number} height
207  * @const
208  */
209 createjs.Composer.Renderer.prototype.addOffset =
210     function(red, green, blue, width, height) {
211   /// <param type="number" name="red"/>
212   /// <param type="number" name="green"/>
213   /// <param type="number" name="blue"/>
214   /// <param type="number" name="width"/>
215   /// <param type="number" name="height"/>
216   var output = new createjs.Composer.Renderer(width, height);
217   output.drawImage(this.getCanvas());
218   output.setComposition(createjs.Renderer.Composition.LIGHTER);
219   output.setFillColor('rgb(' + red + ',' + green + ',' + blue + ')');
220   output.fillRect(width, height);
221   this.setComposition(createjs.Renderer.Composition.SOURCE_IN);
222   this.drawImage(output.getCanvas());
223 };
224 
225 /**
226  * Multiplies the specified color to this renderer. This method applies the
227  * following color-matrix filter to the HTMLCanvasElement object associated with
228  * this renderer.
229  *       | red 0     0    0 0 |
230  *       | 0   green 0    0 0 |
231  *   A = | 0   0     blue 0 0 |
232  *       | 0   0     0    1 0 |
233  *       | 0   0     0    0 0 |
234  * @param {Array.<number>} matrix
235  * @param {number} width
236  * @param {number} height
237  * @return {number}
238  * @const
239  */
240 createjs.Composer.Renderer.prototype.multiplyColor =
241     function(matrix, width, height) {
242   /// <param type="Array" elementType="number" name="matrix"/>
243   /// <param type="number" name="width"/>
244   /// <param type="number" name="height"/>
245   /// <returns type="number"/>
246   var red = matrix[0 * 5 + 0];
247   var green = matrix[1 * 5 + 1];
248   var blue = matrix[2 * 5 + 2];
249   if (red == green && green == blue) {
250     if (red == 1) {
251       return 0;
252     }
253     // Draws a black rectangle onto the source image with the 'source-atop'
254     // operation. This 'source-atop' operation multiplies (1 - globalAlpha)
255     // with background colors and writes the multiplied colors onto the
256     // destination if it is not transparent as listed in the following code
257     // snippet. (This operation preserves the alpha values of the original
258     // image.)
259     //    for (var i = 0; i < p.length; i += 4) {
260     //      if (p[i + 3] > 0) {
261     //        p[i + 0] = (1 - red) * 0 + (1 - (1 - red)) * p[i + 0];
262     //        p[i + 1] = (1 - red) * 0 + (1 - (1 - red)) * p[i + 1];
263     //        p[i + 2] = (1 - red) * 0 + (1 - (1 - red)) * p[i + 2];
264     //      }
265     //    }
266     this.setAlpha(1 - red);
267     this.setComposition(createjs.Renderer.Composition.SOURCE_ATOP);
268     this.setFillColor('#000');
269     this.fillRect(width, height);
270     return 0;
271   }
272   if (createjs.UserAgent.isMSIE()) {
273     // Read each pixel in the original image and multiply the specified color on
274     // IE 11 or earlier, which does not provide the blend mode "multiply".
275     var image = this.context_.getImageData(0, 0, width, height);
276     var data = image.data;
277     var length = data.length;
278     for (var i = 0; i < length; i += 4) {
279       data[i] *= red;
280       data[i + 1] *= green;
281       data[i + 2] *= blue;
282     }
283     this.context_.putImageData(image, 0, 0);
284     return 0;
285   }
286   var output = new createjs.Composer.Renderer(width, height);
287   if (!createjs.UserAgent.isAndroidBrowser()) {
288     // Use the blend mode "multiply" for color multiplication on browsers that
289     // provide it. (This blend mode is implemented by all major browsers except
290     // IE (11 or earlier) or Android browsers (on Android 4.3 or earlier), as of
291     // 11 September, 2015.) This operation sets 1 to all alpha values of this
292     // renderer and needs to restore the alpha values of the original image
293     // later.
294     output.setComposition(createjs.Renderer.Composition.COPY);
295     output.drawImage(this.getCanvas());
296     output.setComposition(createjs.Renderer.Composition.MULTIPLY);
297     output.setFillColor('rgb(' +
298         createjs.floor(red * 255) + ',' +
299         createjs.floor(green * 255) + ',' +
300         createjs.floor(blue * 255) + ')');
301     output.fillRect(width, height);
302   } else {
303     // Use the non-standard blend mode "darker" on Android 4.3 or earlier.
304     output.setComposition(createjs.Renderer.Composition.LIGHTER);
305     var renderer = new createjs.Composer.Renderer(width, height);
306     var colors = [
307       { color: 1 - red, mask: '#f00' },
308       { color: 1 - green, mask: '#0f0' },
309       { color: 1 - blue, mask: '#00f' }
310     ];
311     for (var i = 0; i < 3; ++i) {
312       // Copy the source image.
313       renderer.setComposition(createjs.Renderer.Composition.COPY);
314       renderer.drawImage(this.getCanvas());
315 
316       // Extract the specified color component of the source image.
317       renderer.setComposition(createjs.Renderer.Composition.DARKER);
318       renderer.setFillColor(colors[i].mask);
319       renderer.fillRect(width, height);
320 
321       // Draw a black rectangle onto the extracted image with the 'source-over'
322       // operation to multiply the given multiplier with all pixels of the
323       // extracted image as listed in the following formulas. This operation
324       // also sets 1 to all alpha values of this renderer and needs to restore
325       // the alpha values of the original image later.
326       //    for (var i = 0; i < p.length; i += 4) {
327       //      p[i + 0] = (1 - red)   * 0 + (1 - (1 - red))   * p[i + 0];
328       //      p[i + 1] = (1 - green) * 0 + (1 - (1 - green)) * p[i + 1];
329       //      p[i + 2] = (1 - blue)  * 0 + (1 - (1 - blue))  * p[i + 2];
330       //      p[i + 3] = 1;
331       //    }
332       renderer.setComposition(createjs.Renderer.Composition.SOURCE_OVER);
333       if (colors[i].color) {
334         renderer.setAlpha(colors[i].color);
335         renderer.setFillColor('#000');
336         renderer.fillRect(width, height);
337         renderer.setAlpha(1);
338       }
339       output.drawImage(renderer.getCanvas());
340     }
341     renderer.destroy();
342   }
343   // Add an offset color to the output <canvas> element.
344   red = matrix[0 * 5 + 4];
345   green = matrix[1 * 5 + 4];
346   blue = matrix[2 * 5 + 4];
347   if (red || green || blue) {
348     // Add each pixel of the output canvas by the offset color. (
349     // Fill the output canvas with the offset color and the "lighter" operation
350     output.setComposition(createjs.Renderer.Composition.LIGHTER);
351     output.setFillColor('rgb(' + red + ',' + green + ',' + blue + ')');
352     output.fillRect(width, height);
353   }
354   // Draws the composed image into the original image to restore the alpha
355   // values of the original one.
356   this.setComposition(createjs.Renderer.Composition.SOURCE_IN);
357   this.drawImage(output.getCanvas());
358   output.destroy();
359   return 1;
360 };
361 
362 /**
363  * Whether this composer can use the blend mode 'multiply'.
364  * @type {number}
365  * @private
366  */
367 createjs.Composer.hasMultiply_ = -1;
368 
369 /**
370  * The renderer that represents the composed image.
371  * @type {createjs.Composer.Renderer}
372  * @private
373  */
374 createjs.Composer.prototype.renderer_ = null;
375 
376 /**
377  * The image that represents the alpha component of the source image. This
378  * composer copies the red component of this image to the alpha component of the
379  * source image.
380  * @type {HTMLImageElement}
381  * @private
382  */
383 createjs.Composer.prototype.alpha_ = null;
384 
385 /**
386  * The color matrix applied to the source image.
387  * @type {Array.<number>}
388  * @private
389  */
390 createjs.Composer.prototype.matrix_ = null;
391 
392 /**
393  * Returns whether the hosting browser supports the blend mode "multiply".
394  * @return {number}
395  * @private
396  */
397 createjs.Composer.prototype.hasMultiply = function() {
398   if (createjs.Composer.hasMultiply_ < 0) {
399     if (createjs.UserAgent.isMSIE() || createjs.UserAgent.isAndroidBrowser()) {
400       createjs.Composer.hasMultiply_ = 0;
401     } else {
402       createjs.Composer.hasMultiply_ = 1;
403     }
404   }
405   return createjs.Composer.hasMultiply_;
406 };
407 
408 /**
409  * Returns whether the hosting browser supports the blend mode "multiply".
410  * @return {HTMLCanvasElement}
411  * @const
412  */
413 createjs.Composer.prototype.getOutput = function() {
414   /// <returns type="HTMLCanvasElement"/>
415   return this.renderer_ ? this.renderer_.getCanvas() : null;
416 };
417 
418 /**
419  * Destroys all resources owned by this composer.
420  * @const
421  */
422 createjs.Composer.prototype.destroy = function() {
423   if (this.renderer_) {
424     this.renderer_.destroy();
425     this.renderer_ = null;
426   }
427 };
428 
429 /**
430  * Applies an alpha-map filter to the specified image.
431  * @param {HTMLImageElement} image
432  * @param {HTMLImageElement} alpha
433  * @const
434  */
435 createjs.Composer.prototype.applyAlphaMap = function(image, alpha) {
436   /// <param type="HTMLImageElement" name="image"/>
437   /// <param type="HTMLImageElement" name="alpha"/>
438   if (this.alpha_ === alpha) {
439     return;
440   }
441   this.alpha_ = alpha;
442   this.matrix_ = null;
443 
444   // Create a renderer that stores the output <canvas> element and apply the
445   // specified alpha-mask filter to the source image. (The output <canvas>
446   // element represents an alpha-masked image.)
447   var width = image.width;
448   var height = image.height;
449   if (!this.renderer_) {
450     this.renderer_ = new createjs.Composer.Renderer(width, height);
451   }
452   this.renderer_.setComposition(createjs.Renderer.Composition.COPY);
453   this.renderer_.drawAlphaMask(alpha, width, height);
454   this.renderer_.setComposition(createjs.Renderer.Composition.SOURCE_IN);
455   this.renderer_.drawImage(image);
456 };
457 
458 /**
459  * Applies a color filter to the specified image.
460  * @param {HTMLImageElement} image
461  * @param {Array.<number>} matrix
462  * @const
463  */
464 createjs.Composer.prototype.applyColorFilter = function(image, matrix) {
465   /// <param type="HTMLImageElement" name="image"/>
466   /// <param type="Array" elementType="number" name="matrix"/>
467   if (!this.alpha_ && this.matrix_ === matrix) {
468     return;
469   }
470   this.alpha_ = null;
471   this.matrix_ = matrix;
472 
473   var width = image.width;
474   var height = image.height;
475   var red = matrix[0 * 5 + 0];
476   var green = matrix[1 * 5 + 1];
477   var blue = matrix[2 * 5 + 2];
478   if (red == 1 && green == 1 && blue == 1) {
479     red = matrix[0 * 5 + 4];
480     green = matrix[1 * 5 + 4];
481     blue = matrix[2 * 5 + 4];
482     if (!red && !green && !blue) {
483       this.destroy();
484       return;
485     }
486     if (!this.renderer_) {
487       this.renderer_ = new createjs.Composer.Renderer(width, height);
488     }
489     this.renderer_.drawImage(image);
490     this.renderer_.setComposition(createjs.Renderer.Composition.LIGHTER);
491     this.renderer_.setFillColor('rgb(' + red + ',' + green + ',' + blue + ')');
492     this.renderer_.fillRect(width, height);
493     this.renderer_.setComposition(createjs.Renderer.Composition.DESTINATION_IN);
494     this.renderer_.drawImage(image);
495     return;
496   }
497   // Apply a color filter to the output.
498   if (!this.renderer_) {
499     this.renderer_ = new createjs.Composer.Renderer(width, height);
500   }
501   this.renderer_.setComposition(createjs.Renderer.Composition.COPY);
502   this.renderer_.drawImage(image);
503   if (!this.renderer_.multiplyColor(matrix, width, height)) {
504     red = matrix[0 * 5 + 4];
505     green = matrix[1 * 5 + 4];
506     blue = matrix[2 * 5 + 4];
507     this.renderer_.addOffset(red, green, blue, width, height);
508   }
509 };
510