6
\$\begingroup\$

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 (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 < Min then Result:= Min;
  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);
\$\endgroup\$
4
\$\begingroup\$

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 TweakColor() function into JavaScript for demonstration purposes.

It has three significant flaws, in my opinion:

  1. 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.

  2. 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.

  3. 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 < min ? min
         : 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 < 1 || h >= 5) ? c
         : (h < 2 || h >= 4) ? x : 0;
  var g1 = (h >= 1 && h < 3) ? c
         : (h >= 0 && h < 4) ? x : 0;
  var b1 = (h >= 3 && h < 5) ? c
         : (h >= 2 && h < 6) ? x : 0;
  var m = this.v - c;
  return new RGBColor(Math.round(255 * (r1 + m)),
                      Math.round(255 * (g1 + m)),
                      Math.round(255 * (b1 + m)));
};

var ColorTweakerCtrl = function ColorTweakerCtrl($scope) {
  $scope.inColor  = new RGBColor(234, 150, 0);
  $scope.diff = 127;
  
  $scope.tweak = function tweak() {
    $scope.inColor.r = Math.round($scope.inColor.r);
    $scope.inColor.g = Math.round($scope.inColor.g);
    $scope.inColor.b = Math.round($scope.inColor.b);
    $scope.diff = Math.round($scope.diff);
    $scope.outColor = tweakColor($scope.inColor, $scope.diff);
    $scope.newColor = newTweakColor($scope.inColor, $scope.diff);
  };
  $scope.tweak();
};
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%;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app>
  <form ng-controller="ColorTweakerCtrl">
    <fieldset id="tweak">
      <label>Diff
        <input ng-model="diff" type="range" min="0" max="255" ng-change="tweak()">
        <input ng-model="diff" type="number" min="0" max="255" ng-change="tweak()">
      </label>
    </fieldset>
    
    <fieldset>
      <legend>Input Color</legend>
      <div class="swatch" style="background-color: {{ inColor.toString() }}"></div>
      <label>R
        <input ng-model="inColor.r" type="range" min="0" max="255" ng-change="tweak()">
        <input ng-model="inColor.r" type="number" min="0" max="255" ng-change="tweak()">
      </label>
      <label>G
        <input ng-model="inColor.g" type="range" min="0" max="255" ng-change="tweak()">
        <input ng-model="inColor.g" type="number" min="0" max="255" ng-change="tweak()">
      </label>
      <label>B
        <input ng-model="inColor.b" type="range" min="0" max="255" ng-change="tweak()">
        <input ng-model="inColor.b" type="number" min="0" max="255" ng-change="tweak()">
      </label>
    </fieldset>

    <fieldset>
      <legend>Output (Yours)</legend>
      <div class="swatch" style="background-color: {{ outColor.toString() }}"></div>
      <label>R
        <input ng-model="outColor.r" type="range" min="0" max="255" disabled>
        <input ng-model="outColor.r" type="number" min="0" max="255" disabled>
      </label>
      <label>G
        <input ng-model="outColor.g" type="range" min="0" max="255" disabled>
        <input ng-model="outColor.g" type="number" min="0" max="255" disabled>
      </label>
      <label>B
        <input ng-model="outColor.b" type="range" min="0" max="255" disabled>
        <input ng-model="outColor.b" type="number" min="0" max="255" disabled>
      </label>
    </fieldset>


    <fieldset>
      <legend>Output (Suggested)</legend>
      <div class="swatch" style="background-color: {{ newColor.toString() }}"></div>
      <label>R
        <input ng-model="newColor.r" type="range" min="0" max="255" disabled>
        <input ng-model="newColor.r" type="number" min="0" max="255" disabled>
      </label>
      <label>G
        <input ng-model="newColor.g" type="range" min="0" max="255" disabled>
        <input ng-model="newColor.g" type="number" min="0" max="255" disabled>
      </label>
      <label>B
        <input ng-model="newColor.b" type="range" min="0" max="255" disabled>
        <input ng-model="newColor.b" type="number" min="0" max="255" disabled>
      </label>
    </fieldset>
  </form>
</div>

\$\endgroup\$
1
\$\begingroup\$

I see as open question how does your code handle the edge cases (extremely black "theme" or extremely white "theme").

In order to decide if your shading algorithm produces good results, the only way is to draw complete shaded color space map and decide if it is "pleasant" by consulting several users. There is no one clear answer and although a various color theories exist it is mostly the graphics designer who makes the decision (not a machine).

Some decade ago the code below worked for me (found by guessing) and was able to reproduce what the lighter/darker color calculator used by Microsoft Windows Control Panel did. When user chose a COLOR_3DFACE this calculated appropriate COLOR_3DLIGHT and COLOR_3DSHADOW

h := ColorToHLS(c);
t := h;
t.luminance := Round(h.luminance * 1.23);
FPens[pbLight, j].Color := HLSToColor(t);
t.luminance := Round(h.luminance * 0.67);
FPens[pbDark, j].Color := HLSToColor(t);

Nowadays I would look for the appropriate algorithm in open source codes of a library that does it well, besides Google I'd take a look at Windows Presentation Foundation (WPF) source codes, e.g. http://referencesource.microsoft.com/#q=dark

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.