Representing colors in Helm is pretty straightforward. All colors are stored as RGBA values, with each component (R, G, B and A) being a 64-bit floating point value. This has an advantage over the regular way of representing RGBA colors (with each component being represented as a single byte) because it allows a higher range of colors to be composed. Unfortunately this higher range of colors is still clamped to a 32-bit color when being rendered to the screen, but it is still incredibly useful for accurately calculating things such as complementary colors, averaging colors, etc.
To create colors you can use the following functions as explained in the Helm.Color documentation:
rgba :: Double -> Double -> Double -> Double -> Color
rgb :: Double -> Double -> Double -> Color
hsva :: Double -> Double -> Double -> Double -> Color
hsv :: Double -> Double -> Double -> Color
The rgb
function just takes three double values (red, green and blue)
that should be in the range 0.0
to 1.0
, where 0.0
is no color and 1.0
is full color. The rgba
function is basically the same except it takes an
additional double value for the alpha component (or transparency), where 0.0
is completely transparent and 1.0
is opaque. In other words, rgb
creates
a solid color (with an alpha value of 1.0
) and rgb
can create a variably
transparent color.
For those who prefer HSV (or if you need to do complex color operations in HSV
space), there is also the hsv
and hsva
functions. These functions simply
convert from HSV[A] space to RGB[A] — the final representation of the
color remains in RGBA form.
There are many functions that use colors, such as filling or outlining shapes. Check out the Helm.Graphics2D documentation for some examples of these functions.
Colors can be applied directly to shapes to create “forms” of them that are filled with the color, or can be applied to line styles or gradients which are then applied to shapes.
Let’s try making something familiar with our new-found knowledge of colors in Helm — a color wheel. We want to end up with something that looks like a pizza made up of differently-colored slices. First let’s define a list of colors we actually want to appear in the color wheel:
red :: Color
red = rgb 1 0 0
lime :: Color
lime = rgb 0 1 0
blue :: Color
blue = rgb 0 0 1
yellow :: Color
yellow = rgb 1 1 0
cyan :: Color
cyan = rgb 0 1 1
magenta :: Color
magenta = rgb 1 0 1
maroon :: Color
maroon = rgb 0.5 0 0
navy :: Color
navy = rgb 0 0 0.5
green :: Color
green = rgb 0 0.5 0
teal :: Color
teal = rgb 0 0.5 0.5
purple :: Color
purple = rgb 0.5 0 0.5
colors :: [Color]
colors =
[ red
, lime
, blue
, yellow
, cyan
, magenta
, maroon
, navy
, green
, teal
, purple
]
Helm doesn’t allow assigning specific colors to specific points in a polygon, so we need to create a polygon for each slice in the color wheel. A good start on a function fingerprint for a function to create one of these slices is something like the following:
slice :: Int -> Form e
Where the given integer is the index of the color in the color list. The reason we take the index instead of the color itself is so we can calculate the position of the points in the slice polygon relative to the other colors (remembering that each color is going to be assigned its own slice).
We also have to give the Form
type a concrete type to parameterize over,
since a Form
only makes sense in terms of a graphics engine. Luckily, we can
just be agnostic about this and give the generic (forall) type e
, for
e
ngine. Let’s define the core of this function that we’re going to use to
build the actual slice generating code:
slice :: Int -> Form e
slice n = filled color $ polygon (path points)
where
color = colors !! n
r = 150
points = [{- ... -}]
The color
variable has been defined as the color in colors
at index n
(i.e. the color we want to paint the slice) and r
is a constant representing
the radius of the color wheel. This won’t actually render anything yet, because
the points
variable is just a blank list, so the path generated by path
will just be empty. Let’s think about how we actually calculate the points for
the slice polygon.
Firstly, let’s only use 3 points for the slice. This means that the outer edge of the slice will unfortunately be straight, but making it smooth is over-complicating it for this first simple example. The first point will be at the center of the color wheel (all slices will have this point in common), the second point will be at some arbitrary point on the circumference of the color-wheel and the third will be along the circumference at some fixed increment from the second point.
Essentially, in order to construct the slice we need to think of the whole
color wheel as containing exactly 2 * pi
radians (i.e. a circle). Then we can
calculate this fixed increment by dividing all of these radians up into a
certain amount of radians allocated to each slice, based on the number of
slices (or number of colors). Let’s model this thought process in actual code:
import Linear.V2 (V2 (V2))
slice :: Int -> Form e
slice n = filled color $ polygon points
where
color = colors !! n
r = 150
increment = 2 * pi / realToFrac (length colors)
t1 = increment * realToFrac n
t2 = t1 + increment
points = [V2 0 0, {- ... -}]
Note that we import
2-dimensional vectors from Linear
. This is almost
always how 2D quantities will be represented.
We’ve now defined increment
which, as previously mentioned, is the amount of
radians allocated to each slice. The variables t1
and t2
are the angle
where the n
th slice starts and the angle where the slice after it
starts, respectively. We have the angles, now we need to figure out how to find
the actual points that represent the outside edge of the slice. To do that we
need to remember the concept of the unit circle and the equations
x′ = r cos(θ)
and
y′ = r sin(θ).
pointOnCircum :: Double -> Double -> V2 Double
pointOnCircum r theta = V2 (r * cos theta) (r * sin theta)
Using this newly defined function will finally result in what we want: a colored slice on our color wheel.
slice :: Int -> Form e
slice n = filled color $ polygon (path points)
where
color = colors !! n
increment = 2 * pi / realToFrac (length colors)
t1 = increment * realToFrac n
t2 = t1 + increment
r = 150
points = [V2 0 0, pointOnCircum r t1, pointOnCircum r t2]
Now that the business logic is done, we just need to place it into a little bit of boilerplate code that defines the skeleton for a Helm game. Since right now we just have some static polygons being rendered without any animation or user interaction, we won’t explicitly need a lot of it, but it will serve you well in the future when making games.
First, let’s make sure we’re importing the stuff we need for the code that
we’ve already written. We’re using the Color
type and the rgb
function, so
we have to import Helm.Color
. We’re using the Form
type and functions like
filled
and color
, so we’ve got to import Helm.Graphics2D
. The rest so far
is covered by the Prelude, so that’s all we need for now.
We’ll need an engine to do the rendering of the polygonal forms we’re making,
and since that involves some nasty I/O to start up, we’ll do it inside of the
IO
monad of the main
function. First we will
import qualified Helm.Engine.SDL as SDL
and then, within main
, we initialize the SDL engine:
main :: IO ()
main = do
engine <- SDL.startupWith $ SDL.defaultConfig
{ SDL.windowDimensions = windowDims
, SDL.windowIsResizable = False
}
This starts up the SDL engine, using the default configuration with some record
update syntax. We update the “default” record to set the dimensions of the
display window to our own value instead of the default and also to make it
non-resizable. Then we bind our started-up engine to engine
. The window
dimensions are specified as a 2-dimensional vector, so we can specify
windowDims
like so:
windowDims :: V2 Int
windowDims = V2 800 600
Now the engine is ready to be “run”. But we need to tell it what to run.
Luckily, Helm provides a nifty record type called GameConfig
that simply
allows us to plug in four functions. One is called at the very beginning of
execution, initializing the game (initialFn
). One allows for “subscriptions”
to be registered with the engine (subscriptionsFn
), causing an update every
time something occurs (i.e., based on a signal), like user input or a time
frame (a time frame occuring about every 0.01667 seconds for a 60 FPS game).
One is updateFn
, which is called every time a subscription is triggered and
can take in the state of the game and return a new (updated) one, and also can
execute commands (basically I/O actions). Finally, viewFn
is called every
time the game is rendered to the screen, and its job is to use the state of the
game to produce graphics that can be rendered. This is what our set-up will
look like; this also goes in the do
block of our main
function:
run engine GameConfig
{ initialFn = initial
, updateFn = update
, subscriptionsFn = subscriptions
, viewFn = view
}
And since we’re using run
and GameConfig
, we’ll need to import Helm
.
Now it remains to simply fill in these four function bodies. Since we don’t have any animation or user interaction, only one of the functions will have anything interesting, so let’s start there:
view :: Model -> Graphics SDL.SDLEngine
view Model = Graphics2D
$ center windowCenter
$ collage [slice n | n <- [0..length colors - 1]]
Here, we’re using the collage
function from Helm.Graphics2D
to put some
Form
s together into a Collage
that can be accepted by the rendering engine.
We use a list comprehension to essentially map each color in colors
(by
index) to a slice, all of which are incorporated into the Collage
. Then we
use center
to center our Collage
about a certain point. In this case, that
point is windowCenter
, which we can define thusly:
windowCenter :: V2 Double
windowCenter = (fromIntegral.(`div` 2)) <$> windowDims
This simply halves the x- and y-dimensions of the window and converts their
values to Double
.
Now we can fill in the uninteresting functions:
initial :: (Model, Cmd SDL.SDLEngine Action)
initial = (Model, Cmd.none)
update :: Model -> Action -> (Model, Cmd SDL.SDLEngine Action)
update Model DoNothing = (Model, Cmd.none)
subscriptions :: Sub SDL.SDLEngine Action
subscriptions = Sub.none
The initial
function here is yielding an initial Model
of the “game”, along
with some initial IO action to perform. Since we don’t need a model, really, we
just substitute in a bogus datatype data Model = Model
(we use data
only
because normally this would be a much larger data type) in here. There’s no
outgoing command, so we can use Cmd.none
after we import qualified Helm.Cmd
as Cmd
.
update
does a similar thing, but only does so given the current Model
of
the game and the associated Action
that triggered the update. Since there are
no actions here, we again use a bogus datatype data Action = DoNothing
.
Our game has no subscriptions, so we simply import qualified Helm.Sub as Sub
and use Sub.none
.
The final product renders a (flat-edged) color wheel with 11 CSS-named colors on it.
© 2013-2014 Switchface Studios