In this article, you will see how you can bring the goodness of Processing on WPF.
![image image](/KB/Articles/637818/image_thumb_27_.png)
Contents
- Introduction
- Drawing/Drawer Space Paradigm
- ProcessingContext
- Mini Language
- In Depth, Matrix Power
- Conclusion
- History
Introduction
Why do visual artists and data visualization experts like processing so much when WPF can do better?
That’s the question I wanted to answer when I bought a book about Processing… and here is what I found.
- Processing is only about drawing, it makes things simpler for its audience by limiting its reach (even if theoretically you can do everything, it would be too cumbersome to create a whole application with it)
- Processing makes the difference, out of the box, between drawing space and drawer space.
I can’t do anything about the first point. C# is about general purpose development and it must stay like that.
However, for the second point, an idea sparks in my mind: What if we could draw a geometry in WPF with this drawing/drawer space paradigm?
Drawing/Drawer Space Paradigm
With the Drawing/Drawer space paradigm, the way to draw a line of 45 degree with length 10 on a sheet, is to rotate the sheet 45 degree, and then trace a straight line of length 10.
In the Drawing only space paradigm, you would calculate the coordinate of the 2nd point of the line with trigonometry rules: end.X = COS(45) * 10, end.Y = SIN(45) * 10.
Then trace the line to this point.
On a more complex drawing, what does it mean?
To draw that cross with the Drawing only space paradigm, you’ll need to specify all coordinates.
![image image](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
In plain xaml, it is something like that.
<Path x:Name="path" Data="M 0 0 L 5 0 L 5 -5 L 10 -5 L 10 0 L 15 0
L 15 5 L 10 5 L 10 10 L 5 10 L 5 5 L 0 5 Z"
Stretch="Uniform"
Fill="LightBlue"
Stroke="Black" ></Path>
On the other hand, with the Drawing/Drawer space paradigm, you only specify how to draw it, by moving your pencil and rotating the sheet.
![image image](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
Here, I specify only the moves of my hand and sheet to draw that.
I move 5 forward, then rotate the sheet of –90 degree, then move 5 forward, then rotate 90, then move 5 forward, then rotate 90, etc.
In XAML:
<Path local:GeometryProperties.Data="L 5 ROT -90 L 5 ROT 90 L 5 ROT 90
L 5 ROT -90 L 5 ROT 90 L 5 ROT 90
L 5 ROT -90 L 5 ROT 90 L 5 ROT 90
L 5 ROT -90 L 5 ROT 90 L 5 ROT 90 F Z"
Stretch="Uniform"
Fill="LightBlue"
Stroke="Black" ></Path>
When you do a rotation, you don’t rotate the figure, you are rotating the sheet on which you draw… what I call the drawer space, as opposed to the sheet space (or drawing space).
Let’s now take a look at a harder example:
With the drawing only space paradigm, to do the following geometry, you need to specify the coordinates of each ellipse. That means using your forgotten trigonometry.
![image image](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
Can you draw the same thing without trigonometry? Sure you can.
With the drawing/drawer space paradigm, you can say:
- Save basis, Move 10, Draw Ellipse, Load basis, Rotate 45
- Repeat 7 times
You can see I show you the drawer’s basis when I Draw Ellipse… as you can see, the Drawer’s basis is rotated.
![image image](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
In this new mini language that is efficiently coded like that: (Push and Pop save and load the current/latest basis):
<Path x:Name="path" local:GeometryProperties.Data="F E 20
ROT 45 PUSH M 100 E 20 POP
ROT 45 PUSH M 100 E 20 POP
ROT 45 PUSH M 100 E 20 POP
ROT 45 PUSH M 100 E 20 POP
ROT 45 PUSH M 100 E 20 POP
ROT 45 PUSH M 100 E 20 POP
ROT 45 PUSH M 100 E 20 POP
ROT 45 PUSH M 100 E 20 POP"
Stretch="Uniform"
Fill="LightBlue"
Stroke="Black" ></Path>
How easy is that?
ProcessingContext
I called the most important class, with the name of what seed this idea in my mind.
![image image](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
The important part is that each method on ProcessingContext
modifies the GeometryGroup
we pass in the constructor:
public ProcessingContext(GeometryGroup group)
{
_Path = new PathGeometry();
_Geometries = group;
_Geometries.Children.Add(_Path);
}
Origin
is the origin of the drawer’s space, and _CurrentTransform
, how to transform a point in drawing coordinate into the drawer coordinate. (I’ll talk about that in depth later.)
Mini-Language
Almost each operation is mapped to the mini language.
[GeometryCommand("S")]
public void Scale(double factor)
{
this.PushTransform(new ScaleTransform(factor, factor)
{
CenterX = _Origin.X,
CenterY = _Origin.Y
});
}
This means that “S 2.0” will be interpreted by Scale(2.0);
You can execute the mini language with ProcessingContext.Execute(string data)
.
Then, coding an attached property to use on Shape
was relatively easy.
public class GeometryProperties
{
public static string GetData(DependencyObject obj)
{
return (string)obj.GetValue(DataProperty);
}
public static void SetData(DependencyObject obj, string value)
{
obj.SetValue(DataProperty, value);
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.RegisterAttached("Data", typeof(string),
typeof(GeometryProperties), new PropertyMetadata(null, OnDataChanged));
static void OnDataChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
{
var path = source as Path;
GeometryGroup pathGeometry = source as GeometryGroup;
if(path != null)
{
pathGeometry = new GeometryGroup();
path.Data = pathGeometry;
}
if(pathGeometry != null)
{
var processing = new ProcessingContext(pathGeometry);
processing.Execute(args.NewValue as string);
}
}
}
So here is a summary of the mini language (you can see a sample in the source code), all parameters are relative to the drawer’s basis.
M x y? | Move space from origin |
L x y? stroked? | Trace a line, and move basis |
ROT deg | Rotate basis from origin |
S x y? | Scale basis from origin |
PUSH | Save current basis |
POP | Load last pushed basis |
BASIS size | Draw the current basis |
BEZ x y xControl yControl, stroked | Draw a bezier line and move basis to x y |
E xRadius yRadius? | Draw ellipse |
R xRadius yRadius? xcornRadius? ycornRadius? | Draw rectangle with rounded borders |
C | Figures will be closed |
NC | Figures will not be closed |
F | Figures will be filled |
NF | Figures will not be filled |
Z | New figure |
In Depth, Matrix Power
As you have seen, the ProcessingContext
class holds a _CurrentTransform
, this transform can transform any point back and forth between drawing space and drawer space. Each transform of basis will change _CurrentTransform
which is internally nothing more than a Transform
(which is inside hold a Matrix
).
[GeometryCommand("ROT")]
public void Rotate(double degree)
{
var rotation = new RotateTransform(degree)
{
CenterX = _Origin.X,
CenterY = _Origin.Y
};
this.PushTransform(rotation);
}
private void PushTransform(Transform transform)
{
Append(transform.Value);
_Origin = _CurrentTransform.Transform(new Point(0, 0));
}
private void Append(Matrix matrix)
{
var currentMatrix = _CurrentTransform.Value;
currentMatrix.Append(matrix);
_CurrentTransform = new MatrixTransform(currentMatrix);
}
With this _CurrentTransform
, I can safely transform every parameter from drawer’s space to drawing space, create a segment, and then translate the basis.
[GeometryCommand("L")]
public LineSegment Line(double x, double y, bool stroked)
{
var nextPoint = _CurrentTransform.Transform(new Point(x, y));
var line = new LineSegment(nextPoint, stroked);
this.CurrentFigure.Segments.Add(line);
Translate(x, y);
return line;
}
private void Translate(double x, double y)
{
var t = _CurrentTransform.Transform(new Point(x, y));
PushTransform(new TranslateTransform(t.X - _Origin.X, t.Y - _Origin.Y));
}
So what Push
, and Pop
are doing? Well, they are just saving the basis…
[GeometryCommand("PUSH")]
public void PushMatrix()
{
_Matrixes.Push(_CurrentTransform.Value);
}
[GeometryCommand("POP")]
public void PopMatrix()
{
_CurrentTransform = new MatrixTransform(_Matrixes.Pop());
_Origin = _CurrentTransform.Transform(new Point(0, 0));
}
That’s all I have to say about the implementation… I only convert coordinates from one system to another, and default transformations centered on the drawer’s origin…
One more example for you:
[GeometryCommand("BEZ")]
public BezierSegment Bezier(int x, int y, int controlX, int controlY, bool stroked = true)
{
var nextPoint = _CurrentTransform.Transform(new Point(x, y));
var controlPoint = _CurrentTransform.Transform(new Point(controlX, controlY));
var bezier = new BezierSegment(_Origin, controlPoint, nextPoint, stroked);
this.CurrentFigure.Segments.Add(bezier);
Translate(x, y);
return bezier;
}
The code is really lean, using the right abstraction for the task makes things so much simple.
Conclusion
Processing is sure a great way to create powerful visuals… but its only power, for a developer that knows a general purpose language, is its paradigm for drawing stuff.
I understood how Processing worked internally when I read an article about cameras in OpenGL… (if the link is dead, check Google cache) this is when I got the “Ahah” moment of understanding a matrix as a basis, then everything popped in my mind: aaah processing ? that’s how they do it ! (and the second “Ahah” moment : aah that’s why matrices are cool!)
OpenGL and Processing seems far enough but the dot connected, and I could bring the goodness of Processing on WPF.
History
- 15th August, 2013: Initial version