Last active
April 26, 2023 15:02
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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