This is a plugin I wrote to test a method for computing the color gradient, but which might be useful for non-testing reasons. I call it (the rather unwieldy) "Display Color Change Direction" because "color gradient" has another common meaning for plugins, and I didn't want to be confusing. The maximum direction of the color change is shown by the hue. It's in the Effects>Stylize menu.
The interface is:
The Color Scale controls control the color brightness. To allow for a wide range, I have coarse and fine controls.
Color Range expands or compresses the range of colors. Because the colors no longer form a continuous circle, some anomalies may a occur when adjacent colors with almost the same color-change direction are colored with colors at the opposite ends of the color range. This problem can be reduced by using Reflected Color Range.
Color Shift shifts the hue around the color circle.
Color Phase changes which color is associated with which color-change direction. When the full range of colors is used, it duplicates the function of the Color Shift control. When used with a restricted range, the Color Shift selects which colors are used, and the Color Phase selects how the current range of colors are associated with the color change directions.
Reverse Color Order reverses the order around the color circle.
Full Circle assigns a direction to the color change. Normally, since the color changes as rapidly in the opposite direction, the direction of maximum color change is between 0° and 180°. When this option is set, a direction is assigned to the color, based on the change in intensity of the back-and-white image. I made True the default, both because I think the images generally look better, and because it makes the encoded color change direction correspond to the PDN color wheel.
Reflected Color Range reflects the current color range so instead of the going from the beginning color to the ending color, it goes from the beginning color to the ending color at mid-range, then in the opposite order, back to the beginning color. (E.g., Red->Yellow->Green->Yellow->Red.) This makes the color range continuous around the circle of color-directions, avoiding color anomalies when using restricted color ranges.
Transparent Background colors makes the background transparent instead of black.
As I've mentioned, the purpose was to test the algorithm, but it can be used to produce some interesting effects. For example, the background for this sig was produced using only Clouds and this plugin:
The plugin can also be used for edge detection. For example, by running the plugin, then converting to black-and-white and inverting the colors.
Here is the CodeLab code:
// Author: MJW
// Name: Display Color Change Direction
// Title: Display Color Change Direction
// Submenu: Stylize
// Desc: Show the direction of the maximum color change as a color.
// Keywords: color change direction
double Amount1 = 10; //[0, 25]Color Scale (Coarse)
double Amount2 = 0; //[-1, 1]Color Scale (Fine)
double Amount3 = 1.0; //[0, 2]Color Range
double Amount4 = 0.0; //[0, 1]Color Shift
double Amount5 = 0.0; //[0, 1]Color Phase
bool Amount6 = false; //Reverse Color Order
bool Amount7 = true; //Full Circle
bool Amount8 = false; //Reflected Color Range
bool Amount9 = false; //Transparent Background
Surface Src, Dst;
int maxX, maxY;
double colorOrder, colorOrderAdj;
const int middleWeight = 2;
const double angleScale = 1.0 / Math.PI;
void Render(Surface dst, Surface src, Rectangle rect)
colorScale = 0.002 * (Amount1 + Amount2);
if (colorScale < 0)
colorScale = 0;
maxScale = (colorScale >= 25.0); // If scale is max., display all colors at full range.
colorScale *= 4.0 / (2.0 + (double)middleWeight);
colorShift = Amount4;
colorPhase = Amount5;
colorRange = Amount3;
if (Amount6) // Sign of scaling chosen to match color menu wheel.
colorOrder = -angleScale;
colorOrderAdj = 1.0;
colorOrder = angleScale;
colorOrderAdj = 0.0;
fullCircle = Amount7;
reflectColors = Amount8;
transparentBackground = Amount9;
Src = src;
Dst = dst;
Rectangle selection = this.EnvironmentParameters.GetSelection(src.Bounds).GetBoundsInt();
int left = rect.Left;
int right = rect.Right;
int top = rect.Top;
int bottom = rect.Bottom;
maxX = src.Width - 1;
maxY = src.Height - 1;
for (int y = top; y < bottom; y++)
for (int x = left; x < right; x++)
dst[x, y] = FindGradientColor(x, y);
protected ColorBgra FindGradientColor(int x, int y)
ColorBgra MM, UM, UR, MR, LR, LM, LL, ML, UL;
ColorBgra gradColor = ColorBgra.Black;
int lX, rX, uY, lY;
lX = (x == 0) ? x : x - 1;
rX = (x == maxX) ? x : x + 1;
uY = (y == 0) ? y : y - 1;
lY = (y == maxY) ? y : y + 1;
MM = Src.GetPointUnchecked(x, y);
UM = Src.GetPointUnchecked(x, uY);
UR = Src.GetPointUnchecked(rX, uY);
MR = Src.GetPointUnchecked(rX, y);
LR = Src.GetPointUnchecked(rX, lY);
LM = Src.GetPointUnchecked(x, lY);
LL = Src.GetPointUnchecked(lX, lY);
ML = Src.GetPointUnchecked(lX, y);
UL = Src.GetPointUnchecked(lX, uY);
int xR = (UL.R - UR.R) + middleWeight * (ML.R - MR.R) + (LL.R - LR.R);
int xG = (UL.G - UR.G) + middleWeight * (ML.G - MR.G) + (LL.G - LR.G);
int xB = (UL.B - UR. + middleWeight * (ML.B - MR. + (LL.B - LR.;
int yR = (UL.R - LL.R) + middleWeight * (UM.R - LM.R) + (UR.R - LR.R);
int yG = (UL.G - LL.G) + middleWeight * (UM.G - LM.G) + (UR.G - LR.G);
int yB = (UL.B - LL. + middleWeight * (UM.B - LM. + (UR.B - LR.;
int xDelta, yDelta, mag2;
// The straight-forward implementation.
// The change in each color component can either be consdered positive or negative.
// Since changing all the signs won't change the magnitude of the change, red is
// fixed and blue and green can have either sign.
// Compute all four versions and choose the on that gives the largest magnitude.
int xDeltaA, yDeltaA, xDeltaB, yDeltaB, xDeltaC, yDeltaC;
int mag2A, mag2B, mag2C;
xDelta = xR + xG + xB; yDelta = yR + yG + yB;
xDeltaA = xR + xG - xB; yDeltaA = yR + yG - yB;
xDeltaB = xR - xG + xB; yDeltaB = yR - yG + yB;
xDeltaC = xR - xG - xB; yDeltaC = yR - yG - yB;
mag2 = xDelta * xDelta + yDelta * yDelta;
mag2A = xDeltaA * xDeltaA + yDeltaA * yDeltaA;
mag2B = xDeltaB * xDeltaB + yDeltaB * yDeltaB;
mag2C = xDeltaC * xDeltaC + yDeltaC * yDeltaC;
// Use the one with the largest magnitude.
if (mag2A > mag2)
xDelta = xDeltaA; yDelta = yDeltaA; mag2 = mag2A;
if (mag2C > mag2B)
xDeltaB = xDeltaC; yDeltaB = yDeltaC; mag2B = mag2C;
if (mag2B > mag2)
xDelta = xDeltaB; yDelta = yDeltaB; mag2 = mag2B;
if (mag2 == 0)
return transparentBackground ? ColorBgra.Transparent : ColorBgra.Black;
// Adjust sign of deltas so yDelta >= 0. This will produce an ATan between 0 and PI.
if (yDelta < 0)
xDelta = -xDelta; yDelta = -yDelta;
double magnitude = Math.Sqrt((double)mag2);
double xComponent = xDelta / magnitude;
double yComponent = yDelta / magnitude;
double value = maxScale ? 1.0 : Math.Min(1.0, colorScale * magnitude);
// 0 <= hue <= 1.0
double hue = colorOrder * Math.Atan2(yComponent, xComponent) + colorOrderAdj;
// If full circle, use the intensity to determine the direction.
hue *= 0.5;
double intensity = xComponent * (xR + xG + xB) + yComponent * (yR + yG + yB);
if (intensity > 0.0)
hue += 0.5;
// Adjust the phase before restricting the range.
hue += colorPhase;
if (hue >= 1.0)
hue -= 1.0;
// Reflected colors match at the endpoints for better restricted range colors.
hue *= 2.0;
if (hue > 1.0)
hue = 2.0 - hue;
hue = colorRange * hue + colorShift;
gradColor = transparentBackground ?
HSVtoRGBA(hue, 1.0, 1.0, value) :
HSVtoRGB(hue, 1.0, value);
public ColorBgra HSVtoRGBA(double H, double S, double V, double A)
byte r, g, b;
HSVtoRGB(H, S, V, out r, out g, out ;
return ColorBgra.FromBgra(b, g, r, (byte)(255 * A + 0.5));
public ColorBgra HSVtoRGB(double H, double S, double V)
byte r, g, b;
HSVtoRGB(H, S, V, out r, out g, out ;
return ColorBgra.FromBgr(b, g, r);
public void HSVtoRGB(double H, double S, double V, out byte bR, out byte bG, out byte bB)
// Parameters must satisfy the following ranges:
// 0.0 <= H < 1.0
// 0.0 <= S <= 1.0
// 0.0 <= V <= 1.0
// Handle special case of gray (so no Hue) first
if ((S == 0.0) || (V == 0.0))
byte x = (byte)(int)(V * 255.0);
bR = x;
bG = x;
bB = x;
H = HueConstrain(H);
double R = V, G = V, B = V;
double Hi = Math.Floor(6.0 * H);
double f = 6.0 * H - Hi;
double p = V * (1.0 - S);
double q = V * (1.0 - f * S);
double t = V * (1.0 - (1.0 - f) * S);
if (Hi == 0.0)
R = V;
G = t;
B = p;
else if (Hi == 1.0)
R = q;
G = V;
B = p;
else if (Hi == 2.0)
R = p;
G = V;
B = t;
else if (Hi == 3.0)
R = p;
G = q;
B = V;
else if (Hi == 4.0)
R = t;
G = p;
B = V;
else // if (Hi == 5.0)
R = V;
G = p;
B = q;
int iR = (int)(R * 255.0 + 0.5);
int iG = (int)(G * 255.0 + 0.5);
int iB = (int)(B * 255.0 + 0.5);
bR = (byte)iR;
bG = (byte)iG;
bB = (byte)iB;
public double HueConstrain(double MyHue)
// Makes sure that 0.0 <= MyAngle < 1.0
// Wraps around the value if its outside this range
while (MyHue >= 1.0)
MyHue -= 1.0;
while (MyHue < 0.0)
MyHue += 1.0;
Here's the (not especially attractive!) icon:
Here is the plugin: DisplayColorChangeDirection.zip
EDIT: Fixed spelling of "Coarse" (H/T, Djisves). Changed version to 1.1.
EDIT: Restored icon, which I forgot in the previous version (sorry about that). Changed version to 1.2.
EDIT: Added Color Range control. Changed version to 1.3.
EDIT: Added Color Phase control. Changed version to 1.4.
EDIT: Removed mostly unnecessary Middle Weight and Original Image controls. Replaced (at Eli's suggestion) White Background with Transparent Background. Added Reflected Color Range for improved colors when using restricted ranges. Changed version to 2.0.