Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Adobe Gradient Picker Clone

0.00/5 (No votes)
22 Oct 2009 1  
An article about implementing a gradient manager
Gradient Editor

Introduction

Whenever designing a graphic editor, a lot of effort is about usability. The way how controls react to the user heavily influences how artists work with your application. And each time you change the workflow, the users have to re-adapt the functionality.

So why always start from scratch when designing graphical controls? Over the years, Adobe(r) Photoshop has become some kind of industry standard in how graphic editors work. And this article brings the gradient editing ability to your standard .NET LinearGradient- and PathGradient-Brushes.

Background

Basically there are two usual ways of modelling the gradient stops. The first way which is also how Adobe Illustrator's gradients are handled, is the following:

illustrator gradient

Here, each gradient stop has a position (0-100%) and an opacity-value associated
to it. This way, Color and Alpha are specifically to each gradient stop.

The other way is treating gradients like Adobe Photoshop:

photoshop gradient

Here, the Color- and Alpha- gradients are separated, making it easy to apply various different patterns that are more difficult to create in Illustrator.

So, I decided to implement my gradient manager the second way. But the problem is that .NET GDI+ wrapper cannot handle separated color and alpha gradients. Only Colors with alpha channels can be put on a ColorBlend-object, making only the first implementation possible...

I had to find a way of joining the two-per channel gradients together, and it resulted in this algorithm.

Gradient Conversion Algorithm

At first, there is a new data structure for storing gradients, which can handle the different types of stops:

/// <summary>
/// ColorBlend object
/// </summary>
public class Gradient:ICloneable
{
	/// <summary>
	/// class for holding a gradient point
	/// </summary>
	public abstract class Point : IComparable<double>
	/// <summary>
	/// class for holding points and updating
	/// controls connected to this colorblend
	/// </summary>
	public class PointList<T> : CollectionBase<Gradient, T> where T : Point

	public static implicit operator ColorBlend(Gradient blend)
}

public class AlphaPoint : Gradient.Point, IComparable<AlphaPoint>,ICloneable

public class ColorPoint : Gradient.Point, IComparable<ColorPoint>,ICloneable

This class can be used just like any ColorBlend object, cause it gets implicitly converted to a ColorBlend by a call to the conversion operator, which calls the gradient conversion algorithm and caches the results, so it can be used more efficiently.

Now, the first step of the algorithm is to sort the lists of color and alpha points, so efficient searching and interpolation by a modified version of binsearch algorithm can be performed:

/// <summary>
/// creates color blend out of color point data
/// </summary>
/// <returns></returns>
private ColorBlend CreateColorBlend()
{
	//sort all points
	ColorPoint[] colpoints = _colors.SortedArray();
	AlphaPoint[] alphapoints = _alphas.SortedArray();

	//...
}

//generic interval searching in O(log(n))
private void SearchPos<T>(T[] list, T pos, out int a, out int b) where T : IComparable<T>
{
	int start = a = 0, end = b = list.Length - 1;
	while (end >= start)
	{
		int mid = start + (end - start) / 2;
		switch (list[mid].CompareTo(pos))
		{
			case 0://found point
				a = b = mid;
				return;
			case 1: end = mid - 1; b = mid; break;//search left
			default: start = mid + 1; a = mid; break;//search right
		}
	}
	//found interval
}

Now, for each Alpha- and Color point, add them to the list of output values, interpolating on both the Color channels and the alpha channels. If the point being processed was an Alpha point, looking up the Position on the list of Alpha points will result in the original position of the point, whereas looking up the Position on the Color Array will likely result in an interval, except there is a color point at the exact same position.
Whenever the searching runs into one single point instead of an interval, the interpolation will be skipped:

//adds a new position to the list
private void AddPosition(ColorPoint[] colpoints, AlphaPoint[] alphapoints,
	SortedList<float, Color> positions, double pos)
{
	if (positions.ContainsKey((float)pos))
		return;
	int alpha_a, alpha_b;
	int color_a, color_b;
	//evaluate positions
	SearchPos<AlphaPoint>(alphapoints, 
		new AlphaPoint(0, pos), out alpha_a, out alpha_b);
	SearchPos<ColorPoint>(colpoints, 
		new ColorPoint(Color.Black, pos), out color_a, out color_b);
	//interpolate
	positions.Add((float)pos, Color.FromArgb(
		Interpolate(alphapoints, alpha_a, alpha_b, pos),
		Interpolate(colpoints, color_a, color_b, pos)));
}
// interpolates alpha list
private byte Interpolate(AlphaPoint[] list, int a, int b, double pos)
{
	if (b < a)
		return 0;
	if (a == b) return (byte)(list[a].Alpha * 255.0);
	//compute involving focus position
	return (byte)XYZ.ClipValue(
		(list[a].Alpha + FocusToBalance(list[a].Position, 
		list[b].Position, list[b].Focus, pos)
		* (list[b].Alpha - list[a].Alpha)) * 255.0, 0.0, 255.0);
}

