Share via


VB.Net: Generate Color Sequences Using RGB Color Cube

Introduction

Sometimes when you are designing a form, or creating some other kind of visual output, you'd like to generate an array of colors which may be shades of a single color or a discernible sequence of individual colors such as the spectrum of a rainbow.  This can be useful for coloring bars in a graph or generating a gradient around some specified color.  Unfortunately the .Net framework does not give us any sophisticated solution for this.

While any number of complex code solutions could be created to attempt to address this problem, if we think of the RGB color space spatially, we can construct a three-dimensional cube which represents all possible colors and can easily be traversed mathematically.

The RGB Color Cube

This is a RGB Color Cube:

A color value in the RGB color space has three components; the Red, Green and Blue intensity values.  These values are expressed as Bytes and are therefore limited in range to 0 through 255.  We can think of these R, G, and B values as being the axis of a three dimensional space; in other words, the x, y, and z axis of a grid.  So we can define the RGB color space as a cube, 255 units on a side, with an origin at [0,0,0] which represents the color Black.  If we are facing the cube then, the red axis runs to the left, the blue axis to the right, and the green axis points upward from black.  The furthest corner from black would be [255,255,255], or White.

The primary RGB and CMYK colors (red, green, blue, yellow, cyan, magenta, black, and white) exist at each corner of the cube:

The spectrum of full intensity colors (that is, the rainbow) is represented by following the outside edges of the cube from red, to yellow, to green and around to magenta.

And since we have defined a three dimensional space we can find any sequence of colors between two colors by measuring angles and distances.

Coding a ColorCube Class

Creating a class to represent this concept of a RGB Color Cube is really little more than a collection of functions.  There is no data-set to work with as, truly, the entire color cube concept is represented in the diagrams above.  Once we have decided to think of a color as a location within a predefined three-dimensional space, there is little more to do than math in order to generate various color sequences.  So rather than walk though writing the whole class step-by-step, we will just look at a few interesting properties of the cube and the related code.  A complete class example will be provided at the end of the article.

Brightness is Distance from Black

One interesting feature of the cube is that we can quickly determine the overall brightness of a color by measuring the distance from black using a basic distance formula:

Public Shared  Function GetDistance(ByVal source As Color, ByVal target  As Color) As Double
 Dim squareR As  Double = CDbl(target.R) - CDbl(source.R)
 squareR *= squareR
 Dim squareG As  Double = CDbl(target.G) - CDbl(source.G)
 squareG *= squareG
 Dim squareB As  Double = CDbl(target.B) - CDbl(source.B)
 squareB *= squareB
 Return System.Math.Sqrt(squareR + squareG + squareB)
End Function

While searching the net for some other related information, I came across some code to calculate a similar value in terms which can be represented by a Byte.  A translation of that code is included in the class below.

Colors can be compared by brightness as part of determining similarity or to ensure sufficient visible difference in a series of similar colors.

Public Shared  Function Compare(source As Color, target As  Color) As  Integer
 Dim delta1 As  Double = GetDistance(Color.Black, source)
 Dim delta2 As  Double = GetDistance(Color.Black, target)
 Return delta1.CompareTo(delta2)
End Function

Walking the Edges of the Cube

There is an algorithm used to move around the edges of the cube when generating a rainbow spectrum sequence which may be worth looking at for a moment.  The most basic color cube could be implemented with just this routine:

Public NotInheritable  Class ColorCube
 Protected Sub  New()
 End Sub
 
 Public Shared  Function GetColorSpectrum(increment As Integer) As Color()
 Dim result As  New List(Of Color)
 Dim rgb(2) As  Integer
 Dim idx As  Integer = 1
 Dim inc As  Integer = increment
 Dim cmp As Func(Of Integer, Integer, Boolean)
 
 rgb(0) = 255
 cmp = AddressOf CompareLess
 Do
 result.Add(Color.FromArgb(rgb(0), rgb(1), rgb(2)))
 If cmp(rgb(idx), inc) Then
 rgb(idx) += inc
 Else
 Select Case idx
 Case 1
 If rgb(2) < 255 Then
 rgb(idx) = 255
 idx = 0
 cmp = AddressOf CompareGreater
 Else
 rgb(idx) = 0
 idx = 0
 cmp = AddressOf CompareLess
 End If
 Case 2
 rgb(idx) = 255
 idx = 1
 cmp = AddressOf CompareGreater
 Case 0
 If rgb(2) < 255 Then
 rgb(idx) = 0
 idx = 2
 cmp = AddressOf CompareLess
 Else
 rgb(idx) = 255
 Exit Do
 End If
 End Select
 inc *= -1
 End If
 Loop
 result.Add(Color.FromArgb(rgb(0), rgb(1), rgb(2)))
 Return result.ToArray
 End Function
 
 Protected Shared  Function CompareLess(value As Integer, inc As Integer) As Boolean
 Return value < 255 - Math.Abs(inc)
 End Function
 
 Protected Shared  Function CompareGreater(value As Integer, inc As Integer) As Boolean
 Return value > Math.Abs(inc)
 End Function
End Class

This code is utilizing two helper methods making it easier to change functionality between moving from 0 to 255 and from 255 to 0 through the color component values.  This allows a relatively simple loop to walk around the edges of the cube as depicted in the image above.

Getting a Range of Colors

By organizing the RGB color space into a cube, generating a sequence of colors is little more than picking a starting color, a direction to move, and an increment of movement and then calculating a series of points along that line.  The following function will return a color which is at a given angle and distance from the specified color.  The angle is expressed in terms of azimuth (red to blue) and elevation (no green to full green).

Public Shared  Function GetColorFrom(source As Color, azimuth As  Double, elevation As Double, distance As Double) As Color
 Dim a, e, r, g, b As  Double
 a = azimuth
 e = elevation
 r = distance * Math.Cos(a) * Math.Cos(e)
 b = distance * Math.Sin(a) * Math.Cos(e)
 g = distance * Math.Sin(e)
 If Double.IsNaN(r) Then r = 0
 If Double.IsNaN(g) Then g = 0
 If Double.IsNaN(b) Then b = 0
 Return Color.FromArgb(Math.Max(Math.Min(source.R + r, 255), 0), Math.Max(Math.Min(source.G + g, 255), 0), Math.Max(Math.Min(source.B + b, 255), 0))
End Function

This allows us, for example, to create a function which can return a selection of colors taken from a sphere surrounding a target color:

Public Shared  Function GetColorsAround(target As Color, distance As Integer, increment  As  Integer) As Color()
 Dim result As  New List(Of Color)
 For a As  Integer = 0 To 359 Step increment
 For e As  Integer = 0 To 359 Step increment
 Dim c As Color = GetColorFrom(target, a, e, distance)
 If Not result.Contains(c)  Then
 result.Add(c)
 End If
 Next
 Next
 result.Sort(AddressOf Compare)
 Return result.ToArray
End Function

Or, with some additional helper methods, a function which can get the series of colors on a line between two given colors:

Public Shared  Function GetColorSequence(source As Color, target As  Color, increment As  Integer) As Color()
 Dim result As  New List(Of Color)
 Dim a As  Double = GetAzimuthTo(source, target)
 Dim e As  Double = GetElevationTo(source, target)
 Dim d As  Double = GetDistance(source, target)
 For i As  Integer = 0 To d Step increment
 result.Add(GetColorFrom(source, a, e, i, True))
 Next
 Return result.ToArray
End Function
Protected Shared  Function GetAzimuthTo(ByVal source As Color, ByVal target  As Color) As Double
 Return WrapAngle(Math.Atan2(CDbl(target.B) - CDbl(source.B),  CDbl(target.R) - CDbl(source.R)))
End Function
 
Protected Shared  Function GetElevationTo(ByVal source As Color, ByVal target  As Color) As Double
 Return WrapAngle(Math.Atan2(CDbl(target.G) - CDbl(source.G), 255))
End Function
 
Protected Shared  Function WrapAngle(ByVal radians As  Double) As  Double
 While radians < -Math.PI
 radians += Math.PI * 2
 End While
 While radians > Math.PI
 radians -= Math.PI * 2
 End While
 Return radians
End Function

As you can see these are mostly simple functions performing some minor trigonometry.  There's no magic in the code... the magic is in the concept of the color cube.

Summary

