The trouble with tribbles….

Or in this case linear gradients. If you want linear gradients to work well you really need to calculate the correct start and end points.

This is especially true if you dont want a gradient that simply runs from left to right or top to bottom (or their reverse directions)

While figuring out exactly what I needed to do I had several different suggestions from different people that I found dont work quite right.

If you simply compute the points using sin() and cos() of whatever angle you wont get the right answer. Depending on what radius you use you’ll either get clipping of the gradient inside the rectangle you want to fill or you’ll position the start & end points far enough outside that rectangle that if you have lots of gradient steps some wont show.

For example, if we set the start and end points so they are slightly angled across a square canvas with code like

Var linearBrush As New LinearGradientBrush
linearBrush.StartPoint = New Point(0, g.Height/2-20)
linearBrush.EndPoint = New Point(g.Width, g.Height/2+20)

linearBrush.GradientStops.Add(New Pair(0, Color.Red))
linearBrush.GradientStops.Add(New Pair(0.4, Color.Yellow))
linearBrush.GradientStops.Add(New Pair(0.7, Color.Magenta))
linearBrush.GradientStops.Add(New Pair(1.0, Color.Blue))

g.Brush = linearBrush

g.FillRect(0, 0, g.Width, g.Height)

we get a clipped gradient fill in the upper left corner

because the gradient “rectangle”, when rotated, doesnt overlap the entire rectangle we filled. But notice how the gradient starts at blue & ends at red very nicely. It shows all our steps quite completely.

When you start the gradient too far outside the rectangle you get some steps in the gradient not being as visible as they should be (this one IS much harder to see but notice how little blue there is)

I set the rotation to be 22.5 degree and then set the start & end points using sin & cos using a radius that is the hypotenuse of the right trangle formed from the rectangle center point to the lower right corner (any corner would do since this is a square – it get worse if you use a non-square rectangle) The red circle around the filled rectangle outlines where the start and end points might be place for any rotation. Note how far outside the rectangle they are and the more color steps there are the more get clipped out. The code is as follows

Const pi As Double = 3.1415926535897932384626433
Const degreesToRadians As Double = pi / 180
Const degrees As Double = 22.5


Var linearBrush As New LinearGradientBrush

Var hypot As Double = sqrt( (g.width - g.width/2)^2 + (g.height - g.height/2)^2)



Dim startx As Double = cos(degrees * degreesToRadians) * hypot
Dim starty As Double = sin(degrees * degreesToRadians) * hypot

linearBrush.StartPoint = New Point(g.width/2+startX, g.height/2+startY)
linearBrush.EndPoint = New Point(g.Width/2-startx, g.Height/2-startY)

linearBrush.GradientStops.Add(New Pair(0, Color.Red))
linearBrush.GradientStops.Add(New Pair(0.4, Color.Yellow))
linearBrush.GradientStops.Add(New Pair(0.7, Color.Magenta))
linearBrush.GradientStops.Add(New Pair(1.0, Color.Blue))

g.Brush = linearBrush

g.FillRect(0, 0, g.Width, g.Height)

You can see that in the two images about. Nove how much red & blue the first has compared to the second,. Yet the gradient is set up identically between them to trabsition between the same number of colors using the same number of steps.

The problem is that circle is much too large. Of course we could use a much smaller circle but if we set it so its inside the rectangle then we get the clipping problem back.

So whats the “right answer” ? So far it seems to me that the correct answer is to find a minimal bounding rectangle that is rotated however we want. And to make sure our gradient starts on the edges of that bounding box.

OK so what the kec does that mean ? 😛

Well – in action it looks like this

The white square is the rectangle we’re going to fill with the linear gradient. The light purple circle is the same circile that encloses the rectangle computed much like the one just above here. And the light blue line is where our gradient will run either left to right or right to left (we can invert those two trivially)

Where the gradient runs exactly left to right or top to bottom we can put the start & end points right on the white square. But as we start to rotate the gradient you’ll see some yellow and red lines show. Where the blue line crosses the yellow line is where we need to put the start & points of the gradient. And note those points are NOT on that circle drawn outside the rectangle. But those red & tyellow lines for the minimum enclosing rectangle that, if the gradient starts on its edges, then the entire rectangle in the white square will be filled. The result is this

I expect this mechanism can be extended to apply to any shape using a convex hull approach.

I’ll see about whether I can release this code or not.

Either way you have an explanation of how to compute the right start and end points for linear gradients and apply then to rectangular areas. And you’ll know why things might get clipped off or not fill quite right if you do something else.

Now I’m sure some of you will get to the end of this and go “Well Duh Norm”

Take this post purely as evidence my trig sucks 😛
I freely admit that. It does suck.

Enjoy !

4 Replies to “The trouble with tribbles….”

  1. Norman, a great blog post. Thanks for sharing your insights with us. I don’t want to know how many hours you sat on this seemingly small problem.

    One more small note about the implementation of the gradient, the code for the GradientStops can be shortened a bit:

    linearBrush.GradientStops.Add(0.0 : Color.Red)
    linearBrush.GradientStops.Add(0.4 : Color.Yellow)
    linearBrush.GradientStops.Add(0.7 : Color.Magenta)
    linearBrush.GradientStops.Add(1.0 : Color.Blue)

    1. Unfortunately because linear gradient seemed so seductively easy to use I spent way more time that I probably should have to get the behaviour as perfect as I possibly could (and if my trig was better it might not have taken so long)
      But now it looks so very right its hard not to see the other issues when its not done this way 🙂

    1. Probably
      I just copied the value from Wikipedia
      Alternatively I could have copied from Calculator which gives 3.141592653589793

Comments are closed.