Thursday, August 20, 2009

Drawing multiple color gradients

I've seen many examples over the years regarding gradients. Start with one color, then progressively blend it into another. What I haven't seen yet is how to create complex gradients with 3 or more colors. If you can create a two color gradient, a multi-color gradient is conceptually similar.

Pairs are handy for this, because we can prepare a list of percentages and colors and pass that to our method. The left value will contain the percentage, the right value will contain the color. Our gradient will be a simple red-yellow-green gradient:

dim colors(2) as pair
colors(0) = 0.0 : &cFF0000
colors(1) = 0.5 : &cFFFF00
colors(2) = 1.0 : &c00FF00

This basically says that at 0%, it should be red. At 50%, it should be yellow. At 100%, it should be green. We'll pass this array a method which defines the width, height, and direction of the gradient:

Function CreateGradient(Width As Integer, Height As Integer, Points() As Pair, Vertical As Boolean = false) As Picture
  dim p as new picture(width,height,32)
  
  dim i,x,w,nextstart as integer
  dim pointOne,pointTwo as pair
  dim colorOne,colorTwo,blendedColor as color
  dim pct as double
  
  for i = 0 to ubound(points) - 1
   pointone = points(i)
   pointtwo = points(i + 1)
   colorone = pointone.right
   colortwo = pointtwo.right
   if vertical then
     w = (pointtwo.left.doublevalue * height) - nextstart
   else
     w = (pointtwo.left.doublevalue * width) - nextstart
   end
  
   for x = 0 to w
     pct = x / w
     blendedcolor = rgb((colorone.red * (1 - pct)) + (colortwo.red * pct),(colorone.green * (1 - pct)) + (colortwo.green * pct),(colorone.blue * (1 - pct)) + (colortwo.blue * pct))
     p.graphics.forecolor = blendedcolor
     if vertical then
       p.graphics.drawline(-1,x + nextstart,p.width + 1,x + nextstart)
     else
       p.graphics.drawline(x + nextstart,-1,x + nextstart,p.height + 1)
     end
   next
  
   nextstart = nextstart + w
  next
  
  return p
End Function

And that's all you need to create better gradients on-the-fly. Just for fun, here's the colors you need to draw a gradient similar to Apple Mail's toolbar buttons:

dim colors(3) as pair
colors(0) = 0.00 : &cDADBDD
colors(1) = 0.50 : &cD6D7DA
colors(2) = 0.51 : &cC8CBCE
colors(3) = 1.00 : &cDDE0E3

6 comments:

Anonymous said...

That sounds really good but what if you needed to draw the gradient at an angle? Or even harder - radial gradients. How might one tackle that?

Thanks much.

Thom McGrath said...

Angled gradients are probably the more difficult. You basically need to recall some high school math and draw lines. Radials are simple, draw circles.

Anonymous said...

How about providing a couple of screenshots of the effect. Nothing large; doesn't even need to be the entire window -- just a snapshot of what the code does.

Steve Garman said...

@Anonymous: It took me four minutes to paste Thom's code into a project and test it.
I've put a very amateurish screenshot at
http://rbjottings.co.uk/picstuff/gradient.png

Anonymous said...

Ahhhh but your missing the point of a screenshot request -- it's for users who DO NOT HAVE REALbasic installed.
For example, I'm posting this from a BlackBerry.

Thom McGrath said...

I've decided to take part of the challenge. Here's the code to create radial gradients:

Function CreateRadialGradient(Width As Integer, Height As Integer, Points() As Pair) As Picture
// Change this constant to modify the quality. The higher the number, the more
// passes will be made to refine the gradient, at the cost of rendering speed.
const quality = 50

dim p as new picture(width,height,32)
dim i,x,y,v,w,h,s,e as integer
dim pointOne,pointTwo as pair
dim colorOne,colorTwo,blendedColor as color
dim pct,amt as double
dim curLeft,curTop,curWidth,curHeight,destLeft,destTop,destWidth,destHeight as integer

p.graphics.forecolor = points(0).right.colorvalue
p.graphics.fillrect(0,0,p.width,p.height)

for i = 0 to ubound(points) - 1
pointone = points(i)
pointtwo = points(i + 1)
colorone = pointone.right
colortwo = pointtwo.right

destwidth = ((1 - pointtwo.left.doublevalue) * width)
destheight = ((1 - pointtwo.left.doublevalue) * height)
destleft = curleft + (destwidth / 2)
desttop = curtop + (destheight / 2)
curwidth = (1 - pointone.left.doublevalue) * width
curheight = (1 - pointone.left.doublevalue) * height

s = pointone.left.doublevalue * quality
e = pointtwo.left.doublevalue * quality
for v = s to e
amt = v / quality
x = amt * (width / 2)
y = amt * (height / 2)
w = width * (1 - (v / quality))
h = height * (1 - (v / quality))
pct = (v - s) / (e - s)
blendedcolor = rgb((colorone.red * (1 - pct)) + (colortwo.red * pct),(colorone.green * (1 - pct)) + (colortwo.green * pct),(colorone.blue * (1 - pct)) + (colortwo.blue * pct))
p.graphics.forecolor = blendedcolor
p.graphics.filloval(x,y,w,h)
next

curleft = curleft + destleft
curtop = curtop + desttop
next

return p
End Function

Not sure how well that code will appear in the comments though.