Skip to content

Instantly share code, notes, and snippets.

@ja72
Last active April 26, 2023 15:02
Show Gist options
  • Save ja72/54439e59570c0292858085ea3a5356a5 to your computer and use it in GitHub Desktop.
Save ja72/54439e59570c0292858085ea3a5356a5 to your computer and use it in GitHub Desktop.
Extension methods to draw shapes defined by `System.Numerics.Vector2` coordinates using GDI and `System.Drawing.Graphics` with example code for a WinForms project (Framework 4.8)
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Numerics;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
SetStyle(ControlStyles.ResizeRedraw, true);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.TranslateTransform(ClientSize.Width / 2, ClientSize.Height / 2);
float scale = 20;
Gdi.Stroke.Color = Color.Black;
Gdi.Stroke.DashStyle = DashStyle.Dash;
Vector2 center = Vector2.Zero;
float semiMajor = 5f, semiMinor = 3f;
float tiltAngle = 15;
e.Graphics.DrawEllipse(scale, center, semiMajor, semiMinor, tiltAngle);
Gdi.Stroke.DashStyle = DashStyle.Solid;
Gdi.Stroke.AddEndArrow(4f);
Gdi.Stroke.Color = Color.Red;
float factor = 4 / 3f;
e.Graphics.DrawEllipseArc(scale, center, factor*semiMajor, factor*semiMinor, 0, 20, tiltAngle);
Gdi.Stroke.RemoveEndArrow();
float focusDistance = (float)Math.Sqrt(semiMajor * semiMajor - semiMinor * semiMinor);
Vector2 focus = center + new Vector2(focusDistance * (float)Math.Cos(tiltAngle*Geometry.deg), focusDistance * (float)Math.Sin(tiltAngle*Geometry.deg));
Gdi.Fill.Color = Color.Black;
e.Graphics.FillPoint(scale, focus);
Gdi.Stroke.Color = Color.Purple;
var points = Geometry.GetPointsOnEllipseArc(focus, semiMajor, semiMinor, 24, 0, 180 * Geometry.deg, tiltAngle * Geometry.deg);
foreach (var point in points)
{
e.Graphics.DrawPoint(scale, point);
}
}
}
}
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Numerics;
namespace WindowsFormsApp1
{
public static class Gdi
{
public static Pen Stroke { get; set; } = new Pen(Color.Black, 0);
public static SolidBrush Fill { get; set; } = new SolidBrush(Color.Black);
public static Font Font { get; set; } = SystemFonts.CaptionFont;
/// <summary>
/// Adds an arrow to the start of a <see cref="Pen"/> object.
/// </summary>
/// <remarks>Intended to be used with <see cref="Stroke"/></remarks>
/// <param name="pen">The pen to modify.</param>
/// <param name="arrowSize">Size of the arrow in pixels.</param>
public static void AddStartArrow(this Pen pen, float arrowSize)
{
pen.CustomStartCap = new AdjustableArrowCap(arrowSize/2, arrowSize);
}
/// <summary>
/// Removes the start arrow from a <see cref="Pen"/> object.
/// </summary>
/// <remarks>Intended to be used with <see cref="Stroke"/></remarks>
/// <param name="pen">The pen object to modify.</param>
public static void RemoveStartArrow(this Pen pen)
{
pen.StartCap = LineCap.NoAnchor;
}
/// <summary>
/// Adds an arrow to the end of a <see cref="Pen"/> object.
/// </summary>
/// <remarks>Intended to be used with <see cref="Stroke"/></remarks>
/// <param name="pen">The pen to modify.</param>
/// <param name="arrowSize">Size of the arrow in pixels.</param>
public static void AddEndArrow(this Pen pen, float arrowSize)
{
pen.CustomEndCap = new AdjustableArrowCap(arrowSize/2, arrowSize);
}
/// <summary>
/// Removes the end arrow from a <see cref="Pen"/> object.
/// </summary>
/// <remarks>Intended to be used with <see cref="Stroke"/></remarks>
/// <param name="pen">The pen object to modify.</param>
public static void RemoveEndArrow(this Pen pen)
{
pen.EndCap = LineCap.NoAnchor;
}
/// <summary>
/// Draws a small circle representing a point on the screen.
/// </summary>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="point">The point location.</param>
/// <param name="size">The size of the circle.</param>
/// <remarks>Uses <see cref="Stroke"/> to draw the shape.</remarks>
public static void DrawPoint(this Graphics g, float scale, Vector2 point, float size = 6f)
{
float x = scale * point.X, y = -scale * point.Y;
g.DrawEllipse(Stroke, x - size / 2, y - size / 2, size, size);
}
/// <summary>
/// Fills a small circle representing a point on the screen.
/// </summary>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="point">The point location.</param>
/// <param name="size">The size of the circle.</param>
/// <remarks>Uses <see cref="Fill"/> to fill the shape.</remarks>
public static void FillPoint(this Graphics g, float scale, Vector2 point, float size = 6f)
{
float x = scale * point.X, y = -scale * point.Y;
g.FillEllipse(Fill, x - size / 2, y - size / 2, size, size);
}
/// <summary>
/// Draws consecutive lines defined by their end points (nodes)
/// </summary>
/// <remarks>There must be more than one node to draw anything.</remarks>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="nodes">The array of locations defining the nodes of the (consecutive) lines</param>
/// <remarks>Uses <see cref="Stroke"/> to draw the shape.</remarks>
public static void DrawLines(this Graphics g, float scale, params Vector2[] nodes)
{
if (nodes.Length <= 1) return;
PointF[] points = new PointF[nodes.Length];
for (int i = 0; i < points.Length; i++)
{
points[i] = new PointF(scale * nodes[i].X, -scale * nodes[i].Y);
}
g.DrawLines(Stroke, points);
}
/// <summary>
/// Draws a curve through a set of points (nodes)
/// </summary>
/// <remarks>There must be more than one node to draw anything.</remarks>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="nodes">The array of locations defining the nodes of the (consecutive) lines</param>
/// <remarks>Uses <see cref="Stroke"/> to draw the shape.</remarks>
public static void DrawCurve(this Graphics g, float scale, params Vector2[] nodes)
{
if (nodes.Length <= 1) return;
PointF[] points = new PointF[nodes.Length];
for (int i = 0; i < points.Length; i++)
{
points[i] = new PointF(scale * nodes[i].X, -scale * nodes[i].Y);
}
g.DrawCurve(Stroke, points);
}
/// <summary>
/// Draws a closed shape with lines defined by the end points (nodes).
/// </summary>
/// <remarks>There must be more than one node to draw anything.</remarks>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="nodes">The array of locations defining the nodes of the (consecutive) lines</param>
/// <remarks>Uses <see cref="Stroke"/> to draw the shape.</remarks>
public static void DrawPolygon(this Graphics g, float scale, params Vector2[] nodes)
{
if (nodes.Length <= 1) return;
PointF[] points = new PointF[nodes.Length];
for (int i = 0; i < points.Length; i++)
{
points[i] = new PointF(scale * nodes[i].X, -scale * nodes[i].Y);
}
g.DrawPolygon(Stroke, points);
}
/// <summary>
/// Draws a closed curve though several points (nodes).
/// </summary>
/// <remarks>There must be more than one node to draw anything.</remarks>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="nodes">The array of locations defining the nodes of the (consecutive) lines</param>
/// <remarks>Uses <see cref="Stroke"/> to draw the shape.</remarks>
public static void DrawClosedCurve(this Graphics g, float scale, params Vector2[] nodes)
{
if (nodes.Length <= 1) return;
PointF[] points = new PointF[nodes.Length];
for (int i = 0; i < points.Length; i++)
{
points[i] = new PointF(scale * nodes[i].X, -scale * nodes[i].Y);
}
g.DrawClosedCurve(Stroke, points);
}
/// <summary>
/// Fills a closed shape with lines defined by the end points (nodes).
/// </summary>
/// <remarks>There must be more than one node to draw anything.</remarks>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="nodes">The array of locations defining the nodes of the (consecutive) lines</param>
/// <remarks>Uses <see cref="Fill"/> to fill the shape.</remarks>
public static void FillPolygon(this Graphics g, float scale, params Vector2[] nodes)
{
if (nodes.Length <= 1) return;
PointF[] points = new PointF[nodes.Length];
for (int i = 0; i < points.Length; i++)
{
points[i] = new PointF(scale * nodes[i].X, -scale * nodes[i].Y);
}
g.FillPolygon(Fill, points);
}
/// <summary>
/// Fills a closed curve though several points (nodes).
/// </summary>
/// <remarks>There must be more than one node to draw anything.</remarks>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="nodes">The array of locations defining the nodes of the (consecutive) lines</param>
/// <remarks>Uses <see cref="Fill"/> to fill the shape.</remarks>
public static void FillClosedCurve(this Graphics g, float scale, params Vector2[] nodes)
{
if (nodes.Length <= 1) return;
PointF[] points = new PointF[nodes.Length];
for (int i = 0; i < points.Length; i++)
{
points[i] = new PointF(scale * nodes[i].X, -scale * nodes[i].Y);
}
g.FillClosedCurve(Fill, points);
}
/// <summary>
/// Draw a line between two points.
/// </summary>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="fromPosition">The start position.</param>
/// <param name="toPosition">The end position.</param>
/// <remarks>Uses <see cref="Stroke"/> to draw the shape.</remarks>
public static void DrawLine(this Graphics g, float scale, Vector2 fromPosition, Vector2 toPosition)
{
float x1 = scale * fromPosition.X, y1 = -scale * fromPosition.Y;
float x2 = scale * toPosition.X, y2 = -scale * toPosition.Y;
g.DrawLine(Stroke, x1 ,y1, x2, y2);
}
/// <summary>
/// Draw a line from a point, to a certain offset using pixel coordinates.
/// </summary>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="fromPosition">The start position.</param>
/// <param name="xPixelOffset">The horizontal offset.</param>
/// <param name="yPixelOffset">The vertical offset.</param>
/// <remarks>Uses <see cref="Stroke"/> to draw the shape.</remarks>
public static void DrawLine(this Graphics g, float scale, Vector2 fromPosition, float xPixelOffset, int yPixelOffset)
{
float x = scale * fromPosition.X, y = -scale * fromPosition.Y;
g.DrawLine(Stroke, x, y, xPixelOffset, -yPixelOffset);
}
/// <summary>
/// Draws an ellipse
/// </summary>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="center">The center of the ellipse.</param>
/// <param name="semiMajor">The semi-major axis.</param>
/// <param name="semiMinor">The semi-minor axis.</param>
/// <param name="tiltAngleDegrees">The tilt angle degrees CCW from horizontal.</param>
/// <remarks>Uses <see cref="Stroke"/> to draw lines.</remarks>
public static void DrawEllipse(this Graphics g, float scale, Vector2 center, float semiMajor, float semiMinor, float tiltAngleDegrees = 0)
{
float x = scale * center.X, y = scale * center.Y;
float dx = scale * semiMajor, dy = scale * semiMinor;
var save = g.Save();
g.RotateTransform(-tiltAngleDegrees);
g.DrawEllipse(Stroke, x - dx, y - dy, 2 * dx, 2 * dy);
g.Restore(save);
}
/// <summary>
/// Fills an ellipse
/// </summary>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="center">The center of the ellipse.</param>
/// <param name="semiMajor">The semi-major axis.</param>
/// <param name="semiMinor">The semi-minor axis.</param>
/// <param name="tiltAngleDegrees">The tilt angle degrees CCW from horizontal.</param>
/// <remarks>Uses <see cref="Fill"/> to fill shapes.</remarks>
public static void FillEllipse(this Graphics g, float scale, Vector2 center, float semiMajor, float semiMinor, float tiltAngleDegrees = 0)
{
float x = scale * center.X, y = scale * center.Y;
float dx = scale * semiMajor, dy = scale * semiMinor;
var save = g.Save();
g.RotateTransform(-tiltAngleDegrees);
g.FillEllipse(Fill, x - dx, y - dy, 2 * dx, 2 * dy);
g.Restore(save);
}
/// <summary>
/// Draws an arc of an ellipse (partial ellipse)
/// </summary>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="center">The center of the ellipse.</param>
/// <param name="semiMajor">The semi-major axis.</param>
/// <param name="semiMinor">The semi-minor axis.</param>
/// <param name="startAngleDegrees">The start angle in degrees CCW from 3 o'clock.</param>
/// <param name="sweepAngleDegrees">The sweep angle in degrees CCW.</param>
/// <param name="tiltAngleDegrees">The tilt angle degrees CCW from horizontal.</param>
/// <remarks>Uses <see cref="Stroke"/> to draw lines.</remarks>
public static void DrawEllipseArc(this Graphics g, float scale, Vector2 center, float semiMajor, float semiMinor, float startAngleDegrees, float sweepAngleDegrees, float tiltAngleDegrees = 0)
{
float x = scale * center.X, y = scale * center.Y;
float dx = scale * semiMajor, dy = scale * semiMinor;
//float θ =
var save = g.Save();
g.RotateTransform(-tiltAngleDegrees);
g.DrawArc(Stroke, x - dx, y - dy, 2 * dx, 2 * dy, 360 - startAngleDegrees, -sweepAngleDegrees);
g.Restore(save);
}
/// <summary>
/// Draws the text.
/// </summary>
/// <param name="g">The graphics object to draw on.</param>
/// <param name="scale">The drawing scale converting <see cref="Vector2"/> to <see cref="PointF"/>.</param>
/// <param name="text">The text to draw.</param>
/// <param name="anchor">The anchor point.</param>
/// <param name="alignment">The alignment of the text relative to the anchor point.</param>
/// <param name="padding">The padding in pixels to space from anchor point.</param>
/// <exception cref="System.NotSupportedException">For invalid <paramref name="alignment"/>.</exception>
/// <remarks>Uses <see cref="Stroke"/> to draw text.</remarks>
public static void DrawText(this Graphics g, float scale, string text, Vector2 anchor, ContentAlignment alignment, int padding=4)
{
SizeF size = g.MeasureString(text, Font);
float x = scale * anchor.X, y = -scale * anchor.Y;
switch (alignment)
{
case ContentAlignment.TopLeft:
x = x - size.Width - padding;
y = y - size.Height - padding;
break;
case ContentAlignment.TopCenter:
x = x - size.Width/2;
y = y - size.Height - padding;
break;
case ContentAlignment.TopRight:
x = x + padding;
y = y - size.Height - padding;
break;
case ContentAlignment.MiddleLeft:
x = x - size.Width - padding;
y = y - size.Height/2;
break;
case ContentAlignment.MiddleCenter:
x = x - size.Width/2;
y = y - size.Height/2;
break;
case ContentAlignment.MiddleRight:
x = x + padding;
y = y - size.Height/2;
break;
case ContentAlignment.BottomLeft:
x = x - size.Width - padding;
y = y + padding;
break;
case ContentAlignment.BottomCenter:
x = x - size.Width/2;
y = y + padding;
break;
case ContentAlignment.BottomRight:
x = x + padding;
y = y + padding;
break;
default:
throw new NotSupportedException();
}
g.DrawString(text, Font, Fill, x, y);
}
}
}
using System;
using System.Numerics;
namespace WindowsFormsApp1
{
public static class Geometry
{
public const float pi = 3.14159274f; // (float)Math.PI
public const float deg = pi / 180; // 1 deg = π/180
/// <summary>
/// Gets points on an ellipse.
/// </summary>
/// <remarks>
/// The points are drawn from the focus of the ellipse and are spaced
/// by equal angles in polar coordinates.
/// </remarks>
/// <param name="focus">The focus point of the ellipse.</param>
/// <param name="semiMajor">The semi-major axis.</param>
/// <param name="semiMinor">The semi-minor axis.</param>
/// <param name="numPoints">The number points.</param>
/// <param name="tiltAngle">The tilt angle in radians, CCW from horizontal.</param>
/// <returns>An array of <see cref="Vector2"/> points.</returns>
static public Vector2[] GetPointsOnEllipse(Vector2 focus, float semiMajor, float semiMinor, int numPoints, float tiltAngle = 0)
=> GetPointsOnEllipseArc(focus, semiMajor, semiMinor, numPoints, 0, 2 * pi, tiltAngle);
/// <summary>
/// Gets points on an ellipse arc (partial ellipse).
/// </summary>
/// <remarks>
/// The points are drawn from the focus of the ellipse and are spaced
/// by equal angles in polar coordinates.
/// </remarks>
/// <param name="focus">The focus point of the ellipse.</param>
/// <param name="semiMajor">The semi-major axis.</param>
/// <param name="semiMinor">The semi-minor axis.</param>
/// <param name="numPoints">The number points.</param>
/// <param name="azimuthOffset">The azimuth offset angle in radians, CCW.</param>
/// <param name="azimuthSweep">The azimuth sweep angle in radians, CCW.</param>
/// <param name="tiltAngle">The tilt angle in radians, CCW from horizontal.</param>
/// <returns>An array of <see cref="Vector2"/> points.</returns>
static public Vector2[] GetPointsOnEllipseArc(Vector2 focus, float semiMajor, float semiMinor, int numPoints, float azimuthOffset, float azimuthSweep, float tiltAngle = 0)
{
float e = (float)Math.Sqrt(1 - (semiMinor / semiMajor) * (semiMinor / semiMajor));
float Δθ = azimuthSweep / (numPoints - 1);
float ct = (float)Math.Cos(tiltAngle), st = (float)Math.Sin(tiltAngle);
Vector2[] points = new Vector2[numPoints];
for (int i = 0; i < numPoints; i++)
{
// get azimuth angle along the ellipse
float θ = azimuthOffset + i * Δθ;
// get polar coordinate of ellipse r(θ)
float r = semiMajor * (1 - e * e) /(float)(1 + e * Math.Cos(θ));
// get axis aligned (x,y) point from foci
float x = r * (float)Math.Cos(θ), y = r * (float)Math.Sin(θ);
// get rotated (x,y) point
Vector2 localPoint = new Vector2(ct * x - st * y, st * x + ct * y);
points[i] = focus + localPoint;
}
return points;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment