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.
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
UnsignedByteFootnote byte. There is no literal expression for
UnsignedByte like there is for
Int, so we must convert an
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 // Too small, is transformed to 0 1000.uByte.get // Too big, is transformed to 255 // res4: Int = 255
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
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
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.
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 an
desaturate, which respectively add and subtract a
Normalisedvalue from the color; and
darken, which respectively add and subtract a
Normalisedvalue from the lightness.
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)) )
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.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)))
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.|