Tubefy Explained, part I:
Lerping Colors

"Tubefy" is a system of JavaScript methods to produce gradients that are neither linear nor radial which, true for the time of writing (Aug2009), are the only gradients described in the SVG/Canvas specs and supported by the major browsers.

I coined the name "Tubefy" at the time when the only thing Tubefy could do was to make any stroke looks like a tube. Since then, Tubefy expanded to produce gradients to whole shapes and also to spread/converge gradients of different colors into/from multiple directions.

Beyond the idea of a new breed of gradients, I think at least one of Tubefy features, has much wider scope than the Tubefy system, and is much desired in the existing gradients as well.

The necessity of this feature is easily understood visually, and it is not so difficult to achieve either, but I am still struggling to find a way to demonstrate how this feature is achieved to people who are not so comfortable with math, and also, to find a simple way to integrate it into the existing native gradients.

In order not to delay this article, this feature will be the subject of the next article.


First Task - Tubefy This:

What we are actually looking for is conceptually the same as radialGradient, but unlike radialGradient, which radiates from a point, our "tubeGradient" radiates from a line - the stroke "skeleton" (outline). In all other respects, tubeGradient should look exactly the same as radialGradient. Practically, whatever gradient we create will have to follow the behavior of native gradients, our task is then, to imitate that behavior.

I use "skeleton" instead of "outline" because stroke is dealt here like a shape so "outline" can become vague. I also use "edge" to reference the border of the stroke-width.

The way I think of radialGradient is as an ordered set of rings, each ring colored gradually different than the next ring and has 1 pixel width. Why 1px? Well, we try to imitate what the SVG engine is doing and my guess is that the finest gradation of colors it can possibly achieve is limited by the granularity of the screen, which is 1px. This is based on the assumption that the screen cannot paint 1px in two different colors. I really don't know if this assumption is true other than it sounds logical to me, In any case, this is our working assumption for the moment, and if we find the results not fine grained enough, we can always reduce the rings width in our calculations.

Those rings must precisely imitate the stroke contour, and also be tightly packed so when painted to the screen there will not be any gaps between them (or else we can see the background through these gaps). The notion of "radiating" means each consecutive ring distance from the stroke skeleton must be larger than its predecessor by exactly 1px. Outer ring is the stroke edge and most inner ring is the skeleton (practically, most inner ring is either 1 or 2 px wide, depend on the original stroke-width being even or odd).

Also don't forget that after we draw those rings, we still have to paint them, but unless we find a way to draw them first, there is nothing to paint, so lets put the paint job aside for the moment. Our first task is then, to create those rings.

Below demo draws the edge ring and the skeleton of the bezier worm we want to gradient. May I suggest before you continue, think how would you draw that ring.

If you click inside the ring it will reveal its structure.

I will now attempt to break down the "stroke-width" attribute to show that the basic definition of "stroke-width" is all we need in order to draw our set of rings.
But what is this definition? How does the stroke-edge relate to the skeleton?

