VB.Net: Graphical Mathematical Transformations
Overview
Mathematical transformations are usually drawn on hard copy graph paper. This application demonstrates simple transformations. There are four types of transformation shown here, which are Enlargement, which can be positive or negative, Reflection, with a variable line of symmetry, Rotation with a variable centre of rotation and positive and negative angles of rotation, and the fourth transformation is Translation. These are the type of transformations that you're often asked to draw in maths exams...
The Form
This is a basic Windows Form containing just a MenuStrip, and four extended Panels, one for each type of transformation. The Form hosts the event handlers for the MouseMove and MouseClick events for all four of the Panels. It also hosts the event handlers for the ValueChanged and KeyPress events for all of the NumericUpDown controls used throughout the application.
The handlers are wired up with AddHandler statements:
AddHandler Enlargement1.MouseMove, AddressOf panels_MouseMove
AddHandler Enlargement1.MouseClick, AddressOf panels_MouseClick
AddHandler Reflection1.MouseMove, AddressOf panels_MouseMove
AddHandler Reflection1.MouseClick, AddressOf panels_MouseClick
AddHandler Rotation1.MouseMove, AddressOf panels_MouseMove
AddHandler Rotation1.MouseClick, AddressOf panels_MouseClick
AddHandler Translation1.MouseMove, AddressOf panels_MouseMove
AddHandler Translation1.MouseClick, AddressOf panels_MouseClick
These are the handlers panels_MouseMove and panels_MouseClick. These two handlers record the user mouse input used in drawing a base shape through clicking on the grid vertices in the graph image...
Private Sub panels_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs)
Dim index As Integer = Array.IndexOf(panels, sender)
Dim r As Integer = -1
Dim c As Integer = -1
For x As Integer = 0 To 16
If e.Y >= rowY(x) - 3 AndAlso e.Y <= rowY(x) + 3 Then
r = rowY(x)
Exit For
End If
Next
For x As Integer = 0 To 16
If e.X >= columnX(x) - 3 AndAlso e.X <= columnX(x) + 3 Then
c = columnX(x)
Exit For
End If
Next
If c > -1 AndAlso r > -1 Then
sharedVariables.allHighlights(index) = New Point(c, r)
Else
sharedVariables.allHighlights(index) = Nothing
End If
DirectCast(sender, Panel).Refresh()
End Sub
Private Sub panels_MouseClick(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs)
Dim index As Integer = Array.IndexOf(panels, sender)
If sharedVariables.allHighlights(index) <> Nothing Then
If sharedVariables.allVertices(index).Contains(sharedVariables.allHighlights(index)) Then
sharedVariables.allVertices(index).Remove(sharedVariables.allHighlights(index))
Else
sharedVariables.allVertices(index).Add(sharedVariables.allHighlights(index))
End If
DirectCast(sender, Panel).Refresh()
End If
End Sub
The other events handled in the Form code are the ValueChanged and the KeyPress events for all of the NumericUpDown controls used in the application...
AddHandler Enlargement1.nudXPosition.ValueChanged, AddressOf numericupdowns_ValueChanged
AddHandler Enlargement1.nudYPosition.ValueChanged, AddressOf numericupdowns_ValueChanged
AddHandler Enlargement1.nudScale.ValueChanged, AddressOf numericupdowns_ValueChanged
AddHandler Enlargement1.nudXPosition.KeyPress, AddressOf numericupdowns_KeyPress
AddHandler Enlargement1.nudYPosition.KeyPress, AddressOf numericupdowns_KeyPress
AddHandler Enlargement1.nudScale.KeyPress, AddressOf numericupdowns_KeyPress
AddHandler Reflection1.nudIntersection.ValueChanged, AddressOf numericupdowns_ValueChanged
AddHandler Reflection1.nudIntersection.KeyPress, AddressOf numericupdowns_KeyPress
AddHandler Rotation1.nudXCentre.ValueChanged, AddressOf numericupdowns_ValueChanged
AddHandler Rotation1.nudYCentre.ValueChanged, AddressOf numericupdowns_ValueChanged
AddHandler Rotation1.nudAngle.ValueChanged, AddressOf numericupdowns_ValueChanged
AddHandler Rotation1.nudXCentre.KeyPress, AddressOf numericupdowns_KeyPress
AddHandler Rotation1.nudYCentre.KeyPress, AddressOf numericupdowns_KeyPress
AddHandler Rotation1.nudAngle.KeyPress, AddressOf numericupdowns_KeyPress
AddHandler Translation1.nudXPosition.ValueChanged, AddressOf numericupdowns_ValueChanged
AddHandler Translation1.nudYPosition.ValueChanged, AddressOf numericupdowns_ValueChanged
AddHandler Translation1.nudXPosition.KeyPress, AddressOf numericupdowns_KeyPress
AddHandler Translation1.nudYPosition.KeyPress, AddressOf numericupdowns_KeyPress
These are the event handlers for those controls...
The calculations for the transformed shapes are all done in the Panel's paint event. Changing any input just causes the Panel at the top of the z-order to repaint. All of the controls used are primarily value changers for the graphical output.
The KeyPress event is used to restrict user input to just mouse input.
Private Sub numericupdowns_ValueChanged(ByVal sender As System.Object, ByVal e As System.EventArgs)
Enlargement1.Refresh()
Reflection1.Refresh()
Rotation1.Refresh()
Translation1.Refresh()
End Sub
Private Sub numericupdowns_KeyPress(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyPressEventArgs)
e.Handled = True
End Sub
The Panels
Three of the Panels have a cross indicator which shows the centre of the transformation, or the offset applied in the transformation. The remaining Panel has a line of symmetry. These serve as a visual representation of the variables used in the transformation
Public Shared Sub drawCross(ByVal g As Graphics, ByVal l As Integer, ByVal t As Integer)
Dim p As New Pen(Color.LimeGreen, 2)
g.DrawLine(p, l, t - 10, l, t + 10)
g.DrawLine(p, l - 10, t, l + 10, t)
End Sub
Public Shared Sub drawMirrorLine(ByVal g As Graphics, ByVal l As Integer, ByVal t As Integer, ByVal v As Integer, ByVal x As Integer)
Dim p As New Pen(Color.LimeGreen, 2)
p.DashStyle = Drawing2D.DashStyle.Dash
Dim diagonals() As Integer = {5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5}
Dim x1 As Integer
Dim y1 As Integer
Dim x2 As Integer
Dim y2 As Integer
Select Case x
Case 0 'Vertical
g.drawLine(p, l, 25, l, 425)
Case 1 'Horizontal
g.drawLine(p, 25, t, 425, t)
Case 2 'Diagonal - SW to NE
If v <= 2 Then
x1 = 25 + diagonals(v + 3) * 25
y1 = 425
x2 = 425
y2 = 25 + diagonals(v + 3) * 25
Else
x1 = 25
y1 = 425 - diagonals(v + 3) * 25
x2 = 425 - diagonals(v + 3) * 25
y2 = 25
End If
g.drawLine(p, x1, y1, x2, y2)
Case 3 'Diagonal - NW to SE
If v <= 2 Then
x1 = 25
y1 = 25 + diagonals(v + 3) * 25
x2 = 25 + (16 - diagonals(v + 3)) * 25
y2 = 425
Else
x1 = 25 + diagonals(v + 3) * 25
y1 = 25
x2 = 425
y2 = 25 + (16 - diagonals(v + 3)) * 25
End If
g.drawLine(p, x1, y1, x2, y2)
End Select
End Sub
Enlargement Panel
The first of the extended Panels is the Enlargement Panel...
As you can see, mouse hovering over a grid vertex highlights that vertex.
The code for the Enlargement Panel is fairly simple.
The overridden Paint method is the coordinating procedure for all of the various drawing methods used.
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.HighQuality
methods.drawGrid(e.Graphics, Me.Font)
Dim l As Integer = pointMethods.c2X(CInt(nudXPosition.Value))
Dim t As Integer = pointMethods.r2Y(CInt(nudYPosition.Value))
Dim s As Decimal = nudScale.Value
methods.drawShape(e.Graphics, sharedVariables.allVertices(0))
methods.drawEnlargedShape(e.Graphics, l, t, s, lblPolarity.Text, sharedVariables.allVertices(0))
methods.drawBorders(e.Graphics, Me.BackColor)
methods.drawCross(e.Graphics, l, t)
methods.drawHighlight(e.Graphics, sharedVariables.allHighlights(0))
MyBase.OnPaint(e)
End Sub
There are two other handlers contained in the Enlargement Panel code...
One is for a context menu providing a simple way to clear a drawing. The other is the Click handler for the positive/negative Label. Clicking the Label changes the text of the Label, and causes a repaint.
Private Sub ClearToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ClearToolStripMenuItem.Click
sharedVariables.allVertices(0).Clear()
Me.Refresh()
End Sub
Private Sub lblPolarity_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lblPolarity.Click
If lblPolarity.Text = "+" Then lblPolarity.Text = "-" Else lblPolarity.Text = "+"
Me.Refresh()
End Sub
Reflection Panel
This is how the Reflection Panel appears at run time...
The overridden Paint event is again kept very simple, calling a series of helper methods, each performing a single task...
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
methods.drawGrid(e.Graphics, Me.Font)
Dim l As Integer = 25 + ((CInt(nudIntersection.Value) + 8) * 25)
Dim t As Integer = 25 + (Math.Abs(CInt(nudIntersection.Value) - 10) * 25)
Dim v As Integer = CInt(nudIntersection.Value)
methods.drawMirrorLine(e.Graphics, l, t, v, cboLine.SelectedIndex)
methods.drawShape(e.Graphics, sharedVariables.allVertices(1))
methods.drawMirrorShape(e.Graphics, sharedVariables.allVertices(1), v, cboLine.SelectedIndex)
methods.drawBorders(e.Graphics, Me.BackColor)
methods.drawHighlight(e.Graphics, sharedVariables.allHighlights(1))
MyBase.OnPaint(e)
End Sub
There are two other events handled, which are local to the Reflection Panel.
The first is a context menu click handler. All of the Panels have an encapsulated context menu.
The second handler is for the ComboBox used in choosing where to place the line of symmetry.
Private Sub ClearToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ClearToolStripMenuItem.Click
sharedVariables.allVertices(1).Clear()
Me.Refresh()
End Sub
Private Sub cboLine_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cboLine.SelectedIndexChanged
nudIntersection.Value = 0
nudIntersection.Minimum = -3
Me.Refresh()
End Sub
Rotation Panel
This is how the Rotation Panel appears at run time...
Continuing the application-wide theme, the Rotation Panel Paint event is neat and kept simple...
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
methods.drawGrid(e.Graphics, Me.Font)
Dim l As Integer = 25 + ((CInt(nudXCentre.Value) + 8) * 25)
Dim t As Integer = 25 + (Math.Abs(CInt(nudYCentre.Value) - 10) * 25)
Dim v As Integer = CInt(nudAngle.Value)
methods.drawShape(e.Graphics, sharedVariables.allVertices(2))
methods.drawRotatedShape(e.Graphics, sharedVariables.allVertices(2), v, l, t, lblPolarity.Text)
methods.drawBorders(e.Graphics, Me.BackColor)
methods.drawCross(e.Graphics, l, t)
methods.drawHighlight(e.Graphics, sharedVariables.allHighlights(2))
MyBase.OnPaint(e)
End Sub
This Panel also has a context menu and a polarity Label...
Private Sub lblPolarity_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lblPolarity.Click
If lblPolarity.Text = "-" Then lblPolarity.Text = "+" Else lblPolarity.Text = "-"
Me.Refresh()
End Sub
Private Sub ClearToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ClearToolStripMenuItem.Click
sharedVariables.allVertices(2).Clear()
Me.Refresh()
End Sub
Translation Panel
This is how the fourth Panel appears at runtime...
Again the code is concise and easy to read...
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
methods.drawGrid(e.Graphics, Me.Font)
Dim l As Integer = 25 + ((CInt(nudXPosition.Value) + 8) * 25)
Dim t As Integer = 25 + (Math.Abs(CInt(nudYPosition.Value) - 10) * 25)
methods.drawShape(e.Graphics, sharedVariables.allVertices(3))
methods.drawTranslatedShape(e.Graphics, sharedVariables.allVertices(3), l, t)
methods.drawBorders(e.Graphics, Me.BackColor)
methods.drawCross(e.Graphics, l, t)
methods.drawHighlight(e.Graphics, sharedVariables.allHighlights(3))
MyBase.OnPaint(e)
End Sub
The Translation Panel, as all the other Panels, has a context menu
Private Sub ClearToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ClearToolStripMenuItem.Click
sharedVariables.allVertices(3).Clear()
Me.Refresh()
End Sub
The Drawing Methods
drawGrid is common to all of the Panels. It draws the 'Graph' grid and labels.
Public Shared Sub drawGrid(ByVal g As Graphics, ByVal f As Font)
g.DrawRectangle(Pens.LightGray, New Rectangle(25, 25, 400, 400))
For c As Integer = 50 To 400 Step 25
g.DrawLine(Pens.LightGray, c, 25, c, 425)
Next
For r As Integer = 50 To 400 Step 25
g.DrawLine(Pens.LightGray, 25, r, 425, r)
Next
g.DrawLine(Pens.Black, 225, 25, 225, 425)
g.DrawLine(Pens.Black, 25, 275, 425, 275)
Dim lblWidth As Integer
For x As Integer = -8 To 8
lblWidth = CInt(g.MeasureString(x.ToString, New Font(f.FontFamily, 7)).Width)
g.DrawString(x.ToString, New Font(f.FontFamily, 7), Brushes.Black, 25 + ((x + 8) * 25) - lblWidth, 276)
Next
For y As Integer = -6 To 10
lblWidth = CInt(g.MeasureString(y.ToString, New Font(f.FontFamily, 7)).Width)
g.DrawString(y.ToString, New Font(f.FontFamily, 7), Brushes.Black, 225 - lblWidth, 426 - (y + 6) * 25)
Next
End Sub
drawShape is common to all of the Panels. The main difference in the four Panels' Paint event is one unique drawing method for each Panel...
Public Shared Sub drawShape(ByVal g As Graphics, ByVal vertices As List(Of Point))
If vertices.Count > 2 Then
g.DrawPolygon(New Pen(Color.Red, 2), vertices.ToArray)
Else
For x As Integer = 0 To vertices.Count - 1
g.FillEllipse(Brushes.Red, New Rectangle(vertices(x).X - 3, vertices(x).Y - 3, 6, 6))
Next
End If
End Sub
These are the four unique drawing methods
drawEnlargedShape
This contains all of the calculations used in rendering an enlarged shape...
Public Shared Sub drawEnlargedShape(ByVal g As Graphics, ByVal l As Integer, ByVal t As Integer, ByVal s As Decimal, ByVal polarity As String, ByVal vertices As List(Of Point))
If vertices.Count >= 3 Then
Dim pen As New Pen(Color.LimeGreen)
pen.DashStyle = Drawing2D.DashStyle.Dash
Dim newPoints As New List(Of Point)()
For x As Integer = 0 To vertices.Count - 1
Dim distance As Single = measurement.lineLength(New Point(l, t), vertices(x))
Dim angle As Double = (If(polarity.Equals("+"), 360, 180)) - angles.FindAngle(New Point(l, t), vertices(x))
Dim scaleDistance As Decimal = CDec(distance * s)
'Turn degrees to radians (because of the sin and cos operations)
Dim angleRadians As Double = angle * (Math.PI / 180)
'Calculate X1 And Y1
Dim pointX1 As Integer = CInt(l + Math.Cos(angleRadians) * scaleDistance)
Dim pointY1 As Integer = CInt(t - Math.Sin(angleRadians) * scaleDistance)
newPoints.Add(New Point(pointX1, pointY1))
Next
If polarity.Equals("+") Then
For x As Integer = 0 To vertices.Count - 1
g.DrawLine(pen, l, t, If(s > 1D, newPoints(x).X, vertices(x).X), If(s > 1D, newPoints(x).Y, vertices(x).Y))
Next
Else
For x As Integer = 0 To vertices.Count - 1
g.DrawLine(pen, l, t, vertices(x).X, vertices(x).Y)
g.DrawLine(pen, l, t, newPoints(x).X, newPoints(x).Y)
Next
End If
Dim pen2 As New Pen(Color.DodgerBlue)
pen2.DashStyle = Drawing2D.DashStyle.Dash
g.DrawPolygon(pen2, newPoints.ToArray)
End If
End Sub
drawMirrorShape
This again contains all of the calculations needed for the specified transformation...
Public Shared Sub drawMirrorShape(ByVal g As Graphics, ByVal vertices As List(Of Point), ByVal v As Integer, ByVal x As Integer)
If vertices.Count < 3 Then
Return
End If
Dim pen As New Pen(Color.DodgerBlue, 2)
pen.DashStyle = Drawing2D.DashStyle.Dash
Dim gridPoints As New List(Of Point)
For Each p As Point In vertices
gridPoints.Add(New Point(pointMethods.X2c(p.X), pointMethods.Y2r(p.Y)))
Next p
Dim reflectionPoints As New List(Of Point)
Select Case x
Case 0 'Vertical
For Each p As Point In gridPoints
reflectionPoints.Add(New Point(pointMethods.swapPolarity(p.X - v) + v, p.Y))
Next p
Case 1 'Horizontal
For Each p As Point In gridPoints
reflectionPoints.Add(New Point(p.X, pointMethods.swapPolarity(p.Y - v) + v))
Next p
Case 2 'Diagonal - SW to NE
For Each p As Point In gridPoints
reflectionPoints.Add(New Point(p.Y - v, p.X + v))
Next p
Case 3 'Diagonal - NW to SE
For Each p As Point In gridPoints
reflectionPoints.Add(New Point(pointMethods.swapPolarity(p.Y - v), pointMethods.swapPolarity(p.X) + v))
Next p
End Select
Dim reflectionCoordinates As New List(Of Point)
For Each p As Point In reflectionPoints
reflectionCoordinates.Add(New Point(pointMethods.c2X(p.X), pointMethods.r2Y(p.Y)))
Next
g.DrawPolygon(pen, reflectionCoordinates.ToArray)
End Sub
drawRotatedShape
This contains all of the calculations necessary to draw a rotation transformation...
Public Shared Sub drawRotatedShape(ByVal g As Graphics, ByVal vertices As List(Of Point), ByVal r As Integer, ByVal l As Integer, ByVal t As Integer, ByVal polarity As String)
If vertices.Count < 3 Then
Return
End If
Dim pen As New Pen(Color.DodgerBlue, 2)
pen.DashStyle = Drawing2D.DashStyle.Dash
Dim gridPoints As New List(Of Point)
For Each p As Point In vertices
gridPoints.Add(New Point(pointMethods.X2c(p.X), pointMethods.Y2r(p.Y)))
Next p
l = pointMethods.X2c(l)
t = pointMethods.Y2r(t)
Dim rotationPoints As New List(Of Point)
Select Case polarity & r.ToString()
Case "+90", "-270"
For Each p As Point In gridPoints
rotationPoints.Add(New Point((p.Y - t) + l, -(p.X - l) + t))
Next p
Case "-180", "+180"
For Each p As Point In gridPoints
rotationPoints.Add(New Point(-(p.X - l) + l, -(p.Y - t) + t))
Next p
Case "+270", "-90"
For Each p As Point In gridPoints
rotationPoints.Add(New Point(-(p.Y - t) + l, p.X - l + t))
Next p
Case Else
For Each p As Point In gridPoints
rotationPoints.Add(New Point(p.X, p.Y))
Next p
End Select
Dim rotationCoordinates As New List(Of Point)
For Each p As Point In rotationPoints
rotationCoordinates.Add(New Point(pointMethods.c2X(p.X), pointMethods.r2Y(p.Y)))
Next
g.DrawPolygon(pen, rotationCoordinates.ToArray)
End Sub
drawTranslatedShape
This is the simplest of the transformations. A translated shape is simply offset from the original shape...
Public Shared Sub drawTranslatedShape(ByVal g As Graphics, ByVal vertices As List(Of Point), ByVal l As Integer, ByVal t As Integer)
If vertices.Count < 3 Then
Return
End If
Dim pen As New Pen(Color.DodgerBlue, 2)
pen.DashStyle = Drawing2D.DashStyle.Dash
Dim gridPoints As New List(Of Point)
For Each p As Point In vertices
gridPoints.Add(New Point(pointMethods.X2c(p.X), pointMethods.Y2r(p.Y)))
Next p
l = pointMethods.X2c(l)
t = pointMethods.Y2r(t)
Dim translationPoints As New List(Of Point)
For Each p As Point In gridPoints
translationPoints.Add(New Point(p.X + l, p.Y + t))
Next p
Dim translationCoordinates As New List(Of Point)
For Each p As Point In translationPoints
translationCoordinates.Add(New Point(pointMethods.c2X(p.X), pointMethods.r2Y(p.Y)))
Next
g.DrawPolygon(pen, translationCoordinates.ToArray)
End Sub
drawBorders
This ensures any overhanging drawing is truncated at the bounds of the 'Graph paper' grid...
Public Shared Sub drawBorders(ByVal g As Graphics, ByVal c As Color)
g.FillRectangle(New SolidBrush(c), New Rectangle(0, 0, 25, 500))
g.FillRectangle(New SolidBrush(c), New Rectangle(426, 0, 25, 500))
g.FillRectangle(New SolidBrush(c), New Rectangle(0, 0, 450, 25))
g.FillRectangle(New SolidBrush(c), New Rectangle(0, 426, 450, 75))
End Sub
drawHighlight
This is the last drawing method. It draws the mouse hover highlighting on the grids...
Public Shared Sub drawHighlight(ByVal g As Graphics, ByVal highlight As Point)
If highlight <> Nothing Then
Dim shadowPath As New Drawing.Drawing2D.GraphicsPath
shadowPath.AddEllipse(New Rectangle(highlight.X - 10, highlight.Y - 10, 20, 20))
Using pgb As New Drawing2D.PathGradientBrush(shadowPath)
'Normal
pgb.CenterColor = Color.Red 'This is the real one
pgb.SurroundColors = New Color() {Color.Transparent}
pgb.FocusScales = New PointF(0.1F, 1.0F)
g.FillPath(pgb, shadowPath)
End Using
End If
End Sub
pointMethods
Each grid cell is 25*25 pixels.
c2X and r2Y
These convert grid coordinates to pixel coordinates...
Public Shared Function c2X(ByVal x As Integer) As Integer
Return 25 + (x + 8) * 25
End Function
Public Shared Function r2Y(ByVal y As Integer) As Integer
If y <= 10 Then
Return 25 + (CInt(Math.Abs(y - 10)) * 25)
Else
Return 25 + ((10 - y) * 25)
End If
End Function
X2c and Y2r
These convert pixel coordinates to grid coordinates.
Public Shared Function X2c(ByVal x As Integer) As Integer
Dim c As Integer = ((x - 25) \ 25)
If c <= 8 Then
Return -(8 - c)
Else
Return (c - 8)
End If
End Function
Public Shared Function Y2r(ByVal y As Integer) As Integer
Dim r As Integer = (y - 25) \ 25
Return (10 - r)
End Function
swapPolarity
This is used in plotting points...
Public Shared Function swapPolarity(ByVal x As Integer) As Integer
Return -x
End Function
Conclusion
Graphical programming in VB.Net can be challenging and rewarding. This application relies on GDI+ methods and some fairly simple coding methods to achieve its aim.
The code behind the transformations used for plotting, is a simplified implementation of some commonly used methods in the world of Mathematics. Using a 'Graph paper' type grid and only drawing vertex to vertex, makes plotting the transformations a simple task.
Download
Full article and download here...