By visualizing the RGB color space as a three-dimensional cube, it becomes possible to generate arrays of Color instances representing a spectrum or other sequence using relatively simple mathematical calculations.  Simple functions can be written to generate a series of colors for a number of different sequences.

Appendix A: Complete Code Sample

''' <summary>
''' Describes the RGB color space as a 3D cube with the origin at Black.
''' </summary>
''' <remarks></remarks>
Public NotInheritable  Class ColorCube
 Protected Sub  New()
 End Sub
 
 ''' <summary>
 ''' Compares two colors according to their distance from the origin of the cube (black).
 ''' </summary>
 ''' <param name="source"></param>
 ''' <param name="target"></param>
 ''' <returns></returns>
 ''' <remarks></remarks>
 Public Shared  Function Compare(source As Color, target As  Color) As  Integer
 Dim delta1 As  Double = GetDistance(Color.Black, source)
 Dim delta2 As  Double = GetDistance(Color.Black, target)
 Return delta1.CompareTo(delta2)
 End Function
 
 ''' <summary>
 ''' Returns an integer between 0 and 255 indicating the perceived brightness of the color.
 ''' </summary>
 ''' <param name="target">A System.Drawing.Color instance.</param>
 ''' <returns>An integer indicating the brightness with 0 being dark and 255 being bright.</returns>
 ''' <remarks>
 ''' Formula found using web search at:
 ''' http://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
 ''' with reference to : http://alienryderflex.com/hsp.html
 ''' Effectively the same as measuring a color's distance from black, but constrained to a 0-255 range.
 ''' </remarks>
 Public Shared  Function GetBrightness(target As Color) As  Integer
 Return CInt(Math.Sqrt(0.241 * target.R ^ 2 + 0.691 * target.G ^ 2 + 0.068 * target.B ^ 2))
 End Function
 
 ''' <summary>
 ''' Gets a color from within the cube starting at the origin and moving a given distance in the specified direction.
 ''' </summary>
 ''' <param name="azimuth">The side-to-side angle in degrees; 0 points toward red and 90 points toward blue.</param>
 ''' <param name="elevation">The top-to-bottom angle in degrees; 0 is no green and 90 points toward full green.</param>
 ''' <param name="distance">The distance to travel within the cube; 500 is max.</param>
 ''' <returns>The color within the cube at the given distance in the specified direction.</returns>
 ''' <remarks></remarks>
 Public Shared  Function GetColorFrom(azimuth As Integer, elevation  As  Integer, distance As Integer) As Color
 Return GetColorFrom(Color.Black, azimuth, elevation, distance)
 End Function
 
 ''' <summary>
 ''' Gets a color from within the cube starting at the specified location and moving a given distance in the specified direction.
 ''' </summary>
 ''' <param name="source">The source location within the cube from which to start moving.</param>
 ''' <param name="azimuth">The side-to-side angle in degrees; 0 points toward red and 90 points toward blue.</param>
 ''' <param name="elevation">The top-to-bottom angle in degrees; 0 is no green and 90 points toward full green.</param>
 ''' <param name="distance">The distance to travel within the cube; the approximate distance from black to white is 500.</param>
 ''' <returns>The color within the cube at the given distance in the specified direction.</returns>
 ''' <remarks></remarks>
 Public Shared  Function GetColorFrom(source As Color, azimuth As  Double, elevation As Double, distance As Double, Optional isRadians As  Boolean = False) As Color
 If azimuth < 0 OrElse azimuth > 90 Then  Throw New ArgumentException("azimuth", "Value must be between 0 and 90.")
 If elevation < 0 OrElse elevation > 90 Then Throw  New ArgumentException("elevation", "Value must be between 0 and 90.")
 Dim a, e, r, g, b As  Double
 If isRadians Then
 a = azimuth
 e = elevation
 Else
 a = DegreesToRadians(azimuth)
 e = DegreesToRadians(elevation)
 End If
 r = distance * Math.Cos(a) * Math.Cos(e)
 b = distance * Math.Sin(a) * Math.Cos(e)
 g = distance * Math.Sin(e)
 If Double.IsNaN(r) Then r = 0
 If Double.IsNaN(g) Then g = 0
 If Double.IsNaN(b) Then b = 0
 Return Color.FromArgb(Math.Max(Math.Min(source.R + r, 255), 0), Math.Max(Math.Min(source.G + g, 255), 0), Math.Max(Math.Min(source.B + b, 255), 0))
 End Function
 
 ''' <summary>
 ''' Creates an array of colors from a selection within a sphere around the specified color.
 ''' </summary>
 ''' <param name="target">The color to select around.</param>
 ''' <param name="distance">The radius of the selection sphere.</param>
 ''' <param name="increment">The increment within the sphere at which a selection is taken; larger numbers result in smaller selection sets.</param>
 ''' <returns>An array of colors located around the specified color within the cube.</returns>
 ''' <remarks></remarks>
 Public Shared  Function GetColorsAround(target As Color, distance As Integer, increment  As  Integer) As Color()
 Dim result As  New List(Of Color)
 For a As  Integer = 0 To 359 Step increment
 For e As  Integer = 0 To 359 Step increment
 Dim c As Color = GetColorFrom(target, a, e, distance)
 If Not result.Contains(c)  Then
 result.Add(c)
 End If
 Next
 Next
 result.Sort(AddressOf Compare)
 Return result.ToArray
 End Function
 
 ''' <summary>
 ''' Creates an array of colors in a gradient sequence between two specified colors.
 ''' </summary>
 ''' <param name="source">The starting color in the sequence.</param>
 ''' <param name="target">The end color in the sequence.</param>
 ''' <param name="increment">The increment between colors.</param>
 ''' <returns>A gradient array of colors.</returns>
 ''' <remarks></remarks>
 Public Shared  Function GetColorSequence(source As Color, target As  Color, increment As  Integer) As Color()
 Dim result As  New List(Of Color)
 Dim a As  Double = GetAzimuthTo(source, target)
 Dim e As  Double = GetElevationTo(source, target)
 Dim d As  Double = GetDistance(source, target)
 For i As  Integer = 0 To d Step increment
 result.Add(GetColorFrom(source, a, e, i, True))
 Next
 Return result.ToArray
 End Function
 
 ''' <summary>
 ''' Creates a rainbow array of colors by selecting from the edges of the cube in ROYGBIV order at the specified increment.
 ''' </summary>
 ''' <param name="increment">The increment along the edges at which a selection is taken; larger numbers result in smaller selection sets.</param>
 ''' <returns>An array of colors in ROYGBIV order at the given increment.</returns>
 ''' <remarks></remarks>
 Public Shared  Function GetColorSpectrum(increment As Integer) As Color()
 Dim result As  New List(Of Color)
 Dim rgb(2) As  Integer
 Dim idx As  Integer = 1
 Dim inc As  Integer = increment
 Dim cmp As Func(Of Integer, Integer, Boolean)
 
 rgb(0) = 255
 cmp = AddressOf CompareLess
 Do
 result.Add(Color.FromArgb(rgb(0), rgb(1), rgb(2)))
 If cmp(rgb(idx), inc) Then
 rgb(idx) += inc
 Else
 Select Case idx
 Case 1
 If rgb(2) < 255 Then
 rgb(idx) = 255
 idx = 0
 cmp = AddressOf CompareGreater
 Else
 rgb(idx) = 0
 idx = 0
 cmp = AddressOf CompareLess
 End If
 Case 2
 rgb(idx) = 255
 idx = 1
 cmp = AddressOf CompareGreater
 Case 0
 If rgb(2) < 255 Then
 rgb(idx) = 0
 idx = 2
 cmp = AddressOf CompareLess
 Else
 rgb(idx) = 255
 Exit Do
 End If
 End Select
 inc *= -1
 End If
 Loop
 result.Add(Color.FromArgb(rgb(0), rgb(1), rgb(2)))
 Return result.ToArray
 End Function
 
 ''' <summary>
 ''' Gets the distance between two colors within the cube.
 ''' </summary>
 ''' <param name="source">The source color in the cube.</param>
 ''' <param name="target">The target color in the cube.</param>
 ''' <returns>The distance between the source and target colors.</returns>
 ''' <remarks></remarks>
 Public Shared  Function GetDistance(ByVal source As Color, ByVal target  As Color) As Double
 Dim squareR As  Double = CDbl(target.R) - CDbl(source.R)
 squareR *= squareR
 Dim squareG As  Double = CDbl(target.G) - CDbl(source.G)
 squareG *= squareG
 Dim squareB As  Double = CDbl(target.B) - CDbl(source.B)
 squareB *= squareB
 Return System.Math.Sqrt(squareR + squareG + squareB)
 End Function
 
 ''' <summary>
 ''' Converts a RGB color into its Hue, Saturation, and Luminance (HSL) values.
 ''' </summary>
 ''' <param name="rgb">The color to convert.</param>
 ''' <returns>The HSL representation of the color.</returns>
 ''' <remarks>
 ''' Source algorithm found using web search at:
 ''' http://geekymonkey.com/Programming/CSharp/RGB2HSL_HSL2RGB.htm
 ''' (Adapted to VB)
 ''' </remarks>
 Public Shared  Function GetHSL(ByVal rgb As Color) As HSLColor
 Dim h, s, l As  Double
 Dim r As  Double = rgb.R / 255.0
 Dim g As  Double = rgb.G / 255.0
 Dim b As  Double = rgb.B / 255.0
 Dim v, m, vm As  Double
 Dim r2, g2, b2 As  Double
 
 h = 0
 s = 0
 l = 0
 v = Math.Max(r, g)
 v = Math.Max(v, b)
 m = Math.Min(r, g)
 m = Math.Min(m, b)
 l = (m + v) / 2.0
 If l <= 0.0 Then
 Exit Function
 End If
 
 vm = v - m
 s = vm
 If s > 0.0 Then
 s /= If((l <= 0.5), (v + m), (2.0 - v - m))
 Else
 Exit Function
 End If
 
 r2 = (v - r) / vm
 g2 = (v - g) / vm
 b2 = (v - b) / vm
 
 If (r = v) Then
 h = If(g = m, 5.0 + b2, 1.0 - g2)
 ElseIf (g = v) Then
 h = If(b = m, 1.0 + r2, 3.0 - b2)
 Else
 h = If(r = m, 3.0 + g2, 5.0 - r2)
 End If
 
 h /= 6.0
 Return New HSLColor(h, s, l)
 End Function
 
 Protected Shared  Function CompareLess(value As Integer, inc As Integer) As Boolean
 Return value < 255 - Math.Abs(inc)
 End Function
 
 Protected Shared  Function CompareGreater(value As Integer, inc As Integer) As Boolean
 Return value > 0 + Math.Abs(inc)
 End Function
 
 Protected Shared  Function DegreesToRadians(ByVal degrees As  Double) As  Double
 Return degrees * (Math.PI / 180.0)
 End Function
 
 Protected Shared  Function RadiansToDegrees(ByVal radians As  Double) As  Double
 Return CSng(radians * (180.0 / Math.PI))
 End Function
 
 Protected Shared  Function GetAzimuthTo(ByVal source As Color, ByVal target  As Color) As Double
 Return WrapAngle(Math.Atan2(CDbl(target.B) - CDbl(source.B),  CDbl(target.R) - CDbl(source.R)))
 End Function
 
 Protected Shared  Function GetElevationTo(ByVal source As Color, ByVal target  As Color) As Double
 Return WrapAngle(Math.Atan2(CDbl(target.G) - CDbl(source.G), 255))
 End Function
 
 Protected Shared  Function WrapAngle(ByVal radians As  Double) As  Double
 While radians < -Math.PI
 radians += Math.PI * 2
 End While
 While radians > Math.PI
 radians -= Math.PI * 2
 End While
 Return radians
 End Function
 
 ''' <summary>
 ''' Describes a RGB color in Hue, Saturation, and Luminance values.
 ''' </summary>
 ''' <remarks></remarks>
 Public Structure HSLColor
 ''' <summary>
 ''' The color hue.
 ''' </summary>
 ''' <remarks></remarks>
 Public H As  Double
 ''' <summary>
 ''' The color saturation.
 ''' </summary>
 ''' <remarks></remarks>
 Public S As  Double
 ''' <summary>
 ''' The color luminance.
 ''' </summary>
 ''' <remarks></remarks>
 Public L As  Double
 
 Public Sub  New(hValue As Double, sValue As Double, lValue As Double)
 H = hValue
 S = sValue
 L = lValue
 End Sub
 End Structure
End Class