// =================================================================================================
//
// Starling Framework
// Copyright Gamua GmbH. All Rights Reserved.
//
// This program is free software. You can redistribute and/or modify it
// in accordance with the terms of the accompanying license agreement.
//
// =================================================================================================
package starling.filters
{
import starling.rendering.FilterEffect;
import starling.utils.Color;
/** The ColorMatrixFilter class lets you apply a 4x5 matrix transformation to the color
* and alpha values of every pixel in the input image to produce a result with a new set
* of color and alpha values. This allows saturation changes, hue rotation,
* luminance to alpha, and various other effects.
*
*
The class contains several convenience methods for frequently used color
* adjustments. All those methods change the current matrix, which means you can easily
* combine them in one filter:
*
*
* // create an inverted filter with 50% saturation and 180° hue rotation
* var filter:ColorMatrixFilter = new ColorMatrixFilter();
* filter.invert();
* filter.adjustSaturation(-0.5);
* filter.adjustHue(1.0);
*
* If you want to gradually animate one of the predefined color adjustments, either reset
* the matrix after each step, or use an identical adjustment value for each step; the
* changes will add up.
*/
public class ColorMatrixFilter extends FragmentFilter
{
// Most of the color transformation math was taken from the excellent ColorMatrix class by
// Mario Klingemann: http://www.quasimondo.com/archives/000565.php -- THANKS!!!
private static const LUMA_R:Number = 0.299;
private static const LUMA_G:Number = 0.587;
private static const LUMA_B:Number = 0.114;
// helpers
private static var sMatrix:Vector. = new [];
/** Creates a new ColorMatrixFilter instance with the specified matrix.
* @param matrix a vector of 20 items arranged as a 4x5 matrix.
*/
public function ColorMatrixFilter(matrix:Vector.=null)
{
if (matrix) colorEffect.matrix = matrix;
}
/** @private */
override protected function createEffect():FilterEffect
{
return new ColorMatrixEffect();
}
// color manipulation
/** Inverts the colors of the filtered object. */
public function invert():void
{
concatValues(-1, 0, 0, 0, 255,
0, -1, 0, 0, 255,
0, 0, -1, 0, 255,
0, 0, 0, 1, 0);
}
/** Changes the saturation. Typical values are in the range (-1, 1).
* Values above zero will raise, values below zero will reduce the saturation.
* '-1' will produce a grayscale image. */
public function adjustSaturation(sat:Number):void
{
sat += 1;
var invSat:Number = 1 - sat;
var invLumR:Number = invSat * LUMA_R;
var invLumG:Number = invSat * LUMA_G;
var invLumB:Number = invSat * LUMA_B;
concatValues((invLumR + sat), invLumG, invLumB, 0, 0,
invLumR, (invLumG + sat), invLumB, 0, 0,
invLumR, invLumG, (invLumB + sat), 0, 0,
0, 0, 0, 1, 0);
}
/** Changes the contrast. Typical values are in the range (-1, 1).
* Values above zero will raise, values below zero will reduce the contrast. */
public function adjustContrast(value:Number):void
{
var s:Number = value + 1;
var o:Number = 128 * (1 - s);
concatValues(s, 0, 0, 0, o,
0, s, 0, 0, o,
0, 0, s, 0, o,
0, 0, 0, 1, 0);
}
/** Changes the brightness. Typical values are in the range (-1, 1).
* Values above zero will make the image brighter, values below zero will make it darker.*/
public function adjustBrightness(value:Number):void
{
value *= 255;
concatValues(1, 0, 0, 0, value,
0, 1, 0, 0, value,
0, 0, 1, 0, value,
0, 0, 0, 1, 0);
}
/** Changes the hue of the image. Typical values are in the range (-1, 1). */
public function adjustHue(value:Number):void
{
value *= Math.PI;
var cos:Number = Math.cos(value);
var sin:Number = Math.sin(value);
concatValues(
((LUMA_R + (cos * (1 - LUMA_R))) + (sin * -(LUMA_R))), ((LUMA_G + (cos * -(LUMA_G))) + (sin * -(LUMA_G))), ((LUMA_B + (cos * -(LUMA_B))) + (sin * (1 - LUMA_B))), 0, 0,
((LUMA_R + (cos * -(LUMA_R))) + (sin * 0.143)), ((LUMA_G + (cos * (1 - LUMA_G))) + (sin * 0.14)), ((LUMA_B + (cos * -(LUMA_B))) + (sin * -0.283)), 0, 0,
((LUMA_R + (cos * -(LUMA_R))) + (sin * -((1 - LUMA_R)))), ((LUMA_G + (cos * -(LUMA_G))) + (sin * LUMA_G)), ((LUMA_B + (cos * (1 - LUMA_B))) + (sin * LUMA_B)), 0, 0,
0, 0, 0, 1, 0);
}
/** Tints the image in a certain color, analog to what can be done in Adobe Animate.
*
* @param color the RGB color with which the image should be tinted.
* @param amount the intensity with which tinting should be applied. Range (0, 1).
*/
public function tint(color:uint, amount:Number=1.0):void
{
var r:Number = Color.getRed(color) / 255.0;
var g:Number = Color.getGreen(color) / 255.0;
var b:Number = Color.getBlue(color) / 255.0;
var q:Number = 1 - amount;
var rA:Number = amount * r;
var gA:Number = amount * g;
var bA:Number = amount * b;
concatValues(
q + rA * LUMA_R, rA * LUMA_G, rA * LUMA_B, 0, 0,
gA * LUMA_R, q + gA * LUMA_G, gA * LUMA_B, 0, 0,
bA * LUMA_R, bA * LUMA_G, q + bA * LUMA_B, 0, 0,
0, 0, 0, 1, 0);
}
// matrix manipulation
/** Changes the filter matrix back to the identity matrix. */
public function reset():void
{
matrix = null;
}
/** Concatenates the current matrix with another one. */
public function concat(matrix:Vector.):void
{
colorEffect.concat(matrix);
setRequiresRedraw();
}
/** Concatenates the current matrix with another one, passing its contents directly. */
public function concatValues(m0:Number, m1:Number, m2:Number, m3:Number, m4:Number,
m5:Number, m6:Number, m7:Number, m8:Number, m9:Number,
m10:Number, m11:Number, m12:Number, m13:Number, m14:Number,
m15:Number, m16:Number, m17:Number, m18:Number, m19:Number):void
{
sMatrix.length = 0;
sMatrix.push(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9,
m10, m11, m12, m13, m14, m15, m16, m17, m18, m19);
concat(sMatrix);
}
/** A vector of 20 items arranged as a 4x5 matrix. */
public function get matrix():Vector. { return colorEffect.matrix; }
public function set matrix(value:Vector.):void
{
colorEffect.matrix = value;
setRequiresRedraw();
}
private function get colorEffect():ColorMatrixEffect
{
return this.effect as ColorMatrixEffect;
}
}
}
import flash.display3D.Context3D;
import flash.display3D.Context3DProgramType;
import starling.rendering.FilterEffect;
import starling.rendering.Program;
class ColorMatrixEffect extends FilterEffect
{
private var _userMatrix:Vector.; // offset in range 0-255
private var _shaderMatrix:Vector.; // offset in range 0-1, changed order
private static const MIN_COLOR:Vector. = new [0, 0, 0, 0.0001];
private static const IDENTITY:Array = [1,0,0,0,0, 0,1,0,0,0, 0,0,1,0,0, 0,0,0,1,0];
// helpers
private static var sMatrix:Vector. = new Vector.(20, true);
public function ColorMatrixEffect():void
{
_userMatrix = new [];
_shaderMatrix = new [];
this.matrix = null;
}
override protected function createProgram():Program
{
var vertexShader:String = FilterEffect.STD_VERTEX_SHADER;
var fragmentShader:String = [
tex("ft0", "v0", 0, texture), // read texture color
"max ft0, ft0, fc5 ", // avoid division through zero in next step
"div ft0.xyz, ft0.xyz, ft0.www ", // restore original (non-PMA) RGB values
"m44 ft0, ft0, fc0 ", // multiply color with 4x4 matrix
"add ft0, ft0, fc4 ", // add offset
"mul ft0.xyz, ft0.xyz, ft0.www ", // multiply with alpha again (PMA)
"mov oc, ft0 " // copy to output
].join("\n");
return Program.fromSource(vertexShader, fragmentShader);
}
override protected function beforeDraw(context:Context3D):void
{
super.beforeDraw(context);
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, _shaderMatrix);
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 5, MIN_COLOR);
}
// matrix manipulation
public function reset():void
{
matrix = null;
}
/** Concatenates the current matrix with another one. */
public function concat(matrix:Vector.):void
{
var i:int = 0;
for (var y:int=0; y<4; ++y)
{
for (var x:int=0; x<5; ++x)
{
sMatrix[i+x] = matrix[i ] * _userMatrix[x ] +
matrix[i + 1] * _userMatrix[x + 5] +
matrix[i + 2] * _userMatrix[x + 10] +
matrix[i + 3] * _userMatrix[x + 15] +
(x == 4 ? matrix[i + 4] : 0);
}
i += 5;
}
copyMatrix(sMatrix, _userMatrix);
updateShaderMatrix();
}
private function copyMatrix(from:Vector., to:Vector.):void
{
for (var i:int=0; i<20; ++i)
to[i] = from[i];
}
private function updateShaderMatrix():void
{
// the shader needs the matrix components in a different order,
// and it needs the offsets in the range 0-1.
_shaderMatrix.length = 0;
_shaderMatrix.push(
_userMatrix[0 ], _userMatrix[ 1], _userMatrix[ 2], _userMatrix[ 3],
_userMatrix[5 ], _userMatrix[ 6], _userMatrix[ 7], _userMatrix[ 8],
_userMatrix[10], _userMatrix[11], _userMatrix[12], _userMatrix[13],
_userMatrix[15], _userMatrix[16], _userMatrix[17], _userMatrix[18],
_userMatrix[ 4] / 255.0, _userMatrix[9] / 255.0, _userMatrix[14] / 255.0,
_userMatrix[19] / 255.0
);
}
// properties
public function get matrix():Vector. { return _userMatrix; }
public function set matrix(value:Vector.):void
{
if (value && value.length != 20)
throw new ArgumentError("Invalid matrix length: must be 20");
if (value == null)
{
_userMatrix.length = 0;
_userMatrix.push.apply(_userMatrix, IDENTITY);
}
else
{
copyMatrix(value, _userMatrix);
}
updateShaderMatrix();
}
}