I'm sure all of us agree that stroke-width is intuitive. So intuitive that even the SVG spec describe stroke-width as: "The width of the stroke on the current object"
[ http://www.w3.org/TR/SVG11/painting.html#StrokeProperties ]
This description doesn't help us much to understand what the UA "must" or "should" do so that all stroke-widths on all UA's will look the same, so I will try to put in words what I perceive as so intuitive in stroke-width or more accurately, the practical manifestation of the "stroke-width" attribute:

Any perpendicular line from any point on the stroke skeleton to the stroke edge,
is also perpendicular to the edge and has a length of *exactly* half of the stroke-width.
(If you think that's an easy task, I suggest you try to draw the above ring as a shape with 1px stroke).
Please note that if the skeleton is a single point, the above is the definition of a circle with a radius of half the stroke-width. Just to remind us the resemblance
to radialGradient radiates from a point and "tubeGradient" from a line.

If we replace "stroke skeleton" with "most inner ring" and "stroke edge" with "outer ring" then this definition is precisely what we demand from our set of rings. And because the stroke-width part of the definition is absolutely in our hands (no constrains) we can set it to whatever we want.

In simple words, the algorithm we need in order to draw our rings, already exist inside the SVG engine. We can use it without even knowing how it actually works. All we need to do is simply draw the same stroke again and again, each new copy with a 2px smaller stroke-width than the last copy, until we get to either 1 or 2px, that's it. SVG is automatically doing all the hard work of setting the precise shape of each ring.

Following demo is to show how the rings look like on our worm. If you click the worm, the rings will be drawn as described above. Since we don't have yet any coloring system, there are only two colors: the stroke color and the background color. The worm is also scaled to show finer details and its stroke-width is an odd number so that the skeleton will have 1px width. If you click the worm again, it will reveal its detailed structure.

[ Full page ]

Now that we have our rings exactly the way we want them, it's time to paint them.

What we want is a rather simple gradient with only two end colors, the equivalent of a native gradient with only two offsets, 0 and 1. We can choose these two end colors to be whatever we want, one will be the basic tube color and the other the tube highlight color. We also know exactly how many colors we need (number of rings). What we need is a way to calculate all the graduated shades of the
in between rings.

The way Tubefy achieves this graduation, is by using a well known function - lerp. Tubefy is using this function intensively, not only for colors but any time it needs to move gradually between any two points. As this function is by far the most used in Tubefy, I will attempt to explain it thoroughly, though not in its full math scale but only the way it is used in Tubefy, which is rather simple.
Here is the lerp function written in JavaScript:

       function lerp(p, a, b){ return a+(b-a)*p }

I present the function early so those of you who are not so comfortable with math can see the math involved is really elementary.

Lerp is short for "linear interpolation" which is a simple description in math words of what this function does. Interpolation means finding the value of a point which is *in*between (*in*terpolation) two known points. Linear simply means "straight"
i.e. - the point we search is on a straight line between the two known points.

The two "known" points are what we send to the function as arguments "a" and "b". These points are actually just numbers, any two numbers that in between them we want to find a new point. For simplicity and to better visualize what lerp is doing, think of them as positive numbers and "b" has greater value than "a".

Now comes the important question: how do we tell lerp where exactly is the new point we want it to calculate for us? Here is where the "p" argument enters the picture. "p" tell lerp at exactly what *part* of the way from "a" to "b" is the new point. For example, p=0.1 means 0.1 *part* (which is equivalent to 10%) of the way from "a" to "b" and p=0.34 means 0.34 *part* (same as 34%) of the way
from "a" to "b".
Please note something very important: *from* "a" *to* "b" i.e. - lerp perform its calculation in a certain direction which always start from "a" - the first of the two points sent to it.

Now lets check what lerp is doing with the arguments we send.

(b-a) is simply the distance from "a" to "b". For example if a=200, b=300 then the distance from "a" to "b" is (300-200)=100. Please note again the importance of the direction of this distance: "b" (second point) minus "a" (first point).

The expression (b-a)*p calculate what is the actual size of "p" part of the distance we just calculated. If p=0.25 (25%) *part* of that distance, then 100*0.25 = 25

The last remaining step of the function is to add this result to the *first* point ("a") so: a+25=200+25=225 which is the final result.

Read: "The value of a point which is 0.25 (25%) from 200 to 300 is 225".

One more example in one shot:

lerp(0.75, 100, 200) = 100 + (200-100)*0.75 = 100 + 100*0.75 = 100 + 75 = 175

And in words: "The value of a point which is 0.75 (75%) from 100 to 200 is 175"

Or equivalent sentence: "The value of a point which lies at 0.75 (75%) of the way from 100 to 200 is 175"

Now that we are familier with how lerp works,
lets see how to use it for painting our rings.

If we think of point "a" in the lerp function as being the outer ring (edge) color,
and point "b" being the most inner ring (skeleton) color, then the lerp function is the perfect tool to find the in-between colors, one shade at a time.

To send the "p" argument to lerp for each step is simple:
We know exactly how many rings we need to paint, this is our "totalSteps".
We also know exactly which step we are now at, this is our "currStepNumber",
and the "p" for a particular step (ring) is simply currStepNumber / totalSteps
or if you rather visualize rings: currRingNumber / totalRings.

Example: If totalSteps = 60 and we are now performing step 6 then we are now at 6/60 = 0.1 *part* of the way. If we perform step 12 then we are at 12/60 = 0.2 *part*, and step 15 is at 15/60 = 0.25 *part* of the way. And don't worry about fractions which are not as "nice" as the above examples (like 7/60), the machine will calculate them with far better accuracy than what we practically need.

But web colors has at least 3 dimensions (R, G, B or H, S, L) how then are we going about using lerp, which is one dimension only, to accomplish that? Well, very very simple... one dimension at a time.

Please note one feature of Web colors components which is so fundamental:

   Color components are just numbers.

The specialty of lerp is to take us "as gradually as we wish"
between *any* two numbers, why not between color components?
Just break "lerping colors" to "lerping colors components".

What we need now is just an "organizer" function to accept the two colors we want to lerp, and send to the lerp function a pair of each component at a time i.e. send to lerp the R component of first color together with the R component of the second color, then do the same with the G components and then the B components.

In order to allow this "organizer" function to extract the components of the colors
in an orderly manner, we send these colors as Arrays - [R, G, B].
Please note we send the components as decimals, our lerp is not designed to deal with hexadecimals:

      function lerpA(p, A, B){ 
         var res=[]; // fresh array to hold the result color
         // loop as many times as there are components in A
         for(var i=0; i<A.length; i++){
            // lerp same component from each color
            res[i]=lerp(p, A[i], B[i]);
         }
         return res; // return the result color as an array
      }

Capital letters in the function stand for arrays.

Side note: the above function is not limited to colors.
It can handle any two arrays regardless of how many components (dimensions) they posses. So if we want to lerp between points in 2D space we send [x, y], points in 3D apace we send [x, y, z] and colors with alpha channel (opacity) we send [R, G, B, a]. You can find this function, together with the lerp function, is in the heart of any animation library.

One last step we must take if we lerp colors, is to make the results understood by SVG. As our components are decimals, the easiest way is to format them as rgb:

      function toCss(A){ 
         for(var i=0; i<A.length; i++)
            A[i]=Math.round(A[i]); // round each component
         // return an rgb formated color
         return "rgb(" + A.join() + ")"; 
      }

And just in case we might send arguments to lerp function as strings,
a precaution must be taken:

       function lerp(p, a, b){ return Number(a)+(b-a)*p }

That's it. Time to kick tires.
Put the lerp function to real screen test, see how good it is.

The demo below is to inspect how accurately the lerp function
imitates the native SVG gradient:

If you click the gradient in different places, it will reveal its structure.

Upper half is a single rect filled by native linearGradient.
Lower half is composed of 500 rectangles, each filled with a color
calculated by the lerp function.

Technical detail for acurracy: what the alerts inform you about the lower rectangles is their visible part (1px wide) but they are actually 2px wide.
I made them this way because one of the major browsers paint 1px rects
slightly darker.

Judging the above demo, I think the system we already have for creating
gradients is rather convincing. Time to finish the original task:


[ Source code ]

Above demo is a stand-alone and licensed, feel free to copy it for your own experiments/presentations. In the demo, the tubefy function grabs the edge color from the stroke and the skeleton color from the "tube" attribute which is orphan but works well. You can find some of my Tubefy experiments [ here ] and some of my 3D experiments [ here ]

If even one of you will feel even part of what I felt when I made my first experiments with Tubefy, it will be my reward.

Next article: Tubefy Explained part II: Rounding Colors

I think the following image explains the subject of the next article better than any words. Left filled by radialGradint (0 magenta, 1 blue), right by modified Tubefy:

Copyright © 2009 Israel Eisenberg. All rights reserved.