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:
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:
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:
public class Gradient:ICloneable
{
public abstract class Point : IComparable<double>
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:
private ColorBlend CreateColorBlend()
{
ColorPoint[] colpoints = _colors.SortedArray();
AlphaPoint[] alphapoints = _alphas.SortedArray();
}
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: a = b = mid;
return;
case 1: end = mid - 1; b = mid; break; default: start = mid + 1; a = mid; break; }
}
}
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:
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;
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);
positions.Add((float)pos, Color.FromArgb(
Interpolate(alphapoints, alpha_a, alpha_b, pos),
Interpolate(colpoints, color_a, color_b, pos)));
}
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);
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:
That is basically a modification of the interpolation function, which is linear here:
Now, the concluding step is, to add a first and a last point, since GDI+ ColorBlend
s 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:
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
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
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.
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;
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))
{
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())
{
foreach (Gradient g in coll)
edit.Gradients.Add(g);
edit.SelectedGradient = grd;
if (edit.ShowDialog() == DialogResult.OK)
{
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
{
coll.Save("%TEMP%/default.grdx");
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