Click here to Skip to main content
Click here to Skip to main content

Adobe Gradient Picker Clone

, 22 Oct 2009 CPOL
Rate this:
Please Sign up or sign in to vote.
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:

/// <span class="code-SummaryComment"><summary>
</span>/// ColorBlend object
/// <span class="code-SummaryComment"></summary>
</span>public class Gradient:ICloneable
{
	/// <span class="code-SummaryComment"><summary>
</span>	/// class for holding a gradient point
	/// <span class="code-SummaryComment"></summary>
</span>	public abstract class Point : IComparable<double>
	/// <span class="code-SummaryComment"><summary>
</span>	/// class for holding points and updating
	/// controls connected to this colorblend
	/// <span class="code-SummaryComment"></summary>
</span>	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:

/// <span class="code-SummaryComment"><summary>
</span>/// creates color blend out of color point data
/// <span class="code-SummaryComment"></summary>
</span>/// <span class="code-SummaryComment"><returns></returns>
</span>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 >

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;

/// <span class="code-SummaryComment"><summary>
</span>/// constructor
/// <span class="code-SummaryComment"></summary>
</span>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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Julian Ott
Other VariSoft Industries
Germany Germany
my name is ramon van blech

Comments and Discussions

 
QuestionControlsEx missing? PinmemberMember 802119722-Sep-11 5:53 
AnswerRe: ControlsEx missing? PinmemberJulian Ott22-Sep-11 8:25 
GeneralRe: ControlsEx missing? Pinmembersallz0r22-Sep-11 15:24 
GeneralRe: ControlsEx missing? Pinmembertamilpuyal_2814-Oct-11 0:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141216.1 | Last Updated 22 Oct 2009
Article Copyright 2009 by Julian Ott
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid