Click here to Skip to main content
14,972,994 members
Articles / Desktop Programming / WPF
Article
Posted 15 Aug 2013

Tagged as

Stats

58.7K views
1.4K downloads
71 bookmarked

WPF Geometries, the Processing Way

Rate me:
Please Sign up or sign in to vote.
5.00/5 (32 votes)
15 Aug 2013CPOL5 min read
Create drawings easily with the Drawing/Drawer space paradigm
In this article, you will see how you can bring the goodness of Processing on WPF.

image

Contents

  1. Introduction
  2. Drawing/Drawer Space Paradigm
  3. ProcessingContext
  4. Mini Language
  5. In Depth, Matrix Power
  6. Conclusion
  7. 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

In plain xaml, it is something like that.

XAML
<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

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:

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

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

In this new mini language that is efficiently coded like that: (Push and Pop save and load the current/latest basis):

XML
<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

The important part is that each method on ProcessingContext modifies the GeometryGroup we pass in the constructor:

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

C#
[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.

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

    // Using a DependencyProperty as the backing store for Data. 
    // This enables animation, styling, binding, etc...
    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).

C#
[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.

C#
[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…

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

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Nicolas Dorier
Software Developer Freelance
France France
I am currently the CTO of Metaco, we are leveraging the Bitcoin Blockchain for delivering financial services.

I also developed a tool to make IaaS on Azure more easy to use IaaS Management Studio.

If you want to contact me, go this way Smile | :)

Comments and Discussions

 
QuestionMessage Closed Pin
30-Jan-19 20:19
MemberSanjoyNath@GeometrifyingTrigonometry30-Jan-19 20:19 
GeneralMy vote of 5 Pin
D V L7-Apr-16 21:14
professionalD V L7-Apr-16 21:14 
GeneralMy vote of 5 Pin
fredatcodeproject20-Sep-13 4:32
professionalfredatcodeproject20-Sep-13 4:32 
GeneralMy vote of 5 Pin
Athari10-Sep-13 22:01
MemberAthari10-Sep-13 22:01 
QuestionVery nice Pin
Sacha Barber25-Aug-13 10:45
mvaSacha Barber25-Aug-13 10:45 
AnswerRe: Very nice Pin
Nicolas Dorier25-Aug-13 22:25
professionalNicolas Dorier25-Aug-13 22:25 
GeneralMy vote of 5 Pin
Brian Holsen21-Aug-13 22:36
MemberBrian Holsen21-Aug-13 22:36 
GeneralRe: My vote of 5 Pin
Nicolas Dorier22-Aug-13 0:05
professionalNicolas Dorier22-Aug-13 0:05 
GeneralRe: My vote of 5 Pin
Brian Holsen25-Aug-13 14:42
MemberBrian Holsen25-Aug-13 14:42 
GeneralRe: My vote of 5 Pin
Nicolas Dorier25-Aug-13 22:49
professionalNicolas Dorier25-Aug-13 22:49 
GeneralMy vote of 5 Pin
DrABELL16-Aug-13 11:55
professionalDrABELL16-Aug-13 11:55 
GeneralRe: My vote of 5 Pin
Nicolas Dorier16-Aug-13 12:50
professionalNicolas Dorier16-Aug-13 12:50 
GeneralRe: My vote of 5 Pin
DrABELL16-Aug-13 12:59
professionalDrABELL16-Aug-13 12:59 
GeneralMy vote of 5 Pin
dyma15-Aug-13 19:33
Memberdyma15-Aug-13 19:33 
GeneralRe: My vote of 5 Pin
Nicolas Dorier16-Aug-13 12:41
professionalNicolas Dorier16-Aug-13 12:41 
GeneralMy vote of 5 Pin
Paulo Zemek15-Aug-13 13:51
mvaPaulo Zemek15-Aug-13 13:51 
GeneralRe: My vote of 5 Pin
Nicolas Dorier16-Aug-13 12:40
professionalNicolas Dorier16-Aug-13 12:40 

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.