diff --git a/src/KappaDuck.Quack/Graphics/Drawing/CircleShape.cs b/src/KappaDuck.Quack/Graphics/Drawing/CircleShape.cs
new file mode 100644
index 0000000..498fb07
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Drawing/CircleShape.cs
@@ -0,0 +1,67 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Geometry;
+
+namespace KappaDuck.Quack.Graphics.Drawing;
+
+///
+/// A circle shape, approximated by a configurable number of points.
+///
+public sealed class CircleShape : Shape
+{
+ private float _radius;
+ private int _pointCount;
+
+ ///
+ /// Creates a circle of the given radius.
+ ///
+ /// The radius of the circle, in local units.
+ /// The number of points used to approximate the circle; more points give a smoother circle.
+ public CircleShape(float radius, int pointCount = 30)
+ {
+ _radius = radius;
+ _pointCount = pointCount;
+ Update();
+ }
+
+ ///
+ /// Gets or sets the radius of the circle, in local units.
+ ///
+ public float Radius
+ {
+ get => _radius;
+ set
+ {
+ _radius = value;
+ Update();
+ }
+ }
+
+ ///
+ public override int PointCount => _pointCount;
+
+ ///
+ /// Sets the number of points used to approximate the circle.
+ ///
+ /// The number of points; more points give a smoother circle.
+ public void SetPointCount(int count)
+ {
+ _pointCount = count;
+ Update();
+ }
+
+ ///
+ public override PointF GetPoint(int index)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(index);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _pointCount);
+
+ Angle angle = Angle.FromDegrees((index * 360f / _pointCount) - 90f);
+
+ float x = _radius + (_radius * angle.Cos);
+ float y = _radius + (_radius * angle.Sin);
+
+ return new PointF(x, y);
+ }
+}
diff --git a/src/KappaDuck.Quack/Graphics/Drawing/ConvexShape.cs b/src/KappaDuck.Quack/Graphics/Drawing/ConvexShape.cs
new file mode 100644
index 0000000..7036d12
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Drawing/ConvexShape.cs
@@ -0,0 +1,62 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Geometry;
+
+namespace KappaDuck.Quack.Graphics.Drawing;
+
+///
+/// A convex polygon shape defined by an arbitrary set of points.
+///
+/// The points must form a convex polygon; concave shapes will not fill correctly.
+public sealed class ConvexShape : Shape
+{
+ private PointF[] _points;
+
+ ///
+ /// Creates a convex shape with the given number of points, all at the origin.
+ ///
+ /// The number of points. Set each one with .
+ public ConvexShape(int pointCount)
+ {
+ _points = new PointF[pointCount];
+ Update();
+ }
+
+ ///
+ /// Creates a convex shape from the given points.
+ ///
+ /// The points of the convex polygon, in local coordinates.
+ public ConvexShape(ReadOnlySpan points)
+ {
+ _points = points.ToArray();
+ Update();
+ }
+
+ ///
+ public override int PointCount => _points.Length;
+
+ ///
+ public override PointF GetPoint(int index) => _points[index];
+
+ ///
+ /// Sets the position of a single point of the shape.
+ ///
+ /// The index of the point, from 0 to minus one.
+ /// The new position of the point, in local coordinates.
+ public void SetPoint(int index, PointF point)
+ {
+ _points[index] = point;
+ Update();
+ }
+
+ ///
+ /// Replaces all points of the shape.
+ ///
+ /// The new points of the convex polygon, in local coordinates.
+ public void SetPoints(ReadOnlySpan points)
+ {
+ _points = points.ToArray();
+ Update();
+ }
+}
diff --git a/src/KappaDuck.Quack/Graphics/Drawing/RectangleShape.cs b/src/KappaDuck.Quack/Graphics/Drawing/RectangleShape.cs
new file mode 100644
index 0000000..692dc48
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Drawing/RectangleShape.cs
@@ -0,0 +1,66 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Geometry;
+
+namespace KappaDuck.Quack.Graphics.Drawing;
+
+///
+/// A rectangle shape defined by its width and height.
+///
+public sealed class RectangleShape : Shape
+{
+ private SizeF _size;
+
+ ///
+ /// Creates a rectangle of the given size.
+ ///
+ /// The width and height of the rectangle, in local units.
+ public RectangleShape(SizeF size)
+ {
+ _size = size;
+ Update();
+ }
+
+ ///
+ /// Creates a rectangle of the given position and size.
+ ///
+ /// The position of the rectangle, in local units.
+ /// The width and height of the rectangle, in local units.
+ public RectangleShape(PointF position, SizeF size) : this(size)
+ => Position = position;
+
+ ///
+ /// Creates a rectangle of the given rect.
+ ///
+ /// The rectangle, in local units.
+ public RectangleShape(Rect rect) : this(rect.Position, rect.Size)
+ {
+ }
+
+ ///
+ /// Gets or sets the width and height of the rectangle, in local units.
+ ///
+ public SizeF Size
+ {
+ get => _size;
+ set
+ {
+ _size = value;
+ Update();
+ }
+ }
+
+ ///
+ public override int PointCount { get; } = 4;
+
+ ///
+ public override PointF GetPoint(int index) => index switch
+ {
+ 0 => new PointF(0f, 0f),
+ 1 => new PointF(_size.Width, 0f),
+ 2 => new PointF(_size.Width, _size.Height),
+ 3 => new PointF(0f, _size.Height),
+ _ => throw new ArgumentOutOfRangeException(nameof(index))
+ };
+}
diff --git a/src/KappaDuck.Quack/Graphics/Drawing/RegularPolygon.cs b/src/KappaDuck.Quack/Graphics/Drawing/RegularPolygon.cs
new file mode 100644
index 0000000..f95bee8
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Drawing/RegularPolygon.cs
@@ -0,0 +1,78 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Geometry;
+
+namespace KappaDuck.Quack.Graphics.Drawing;
+
+///
+/// A regular polygon shape whose points are evenly spaced around a circle.
+///
+/// Use it for triangles, pentagons, hexagons and so on; a high side count approximates a circle.
+public sealed class RegularPolygon : Shape
+{
+ private float _radius;
+ private int _sideCount;
+
+ ///
+ /// Creates a regular polygon.
+ ///
+ /// The distance from the center to each point, in local units.
+ /// The number of sides. Must be at least 3.
+ /// is less than 3.
+ public RegularPolygon(float radius, int sideCount)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(sideCount, 3);
+
+ _radius = radius;
+ _sideCount = sideCount;
+
+ Update();
+ }
+
+ ///
+ /// Gets or sets the distance from the center to each point, in local units.
+ ///
+ public float Radius
+ {
+ get => _radius;
+ set
+ {
+ _radius = value;
+ Update();
+ }
+ }
+
+ ///
+ /// Gets or sets the number of sides. Must be at least 3.
+ ///
+ /// The value is less than 3.
+ public int SideCount
+ {
+ get => _sideCount;
+ set
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(value, 3);
+
+ _sideCount = value;
+ Update();
+ }
+ }
+
+ ///
+ public override int PointCount => _sideCount;
+
+ ///
+ public override PointF GetPoint(int index)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(index);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _sideCount);
+
+ Angle angle = Angle.FromDegrees((index * 360f / _sideCount) - 90f);
+
+ float x = _radius + (_radius * angle.Cos);
+ float y = _radius + (_radius * angle.Sin);
+
+ return new PointF(x, y);
+ }
+}
diff --git a/src/KappaDuck.Quack/Graphics/Drawing/Shape.cs b/src/KappaDuck.Quack/Graphics/Drawing/Shape.cs
new file mode 100644
index 0000000..8634a25
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Drawing/Shape.cs
@@ -0,0 +1,304 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Geometry;
+using KappaDuck.Quack.Graphics.Primitives;
+using KappaDuck.Quack.Graphics.Rendering;
+using KappaDuck.Quack.Video.Pixels;
+
+namespace KappaDuck.Quack.Graphics.Drawing;
+
+///
+/// The base class for drawable shapes defined by a set of points such as rectangles, circles and polygons.
+///
+///
+/// A shape has a solid and an optional outline ( and
+/// ), and may be textured. Derive from it by implementing and
+/// , and call whenever those would return different values. The points
+/// must describe a convex polygon.
+///
+public abstract class Shape : Transformable, IDrawable
+{
+ private Vertex[] _shape = [];
+ private int[] _indices = [];
+
+ private Vertex[] _outline = [];
+ private int[] _outlineIndices = [];
+
+ private Color _color = Colors.White;
+ private Color _outlineColor = Colors.White;
+ private RectI _region;
+
+ private Rect _insideBounds;
+ private Rect _bounds;
+
+ ///
+ /// Gets or sets the color that fills the interior of the shape. Defaults to .
+ ///
+ public Color FillColor
+ {
+ get => _color;
+ set
+ {
+ _color = value;
+ UpdateFillColors();
+ }
+ }
+
+ ///
+ /// Gets or sets the color of the shape's outline. Defaults to .
+ ///
+ /// The outline is only visible when is non-zero.
+ public Color OutlineColor
+ {
+ get => _outlineColor;
+ set
+ {
+ _outlineColor = value;
+ UpdateOutlineColors();
+ }
+ }
+
+ ///
+ /// Gets or sets the thickness of the shape's outline, in local units. Defaults to 0 (no outline).
+ ///
+ /// A positive thickness grows the outline outwards, a negative thickness grows it inwards.
+ public float OutlineThickness
+ {
+ get;
+ set
+ {
+ field = value;
+ Update();
+ }
+ }
+
+ ///
+ /// Gets or sets the texture drawn inside the shape, or for a solid fill.
+ ///
+ /// The is mapped across the shape's local bounds. The outline is never textured.
+ public Texture? Texture
+ {
+ get;
+ set
+ {
+ field = value;
+ UpdateTexCoords();
+ }
+ }
+
+ ///
+ /// Gets or sets the region of the mapped across the shape, in texture pixels.
+ ///
+ public RectI Region
+ {
+ get => _region;
+ set
+ {
+ _region = value;
+ UpdateTexCoords();
+ }
+ }
+
+ ///
+ /// Gets the number of points that define the shape.
+ ///
+ public abstract int PointCount { get; }
+
+ ///
+ /// Gets the shape's bounding rectangle in its own local space, before its transform is applied.
+ ///
+ /// The bounds include the outline.
+ public Rect LocalBounds => _bounds;
+
+ ///
+ /// Gets the shape's axis-aligned bounding rectangle in its parent's space, after its transform is applied.
+ ///
+ public Rect GlobalBounds => Transform.TransformRect(_bounds);
+
+ ///
+ /// Gets the position of a point of the shape, in local coordinates.
+ ///
+ /// The index of the point, from 0 to minus one.
+ /// The point at .
+ public abstract PointF GetPoint(int index);
+
+ ///
+ public void Draw(IRenderTarget target, RenderState state)
+ {
+ state = state with { Transform = state.Transform * Transform };
+
+ if (_shape.Length > 0)
+ target.Draw(_shape, _indices, state with { Texture = Texture });
+
+ if (_outline.Length > 0)
+ target.Draw(_outline, _outlineIndices, state with { Texture = null });
+ }
+
+ ///
+ /// Rebuilds the shape's geometry from its points.
+ ///
+ /// Call this from a derived class whenever or would return different values.
+ protected void Update()
+ {
+ if (PointCount < 3)
+ {
+ _shape = [];
+ _indices = [];
+ _outline = [];
+ _outlineIndices = [];
+ _insideBounds = default;
+ _bounds = default;
+
+ return;
+ }
+
+ _shape = new Vertex[PointCount + 1];
+
+ for (int i = 0; i < PointCount; i++)
+ _shape[i + 1].Position = GetPoint(i);
+
+ _insideBounds = ComputeBounds(_shape, start: 1, PointCount);
+ _shape[0].Position = new PointF(_insideBounds.X + (_insideBounds.Width / 2f), _insideBounds.Y + (_insideBounds.Height / 2f));
+
+ _indices = new int[PointCount * 3];
+
+ for (int i = 0; i < PointCount; i++)
+ {
+ _indices[(i * 3) + 0] = 0;
+ _indices[(i * 3) + 1] = i + 1;
+ _indices[(i * 3) + 2] = ((i + 1) % PointCount) + 1;
+ }
+
+ UpdateFillColors();
+ UpdateTexCoords();
+ UpdateOutline();
+
+ _bounds = _outline.Length > 0 ? ComputeBounds(_outline, start: 0, _outline.Length) : _insideBounds;
+ }
+
+ private void UpdateFillColors()
+ {
+ ColorF color = _color.ToColorF();
+
+ for (int i = 0; i < _shape.Length; i++)
+ _shape[i].Color = color;
+ }
+
+ private void UpdateTexCoords()
+ {
+ if (Texture is null || _shape.Length == 0)
+ return;
+
+ float textureWidth = Texture.Width;
+ float textureHeight = Texture.Height;
+
+ for (int i = 0; i < _shape.Length; i++)
+ {
+ float ratioX = _insideBounds.Width > 0f ? (_shape[i].Position.X - _insideBounds.X) / _insideBounds.Width : 0f;
+ float ratioY = _insideBounds.Height > 0f ? (_shape[i].Position.Y - _insideBounds.Y) / _insideBounds.Height : 0f;
+
+ float u = (_region.X + (_region.Width * ratioX)) / textureWidth;
+ float v = (_region.Y + (_region.Height * ratioY)) / textureHeight;
+
+ _shape[i].TextureCoordinate = new PointF(u, v);
+ }
+ }
+
+ private void UpdateOutline()
+ {
+ if (MathF.ApproximatelyZero(OutlineThickness))
+ {
+ _outline = [];
+ _outlineIndices = [];
+
+ return;
+ }
+
+ int count = PointCount;
+ PointF center = _shape[0].Position;
+
+ _outline = new Vertex[(count + 1) * 2];
+
+ for (int i = 0; i < count; i++)
+ {
+ PointF p1 = _shape[i + 1].Position;
+ PointF p0 = _shape[((i - 1 + count) % count) + 1].Position;
+ PointF p2 = _shape[((i + 1) % count) + 1].Position;
+
+ Vector2 n1 = Normal(p0, p1);
+ Vector2 n2 = Normal(p1, p2);
+
+ if (Vector2.Dot(n1, center - p1) > 0f)
+ n1 = -n1;
+
+ if (Vector2.Dot(n2, center - p1) > 0f)
+ n2 = -n2;
+
+ float factor = 1f + Vector2.Dot(n1, n2);
+ Vector2 normal = !MathF.ApproximatelyZero(factor) ? (n1 + n2) / factor : n1;
+
+ _outline[(i * 2) + 0].Position = p1;
+ _outline[(i * 2) + 1].Position = p1 + (normal * OutlineThickness);
+ }
+
+ _outline[(count * 2) + 0].Position = _outline[0].Position;
+ _outline[(count * 2) + 1].Position = _outline[1].Position;
+
+ _outlineIndices = new int[(_outline.Length - 2) * 3];
+
+ for (int i = 0; i < _outline.Length - 2; i++)
+ {
+ _outlineIndices[(i * 3) + 0] = i;
+ _outlineIndices[(i * 3) + 1] = i + 1;
+ _outlineIndices[(i * 3) + 2] = i + 2;
+ }
+
+ UpdateOutlineColors();
+ }
+
+ private void UpdateOutlineColors()
+ {
+ ColorF color = _outlineColor.ToColorF();
+
+ for (int i = 0; i < _outline.Length; i++)
+ _outline[i].Color = color;
+ }
+
+ private static Vector2 Normal(PointF from, PointF to)
+ {
+ Vector2 edge = to - from;
+ Vector2 normal = new(-edge.Y, edge.X);
+
+ float magnitude = normal.Magnitude;
+ return !MathF.ApproximatelyZero(magnitude) ? normal / magnitude : normal;
+ }
+
+ private static Rect ComputeBounds(Vertex[] vertices, int start, int count)
+ {
+ if (count == 0)
+ return default;
+
+ PointF first = vertices[start].Position;
+ float minX = first.X, minY = first.Y, maxX = first.X, maxY = first.Y;
+
+ for (int i = start + 1; i < start + count; i++)
+ {
+ PointF point = vertices[i].Position;
+
+ if (point.X < minX)
+ minX = point.X;
+
+ if (point.X > maxX)
+ maxX = point.X;
+
+ if (point.Y < minY)
+ minY = point.Y;
+
+ if (point.Y > maxY)
+ maxY = point.Y;
+ }
+
+ return new Rect(minX, minY, maxX - minX, maxY - minY);
+ }
+}