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