Dark Bit Factory & Gravity
PROGRAMMING => Coding tutorials => Topic started by: rdc on August 10, 2009
-
Introduction
In the last tutorial we looked at the changes in the screen object to accommodate the new color object. In this tutorial we will examine the color object itself. The demo program showed some of the properties of the color object, but there is actually more to the color object then a set of properties. The color object is more like a variable than a simple type object with properties. Color objects can be assigned a value, can be assigned to other color objects and can be initialized when defined. We will examine each of the these attributes in the color object code.
RGB and HSV Color
Since there are a number of good online resources that explain the color models in depth, such as this (http://en.wikipedia.org/wiki/HSL_and_HSV) Wikipedia article, I am only going to discuss what we need to implement the two models in our code.
RGBA stands for red, green, blue and alpha and refer to the four components of the RGBA color model. The values for each RGBA component range from 0 to 255. The red, green and blue components define a color while the alpha component defines the transparency of the color.
HSV stands for hue, saturation and value. The hue represents the actual color and has a range of 0 to 359. HSV can be modeled as cylinder and the hue value represents the number of degrees in a circle, a cross section of a cylinder. Saturation represents the range of gray in the color, with 0 being neutral gray and 255 being full color. Value represents the brightness or intensity of the color, with 0 being black and 255 being full bright.
FreeBasic uses the RGBA color model for plotting color to the screen so this will be our base type and will be used to write to the screen buffer. The HSV color model can be used to not only define a color, but is especially suited for creating color ramps. However, since FreeBasic uses RGB color, the HSV values will have to be converted to RGB so they can be plotted to the screen. Rather than trying to manually convert between the two models, the color object will handle the conversion automatically.
To get started, we need to define our data structures for both models.
HSV color object.
Type colorhsv
h As Double
s As Double
v As Double
End Type
' Pixel types used in the color object.
' Source: Posted on FB forums by Jofers.
Type pixel_color
b As UByte
g As Ubyte
r As Ubyte
a As Ubyte
End Type
'Use a union to automatically extract/combine color components.
Union pixel
channel As pixel_color
value As Uinteger
End Union
The HSV type is quite simple. We only need to have three values: h for hue, s for saturation and v for value. Hue is normally defined as a floating point number since it represents an angle along the HSV color cylinder. While saturation and value are integer values in the range 0 to 255, these are defined doubles since we are going to be converting from one color model to another and using doubles help offset the rounding errors you get in integer conversion.
The RGB type is slightly more complicated, but for a definite reason. Jofer's posted this clever bit of code over at the FreeBasic forum (http://www.freebasic.net/forum/index.php) sometime ago and I have found it invaluable for working with RGB color. You will notice that the RGB type is actually two types. The pixel_color type contains the RGBA components arranged in the proper order. The type is a Union that combines the pixel_color type and a uinteger data type. In order to see why this is advantageous, we need to explore the Union.
Union
A union is like a type; you can define members of the union just like you would a type, but there is one crucial difference: all of the members of a union share the same memory space. The amount of memory allocated for a union is the largest data member of the union, with all the other data members sharing a portion of the same memory. Keeping this in mind, let's look at the RGB data definition.
The union has two members, channel as pixel_color, which is a type and a uinteger. A uinteger is 32 bits or 4 bytes. The pixel_color has 4 ubyte members, which (not) coincidently is also 4 bytes. This looks something like this:
|BGRA|
Where the | | represents the uinteger value and the BGRA represents the pixel_color type. The color components are arranged in BGRA format since this is the format that the macro RGBA uses when it creates an RGBA color. So we have two different variables pointing to the same memory location. This means that we can create an RGBA color value using the RGBA macro and load that into the value (uinteger) memory location. Since the memory is shared, the blue component of the color value maps to the b parameter of the pixel_color type, the green component maps to the g parameter, and so on. Once the value parameter is loaded with an RGB color, we have instant access to the color components without having to work out a complicated conversion macro or function. We essentially get the conversion for free.
It goes the other way as well. If we want to change the blue value of a color, we can set the pixel_color b parameter to a number between 0 and 255 and we instantly have a new RGBA color value, without using the RGBA color macro. Using a union makes working with the RGBA color model much more efficient.
Color Conversions
As you have seen in the screen object, the color object contains both a private and public section. The private section is our working data and methods that are used internally by the object and are not accessible outside the object.
Private:
_color As pixel 'RGB color type.
_hsv As colorhsv 'HSV color type
Declare Sub _rgb2hsv ()'Convert rgb to hsv.
Declare Sub _hsv2rgb ()'Convert hsv to rgb.
Here we have our data types, and the two conversion routines that will be called to convert between the two different color models. We have examined the data types, so let's look at the conversion routines.
'Conversion code by Antoni posted on FB forums, modified by RDC.
'Converts rgb color to hsv color.
Sub clrobj._rgb2hsv
Dim As Double max = 0, min = 255, r, g, b
r = _color.channel.r
g = _color.channel.g
b = _color.channel.b
If r > max Then max = r
If r < min Then min = r
If g > max Then max = g
If g < min Then min = g
If b > max Then max = b
If b < min Then min = b
_hsv.v = max
If (max = 0) Or (max = min) Then
_hsv.s = 0
_hsv.h = 0
Exit Sub
EndIf
_hsv.s = 255 * (1. - (min / max))
If max = r Then
_hsv.h = 60.0 * (g - b) / (max - min) + IIf (g >= b, 0, 360)
ElseIf max = g Then
_hsv.h = 60.0 * (b - r) / (max - min) + 120
Else
_hsv.h = 60.0 * (r - g) / (max - min) + 240
End If
End Sub
'Conversion code by Antoni posted on FB forums, modified by RDC.
'Converts hsv to rgb.
SUB clrobj._hsv2rgb
'Antoni
'hue 0 TO 359 0=red, 120 green 240 blue
'sat is saturation 0 to 255 0 is neutral grey 255 is full color
'value 0 To 255 is the brightness 0 is black 255 is maximum brightness
If _hsv.s = 0 Then
_color.channel.r = CByte(_hsv.v)
_color.channel.g = CByte(_hsv.v)
_color.channel.b = CByte(_hsv.v)
Exit Sub
EndIf
_hsv.h Mod= 360
Dim As Double h1 = _hsv.h / 60
Dim As Integer i = Int(h1)
Dim As Double f = frac(h1)
Dim As Integer p = _hsv.v * ( 255 - _hsv.s ) / 256
Dim As Integer q = _hsv.v * ( 255 - f * _hsv.s) / 256
Dim As Integer t = _hsv.v * ( 255 - ( 1. - f ) * _hsv.s) / 256
Select Case As Const i
Case 0
_color.channel.r = CByte(_hsv.v)
_color.channel.g = CByte(t)
_color.channel.b = CByte(p)
Case 1
_color.channel.r = CByte(q)
_color.channel.g = CByte(_hsv.v)
_color.channel.b = CByte(p)
Case 2
_color.channel.r = CByte(p)
_color.channel.g = CByte(_hsv.v)
_color.channel.b = CByte(t)
Case 3
_color.channel.r = CByte(p)
_color.channel.g = CByte(q)
_color.channel.b = CByte(_hsv.v)
Case 4
_color.channel.r = CByte(t)
_color.channel.g = CByte(p)
_color.channel.b = CByte(_hsv.v)
Case 5
_color.channel.r = CByte(_hsv.v)
_color.channel.g = CByte(p)
_color.channel.b = CByte(q)
End Select
End Sub
These conversion routines were posted by Antoni (http://www.freebasic.net/forum/viewtopic.php?t=8764&highlight=hsv) on the FreeBasic forums and follow the standard conversion routines posted many places online. Since these routines are both fast and work quite well, I am using them here. You can see that the properties of the union are used in the conversion routines.
Since we want the color object to handle the conversions automatically, we will call these methods whenever we change any of the color components in either color model. That is, if we change the red component of the RGBA model, we will call the _rgb2hsv conversion routine. If we change the hue in the HSL color model, we will call the _hsv2rgb conversion routine. Below are two examples that illustrate the process.
'Set red channel.
Property clrobj.ChannelRed (r As UByte)
_color.channel.r = r
_rgb2hsv
End Property
'Sets the hue.
Property clrobj.ChannelHue (h As Double)
If h < 0 Then
_hsv.h = 0
ElseIf h > 359.0 Then
_hsv.h = 359.0
Else
_hsv.h = h
EndIf
_hsv2rgb
End Property
The ChannelRed property of the RGBA model simply sets the r data item to the passed value and then the _rgb2hsv conversion routine is called to convert the RGB color to HSV color. Likewise, the ChannelHue property validates the passed value to make sure it is the proper range and then calls the _hsv2rgb conversion routine. We could get clever here and only call the conversion routine when the update values are different than the current values, but in most cases the passed values will be different. Using an IF statement each time a property is called would probably generate more overhead than simply calling the conversion routine each time a property is updated. This is a case where we expect the programmer to use some common sense when using the object.
The Constructors
As we have already seen, constructors are called when we create the object. We have defined four constructors for the color object.
Declare Constructor () 'Builds a default color of black.
Declare Constructor (clr As clrobj) 'Pass a color object here.
Declare Constructor (r As UByte, g As UByte, b As UByte, a As UByte = 255) 'Build a color using values.
Declare Constructor (clr As UInteger) 'Build a color using RGB value.
'Builds default color, black.
Constructor clrobj ()
_color.Value = RGBA(0, 0, 0, 255)
_rgb2hsv
End Constructor
'Use a color object.
Constructor clrobj (clr As clrobj)
_color.value = clr.ColorValue
_rgb2hsv
End Constructor
'Builds a color using component values.
Constructor clrobj (r As UByte, g As UByte, b As UByte, a As UByte = 255)
_color.channel.r = r
_color.channel.g = g
_color.channel.b = b
_color.channel.a = a
_rgb2hsv
End Constructor
'Builds a color using RGB value.
Constructor clrobj (clr As UInteger)
_color.value = clr
_rgb2hsv
End Constructor
Four constructors mean there are four ways to create a color object. Each constructor creates a color object using the RGBA color model and then calls the _rgb2hsv conversion method. The default constructor, the one with no parameters, simply creates an RGB color with each component set to 0, making the color of the object black, with the alpha value set to 255 or fully opaque. The second method uses an existing color object. The color value is copied from the passed color object to the new color object. The third method passes individual color components, with a optional alpha value. The last method passes a color value using the RGBA macro to build ,a color value.
Why so many different options? We don’t really know at this point how the color object will be used and which method is best, so it is better to err on the side of flexibility rather than limiting the options. The following code snippet illustrates how to use these constructors.
Dim As clrobj myColor
Dim As clrobj myColor2 = myColor
Dim As clrobj myColor = clrobj(255, 0, 0, 255)
Dim As clrobj myColor = RGBA(255, 0, 0, 255)
The only one here that my seem odd is the Dim As clrobj myColor = clrobj(255, 0, 0, 255) statement. What this does is to create a type conversion using the passed values which are then used to initialize the color object, hence the clrobj(255, 0, 0, 255) portion of the statement. In other words, it lets the compiler know that we are initializing a type of clrobj.
The Property and Subroutine List
The color object has a number of properties that can be used to set and retrieve the different components for each color model.
Declare Property ColorValue (clr As UInteger) 'Sets color based on RGB color value.
Declare Property ColorValue () As UInteger 'Returns color as RGB color.
Declare Property ChannelRed (r As UByte) 'Set red channel.
Declare Property ChannelRed () As UByte'Return red channel.
Declare Property ChannelGreen (g As UByte) 'Set red channel.
Declare Property ChannelGreen () As UByte'Return green channel.
Declare Property ChannelBlue (b As UByte) 'Set red channel.
Declare Property ChannelBlue () As UByte'Return blue channel.
Declare Property ChannelAlpha (a As UByte) 'Set red channel.
Declare Property ChannelAlpha () As UByte'Return alpha channel.
Declare Property ChannelHue (h As Double) 'Set hsv hue channel.
Declare Property ChannelHue () As Double'Return hsv hue channel.
Declare Property ChannelSat (s As Double) 'Set hsv saturation channel.
Declare Property ChannelSat () As Double'Return hsv saturation channel.
Declare Property ChannelValue (v As Double) 'Set hsv value channel.
Declare Property ChannelValue () As Double'Return hsv value channel.
Declare Sub SetRGBA (r As UByte, g As UByte, b As UByte, a As UByte = 255) 'Sets components of color.
The methodology used here is the same as that used in the screen object properties, and should be familiar, so I won’t go over each property. What is new in this object though is the added operator definitions.
Color Object Operators
If you have done any programming at all, or used a calculator, operators should be familiar. 1 + 2 = 3 is an operation using the addition operator. What you may not realize though is that in FreeBasic operators are just functions. The operands (the 1 and 2) are the parameters to the function which return a value of the operation. In code the + operator looks like the following.
Operator + (lhs as Integer, rhs as Integer) as Integer
Return lhs + rhs
End Operator
To handle an addition operation on doubles, the + operator is overloaded so that lhs, rhs and the return value would be doubles rather than integer values. The lhs is the left hand side of the operator (1 in the example) and the rhs is the right hand side of the operator (the 2 in the example). We can also overload the operators to handle custom types, such as our color object.
Declare Operator Let (clr As clrobj) 'Assignment operator.
Declare Operator Let (clr As Uinteger) 'Assignment operator.
'Assign color using object.
Operator clrobj.Let (clr As clrobj)
_color.value = clr.Colorvalue
_rgb2hsv
End Operator
'Assign color using value.
Operator clrobj.Let (clr As UInteger)
_color.value = clr
_rgb2hsv
End Operator
'Muliply operator.
Operator * (lhs As clrobj, rhs As Double) As clrobj
Dim As Double rr, gg, bb, aa
Dim tmp As clrobj
rr = lhs.ChannelRed * rhs
If rr > 255.0 Then rr = 255.0
tmp.ChannelRed = CInt(rr)
gg = lhs.ChannelGreen * rhs
If gg > 255.0 Then gg = 255.0
tmp.ChannelGreen = CInt(gg)
bb = lhs.ChannelBlue * rhs
If bb > 255.0 Then bb = 255.0
tmp.ChannelBlue = CInt(bb)
Return tmp
End Operator
'Subtraction operator. Decreases color toward black.
Operator - (lhs As clrobj, rhs As Integer) As clrobj
Dim As Integer rr, gg, bb, aa
Dim tmp As clrobj
rr = lhs.ChannelRed - rhs
If rr < 0 Then rr = 0
tmp.ChannelRed = rr
gg = lhs.ChannelGreen - rhs
If gg < 0 Then gg = 0
tmp.ChannelGreen = gg
bb = lhs.ChannelBlue - rhs
If bb < 0 Then bb = 0
tmp.ChannelBlue = bb
Return tmp
End Operator
'Addition operator. Increases color toward white.
Operator + (lhs As clrobj, rhs As Integer) As clrobj
Dim As Integer rr, gg, bb, aa
Dim tmp As clrobj
rr = lhs.ChannelRed + rhs
If rr > 255 Then rr = 255
tmp.ChannelRed = rr
gg = lhs.ChannelGreen + rhs
If gg > 255 Then gg = 255
tmp.ChannelGreen = gg
bb = lhs.ChannelBlue + rhs
If bb > 255 Then bb = 255
tmp.ChannelBlue = bb
Return tmp
End Operator
You will notice that the Let operator, the = operator, is defined within the object, while the *, + and - operators are global operators. This is just a limitation of FreeBasic; you cannot define the *, + and - within the object definition. At some point this may change, but for now we will need to define them outside the object.
All of these operators work with our base color model, the RGBA color model, and work with all color components at the same time. Why would you want to do this? To scale an RGB color either toward white or black. This gives us a way to adjust a color without having to work with each individual color component. It is of course not the same as adjusting the saturation or brightness of a color using the HSV model, but it does give us some added flexibility when working with the RGB color model.
You will notice we haven’t defined any operators for the HSV model. In the RGB color model, all the of the color components have the same meaning, that is, the r, g and b refer to a color channel. You could say that the RGB color components are all in the same family. The HSV components are quite different from each other, like neighbors on the same street. The hue is the color, the saturation refers to the mix of gray and the value refers to the brightness. Since each HSV component has a different meaning associated with it, it doesn’t make sense to create an addition operator and add 1 to each HSV component. It is better to handle the HSV operations through the exposed properties so that you can adjust the color exactly to what is needed.
The Let Operator
The Let operator is the assignment operator, =. This allows us to set a color object to an RGBA value using either a color value or another color object. You would use this operator just like you would with any other variable type.
myColor1 = RGBA(255, 0, 0, 255)
myColor2 = myColor1
The *, -, and + Operators
All of these operators work the same as the normal operators would on an integer value, it is just that the operations operate on each of the color components. The lhs value is the color object to operate on, and the rhs is the different values to use to perform the operation. The * operator is used to scale a color by a certain amount. You could think of it as applying a percentage to the current color. Using .5 would halve the color value while 2.0 would double the color value. The - and + operators decrement and increment the color value by the passed amount. In each case the calculated values are clipped to stay in the range of 0 and 255. The following illustrates how you would use these operators.
myColor2 = myColor1 * .5
myColor2 = myColor1 + 10
myColor2 = myColor1 - 10
If myColor was a bright red, then applying the * operator with .5 would result in a darker red. The - and + behave the same way, decreasing the overall color by 10, resulting in a darker color, or increasing the color by 10, resulting in a brighter color.
The Alpha Channel
The alpha component is a special case as it is not really used in the color object itself, but is used in the screen object. The alpha channel determines the transparency of the color when blended with a color in the color buffer. The demo program has an example of using the alpha value.
myColor.ChannelHue = Rnd * 360
myColor.ChannelValue = 255
myColor.ChannelAlpha = 64
For x As Integer = 0 To 63
For y As Integer = 0 To 63
myColor.ChannelSat = x Xor y
aScreen.PokeAlpha x + 290, y + 365, myColor
Next
Next
A random color is generated using the HSV color model and the alpha value is set to 64. The PokeAlpha method is then called which uses the screen object's _AlphaBlend routine to blend the passed color with the color in the buffer located at the x and y position. (The nested For-Next generates a simple XOR texture.)
'Alpha blends passed color object with background at x, y.
Sub screenobject.PokeAlpha (ByRef x As Integer, ByRef y As Integer, ByRef clr As clrobj)
Dim As UInteger bc, cc
Dim al As UByte
'Make sure the object was initialized.
If _initok = TRUE Then
If (x >= 0) And (y >= 0) And (x < _width) And (y < _height) Then
'Get the current buffer value.
bc = _buffer[y * _width + x]
'Get the alpha value.
al = clr.ChannelAlpha
'Get the alpha blended color.
cc = _AlphaBlend (al, clr.ColorValue, bc)
_buffer[y * _width + x] = cc
EndIf
End If
End Sub
The variable bc, defined as a uinteger, is the current value in the buffer at the passed x and y location and the variable al is the alpha value contained within the color object. These two values are passed, along with the current color object color value, to the _AlphaBlend method (which we looked at in the previous tutorial). The return value, cc, is the blended color and is poked into the buffer.
Looking back through the color object code, you will see that none of the color manipulation routines or operators change the alpha value. This is because the alpha value has a different meaning within the context of the color object, although it is contained within the object itself. We want to encapsulate the alpha information within the object, since it is a property of the color, but it doesn’t make sense to use the alpha value in the conversion routines or the operators. The alpha value is a special case and is used in a very specific way, so it needs to be explicitly set and used in any code that wants to implement alpha blending. This isolation helps prevent any unwanted side effects that night arise if we included the alpha value in the conversion methods or operators.
Summary
The color object is actually quite simple, both conceptually and in code, but it offers a great deal of flexibility when working with color. However, you may have noticed that is isn’t quite complete. Specifically, we don’t have a way to initialize the color object to an HSV value. We will correct this oversight in the next tutorial.