Every picture in this README is drawn from the package's own results: each point is what the code beside it returns.
- Generic – every type works with any integer or float type, named types included.
- Immutable – every method returns a new value.
- Shapes – circle, capsule, sector, polygon, rectangle, regular polygon, box and ellipse.
- Paths – segment, ray, line, polyline, arc and Bezier curves.
- Collisions – intersections, containment, raycasts, nearest points, contacts and sweeps.
- Interfaces –
Shapeholds a shape without knowing which one. - Screen space – top-left origin,
+Ydown, one winding order everywhere. - Closed boundaries – the edge counts as inside, within a tolerance matched to
T. - Reproducible – the same bits on amd64 and arm64, trigonometry aside.
- Extras – direction enums,
imageinterop, JSON, parsing andgeomtestassertions.
The rules behind them are in the package documentation.
go get github.com/gravitton/geometryimport geom "github.com/gravitton/geometry"The level at the top, call by call:
ray := geom.Ry(view.Center, geom.VectorFromAngle(angle, 1.0)) // one ray of the sentry's view
exit, _ := ray.ClipBox(scene) // it leaves the level here
hit, ok := geom.Raycast(ray, solid) // or stops at a wall or the spikes: Point, Normal
bounce := ray.Vector.Reflect(hit.Normal) // the facing ray reflected off the floor
light := geom.Pol(outline) // the rays' ends outline what the sentry sees
light.Contains(coin.Center) // a coin in sightEvery figure below is drawn on a grid of one unit per cell, from the values in the code beside it.
Every kind has a shorthand constructor in its own letters, and named constructors that read as phrases
(CircleThrough, RectangleFromMinMax, RegularPolygonWithOrientation).
geom.Circ(geom.Pt(4.5, 4.0), 3.0) // center, radius
geom.Cap(geom.Pt(11.5, 5.5), geom.Pt(15.5, 2.5), 1.5) // every point within the radius of a segment
geom.Sect(geom.Pt(22.5, 6.5), 5.0, -3*geom.Pi/4, geom.Pi/2) // center, radius, angle and sweep in radians
geom.Seg(geom.Pt(28.5, 6.0), geom.Pt(34.5, 1.0)) // from Start to End
geom.Pol([]geom.Point[float64]{{37.5, 1}, {43.5, 1}, {43.5, 6}, {40.5, 3.5}, {37.5, 6}}) // closed, convex or not
geom.Rect(geom.Pt(9.0, 11.0), geom.Sz(6.0, 3.0), 0.4) // center, size and angle
geom.RegularPolygonWithOrientation(geom.Pt(18.0, 11.0), geom.SzU(2.5), 6, geom.OrientationFlatTop)
geom.Bx(geom.Pt(24.0, 9.0), geom.Pt(30.0, 13.0)) // axis-aligned, the corners in either order
geom.Ell(geom.Pt(36.0, 11.0), geom.Sz(3.5, 1.8), 0.5) // center, semi-axes and angle
geom.Ry(geom.Pt(1.0, 6.0), geom.Vec(2.0, -1.0)) // from Origin along a direction, without end
geom.Ln(geom.Pt(11.0, 4.0), geom.Vec(2.0, 1.0)) // without end both ways
geom.PolLn([]geom.Point[float64]{{16, 7}, {18, 1}, {20, 5}, {22, 1}}) // an open path
geom.Ac(geom.Pt(27.0, 4.0), 3.0, -geom.Pi, 1.25*geom.Pi) // a part of a circle, the sweep signed
geom.QuadBez(geom.Pt(31.0, 7.0), geom.Pt(34.0, -1.0), geom.Pt(37.0, 7.0))
geom.CubBez(geom.Pt(38.0, 6.0), geom.Pt(39.0, 0.0), geom.Pt(43.0, 8.0), geom.Pt(44.0, 2.0))A Point is a position and a Vector a displacement; subtracting two points gives the vector between them.
a, b := geom.Pt(2.0, 8.0), geom.Pt(8.0, 5.0)
v := b.Subtract(a) // ⟨6,-3⟩
v.Length() // 6.71
v.Normal() // ⟨3,6⟩, perpendicular, of the same length
v.Reflect(geom.Vec(0.0, 1.0)) // ⟨6,3⟩, bounced off a horizontal wall
a.Lerp(b, 0.25) // (3.5,7.25)
a.RotateAround(b, -geom.Pi/2) // (11,11)Lerp moves by a fraction; a game loop moves by a speed times the frame time, a step that never passes the target:
a.MoveToward(b, 2.5) // 2.5 along the way to b, and b itself once the step reaches it
v.RotateToward(geom.Pi, 0.1) // turned by 0.1 along the shorter arc, the length kept
geom.MoveTowardAngle(angle, target, speed*dt) // the same step on a bare angle
geom.MoveToward(volume, 1.0, fade*dt) // and on any number
screen, slot := geom.Sz(16.0, 9.0), geom.SzU(6.0)
screen.Fit(slot) // 6x3.38, the largest of the same ratio inside
screen.Fill(slot) // 10.67x6, the smallest of the same ratio around
geom.PadXY(1.0, 0.5).Size() // 2x1, the horizontal and vertical totalA Rectangle is a center, a size and an angle. Turned, it keeps its corner names, and Bounds becomes the box
around its vertices.
r := geom.Rect(geom.Pt(6.0, 5.5), geom.Sz(7.0, 3.5), 0.4)
r.TopLeft() // (3.46,2.53), the corner that was top-left before the turn
r.Anchor(geom.Top) // (6.68,3.89), the midpoint of that edge
r.Bounds() // (2.09,2.53)-(9.91,8.47)
r.BoundingCircle() // Circ((6,5.5);3.91)
screen := geom.RectangleFromMinMax(geom.Pt(15.0, 1.0), geom.Pt(27.0, 10.0))
w := geom.RectangleFromCenter(geom.Pt(26.0, 1.5), geom.Sz(5.0, 3.0))
w.Clamp(screen) // Rect((24.5,2.5);5x3), moved by the least that brings it insideA Box is the axis-aligned extent, for clipping, viewports, layout and culling, and the Bounds of every shape.
view := geom.BoxFromSize(geom.Sz(20.0, 12.0))
view.Inset(geom.PadU(1.0)) // (1,1)-(19,11)
hud := geom.Bx(geom.Pt(6.0, 2.0), geom.Pt(0.0, 0.0)) // (0,0)-(6,2), the corners in either order
hud.AlignTo(geom.TopRight, view.Anchor(geom.TopRight)) // (14,0)-(20,2), pinned to the corner of the view
tip := geom.BoxFromMin(geom.Pt(17.0, 8.0), geom.Sz(7.0, 3.0))
tip.Clamp(view) // (13,8)-(20,11)
view.ClipBox(tip) // (17,8)-(20,11), true: the part on screen
c, wheel := geom.Circ(geom.Pt(3.0, 5.0), 2.0), geom.Circ(geom.Pt(8.0, 6.0), 1.0)
c.Tangents(geom.Pt(8.5, 1.5)) // a Pair of the points the lines from there touch
c.OuterTangents(wheel) // a belt around both, each a Segment from c to wheel
c.InnerTangents(wheel) // a crossed belt; none once the circles touch
c.Union(wheel) // Circ((5.01,5.4);4.05)A circle has no Transform of its own, since a matrix takes it to an ellipse. A Capsule is a segment with a
radius, the rounded collider of a character.
o := geom.Circ(geom.Pt(4.0, 5.0), 2.5)
e := o.Ellipse().Transform(geom.ScaleMatrix(2.0, 1.0)) // Ell((8,5);5x2.5), exact for every matrix
e.Contains(geom.Pt(6.0, 4.0)) // true
e.DistanceTo(geom.Pt(10.0, 8.5)) // 1.18, to the nearest point of the boundary
e.Foci() // (3.67,5), (12.33,5)
body := geom.Cap(geom.Pt(4.0, 2.0), geom.Pt(4.0, 8.0), 1.5)
body.DistanceTo(geom.Pt(8.5, 5.0)) // 3, to the side
p := geom.Pol([]geom.Point[float64]{{1, 1}, {9, 1}, {9, 9}, {5, 4}, {1, 9}})
p.Area() // 44
p.IsConvex() // false
p.ConvexHull() // Pol((1,1);(9,1);(9,9);(1,9)), the notch dropped
p.Triangulate() // [1 2 3 0 1 3 4 0 3]: three indices into Points per triangle
p.Grow(0.5) // every edge moved out along its normal; Shrink moves them in
front, back := p.Split(geom.Ln(geom.Pt(3.0, 0.0), geom.Vec(1.0, 2.0))) // meeting exactly along the line
for vertex := range p.Vertices() { // the same loop walks a Rectangle, RegularPolygon or Box
}
hex := geom.RegularPolygonWithOrientation(geom.Pt(5.0, 5.0), geom.SzU(4.0), 6, geom.OrientationFlatTop)
hex.Anchor(geom.Top) // (5,1.54), the midpoint of the top edge; a pointy-top one gives its top vertex
hex.Bounds() // (1,1.54)-(9,8.46)
hex.Area() // 41.57
geom.RegularPolygonWithOrientation(geom.Pt(5.0, 5.0), geom.Sz(4.5, 2.5), 6, geom.OrientationFlatTop) // on an ellipse
geom.RegPol(geom.Pt(5.0, 5.0), geom.Sz(4.5, 2.5), 5, 0, 0.6) // the phase slides the vertices along the ellipse
geom.RegPol(geom.Pt(5.0, 5.0), geom.Sz(4.5, 2.5), 5, 0.6, 0) // the angle turns the ellipseEvery shape has Intersects<Kind>, Contains<Kind> and Intersection<Kind> for each kind, symmetric and counting a
touch. Intersection<Kind> gives the boundary points, a Pair where there are at most two; Clip<Kind> gives the
part in common.
a, b := geom.Circ(geom.Pt(4.0, 5.0), 3.0), geom.Circ(geom.Pt(7.0, 6.0), 2.5)
a.IntersectionCircle(b) // Pair((5.21,7.75);(6.62,3.53)), which allocates nothing
s := geom.Seg(geom.Pt(0.0, 6.0), geom.Pt(10.0, 6.0))
s.ClipPolygon(p) // [Seg((1,6);(3.4,6)) Seg((6.6,6);(9,6))], from Start to End
cone := geom.Sect(geom.Pt(5.0, 5.0), 4.0, -geom.Pi/4, 1.5*geom.Pi)
geom.Seg(geom.Pt(0.0, 3.5), geom.Pt(10.0, 3.5)).ClipSector(cone) // two parts through the notch
r := geom.Rect(geom.Pt(4.0, 4.5), geom.Sz(5.0, 3.5), 0.3)
r.ClipRectangle(geom.Rect(geom.Pt(6.5, 6.0), geom.Sz(4.0, 3.0), 0.3)) // the overlap, a Rectangle of the same angle
geom.Intersects[float64](a, r) // the same answers for two shapes of any kindHit<Kind> on a segment or a ray is the first point of the boundary it reaches from outside, with the unit normal
there and the fraction of the cast; Sweep moves a whole shape.
wall := geom.Rect(geom.Pt(10.0, 5.0), geom.Sz(2.0, 6.0), 0.35)
ray := geom.Ry(geom.Pt(1.0, 7.0), geom.Vec(3.0, -1.0))
hit, ok := ray.HitRectangle(wall) // Hit((9.2,4.27);⟨-0.94,-0.34⟩;2.73), true
ray.Vector.Reflect(hit.Normal) // ⟨-1.65,-2.7⟩, the bounce
ball := geom.Circ(geom.Pt(2.0, 2.0), 1.0)
block := geom.Bx(geom.Pt(8.0, 4.0), geom.Pt(11.0, 9.0))
hit, ok = geom.Sweep[float64](ball, geom.Vec(11.0, 5.0), block) // Fraction 0.45: where the ball first touches
geom.Raycast[float64](ray, wall) // a Hit on a shape of any kindA Line runs without end both ways and tells its two sides apart.
wall := geom.LineThrough(geom.Pt(1.0, 8.0), geom.Pt(13.0, 2.0))
a := geom.Pt(10.0, 8.0)
wall.Side(a) // 1, the side the normal of the direction points to
wall.Nearest(a) // (8.2,4.4)
wall.Reflect(a) // (6.4,0.8)
wall.ClipBox(view) // the part a viewport showsTwo shapes of any kind are parted, measured and moved against each other by package functions.
floor := geom.Bx(geom.Pt(1.0, 6.0), geom.Pt(9.0, 8.0))
ball := geom.Circ(geom.Pt(4.0, 5.0), 1.5)
contact, ok := geom.Collide[float64](ball, floor) // Contact(⟨0,-1⟩;0.5;(4,6)): Normal, Depth, Points
ball.Translate(contact.Separation()) // the shortest move out of the floor
crate := geom.RectangleFromCenter(geom.Pt(5.0, 5.0), geom.Sz(3.0, 2.5))
geom.Collide[float64](crate, floor) // two Points where the sides lie flat
on, at := geom.Nearest[float64](turret, target) // the point of each nearest the other
geom.Distance[float64](turret, target) // 2.21, zero exactly where they intersectShape holds a shape without knowing which one, for a spatial index or a broad pass:
var s geom.Shape[float64] = c // Bounds, Contains, DistanceTo, DistanceSquaredTo, Nearest, SupportThe functions take the type argument where a concrete shape is passed and infer it from a value held as a Shape. A
call through an interface allocates where the same named method does not, so a hot loop that knows its types calls
the method.
ring := geom.Ac(geom.Pt(5.0, 5.0), 4.0, -geom.Pi/2, 1.3*geom.Pi) // a full turn is the circle
ring.PointAt(0.5) // (8.56,6.82), also Start, End and Length
cone := geom.Sect(geom.Pt(1.0, 5.0), 8.5, -geom.Pi/6, geom.Pi/3)
cone.Contains(geom.Pt(5.0, 8.5)) // false; the tolerance is on the distance, never an angle
route := geom.PolLn([]geom.Point[float64]{{1, 8}, {4, 2}, {6, 7}, {9, 2}})
for step := range route.Every(1) { // a point every unit of length, in one walk
}
curve := geom.CubBez(geom.Pt(1.0, 9.0), geom.Pt(1.0, 1.0), geom.Pt(9.0, 1.0), geom.Pt(9.0, 9.0))
curve.PointAt(0.5) // (5,3); the parameter is not a fraction of the length
curve.Polyline(0.2) // a path within the tolerance, to measure the curve byThe origin is top-left and +Y points down. Angles grow counterclockwise in math terms, so clockwise on screen, and
every outline winds that way.
dir := geom.DirectionUp
dir.Turn(2) // DirectionRight, two 45° steps
dir.Vector(5.0) // ⟨0,-5⟩
geom.Vec(3, -7).Direction() // DirectionUpRight, the nearest of the eight
geom.DirectionFromAxes(up, down, left, right) // keyboard input to an 8-way direction
geom.RotationMatrixAround(5.0, 5.0, geom.Pi/4) // turn about a pivot
geom.ScaleMatrixAround(5.0, 5.0, 1.5, 0.5) // scale about a pivot, which stays where it is
geom.ShearMatrix(0.5, 0.0) // x moved by half of y
m := geom.IdentityMatrix[float64]().Rotate(geom.Pi / 4).Scale(2, 2)
arrow.Transform(m) // every shape, point and vector takes a matrix
m.Angle() // π/4, read back from the matrixTranslate, Rotate, RotateAround, Scale, ScaleAround, Shear and Reflect compose with their constructor on
the right, applied first, and their Pre forms on the left, applied last.
geom.Remap(0.5, -1.0, 1.0, -30.0, 30.0) // 15, a stick axis as a steering angle
geom.Lerp(10.0, 20.0, 0.25) // 12.5
geom.InverseLerp(10.0, 20.0, 12.5) // 0.25, the ratio Lerp takes
geom.Clamp(15.0, 0.0, 10.0) // 10
geom.Snap(37, 16) // 32, the nearest multiple of the step
geom.Pt(37, 21).Snap(16) // (32,16), and on points and vectors
geom.LerpAngle(geom.ToRadians(350), geom.ToRadians(10), 0.5) // across the zero angle, the short wayAny type satisfying Number works, named types included. Int() rounds half away from zero, and the ints and
floats packages alias the two common instantiations.
geom.Pt(1.4, 2.6).Int() // (1,3)
geom.Pt(3.5, 1.5).Int() // (4,2), a half rounds away from zero
geom.Pt(6.6, 3.2).Int() // (7,3)
geom.Pt(2.5, 7.5).Int() // (3,8)
geom.Pt(7.2, 6.7).Int() // (7,7)
type Tile int32
geom.Pt[Tile](3, 4).Add(geom.DirectionRight.Unit[Tile]()) // (4,4)
ints.Pt(1.4, 2.6) // (1,3), from github.com/gravitton/geometry/types/intsInt() rounds a float rectangle onto the lattice. An integer rectangle is closed: its corners are lattice points on
the boundary, so a width of 6 spans 7 columns of them. The image.Rectangle it converts to is half-open and covers
exactly 6 pixels across.
f := geom.RectangleFromMin(geom.Pt(1.6, 2.3), geom.Sz(5.8, 5.6))
r := f.Int() // Rect((5,5);6x6), the center and size rounded
r.Rectangle() // (2,2)-(8,8), an image.Rectangle of 6x6 pixels
geom.RectangleFromImage[int](img.Bounds())
geom.BoxFromImage[int](img.Bounds()) // Min and Max as they are
json.Marshal(geom.Rect(geom.Pt(1, 2), geom.Sz(3, 4), 0)) // {"x":1,"y":2,"w":3,"h":4}
json.Marshal(geom.DirectionUp) // "Up"
geom.ParseSize[int]("4x2") // 4x2
geom.ParsePoint[int]("(1,2)") // (1,2)The geomtest package holds one assertion per type, with the tolerance of the asserted type, so the main package
imports no test library:
geomtest.AssertPoint(t, got, geom.Pt(1.0, 2.0))
geomtest.AssertRectangle(t, got, want, "after inset")Full reference: pkg.go.dev.
The MIT License (MIT). Please see License File for more information.