Click here to Skip to main content
15,868,093 members
Articles / Multimedia / GDI+
Article

Adobe Gradient Picker Clone

Rate me:
Please Sign up or sign in to vote.
4.97/5 (50 votes)
22 Oct 2009CPOL4 min read 99.6K   3.5K   95   40
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:

C#
/// <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:

C#
/// <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:

C#
//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:

C#
//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:

C#
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:

C#
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:

C#
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)


Written By
Other VariSoft Industries
Germany Germany
my name is ramon van blech

Comments and Discussions

 
QuestionControlsEx Pin
scottlafoy12-Feb-18 6:41
scottlafoy12-Feb-18 6:41 
QuestionLicense Pin
Nightstar0071-Oct-13 6:21
Nightstar0071-Oct-13 6:21 
AnswerRe: License Pin
Julian Ott2-Oct-13 11:30
Julian Ott2-Oct-13 11:30 
i don't know which license i selected anymore,
it doesn't matter to me... this code
is free to the community in any way, to copy,
to learn from, to get inspired.
GeneralRe: License Pin
Nightstar0073-Oct-13 5:05
Nightstar0073-Oct-13 5:05 
QuestionGreat job!Do you have the same project with C++? Pin
myway2315-Feb-13 1:32
myway2315-Feb-13 1:32 
AnswerRe: Great job!Do you have the same project with C++? Pin
Julian Ott2-Oct-13 11:33
Julian Ott2-Oct-13 11:33 
GeneralMy vote of 5 Pin
kiran dangar4-Oct-11 19:36
kiran dangar4-Oct-11 19:36 
QuestionControlsEx missing? Pin
sallz0r22-Sep-11 4:53
sallz0r22-Sep-11 4:53 
AnswerRe: ControlsEx missing? Pin
Julian Ott22-Sep-11 7:25
Julian Ott22-Sep-11 7:25 
GeneralRe: ControlsEx missing? Pin
sallz0r22-Sep-11 14:24
sallz0r22-Sep-11 14:24 
GeneralRe: ControlsEx missing? Pin
tamilpuyal_2813-Oct-11 23:31
tamilpuyal_2813-Oct-11 23:31 
GeneralRe: ControlsEx missing? Pin
Misiu15-Dec-17 4:42
Misiu15-Dec-17 4:42 
GeneralMy vote of 5 Pin
enishima21-Sep-10 21:39
enishima21-Sep-10 21:39 
QuestionCan i create this in jquery or javascript ?? Pin
enishima20-Sep-10 3:01
enishima20-Sep-10 3:01 
AnswerRe: Can i create this in jquery or javascript ?? Pin
Julian Ott20-Sep-10 5:34
Julian Ott20-Sep-10 5:34 
GeneralRe: Can i create this in jquery or javascript ?? Pin
enishima21-Sep-10 4:34
enishima21-Sep-10 4:34 
GeneralRe: Can i create this in jquery or javascript ?? Pin
Julian Ott22-Sep-10 4:39
Julian Ott22-Sep-10 4:39 
GeneralRe: Can i create this in jquery or javascript ?? Pin
enishima22-Sep-10 5:51
enishima22-Sep-10 5:51 
GeneralMy vote of 5 Pin
Pamodh3-Sep-10 6:53
Pamodh3-Sep-10 6:53 
GeneralRe: My vote of 5 Pin
kiran dangar4-Oct-11 19:36
kiran dangar4-Oct-11 19:36 
GeneralExcellent !! Pin
Xmen Real 5-Jul-10 2:36
professional Xmen Real 5-Jul-10 2:36 
GeneralTu eres groso Pin
nixsus19-May-10 14:28
nixsus19-May-10 14:28 
GeneralExcellent control Pin
Jordy "Kaiwa" Ruiter7-May-10 2:32
Jordy "Kaiwa" Ruiter7-May-10 2:32 
GeneralAmaizing work. Pin
Abhishek Sur4-Nov-09 9:11
professionalAbhishek Sur4-Nov-09 9:11 
GeneralSimply Awesome! Pin
Greg Cadmes29-Oct-09 7:38
Greg Cadmes29-Oct-09 7:38 

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

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