Notice the call to FocusToBalance, which processes gradient stops that have a modified center focus value, like this:

modified focus point

That is basically a modification of the interpolation function, which is linear here:

interpolation curve

Now, the concluding step is, to add a first and a last point, since GDI+ ColorBlends require the first stop at position 0% and the last at 100%. Furthermore, if there aren't any points on the gradient, generate some default values:

//add first/last point
if (positions.Count < 1 || !positions.ContainsKey(0f))
	positions.Add(0f, positions.Count < 1 ?
		Color.Transparent : positions.Values[0]);
if (positions.Count < 2 || !positions.ContainsKey(1f))
	positions.Add(1f, positions.Count < 2 ?
		Color.Transparent : positions.Values[positions.Count - 1]);

The final gradient is now stored in a SortedList<float,Color> and ready to be used in a ColorBlend object.

GradientEdit Controls

As the main purpose of this class is not to allow simple gradient creation in code, which is in fact much simpler by using standard colorblend objects, but to create a user control for this purpose, there are several controls which can be used on any user interface:

gradientedit

GradientEdit is the editing control for one single gradient object, and has events for selectionchange.
Stops can be created by clicking into a free area, and deleted by dragging them outside the control area.

gradienteditpanel

GradientEditPanel wraps up all controls needed for editing single stops into one user control.
Positions and Opacity can be edited by SpinCombo controls, stops can be deleted, and
Color can be selected either off screen or by displaying the color dialog described in the article: Adobe Color Picker Clone.

gradientcollectioneditor

Finally, a GradientEditPanel along with a collection editor is wrapped up in gradientcollectioneditor, which is to be used as popup-dialog.
It supports loading and saving presets from file using the default XML exporter.
Future versions will be able to read Adobe Photoshop/Illustrator .grd collections.

Using the Code

You can use the gradients in two different ways. First, you create any gradient by code, and use it like a colorblend:

private Gradient grd;
private GradientCollection coll;

/// <summary>
/// constructor
/// </summary>
public MyPictureBox(){
	//
	coll = new GradientCollection();
	//
	grd = new Gradient();
	grd.Alphas.Add(new AlphaPoint(128, 0.0));
	grd.Alphas.Add(new AlphaPoint(255, 1.0));
	grd.Colors.Add(new ColorPoint(Color.Red, 0.0));
	grd.Colors.Add(new ColorPoint(Color.Blue, 1.0));
}

protected override void OnPaint(PaintEventArgs e)
{
	using (LinearGradientBrush lnbrs = new LinearGradientBrush(
		new Point(0, 0), new Point(Math.Max(1, this.Width), 0),
		Color.Transparent, Color.Black))
	{
		//implicit conversion here
		lnbrs.InterpolationColors = grd;
		e.Graphics.FillRectangle(lnbrs, this.ClientRectangle);
	}
}

Second, you use the GradientCollectionEditor to edit one or many Gradients:

protected override void OnClick(EventArgs e)
{
	using (GradientCollectionEditor edit = new GradientCollectionEditor())
	{
		//normally, you would use edit.Gradients.Load(...)
		foreach (Gradient g in coll)
			edit.Gradients.Add(g);
		edit.SelectedGradient = grd;
		//
		if (edit.ShowDialog() == DialogResult.OK)
		{
			//normally, you would use edit.Gradients.Save(...)
			coll.Clear();
			foreach (Gradient g in edit.Gradients)
				coll.Add(g);
			grd = edit.SelectedGradient;
			//
			this.Refresh();
		}
	}
}

You can even load and save gradients in XML format. Take a look at the included XMLFormat.xsd Schema which gives the structure of .grdx files. Loading and Saving then works like that:

try
{
	//save
	coll.Save("%TEMP%/default.grdx");

	//load
	coll.Clear();
	coll.Load("%TEMP%/default.grdx");
}
catch (Exception ex)
{
	MessageBox.Show(ex.StackTrace);
}

Note that the XMLFormat Importer/Exporter itself doesn't validate the document, it just throws an exception if a reading error occurs.
For future releases, there will also be a svg formatter.

Conclusion

There are still some issues to be fixed, for example implementing a proper gammacorrection, which isn't enabled right now. I think this is still a very useful component, which you are free to use on your projects. There are some more tools in the DrawingEx namespace, such as a color button, a 32 bpp true-color icon encoder with quantizer, discrete cosine transformer, and some 3D helper classes.

Enjoy and let me know about bugs...

History

  • 20th October, 2009: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here