A Simple Hue Transformation

A while ago I needed a way to calculate colors. I wanted an easy way to get bright colors. I wanted it to be simple, taking a single hue parameter and mapping it into an RGB color.

The solution I found is to choose the red, blue and green channel values as overlapping trapezoids. The color is supposed to be cyclic, so I could used the fmod function in C++ or the % remainder operator in JavaScript. The result would transform any floating point argument into the base domain of\(\lbrack 0, 1)\).

Color map from zero to 1

The algorithm picks the values according to the above graph. As the hue argument moves from zero to 1, each color has a section where it is at its maximum value, part where it is zero. Between the two extremes is a linear transition. A zero value maps to zero in the corresponding RGB component while a 1 gets mapped to 255.

I’ve tried different curves for the transitions and the linear slope seemed to be the best compromise to make bright colors.

What I created is diagrammed here

This shows the colors broken into the individual red, green and blue channels.

By changing the shape of the basis curves, different results can be obtained. This trapezoidal function was chosen in the end because it gives really bright colors and is simple to implement. (Also, I have a long-ago program that already uses this design and if I want to replicate it online, I need the same algorithm.)

The following code in JavaScript calculates the function.


var color = (function() {
    const coords = [ 0, 1, 1, 1, 0, 0, 0];
    const factor = 6;
    const red = 0;
    const green = 2/3;
    const blue = 1/3;

    function fromHue( hue ) {
        hue = ((hue % 1) + 1) % 1;
        const index = Math.floor( hue * factor );
        const fraction = hue * factor - index;

        return coords[index + 1] * fraction + 
                        coords[index] * (1 - fraction);
    }

    return function( hue ) 
    { 
        let r = Math.round( 255 * fromHue( hue + red ) );
        let g = Math.round( 255 * fromHue( hue + green ) );
        let b = Math.round( 255 * fromHue( hue + blue ) );
        if( r !== r || g !== g || b !== b ) { 
             r = g = b = 0; 
        }

        return `rgb(${r},${g},${b})`;
    }
})( );

The coords array specifies the function’s value at each of the evenly spaced corners. The seventh value matches the first so that the curve is cyclic. Each hue that is not at a vertex will be a linear interpolation of the two quantities to the left and right. You can verify the values in the coords array by reading the red graph at the marked points on the hue axis. The coords table adds flexibility because different coords array and adjusting “factor” to match it can create other spectra.

Each color is out of phase by 1/3, Thus adding 1/3 and 2/3 to the hue will shift the graph so that the same trapezoidal curve can be used for each of the red, green and blue channels. Blue is 1/3 rather than green because if you shift the blue graph to the right by 1/3, it lines up with the red graph while green must shift right by 2/3 to match.

Inside the fromHue( ) function, the “((hue % 1) + 1) % 1” expression looks peculiar. The % operator returns the remainder of a division. For positive hue, the remainder of dividing by 1 is the fractional part. If hue is negative, the remainder will negative, between -1 and 0. Adding 1 and then calculating the remainder of that will map the entire domain for each color channel to the range [0, 1) without needing an “if.” By mapping hues into that range of 0 to 1, it is as if the above diagram repeats indefinitely to the left and right along the hue axis.

The Math.floor() operation takes a hue times six and converts it into an integer index for the coords array. By multiplying by 6, the corner points have integer indices and the fractional part left by subtracting index can be used to do the interpolation.

In the return statement of fromHue( ), “index + 1” accesses the value of the function to the right of the calculated fraction. Since the points of the coords array are 1 apart, the slope is \(coords\lbrack index + 1\rbrack – coords\lbrack index\rbrack\)

In point slope form, the value is

$$coords\lbrack index\rbrack + (coords\lbrack index +1\rbrack – coords\lbrack index \rbrack ) * fraction$$

Combining the two references to \(coords\lbrack index \rbrack\) results in the expression above.

