Color Gradients in Processing (v 2.0)

Jeremy Behreandt
21 min readNov 18, 2017

--

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.

RGB Color Wheel

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

Six sectors of the color wheel with dominant color in each (expressed in hex).

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.

Linear interpolation (top) and smoother step (bottom) of color in RGB.
Linear interpolation (top) and smoother step (bottom) of color in HSB.

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.

RGB vertical (left) and HSB horizontal (right) gradients.

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.

RGB radial (left) and HSB radial (right) gradients.

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.

Source image (left). Image resized and blended with gradient (right).

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 ColorStops. 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 floats 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 ColorStops 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 ColorStops, 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

RGB (left) and HSB (right) radial gradients with arbitrarily spaced color stops.

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 vec4s 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 uniforms 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.

Source image (left). Image recolored and mapped onto a Suzanne model from Blender (right).

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 ColorStops.

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.

--

--