principleMinor
Is this an appropriate approach to "tweak" a color to a similar lighter/darker color?
Viewed 0 times
thiscolorappropriatetweaklighterdarkersimilarapproach
Problem
I have written this function for the sole purpose to get a similar color either brighter or darker than any given color, depending on an average lightness/darkness. Essentially, I add up the R/G/B values and divide by 3, thus giving the average intensity of each color channel. Then, depending on that result, if it's less than half (darker) I lighten the color, and if it's more than half (lighter), I darken the color. The amount to lighten/darken depends on a given "difference" parameter (
The purpose and use of this function is in custom drawing controls to color themes, and shading colors based on other colors. If the color is dark, the tweaked color is a little bit lighter. If the color is light, the tweaked color is a little bit darker.
Do you see any shortcomings with my approach? Could I make this any better?
Example Usage:
Diff).The purpose and use of this function is in custom drawing controls to color themes, and shading colors based on other colors. If the color is dark, the tweaked color is a little bit lighter. If the color is light, the tweaked color is a little bit darker.
Do you see any shortcomings with my approach? Could I make this any better?
function IntRange(const Value, Min, Max: Integer): Integer;
begin
Result:= Value;
if Result Max then Result:= Max;
end;
function TweakColor(const AColor: TColor; const ADiff: Integer): TColor;
var
R, G, B: Byte;
D: Integer;
Dir: Integer;
begin
R:= GetRValue(AColor);
G:= GetGValue(AColor);
B:= GetBValue(AColor);
D:= (R + G + B) div 3; //D = average difference of all 3 color channels
if D >= (256 div 2) then begin //If average is a lighter color...
Dir:= -ADiff; //Make the color darker
end else begin //If average is a darker color...
Dir:= ADiff; //Make the color lighter
end;
R:= IntRange(R + Dir, 0, 255);
G:= IntRange(G + Dir, 0, 255);
B:= IntRange(B + Dir, 0, 255);
Result:= RGB(R, G, B);
end;Example Usage:
Canvas.Font.Color:= TweakColor(Canvas.Font.Color, 50);Solution
I think that your algorithm is overly simplistic. To illustrate its problems, try running the Stack Snippet in this answer, in which I have translated your
It has three significant flaws, in my opinion:
-
It is subject to jarring discontinuities when switching from darkening to lightening mode and vice versa.
For example, with Diff=127, try changing the input color from rgb(234,150,0) to rgb(233,150,0). The output color jumps from maroon to creamy yellow.
-
It doesn't preserve the hue, which is what most people would consider to be the most important attribute of a color.
In the example above, with rgb(234,150,0) as the input color, as Diff increases from 0, we see the output change from orange to red. I would expect any tweaking to result in another color that is still recognizably "orange". If you start from the neighboring rgb(233,150,0), then the color changes to yellow as Diff increases from 0.
-
Its behavior is hard to characterize.
For example, starting with rgb(255,130,0), and increasing Diff from 0, we see that the output fades from orange to black. In contrast, starting with rgb(255,125,0), it fades from orange to white.
As another example, starting with rgb(0,80,160) and increasing Diff from 0, we see true blue turning towards cyan as blue is clamped at 255, then trending towards white as green is clamped at 255.
The inconsistency makes no sense to the user.
The remedy is to map the RGB input to a color wheel — I suggest using the hue-saturation-value color model. Define the tweak to be a hue-preserving transformation. My suggestion is to vary just the value, but you could also vary saturation instead.
display: inline-block;
width: 25%;
}
fieldset#tweak {
display: block;
width: 100%;
border-width: 0;
}
label {
display: block;
}
.swatch {
border: 1px solid grey;
width: 100%;
height: 1em;
}
input[type=range] {
width: 40%;
}
Diff
Input Color
R
G
B
Output (Yours)
R
G
B
Output (Suggested)
R
G
B
`
TweakColor() function into JavaScript for demonstration purposes.It has three significant flaws, in my opinion:
-
It is subject to jarring discontinuities when switching from darkening to lightening mode and vice versa.
For example, with Diff=127, try changing the input color from rgb(234,150,0) to rgb(233,150,0). The output color jumps from maroon to creamy yellow.
-
It doesn't preserve the hue, which is what most people would consider to be the most important attribute of a color.
In the example above, with rgb(234,150,0) as the input color, as Diff increases from 0, we see the output change from orange to red. I would expect any tweaking to result in another color that is still recognizably "orange". If you start from the neighboring rgb(233,150,0), then the color changes to yellow as Diff increases from 0.
-
Its behavior is hard to characterize.
For example, starting with rgb(255,130,0), and increasing Diff from 0, we see that the output fades from orange to black. In contrast, starting with rgb(255,125,0), it fades from orange to white.
As another example, starting with rgb(0,80,160) and increasing Diff from 0, we see true blue turning towards cyan as blue is clamped at 255, then trending towards white as green is clamped at 255.
The inconsistency makes no sense to the user.
The remedy is to map the RGB input to a color wheel — I suggest using the hue-saturation-value color model. Define the tweak to be a hue-preserving transformation. My suggestion is to vary just the value, but you could also vary saturation instead.
function TweakColor(const AColor: TRGBColor; const ADiff: Integer): TRGBColor;
var
HSV: THSVColor;
V, VAdj: Single;
begin
HSV := RGBToHSV(AColor);
V := GetVValue(HSV);
VAdj := (ADiff / 128) * (V - 0.5);
Result := HSVToRGB(HSV(GetHValue(HSV), GetSValue(HSV), V - VAdj));
end;/ Your original function, translated into JavaScript /
function tweakColor(aColor, aDiff) {
function intRange(value, min, max) {
return value max ? max
: value;
}
var r = aColor.r,
g = aColor.g,
b = aColor.b;
var d = (r + g + b) / 3;
var dir = (d >= (256 / 2)) ? -aDiff : aDiff;
r = intRange(r + dir, 0, 255);
g = intRange(g + dir, 0, 255);
b = intRange(b + dir, 0, 255);
return new RGBColor(r, g, b);
}
function newTweakColor(aColor, aDiff) {
var hsv = aColor.toHSV();
var vAdj = (aDiff / 128) * (hsv.v - 0.5);
return (new HSVColor(hsv.h, hsv.s, hsv.v - vAdj)).toRGB();
}
function RGBColor(r, g, b) {
this.r = r; this.g = g; this.b = b;
}
RGBColor.prototype.toString = function() {
return 'rgb(' + Math.round(this.r) + ',' + Math.round(this.g) + ',' + Math.round(this.b) + ')';
};
/ Based on formulas from http://www.rapidtables.com/convert/color/rgb-to-hsv.htm /
RGBColor.prototype.toHSV = function toHSV() {
var r = this.r / 255,
g = this.g / 255,
b = this.b / 255;
var cMax = Math.max(r, g, b),
cMin = Math.min(r, g, b);
var Δ = cMax - cMin;
var hue = 60 * ( (cMax == r) ? ((g - b) / Δ) % 6
: (cMax == g) ? ((b - r) / Δ) + 2
: ((r - g) / Δ) + 4 );
var sat = cMax == 0 ? 0 : Δ / cMax;
var val = cMax;
return new HSVColor((hue + 360) % 360, sat, val);
}
function HSVColor(h, s, v) {
this.h = h; this.s = s; this.v = v;
}
HSVColor.prototype.toString = function() {
return 'hsl(' + Math.round(this.h) + ',' + Math.round(100 this.s) + '%,' + Math.round(100 this.v) + '%)';
};
/ http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV /
HSVColor.prototype.toRGB = function toRGB() {
var c = this.v * this.s;
var h = this.h / 60;
var x = c * (1 - Math.abs(h % 2 - 1));
var r1 = (h = 5) ? c
: (h = 4) ? x : 0;
var g1 = (h >= 1 && h = 0 && h = 3 && h = 2 && h
fieldset {display: inline-block;
width: 25%;
}
fieldset#tweak {
display: block;
width: 100%;
border-width: 0;
}
label {
display: block;
}
.swatch {
border: 1px solid grey;
width: 100%;
height: 1em;
}
input[type=range] {
width: 40%;
}
Diff
Input Color
R
G
B
Output (Yours)
R
G
B
Output (Suggested)
R
G
B
`
Code Snippets
function TweakColor(const AColor: TRGBColor; const ADiff: Integer): TRGBColor;
var
HSV: THSVColor;
V, VAdj: Single;
begin
HSV := RGBToHSV(AColor);
V := GetVValue(HSV);
VAdj := (ADiff / 128) * (V - 0.5);
Result := HSVToRGB(HSV(GetHValue(HSV), GetSValue(HSV), V - VAdj));
end;Context
StackExchange Code Review Q#71376, answer score: 4
Revisions (0)
No revisions yet.