After calling fromHue(), the calls to Math.round( ) take each component and map it to an integer from 0 to 255. The text that it returns is appropriate for using for the color in styles and graphic contexts.

The final “if” statement detects when the results are NaN and replaces that with black. NaN is the only value that is not equal to itself. Whenever the argument hue is not a number, fromHue( ) will return NaN.

For example, if hue were 1.6, for Red, the first expression would replace hue with 0.6. Then, index would be Math.floor( 6 * 0.6 ); or floor( 3.6) or 3. Fraction would be 0.6. Coords[ 3 ] is 1 and coords[4] is 0. The result then is 0 * 0.6 + 1 * (1 – 0.6) which evaluates to 0.4. Then, red would be set to Math.round(255 * 0.4) which is 102. For Green, fromHue would be given about 2.267 which changes to 0.267 when you take the remainder. Index would be Math.floor( 6 * 0.267 ) or floor( 1.6 ) or 1. Coords[1] and coords[2] are both 1 so the return statement would return 1. This would then become 255. For Blue, fromHue would be given about 1.933 which would become 0.933. This leads to an index of 5 and fraction of 0.6. Coords[ 5 ] and coords[ 6] are both 0 so blue would be 0. Since none of these are NaN, the result would be the string “rgb(102,255,0)”

Because the corners in the trapezoidal shapes are positioned at constant intervals, the algebra is simplified. Without a repeating shape or with unevenly spaced key points, the algorithm would need an inelegant sequence of ifs to calculate the curves.

Animation and communication

I’m working on a tool to help me do animation. My underlying technology is HTML and JavaScript. I’m trying to apply some of the principles of traditional animation to my process. The primary target for the tool is math explainers.

One idea from traditional animation that seems important is the idea of creating keyframes while drafting a presentation. In an animation, the keyframes would be images with gaps between them that would be filled in by artists as they create the continuity of the motion. I could apply this idea by making still images of important stages in the product. Then, I can do the magic of making things move smoothly with polish and flair. However, when the animation is generated algorithmically, the keyframes might be similar to an advanced storyboard.

Making a storyboard would also be helpful for planning. I could make (paper) sketches of where I want the narrative to go. However, I haven’t tried this for my current video, even though it probably should be the first thing I do. I’ve been so enamored with the animation software development that that’s been a distraction from my ultimate goal.

Another (potential) idea from storytelling is that the outline is often structured in a three-act framework. I don’t really have an intuition about how to apply that idea to communicate mathematics. It sounds promising, but I haven’t explored it yet. Part of my uncertainty is my lack of experience in analyzing the three-act structure.

Animation can be a really useful tool. I don’t want to lose sight that it is just a tool and should not override the process of communication. It’s so easy to get excited about animating things in a pretty way. I also want to explain each topic effectively.

This summer, Grant Sanderson, the creator behind the YouTube channel 3Blue1Brown, sponsored a math exposition contest. His announcement of the winners shared some principles that he used evaluate the submissions.

My understanding of his discussion about the contest is that first, the information should have a motivation. Why is the information interesting? What about the topic is meaningful to the videographer? He also wanted the videos to have clarity, empathy and to be engaging. Clarity requires the video to be succinct and direct. To me, empathetic means that the video should talk to its audience. It should be aware of difficult concepts and acknowledge the difficulty. Engaging means that the audience should want to pay attention to the explanation.

Other attributes that he used to evaluate submissions to the contest was whether the videos had good quality, novelty and were memorable. To me, quality means that the chosen tools are effective and used skillfully. Novelty means that the ideas should include something new. Perhaps a new explanation, an insight that unlocks the topic to a larger audience or that it is just different. Memorable content allows the important ideas to stick with the viewer.

I think these concepts are good benchmarks to aspire to in my releases.

Sometimes a video is a good way to present the information. At other times, the ideas are better shown with a document or an interactive website. Of course, these can all be used together. Animation can help a video contain these attributes from the contest. The right information for the right audience might need more than animation technology.