Transmogrifying colours (or, gradients the hard way)

I found myself needing a way to convert a colour some of the way toward another colour. Possible use cases include

  • Lightening or darkening a shade
  • Recolouring a palette to match a colour scheme
  • Gradients and blends

Now, this is probably a specialised sort of function. With GDI+ (or the .NET System.Drawing) there are better ways to do gradient fills, and transparency may be a more understandable way of doing blending or merging. Still, it was a short function that took a deceptively long time to write, so you might find it useful.

In my case I had a multi-select grid, and wanted to change the colors to distinguish the current row (which using default windows colours is dark blue, but on my pc currently is dark purple) from other, selected but not current, rows. A selected row would be coloured similarly to the current row, but less so. Something like this:
Interpolated colour used in a multi-select grid

I could have hardcoded the colours, but felt that users should have a choice (not to mention, purple isn’t for everyone). So my problem was how to generate the intermediate colour.

With simple numbers, finding an average is simple: average = (number1 + number2) / 2, but this does not work for colours at all. If I take the average of the purple rgb(128,0,128) and white rgb(255,255,255) the result is not what you might expect:
The average of purple and white is .. blue?

So the average of purple and white would appear to be .. blue?

The problem is that each of the red, green and blue components need to be averaged as separate numbers, and then recombined. Doing this efficiently involves some horribly dense code. For instance, the average of the green components is

nGreenBit = bitand( 0xFF00, (bitand(0xFF00,m.nFromRGB) + bitand(0xFF00,m.nToRGB) )/2 )

I’m also going to make it worse. I’d like to specify any point on a continuum between the two colours. For example, I’d like to shift from purple 75% of the way to white, to get a very light purple. This isn’t difficult, it just makes the code truly terrifying:

* Shift RGB value toward another color
*
* RGBShift( Red, White, 50% ) = pink  sort of thing
* Proportion may range from 0 to 1.0, default is 0.5
*
function RGBShift( nFromRGB, nToRGB, nProportion )
*
* For each of the ARGB components
*   newcomponent =  from + (to-from)*scale 
*
	assert vartype(m.nFromRGB)+vartype(m.nToRGB)=='NN'
	assert (vartype(m.nProportion)=='L' and !m.nProportion) ;
			or (vartype(m.nProportion)=='N' and between(m.nProportion,0.0,1.0))
	if vartype(m.nProportion)='L'
		nProportion = 0.5	&& Default
	endif
	return ;
		bitand( 0xFF, bitand(0xFF,m.nFromRGB) ;
			+ m.nProportion * ( bitand(0xFF,m.nToRGB) - bitand(0xFF,m.nFromRGB) ) ) ;
		+bitand( 0xFF00, bitand(0xFF00,m.nFromRGB) ;
			+ m.nProportion * ( bitand(0xFF00,m.nToRGB) - bitand(0xFF00,m.nFromRGB) ) ) ;
		+bitand( 0xFF0000, bitand(0xFF0000,m.nFromRGB) ;
			+ m.nProportion * ( bitand(0xFF0000,m.nToRGB) - bitand(0xFF0000,m.nFromRGB) ) ) ;
		+bitand( 0xFF000000, bitand(0xFF000000,m.nFromRGB) ;
			+ m.nProportion * ( bitand(0xFF000000,m.nToRGB) - bitand(0xFF000000,m.nFromRGB) ) )
endfunc

However, now I can do gradients. Here’s an example:

* Draw gradient fill
#define ORANGE rgb(255,128,0)  
#define CYAN rgb(33,200,200)
activate screen
clear
LinearGradient( ORANGE , CYAN, 50, 0, 0, 300, 50 )

function LinearGradient( nFromRGB, nToRGB, nSteps, nX, nY, nWidth, nHeight )
	local nCurrentX, nCurrentY, nSliceWidth
	nSliceWidth = m.nWidth/ nSteps
	nCurrentX = m.nX
	nCurrentY = m.nY
	
	_screen.FillStyle = 0
	for ii = 0 to nSteps
		store CfcRGBShift( m.nFromRGB, m.nToRGB, m.ii /m.nSteps) ;
			to _screen.ForeColor, _screen.FillColor
		_screen.Box( m.nCurrentX, m.nCurrentY, m.nCurrentX + m.nSliceWidth, m.nCurrentY + m.nHeight )
		m.nCurrentX = m.nCurrentX + m.nSliceWidth
	endfor
endfunc

This program drew this:
Color gradient from orange to cyan, in VFP

(Incidentally, the LinearGradient function above has more than one bug in it. Since it isn’t my main point, I’m not going to fix them).

I am now in a position to use that function to do my original grid colouring. Assuming the cursor underneath the grid has a logical field marked which is .T. for selected rows, .F. otherwise; I get my intermediate colour like this:

with Thisform.Grid1
    local nSelectionColour 
    nSelectionColour = RgbShift( .HighlightBackColor, .BackColor )
    .Column1.DynamicBackColor 
        = "iif(marked,"+transform(m.nSelectedColor)+","+transform(.BackColor)+")"
endwith

I hope you find this useful.

No Comments

No comments yet.

RSS feed for comments on this post. TrackBack URI

Sorry, the comment form is closed at this time.