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