Creating Colors
We've seen how to use predefined colors in our images. What about creating our own colors? In this section we will see how to create colors of our own, and transform existing colors into new ones.
RGB Colors
Computers work with colors defined by mixing together different amounts of red, green, and blue. This "RGB" model is an additive model of color, which means adding more colors gets us closer to white. This is the oppositive of paint, which is a subtractive model where adding more paints gets us closer to black. Each red, green, or blue component can have a value between zero and 255. If all three components are set to the maximum of 255 we get pure white. If all components are zero we get black.
We can create our own RGB colors using the rgb
method on the Color
object. This method takes three parameters: the red, green, and blue components. These are numbers between 0 and 255, called an UnsignedByte
Footnote byte. There is no literal expression for UnsignedByte
like there is for Int
, so we must convert an Int
to UnsignedByte
. We can do this with the uByte
method. An Int
can take on more values that an UnsignedByte
, so if the number is too small or too large to be represented as a UnsignedByte
it will be converted to the closest values is the range 0 to 255. These examples illustrate the conversion.
0.uByte.get
// res0: Int = 0
255.uByte.get
// res1: Int = 255
128.uByte.get
// res2: Int = 128
-100.uByte.get // Too small, is transformed to 0
// res3: Int = 0
1000.uByte.get // Too big, is transformed to 255
// res4: Int = 255
(Note that UnsignedByte
is a feature of Doodle. It is not something provided by Scala.)
Now we know how to construct UnsignedBytes
we can make RGB colors.
Color.rgb(255.uByte, 255.uByte, 255.uByte) // White
Color.rgb(0.uByte, 0.uByte, 0.uByte) // Black
Color.rgb(255.uByte, 0.uByte, 0.uByte) // Red
HSL Colors
The RGB color representation is not very easy to use. The hue-saturation-lightness (HSL) format more closely corresponds to how we perceive color. In this representation a color consists of:
- hue, which is an angle between 0 and 360 degrees giving a rotation around the color wheel.
- saturation, which is a number between 0 and 1 giving the intensity of the color from a drab gray to a pure color; and
- lightness between 0 and 1 giving the color a brightness varying from black to pure white.
The picture below shows how colors vary as we change hue and lightness.
The picture below shows the effect of changing saturation.
We can construct a color in the HSL representation using the Color.hsl
method. This method takes as parameters the hue, saturation, and lightness. The hue is an Angle
. We can convert a Double
to an Angle
using the degrees
(or radians
) methods.
0.degrees
// res8: Angle = Angle(0.0)
180.degrees
// res9: Angle = Angle(3.141592653589793)
3.14.radians
// res10: Angle = Angle(3.14)
Saturation and lightness are both Doubles
that should be between 0.0 and 1.0. Values outside this range will be converted to the closest number within the range.
We can now create colors using the HSL representation.
Color.hsl(0.degrees, 0.8, 0.6) // A pastel red
To view this color we can render it in a picture. See below for an example.
Manipulating Colors
The effectiveness of a composition often depends as much on the relationships between colors as the actual colors used. Colors have several methods that allow us to create a new color from an existing one. The most commonly used ones are:
spin
, which rotates the hue by anAngle
;saturate
anddesaturate
, which respectively add and subtract aNormalised
value from the color; andlighten
anddarken
, which respectively add and subtract aNormalised
value from the lightness.
For example,
Image
.circle(100)
.fillColor(Color.red)
.beside(
Image
.circle(100)
.fillColor(Color.red.spin(15.degrees))
)
.beside(
Image
.circle(100)
.fillColor(Color.red.spin(30.degrees))
)
.strokeWidth(5.0)
produces the below.
Here's a similar example, this time manipulating saturation and lightness, shown in below.
Image
.circle(100)
.fillColor(Color.red.darken(0.2.normalized))
.beside(Image.circle(100).fillColor(Color.red))
.beside(Image.circle(100).fillColor((Color.red.lighten(0.2.normalized))))
.above(
Image
.square(100)
.fillColor(Color.red.desaturate(0.6.normalized))
.beside(
Image
.square(100)
.fillColor(Color.red.desaturate(0.3.normalized))
)
.beside(Image.square(100).fillColor(Color.red))
)
Transparency
We can also add a degree of transparency to our colors, by adding an alpha value. An alpha value of 0.0 indicates a completely transparent color, while a color with an alpha of 1.0 is completely opaque. The methods Color.rgba
and Color.hsla
have a fourth parameter that is a Normalized
alpha value. We can also create a new color with a different transparency by using the alpha
method on a color. Here's an example, shown below.
Image
.circle(100)
.fillColor(Color.red.alpha(0.5.normalized))
.beside(Image.circle(100).fillColor(Color.blue.alpha(0.5.normalized)))
.on(Image.circle(100).fillColor(Color.green.alpha(0.5.normalized)))
Exercises
Exercise: Analogous Triangles
Create three triangles, arranged in a triangle, with analogous colors. Analogous colors are colors that are similar in hue. See a (fairly elaborate) example below.
These sort of examples are getting a tedious to write out. We'll soon see how to make them a bit shorter.
Image
.equilateralTriangle(80)
.strokeWidth(6.0)
.strokeColor(Color.darkSlateBlue)
.fillColor(
Color.darkSlateBlue
.lighten(0.3.normalized)
.saturate(0.2.normalized)
.spin(10.degrees)
)
.above(
Image
.equilateralTriangle(80)
.strokeWidth(6.0)
.strokeColor(Color.darkSlateBlue.spin(-30.degrees))
.fillColor(
Color.darkSlateBlue
.lighten(0.3.normalized)
.saturate(0.2.normalized)
.spin(-20.degrees)
)
.beside(
Image
.equilateralTriangle(80)
.strokeWidth(6.0)
.strokeColor(Color.darkSlateBlue.spin(30.degrees))
.fillColor(
Color.darkSlateBlue
.lighten(0.3.normalized)
.saturate(0.2.normalized)
.spin(40.degrees)
)
)
)
[byte] | A byte is a number with 256 possible values, which takes 8 bits within a computer to represent. A signed byte has integer values from -128 to 127, while an unsigned byte ranges from 0 to 255. |