Color Gradients in Processing (v 2.0)
This tutorial on color gradients is an overhaul of a prior version; hopefully it will prove more useful to creative coders who wish to work with Processing beyond introductory class projects. It presumes a familiarity with where and how to find documentation (examples, tutorials and reference) should a concept go unexplained. Even so, we will review basics with the aim of deepening and expanding our understanding of them. Our goal is to produce
- a multi-color gradient,
- in which color stops can be placed at arbitrary percentages,
- independent of the shape to which it is applied,
- effective in RGBA and HSBA,
- can be displayed as a radial gradient, angled linear gradient, and be blended with images,
- where most of the underlying logic can be transferred to GLSL.
This article takes an iterative approach; no one example shown here will satisfy every need. Those in search of a fast and dirty gradient, are encouraged to skip ahead to the ‘Straightforward Cases’ section. These examples were created in the Processing IDE v3.3.6 (as opposed to Eclipse, IntelliJ, Net Beans, and so on).
Some Basics of Color
Processing offers many conveniences when creating a color; for example, red, green, blue and alpha values — all defaulting to the range 0 .. 255 — can be supplied directly to fill
, stroke
and background
; a hexadecimal code — six digits long and preceded by a hash-tag, with each digit being in the range 0 .. 9, A .. F — can be copied from the Integrated Development Environment (IDE)’s Color Selector
tool; a value can be assigned to the color
data type using the color
function; colorMode not only switches how the values are interpreted from red-green-blue (RGB) to hue-saturation-brightness (HSB), it also sets the upper bound of each channel.
These conveniences come with a few caveats. The primitive color
data-type is unavailable outside of Processing, and is actually a disguised int
, as can be seen when we supply a color to println
without the helper hex. This is worth keeping in mind when interpreting error messages, for example, color r = 255.0;
will yield the red band Type mismatch, “float” does not match with “int”.
Hex codes generated by the Color Selector
(in the Tools
menu), such as #FF7F00
, accommodate those familiar with HTML, CSS, etc., but do not record the alpha channel (transparency). Like the color
data type above, this hash-tag representation is not conventional for Java outside Processing; the notation 0xffff7f00
is used for hexadecimal instead, where the 0x
prefix replaces the #
and the first two values specify the alpha channel.
Lastly, the ability by the user to set new maximums for HSB or RGB with colorMode
means that, behind the scenes, values must constantly be rescaled to or from 0 .. 255 or 0 .. 1 or the user specified maximums.
The benefit, however, of packing color information into a primitive data type rather than defining an object, such as class Color { float r, g, b, a; }
, is that of performance. Even a relatively small sketch 512 x 512 pixels in dimension contains 262,144 pixels; calculations can mount quickly when manipulating 262,144 colors, each with 4 channels of information, at 60 frames per second. To take advantage of the potential efficiency, then, we have to learn how to unpack and change these integers.
Hexadecimal Operations
In preparation for working on a sketch pixel-by-pixel, we compare convenient and efficient ways of synthesizing and analyzing a color out of and into its four channels.
The bitwise operators >>
(signed shift right), &
(and), <<
(signed shift left) and |
(inclusive-or) may be unfamiliar. Let’s compare four values expressed in the Java notation for color introduced above: println(0x0000000f, 0x000000f0, 0x00000f00, 0x000000ff);
. Just as in decimal counting, where each digit is multiplied by 10 for each place it advances to the left (100 is 10 times 10, which is 10 times 1), in hexadecimal each digit is 16 times its predecessor. For this case, the console displays 15, 240, 3840, 255
. With this in mind, we can begin to understand the <<
signed shift left operator.
The console will display 1 0x00000001 16 0x00000010 256 0x00000100
. Since 2 raised to the 4th power is 16, we shift left by 4. This shows why a given color channel is slotted into place by shifting left by a multiple of 8 (each channel has two digits) and extracted by shifting right. Alpha is in third place (8 x 3 = 24); red, in second (8 x 2 = 16); green, in first (8 x 1 = 8); blue is in zeroth place, and therefore doesn’t need shifting.
When analyzing color, the signed shift right operation is followed by & 0xff
. Why? Suppose we wish to extract the yellow channel of a pale blue
As we observe in the console readout, FFAABBCC -5588020 000000CC 204
, the shift right operation also impacts the alpha, red channels and blue channels: the blue value, dd
, disappears; the alpha channel value moves into the red channel’s spot, leaving ff
in its wake. We use the &
operator to isolate the yellow channel. This technique is referred to as a bit mask, since it hides all but the information we want. To stitch the pale blue back together, we use |
: clr = 0xaa000000 | 0x00bb0000 | 0x0000cc00 | 0x000000dd;
.
Considering all that the color function does behind the curtain, we’ll want to work around it when we can. Not only is efficiency traded for convenience, but color
depends unnecessarily on the AWT Color library, and ugly is the quagmire of Java graphics utilities (AWT, Swing, JavaFX). If we implement our own RGB-to-HSB conversions, we can break color composition into finer, more granular functions, better situating us to customize our gradient’s appearance. We can also lay the groundwork for conversions to other color models. Lastly, it will be easier to translate our logic into other programming languages further down the line.
The Digital Color Wheel
An important preliminary is to observe that, although HSB is referred to as a separate color mode, it is derived from RGB color. Because of that, HSB mode is not an easy way to generate color palettes in conformance to traditional color theory as it may first appear (by, for example, adding 120 and 240 degrees to the hue for triadic colors). The traditional analog color wheel is based on subtractive RYB color, where the opposite of red is cyan, not green; of yellow, blue, not purple, and so on. This is borne out by comparing Processing with Adobe Color or Paletton.
This impacts a color gradient in two ways. First, the relationships between colors in a color model influence how they are distributed along a gradient between any two colors. On the RGB color wheel, the ‘warmer’ colors are in a narrower range than they would be in RYB.
Second, a gradient is only as good as the colors we place within it. For expedience, this tutorial will either pick colors randomly or use primary- to tertiary-colors, but bigger projects will require more sustained intention, for which we recommend GCFLearnFree’s “Beginning Graphic Design: Color” and Herman Tulleken’s “How to Choose Colours Procedurally". Furthermore, bigger projects often require collaboration, and it is important to understand that when coming from a studio art background, or in working with those who do, the basis for selecting, thinking and talking about color will differ.
Color Composition and Decomposition
For any schema in which colors are related outside the RGB model, a sketch must (1) decompose the color, (2) convert the color data from RGB channels to those of the schema, (3) perform any desired transformations on these colors, (4) convert back to RGB, and then (5) recompose the color. Given what we’ve practiced above, step 1 is no problem.
The out
parameter at the end of a function signature may be new. There are two main reasons to use an out
. Suppose within a function we are attempting an operation which might fail, perhaps retrieving the nth element of an array. We want to return true if the element is successfully retrieved and false if n is out-of-bounds; but, we also want the function to return the element if found. The out
variable would let us do both.
In another use case, suppose we want our function to return an object of the data type LargeComplexClass
, for which a lot of work must be done each time an object is created. If we have already created LargeComplexClass a
class in setup
and defined a function LargeComplexClass foo() { return new LargeComplexClass(); }
, there’s no need for a = foo();
to be called in draw
when every frame counts. For a float
array of color channels created by decomposeclr
, every little bit counts. In the event we don’t have a new float
array handy, an overloaded decomposeclr
creates one for us.
HSB to RGB
With p5.js’s color conversion as a reference, we can port across a function into our sketch
The conversion scales hue from 0 .. 1 to 0 .. 6, then finds the integer equivalent, a sector. Depending on which sector color falls under, its brightness will be mixed with two of three tints. These six sectors make more sense when we consider the waxing and waning of RGB channels in each: for sector 0, red (primary); 1, yellow (secondary); 2, green (primary); 3, cyan (secondary); 4, blue (primary); 5 magenta (secondary).
To test drive this function in the main tab of the sketch,
we fill two color arrays, the first with the built-in color
as a control.
RGB to HSB
For the opposite conversion, we refer to this Stack OverFlow discussion.
This can be tested by altering the code in the main sketch to
Easing Between Colors
The last function to excavate is lerpColor. As will be observed in the simple cases below, lerpColor
suffers from three problems. First, when it mixes distant colors over a short space, banding results. Second, it requires a wrapper function to handle more than two colors. Third, unlike other color values, hue is periodic: when its value reaches 1.0 (or TWO_PI
radians or 360 degrees) on the color wheel, it is equivalent to 0.0. Thus, there are four possible directions when easing between hues, two of which will be equivalent: shortest distance, furthest distance, clockwise and counterclockwise. For example, when easing from red to yellow on the color wheel, the shortest distance would be clockwise 60 degrees (0 to 60 degrees); the furthest distance would be counterclockwise 300 degrees (360 to 60 degrees). lerpColor
eases clockwise, meaning two-color gradients quickly devolve into the rainbow.
The first problem can be resolved by choosing a different easing method than linear interpolation, such as Ken Perlin’s smoother step. The reader is encouraged to test other easing functions, such as those of Robert Penner, to find the one best suited to his or her needs. We address the third issue immediately below, then adjust both lerpColor
and our custom function to handle multiple colors later.
To test HSB, we modify this to
For hue, we test shortest distance by finding the delta between the hue of each color, then adding 1 (a full revolution around the color wheel) to either the first or second hue based on the sign of delta. This may throw the result out of the bounds 0 .. 1, so we modulate by 1 before returning the result.
Straightforward Cases
With some fundamentals taken care of, let’s turn to some straightforward cases where out-of-the-box functions will suffice.
Linear Gradients
For drawing a horizontal or vertical gradient between two colors on a rectangular form, we simplify the Processing example, found here.
The for-loop starts from the top and works its way downwards to the bottom of the rectangle, drawing one pixel-wide parallel horizontal lines from left to right as it goes. We can iterate on this approach by switching the gradient from horizontal to vertical and changing the color mode from RGB to HSB.
With a little rearranging, these gradients can be animated.
The primary limitation of this approach, and the one to follow, is that the gradient is not independent of the form… or forms… of which it is composed. We can play around with that form, by increasing the step through the for-loop from i += 1
to 2 or greater for a Venetian blinds effect. But animations which take the lines out of parallel risk losing the illusion of a gradient.
Radial Gradients
Another official example shows a radial gradient, modified below with an offset center.
Since Processing designates color modes with the constants HSB
and RGB
we can flip a coin of which to use with a ternary operator (a condensed if-else block). We first choose a random number between 0 .. 1, then ask (with ?
) if that random number is less than 50% (0.5
). If it is, we opt for HSB
; the alternative (denoted with :
) is RGB
.
Because this technique layers circles one atop another, translucent colors bleed into each other in normal blend mode. This layering could be beneficial to a simple papier mâchè look; but not for a solid gradient. Another issue is that it is expensive to layer so many circles atop one another, each one pixel smaller in radius than the previous. We can mitigate this problem by increasing the detail
so i
takes a greater stride between each run through the for-loop.
Intermediate Cases
Equidistant Multi-Color Gradients
So long as we don’t need to place color stops at arbitrary distances, we can upgrade our gradients by overloading lerpColor
to handle an array of colors.
As a general principle, a function should return results with as few calculations as possible. To apply that principle here, mind that the step
in easing functions is in the range 0 .. 1; while we might expect another easing function to return a color beyond the ‘left edge’ (array element 0), and ‘right edge’ (element size -1), not lerp
. An early draft of this function might check the step with constrain(step, 0, 1)
, then scale, floor
(or cast to int
) and ease. However, we can avoid the hassle and return the appropriate array element (or a copy thereof) if the step is out of bounds.
If we apply this to the animated linear gradients from earlier
we get the following
Pixel-By-Pixel Gradients
For any given sketch, the main tab (where we type setup
and draw
) extends PApplet
and creates a graphics renderer object g
that extends PGraphics
. Many drawing functions that we call are passed on by the sketch to this renderer. Although Processing works hard to make the results consistent across renderers, the inner workings differ based on what we specify as the third argument of size
. We can determine which renderer we have by typing println(g.getClass());
in setup
after this renderer has been initialized; when we don’t supply a third argument, the default is processing.awt.PGraphicsJava2D
. We can also create our own graphics renderer in addition to g
using createGraphics; the advantage of doing so at present is that this second renderer has a transparent background. Though not apparent in the examples immediately following, creating our renderer will let us use a two-pass approach further on.
Linear
The pixel array is one-dimensional, even though it represents a two-dimensional coordinate system. To convert from 2D to 1D, we loop through each row (y
= 0
.. height
), then each column (x
= 0
.. width
). The variable i
, declared in the outer loop but incremented by the inner loop, updates the 1D index.
Any time we want to read pixels from the renderer, we first loadPixels
into its array; after updating the pixels
array, we then updatePixels
for the changes to take effect. To switch our gradient’s orientation, we replace x / w
with y / h
. The PGraphics
object can be displayed in the main sketch’s draw
loop with image
. This is a clue that we can work with PImage
and PGraphics
in similar fashion, which looking at the source code confirms.
Angled Linear
Now that gradients are created pixel-by-pixel, not by lines or circles, we are free to choose the angle. To do so, we project
a given pixel coordinate (x, y) onto the imaginary spine of the gradient. For the implementation below we use this Stack Overflow discussion as reference.
This projection of one vector onto another has a number of useful applications beyond linear gradients, and those interested in further applications of this concept may find Sebastian Lague’s “Distance from point to line", Coding Math’s “Line Intersections Part I" and Dan Shiffman’s “Autonomous Agents: Path Following" helpful.
Radial
Calculating the distance from the center of the radial gradient and the current pixel is straightforward. The dist
function could be used, but any function dependent upon square roots, like the Pythagorean theorem c = sqrt(sq(a) + sq(b))
, is expensive. Furthermore, since the for-loops are row major, the rise
remains the same for every row of pixels. Instead we calculate distance squared, then multiply by 2 terms squared.
The radial gradient can be extended to simulate a ‘flashlight’ effect, as can be seen in this example, without having to resort to a lighting model in P3D
. centerX
and centerY
can be updated to follow the mouse, and the gradient can be blended with an underlying image, as will be explained below.
Conic
With minor adjustments, a radial gradient can be converted to a conic. Instead of squaring the rise and run, we instead use atan2, which returns a value in the range of PI
.. -PI
.
Since we need a value in the range 0 .. TWO_PI
, we use a variant modulo operation called floorMod
as a conversion.
Two-Pass Approach
At present, we are caught between working with mathematically determined shapes and with a grid of pixels. The former is smooth and intuitive, but yields only solid fills and strokes. The latter is rigid, but provides us the granularity needed to refine a gradient’s visualization. To resolve this tension, we take a two-pass approach. First, we draw our shapes in a renderer. In this pass, each shape that we want to be filled with a gradient is given a key color (think of a green-screen). In the second pass, we check for that key color and replace it with the gradient.
Since the edges of the star shape may be translucent as result of the renderer’s smooth
settings, we must take care to preserve in the gradient pass the alpha channel of the source pixel before replacing it with the gradient color.
Blending
Heretofore we’ve only replaced a renderer’s pixel color. What if we want to blend a gradient with an image? If we import an .png file, we can then supply it as an argument to our gradientPass
function. A major issue is to make the dimensions of the image, the sketch and the secondary renderer consistent. To simplify, we’ve resized the image, even though so doing stretches it.
The reference for blend gives us an inkling of how various blend modes are accomplished; and to see the nitty-gritty of its implementation, we reference PImage
’s blend_darkest
, blend_multiply
, and so on. We’ve adapted blend_blend to our own purposes: instead of assuming that the alpha channel is the factor by which we want to blend our gradient, we instead use a step
, which we could then animate.
Advanced Cases
SmootherStep For Arrays
For more involved cases, we’ll use custom functionality developed in the ‘Some Basics of Color’ section. Our goal is to develop an Object-Oriented approach which describes a gradient, but first we’ll pick up where we left off and implement a smootherStep
function to ease between an array of (equidistant) colors.
By updating our test sketch to compare the above with the overload of lerpColor
that worked with arrays, we see the following
As can be seen, the smoother step gives each color a wider breadth before transitioning to the next. Implementing the smootherStepHsb
array function yields even more striking differences.
Object-Orientation
A Gradient
class is little more than a glorified list of ColorStop
s. The toughest decision will be how we represent color data. We choose an integer here so that a color stop can easily work with native Processing functions. Alternatives might include a float
array or a Color
object. Which representation we choose will influence whether or not we have to compose and decompose colors before we ease between them in the gradient.
As a matter of convenience, extra constructors allow the user to create a ColorStop
from float
s representing each channel of information, so long as the color mode is also specified. This is because these four values could represent Red, Green, Blue and Alpha or Hue, Saturation, Brightness and Alpha.
All color stops must be ordered so that, for example, one at 12.5% comes before one at 25%. For that reason, the class implements the generic interface Comparable
. We plan to only compare two color stops, so we specify our target comparisand between the angle brackets. The interface requires that we define the function compareTo
; this function returns 1 when the relevant quality of the color stop is greater than the other’s; -1 when the quality is less than; and to return 0 as a default. This will enable us to use java.util.Collections.sort
once we move on to the Gradient
class.
The …
(referring to varargs
, a variable number of arguments) in the class’s main constructor permits the user supply a comma-separated list of ColorStop
s when creating a new gradient. Secondary constructors allow the user to create a gradient from an array of colors; the percentages are evenly distributed within the loop.
Any time a data structure such as ArrayList is used, the question arises whether a better one is available. For now an ArrayList
is used for simplicity and flexibility. Unlike an array, new elements can be added to the list after a Gradient
has been created. Two disadvantages are (1) that it must be sorted after each added color stop and (2) that it permits two color stops to be placed at the same percent where only one should be allowed. The latter concern is alleviated by modifying add
and the class constructors to check the provided percent against the list of color stops for uniqueness. However, we’re treating percent, a float
, as a unique identifier for each color stop — not a good idea. Is 0.3332 equal to and — in our case, identical with — 0.3334 or not? We instead rely upon an approximation within a given tolerance
.
The most important function is eval
, for which we reference Andrew Noske’s implementation. At the core of this function the ability to locate the two ColorStop
s, the previous and current, in the sorted list which the given percent falls between. The given percent needs to be rescaled to the local difference between the two before easing. If the given percent exceeds the percentage of the last stop, then the color of the last stop is returned.
By initializing our gradient with random colors like so
and displaying it in radial form, we get results such as
OpenGL Renderers
No matter the micro-optimizations we attempt for the above functions, we will be at a disadvantage so long as we don’t use the Graphics Processing Unit (GPU), which is designed to handle calculations in parallel. Our next step is to switch to either the P2D
or P3D
renderer so that we can take advantage of OpenGL. In the short-term, this will involve a small setback: we’ll run into the same issues we did before (color dependent upon form, banding due to linear interpolation, the need for RGB-HSB conversions).
A warning for PC users: in the past, working with OpenGL in Processing has led to considerable lag when running a sketch.
For our first, and simplest case, we observe that color can now be assigned on a per-vertex basis between beginShape
and endShape
.
Since color is assigned by vertex, the order in which we write our vertices is the order in which we write our colors. Furthermore, the argument we supply to beginShape, for example POLYGON
vs. TRIANGLE_FAN
, will determine how the renderer fills in the face of the shape.
With POLYGON
in particular, color streaking, or zig-zagging can occur. This has useful applications, such in as the creation of a radial GUI element with a conic gradient.
However, we do not necessarily want to change how many vertices we draw, or what order we draw them in, for the sake of color.
Separating Color From Shape With A Fragment Shader
We could adapt the two-pass approach to apply a 2D renderer to a 3D shape with texture (immediate) or setTexture
(for retained PShapes). Instead, we’ll write a fragment shader. Since we’ll be switching back and forth between our surrounding framework (Java) and GLSL, an extra editor, such as Atom (with the GLSL Preview add-on) or Hexler KodeLife, might prove helpful.
The lines preceded by a hash-tag are preprocessor directives, instructions that precede the compilation of our script; this one specifies the precision of floating point decimals, and will become customary to place atop any GLSL script we author. The uniform
keyword denotes a variable that we set from outside the shader, in Processing.
The start
and stop
uniform vec4
s will let the user specify two colors, while the dimensions
will pass the sketch’s width
and height
into the shader. Unlike the PVector
object, which contains 3 components, GLSL splits its vectors into vec2
, vec3
and vec4
; the last is used to store color in the range 0 .. 1, where RGBA corresponds to XYZW.
The main
function is where the lion’s share of the work will be done, the most important task being to assign a value to the built-in variable gl_FragColor
. Colors can be blended with mix
; although smootherStep
is not built-in, smoothstep
is. The draw
loop will pass a uniform step
into the shader, animating the gradient.
We save this in the data folder of the sketch, then load it into a PShader object with loadShader. To set the uniform
s from above, we use the set function, which converts Processing data types (PVector
, PMatrix3D
, etc.) into GLSL data types.
We won’t cover the translation of every function into GLSL, since most changes are to syntax, not logic, but will link to the gist here. After porting across, it isn’t long before we catch up to where we left off in Java.
Writing a multi-color gradient shader is tricky though. The set
function, our bridge between Processing and GLSL, does not handle arrays. Depending on GLSL version, arrays are not always supported; when they are supported, they must be of fixed length. GLSL uses structs to hold complex data, which allow member properties but not methods, meaning our Object-Oriented approach must be retooled. How might such a gradient work? Suppose we re-color a source photograph using its brightness as a step.
For this case, we load the source image, shaders and a 3D model in setup
of the sketch’s main tab.
We introduce a vertex shader source file to accompany the fragment shader when creating our PShader
. With Andres Colubri’s Shaders tutorial as a reference, our vertex shader can be written as:
A majority of the work here is to set the model’s position in space relative to the camera, so we won’t focus on particulars. Of primary interest is the vertTexCoord
, marked with the varying
keyword, which means that the vertex shader will set it, then pass it on to the fragment shader.
Given the vertTexCoord
, the fragment shader will find the appropriate texture. It then averages the three channels of that pixel, then uses the brightness as a step for the gradient eval
. We create a ColorStop
struct
, being sure to conclude the declaration with a semicolon. Given that a Gradient
was a glorified list in the first place, we simplify matters by simply using an array of 4 ColorStop
s.
As depicted above, by adapting Processing’s default shaders, our gradients can cooperate with the lighting system. By learning more about GLSL from resources like as The Book of Shaders and LearnOpenGL, we can broaden our ability to work with color in Processing.