Colour Gradient        

Introduction

I made this class because I was in need of an efficient way to access interpolated colour values stored inside a colour gradient, for updating sky colours (but I'm sure you could find this colour gradient implementation useful for anything else). I found a gradient class in SkyX, but I decided to write my own implementation. It handles default values for the gradient bounds (black for position 0 of the gradient, white for position 1) which are use to interpolate values if no bounds (no value for position 0 nor 1) were provided. It also possible to disable the standard bounds, the colour of the lowest value will then fill the gradient positions from 0, and the upper gradient colour will fill the positions to the end. You can have a look to the test cases at the end of this page to see how those parameters impact the gradient data.
It only uses std and Ogre, so it should be portable (although it was only tested in a Windows environment).

Version note:
Developed for Ogre 1.8, should work with 1.7 too.

#ifndef __COLOUR_GRADIENT_H__
#define __COLOUR_GRADIENT_H__

#include <Ogre.h>
#include <map>

/*!
@brief Colour gradient (linear interpolation).
@details This gradient class support "autofilling" of bounds values, ie, if no colour frame with index 0.0 is provided, it use a default black colour for this position of the gradient.
In the same way, if no colour frame is provided for 1.0 value, a default white colour is used for this position of the gradient.
Autobounds default colours (black/white) can be changed or using the appropriate constructor.
Disabling autobounds is also possible (no fading to autocolour), check the default constructor optionnal parameter.
@remark idea comes from SkyX, but totally rewritten in order to be more efficient.
*/
class ColourGradient
{
public:
protected:
private:
  const bool fillBounds;                                   //!< Should we fill bound of the gradient with default colours?
  typedef std::map<float, Ogre::ColourValue> ColourFrames; //!< ColourFrames data type
  ColourFrames colourFrames;                               //!< Colour frame map
  const Ogre::ColourValue minBoundDefaultColour;           //!< The default colour for min bound
  const Ogre::ColourValue maxBoundDefaultColour;           //!< The default colour for max bound
public:
  typedef ColourFrames::value_type ColourFrame;            //!< A single colour frame, first element of the pair is the position in the gradient, second element is the colour to insert in the gradient.

public:
  /*!
  Constructor. Gradient values range is [0.0, 1.0].
  @param fillBoundsWithDefault If true, and no colour frame with index 0.0 was provided, provide a default black colour for this position of the gradient. In the same way, if no colour frame is provided for 1.0 value, a default white colour is provided for this position of the gradient.
  */
  ColourGradient(bool fillBoundsWithDefault=true);

  /*!
  Constructor, bound filling is automatically activated when using this constructor. Gradient values range is [0.0, 1.0]
  @param overridedMinDefaultColour The default "black" colour used by automatic bound filling will be replaced by the provided colour
  @param overridedMaxDefaultColour
  */
  ColourGradient(const Ogre::ColourValue& overridedMinDefaultColour, const Ogre::ColourValue& overridedMaxDefaultColour);

  /*!
  Destructor 
  */
  ~ColourGradient();

  /*!
  @Add colour frame. Gradient values range is [0.0, 1.0]
  @param CFrame Colour frame
  */
  inline void AddColourFrame(const ColourFrame& colourFrame)
  {
    AddColourFrame(colourFrame.first, colourFrame.second);
  }

  /*!
  Add colour frame. Gradient values range is [0.0, 1.0]
  @param colour Coulour of this frame
  @param gradientPosition Position of this frame in the gradient (overwrite if value exists).
  */
  inline void AddColourFrame(const float& gradientPosition, const Ogre::ColourValue& colour)
  {
    assert((gradientPosition >= 0.0f)&&(gradientPosition<=1.0f));
    if(colourFrames.find(gradientPosition) == colourFrames.end())
      colourFrames.insert(ColourFrames::value_type(gradientPosition, colour));
    else
      colourFrames[gradientPosition] = colour;
  }

  /*!
  Clear colour gradient
  */
  inline void Clear()
  {
    colourFrames.clear();
  }

  /*!
  Get colour value
  @param p Gradient values range is [0.0, 1.0]
  @return Colour at the given gradient position
  */
  const Ogre::ColourValue GetColour(float gradientPosition) const;
protected:
private:
  /*!
  Interpolate the given colour frames.
  */
  Ogre::ColourValue Interpolate(const ColourFrame& minColourFrame, const ColourFrame& maxColourFrame, const float& mediumRangeValue) const;
};

Source

#include "ColourGradient.h"

ColourGradient::ColourGradient(bool fillBoundsWithDefault) : fillBounds(fillBoundsWithDefault),
                                                             minBoundDefaultColour(Ogre::ColourValue::Black),
                                                             maxBoundDefaultColour(Ogre::ColourValue::White)
{
}

ColourGradient::ColourGradient(const Ogre::ColourValue& overridedMinDefaultColour, const Ogre::ColourValue& overridedMaxDefaultColour) : fillBounds(true),
                                                                                                                                         minBoundDefaultColour(overridedMinDefaultColour),
                                                                                                                                         maxBoundDefaultColour(overridedMaxDefaultColour)
{
}

ColourGradient::~ColourGradient()
{
}

const Ogre::ColourValue ColourGradient::GetColour(float gradientPosition) const
{
  // Check parameter
  if(gradientPosition < 0.0f)
    gradientPosition = 0.0f;
  if(gradientPosition > 1.0f)
    gradientPosition = 1.0f;

  // No colours in the gradient!
  if (colourFrames.empty())
  {
    if(!fillBounds)
      return Ogre::ColourValue::Black;
    else
      return Interpolate(ColourFrame(0.0f, minBoundDefaultColour), ColourFrame(1.0f, maxBoundDefaultColour), gradientPosition);
  }

  // If the requested colour is exactly on a colour frame position
  ColourFrames::const_iterator iSearchedColourFrame = colourFrames.find(gradientPosition);
  if(iSearchedColourFrame != colourFrames.end())
    return iSearchedColourFrame->second;

  // Only one colour
  if (colourFrames.size() == 1)
  {
    if(!fillBounds)
    {
      // No bound filling, return the unique colour.
      return colourFrames.begin()->second;
    }
    else
    {
      if(gradientPosition < colourFrames.begin()->first)
      {
        // Interpolate between default colour frame (0.0, Ogre::ColourValue::Black) and unique colour that is in the gradient.
        return Interpolate(ColourFrame(0.0f, minBoundDefaultColour), *colourFrames.begin(), gradientPosition);
      }
      else
      {
        // Interpolate between the unique colour that is in the gradient and default colour frame (1.0, Ogre::ColourValue::White).
        return Interpolate(*colourFrames.begin(), ColourFrame(1.0f, maxBoundDefaultColour), gradientPosition);
      }
    }
  }

  // Min colour value
  ColourFrames::const_iterator iMinBound = colourFrames.lower_bound(gradientPosition);
  if(iMinBound != colourFrames.begin())
    iMinBound--;

  // Max value
  ColourFrames::const_iterator iMaxBound = colourFrames.upper_bound(gradientPosition);

  // Handle the case where the requested value is below the smaller value in the gradient
  if(gradientPosition < iMinBound->first)
  {
    if(!fillBounds)
    {
      // No bound filling, return the colour which is just above the searched value.
      return iMinBound->second;
    }
    else
    {
      // Interpolate between default colour frame (0.0, Ogre::ColourValue::Black) and the colour just above the searched value.
      return Interpolate(ColourFrame(0.0f, minBoundDefaultColour), *iMinBound, gradientPosition);
    }
  }

  // Handle the case where the requested value is above the greater value in the gradient
  if(iMaxBound == colourFrames.end())
  {
    iMaxBound--;
    if(!fillBounds)
    {
      // No bound filling, return the colour which is just below the searched value.
      return iMaxBound->second;
    }
    else
    {
      // Interpolate between the colour just below the searched value and the default colour frame (1.0, Ogre::ColourValue::White).
      return Interpolate(*iMaxBound, ColourFrame(1.0f, maxBoundDefaultColour), gradientPosition);
    }
  }

  // Normal case
  return Interpolate(*iMinBound, *iMaxBound, gradientPosition);
}

Ogre::ColourValue ColourGradient::Interpolate(const ColourFrame& minColourFrame, const ColourFrame& maxColourFrame, const float& mediumRangeValue) const
{
  float range = maxColourFrame.first - minColourFrame.first;
  float rangePoint = (mediumRangeValue - minColourFrame.first) / range;
  return ((minColourFrame.second * (1.0f - rangePoint)) + (maxColourFrame.second * rangePoint));
}

Test cases

// Test filling bounds (white/red/green/blue/black)
  Ogre::ColourValue resultColour;
  ColourGradient myTest;
  myTest.AddColourFrame(0.3, Ogre::ColourValue::Red);
  myTest.AddColourFrame(0.5, Ogre::ColourValue::Green);
  myTest.AddColourFrame(0.7, Ogre::ColourValue::Blue);
  resultColour = myTest.GetColour(0.0);   // (0.0, 0.0, 0.0, 1.0)
  resultColour = myTest.GetColour(0.15);  // (0.5, 0.0, 0.0, 1.0)
  resultColour = myTest.GetColour(0.3);   // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest.GetColour(0.4);   // (0.5, 0.5, 0.0, 1.0)
  resultColour = myTest.GetColour(0.5);   // (0.0, 1.0, 0.0, 1.0)
  resultColour = myTest.GetColour(0.6);   // (0.0, 0.5, 0.5, 1.0)
  resultColour = myTest.GetColour(0.7);   // (0.0, 0.0, 1.0, 1.0)
  resultColour = myTest.GetColour(0.85);  // (0.5, 0.5, 1.0, 1.0)
  resultColour = myTest.GetColour(1.0);   // (1.0, 1.0, 1.0, 1.0)

  // Test no filling bounds (red/green/blue)
  ColourGradient myTest2(false);
  myTest2.AddColourFrame(0.3, Ogre::ColourValue::Red);
  myTest2.AddColourFrame(0.5, Ogre::ColourValue::Green);
  myTest2.AddColourFrame(0.7, Ogre::ColourValue::Blue);
  resultColour = myTest2.GetColour(0.0);  // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest2.GetColour(0.15); // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest2.GetColour(0.3);  // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest2.GetColour(0.4);  // (0.5, 0.5, 0.0, 1.0)
  resultColour = myTest2.GetColour(0.5);  // (0.0, 1.0, 0.0, 1.0)
  resultColour = myTest2.GetColour(0.6);  // (0.0, 0.5, 0.5, 1.0)
  resultColour = myTest2.GetColour(0.7);  // (0.0, 0.0, 1.0, 1.0)
  resultColour = myTest2.GetColour(0.85); // (0.0, 0.0, 1.0, 1.0)
  resultColour = myTest2.GetColour(1.0);  // (0.0, 0.0, 1.0, 1.0)

  // Test filling bounds 2 (red/green/blue)
  ColourGradient myTest3;
  myTest3.AddColourFrame(0.0, Ogre::ColourValue::Red);
  myTest3.AddColourFrame(0.5, Ogre::ColourValue::Green);
  myTest3.AddColourFrame(1.0, Ogre::ColourValue::Blue);
  resultColour = myTest3.GetColour(0.0);  // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest3.GetColour(0.25); // (0.5, 0.5, 0.0, 1.0)
  resultColour = myTest3.GetColour(0.5);  // (0.0, 1.0, 0.0, 1.0)
  resultColour = myTest3.GetColour(0.75); // (0.0, 0.5, 0.5, 1.0)
  resultColour = myTest3.GetColour(1.0);  // (0.0, 0.0, 1.0, 1.0)

  // Test no filling bounds 2 (red/green/blue)
  ColourGradient myTest4(false);
  myTest4.AddColourFrame(0.0, Ogre::ColourValue::Red);
  myTest4.AddColourFrame(0.5, Ogre::ColourValue::Green);
  myTest4.AddColourFrame(1.0, Ogre::ColourValue::Blue);
  resultColour = myTest4.GetColour(0.0);  // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest4.GetColour(0.25); // (0.5, 0.5, 0.0, 1.0)
  resultColour = myTest4.GetColour(0.5);  // (0.0, 1.0, 0.0, 1.0)
  resultColour = myTest4.GetColour(0.75); // (0.0, 0.5, 0.5, 1.0)
  resultColour = myTest4.GetColour(1.0);  // (0.0, 0.0, 1.0, 1.0)

  // Test filling bounds 3 (black/red/white)
  ColourGradient myTest5;
  myTest5.AddColourFrame(0.5, Ogre::ColourValue::Red);
  resultColour = myTest5.GetColour(0.0);  // (0.0, 0.0, 0.0, 1.0)
  resultColour = myTest5.GetColour(0.25); // (0.5, 0.0, 0.0, 1.0)
  resultColour = myTest5.GetColour(0.5);  // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest5.GetColour(0.75); // (1.0, 0.5, 0.5, 1.0)
  resultColour = myTest5.GetColour(1.0);  // (1.0, 1.0, 1.0, 1.0)

  // Test no filling bounds 3 (all red)
  ColourGradient myTest6(false);
  myTest6.AddColourFrame(0.5, Ogre::ColourValue::Red);
  resultColour = myTest6.GetColour(0.0);  // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest6.GetColour(0.25); // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest6.GetColour(0.5);  // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest6.GetColour(0.75); // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest6.GetColour(1.0);  // (1.0, 0.0, 0.0, 1.0)

  // Test filling bounds 4 (white/black)
  ColourGradient myTest7;
  resultColour = myTest7.GetColour(0.0);  // (0.0, 0.0, 0.0, 1.0)
  resultColour = myTest7.GetColour(0.5);  // (0.5, 0.5, 0.5, 1.0)
  resultColour = myTest7.GetColour(1.0);  // (1.0, 1.0, 1.0, 1.0)

  // Test no filling bounds 4 (no valid value, all black)
  ColourGradient myTest8(false);
  resultColour = myTest8.GetColour(0.0);  // (0.0, 0.0, 0.0, 1.0)
  resultColour = myTest8.GetColour(0.5);  // (0.0, 0.0, 0.0, 1.0)
  resultColour = myTest8.GetColour(1.0);  // (0.0, 0.0, 0.0, 1.0)

  // Test filling bounds 5 (red/green)
  ColourGradient myTest9(Ogre::ColourValue::Red, Ogre::ColourValue::Green);
  resultColour = myTest9.GetColour(0.0);  // (1.0, 0.0, 0.0, 1.0)
  resultColour = myTest9.GetColour(0.5);  // (0.5, 0.5, 0.0, 1.0)
  resultColour = myTest9.GetColour(1.0);  // (0.0, 1.0, 0.0, 1.0)