shapes.go

  1// Copyright 2018 by the rasterx Authors. All rights reserved.
  2//_
  3// created: 2/06/2018 by S.R.Wiley
  4// Functions that rasterize common shapes easily.
  5
  6package rasterx
  7
  8import (
  9	"math"
 10
 11	"golang.org/x/image/math/fixed"
 12)
 13
 14// MaxDx is the Maximum radians a cubic splice is allowed to span
 15// in ellipse parametric when approximating an off-axis ellipse.
 16const MaxDx float64 = math.Pi / 8
 17
 18// ToFixedP converts two floats to a fixed point.
 19func ToFixedP(x, y float64) (p fixed.Point26_6) {
 20	p.X = fixed.Int26_6(x * 64)
 21	p.Y = fixed.Int26_6(y * 64)
 22	return
 23}
 24
 25// AddCircle adds a circle to the Adder p
 26func AddCircle(cx, cy, r float64, p Adder) {
 27	AddEllipse(cx, cy, r, r, 0, p)
 28}
 29
 30// AddEllipse adds an elipse with center at cx,cy, with the indicated
 31// x and y radius, (rx, ry), rotated around the center by rot degrees.
 32func AddEllipse(cx, cy, rx, ry, rot float64, p Adder) {
 33	rotRads := rot * math.Pi / 180
 34	px, py := Identity.
 35		Translate(cx, cy).Rotate(rotRads).Translate(-cx, -cy).Transform(cx+rx, cy)
 36	points := []float64{rx, ry, rot, 1.0, 0.0, px, py}
 37	p.Start(ToFixedP(px, py))
 38	AddArc(points, cx, cy, px, py, p)
 39	p.Stop(true)
 40}
 41
 42// AddRect adds a rectangle of the indicated size, rotated
 43// around the center by rot degrees.
 44func AddRect(minX, minY, maxX, maxY, rot float64, p Adder) {
 45	rot *= math.Pi / 180
 46	cx, cy := (minX+maxX)/2, (minY+maxY)/2
 47	m := Identity.Translate(cx, cy).Rotate(rot).Translate(-cx, -cy)
 48	q := &MatrixAdder{M: m, Adder: p}
 49	q.Start(ToFixedP(minX, minY))
 50	q.Line(ToFixedP(maxX, minY))
 51	q.Line(ToFixedP(maxX, maxY))
 52	q.Line(ToFixedP(minX, maxY))
 53	q.Stop(true)
 54}
 55
 56// AddRoundRect adds a rectangle of the indicated size, rotated
 57// around the center by rot degrees with rounded corners of radius
 58// rx in the x axis and ry in the y axis. gf specifes the shape of the
 59// filleting function. Valid values are RoundGap, QuadraticGap, CubicGap,
 60// FlatGap, or nil which defaults to a flat gap.
 61func AddRoundRect(minX, minY, maxX, maxY, rx, ry, rot float64, gf GapFunc, p Adder) {
 62	if rx <= 0 || ry <= 0 {
 63		AddRect(minX, minY, maxX, maxY, rot, p)
 64		return
 65	}
 66	rot *= math.Pi / 180
 67	if gf == nil {
 68		gf = FlatGap
 69	}
 70	w := maxX - minX
 71	if w < rx*2 {
 72		rx = w / 2
 73	}
 74	h := maxY - minY
 75	if h < ry*2 {
 76		ry = h / 2
 77	}
 78	stretch := rx / ry
 79	midY := minY + h/2
 80	m := Identity.Translate(minX+w/2, midY).Rotate(rot).Scale(1, 1/stretch).Translate(-minX-w/2, -minY-h/2)
 81	maxY = midY + h/2*stretch
 82	minY = midY - h/2*stretch
 83
 84	q := &MatrixAdder{M: m, Adder: p}
 85
 86	q.Start(ToFixedP(minX+rx, minY))
 87	q.Line(ToFixedP(maxX-rx, minY))
 88	gf(q, ToFixedP(maxX-rx, minY+rx), ToFixedP(0, -rx), ToFixedP(rx, 0))
 89	q.Line(ToFixedP(maxX, maxY-rx))
 90	gf(q, ToFixedP(maxX-rx, maxY-rx), ToFixedP(rx, 0), ToFixedP(0, rx))
 91	q.Line(ToFixedP(minX+rx, maxY))
 92	gf(q, ToFixedP(minX+rx, maxY-rx), ToFixedP(0, rx), ToFixedP(-rx, 0))
 93	q.Line(ToFixedP(minX, minY+rx))
 94	gf(q, ToFixedP(minX+rx, minY+rx), ToFixedP(-rx, 0), ToFixedP(0, -rx))
 95	q.Stop(true)
 96}
 97
 98//AddArc adds an arc to the adder p
 99func AddArc(points []float64, cx, cy, px, py float64, p Adder) (lx, ly float64) {
100	rotX := points[2] * math.Pi / 180 // Convert degress to radians
101	largeArc := points[3] != 0
102	sweep := points[4] != 0
103	startAngle := math.Atan2(py-cy, px-cx) - rotX
104	endAngle := math.Atan2(points[6]-cy, points[5]-cx) - rotX
105	deltaTheta := endAngle - startAngle
106	arcBig := math.Abs(deltaTheta) > math.Pi
107
108	// Approximate ellipse using cubic bezeir splines
109	etaStart := math.Atan2(math.Sin(startAngle)/points[1], math.Cos(startAngle)/points[0])
110	etaEnd := math.Atan2(math.Sin(endAngle)/points[1], math.Cos(endAngle)/points[0])
111	deltaEta := etaEnd - etaStart
112	if (arcBig && !largeArc) || (!arcBig && largeArc) { // Go has no boolean XOR
113		if deltaEta < 0 {
114			deltaEta += math.Pi * 2
115		} else {
116			deltaEta -= math.Pi * 2
117		}
118	}
119	// This check might be needed if the center point of the elipse is
120	// at the midpoint of the start and end lines.
121	if deltaEta < 0 && sweep {
122		deltaEta += math.Pi * 2
123	} else if deltaEta >= 0 && !sweep {
124		deltaEta -= math.Pi * 2
125	}
126
127	// Round up to determine number of cubic splines to approximate bezier curve
128	segs := int(math.Abs(deltaEta)/MaxDx) + 1
129	dEta := deltaEta / float64(segs) // span of each segment
130	// Approximate the ellipse using a set of cubic bezier curves by the method of
131	// L. Maisonobe, "Drawing an elliptical arc using polylines, quadratic
132	// or cubic Bezier curves", 2003
133	// https://www.spaceroots.org/documents/elllipse/elliptical-arc.pdf
134	tde := math.Tan(dEta / 2)
135	alpha := math.Sin(dEta) * (math.Sqrt(4+3*tde*tde) - 1) / 3 // Math is fun!
136	lx, ly = px, py
137	sinTheta, cosTheta := math.Sin(rotX), math.Cos(rotX)
138	ldx, ldy := ellipsePrime(points[0], points[1], sinTheta, cosTheta, etaStart, cx, cy)
139	for i := 1; i <= segs; i++ {
140		eta := etaStart + dEta*float64(i)
141		var px, py float64
142		if i == segs {
143			px, py = points[5], points[6] // Just makes the end point exact; no roundoff error
144		} else {
145			px, py = ellipsePointAt(points[0], points[1], sinTheta, cosTheta, eta, cx, cy)
146		}
147		dx, dy := ellipsePrime(points[0], points[1], sinTheta, cosTheta, eta, cx, cy)
148		p.CubeBezier(ToFixedP(lx+alpha*ldx, ly+alpha*ldy),
149			ToFixedP(px-alpha*dx, py-alpha*dy), ToFixedP(px, py))
150		lx, ly, ldx, ldy = px, py, dx, dy
151	}
152	return lx, ly
153}
154
155// ellipsePrime gives tangent vectors for parameterized elipse; a, b, radii, eta parameter, center cx, cy
156func ellipsePrime(a, b, sinTheta, cosTheta, eta, cx, cy float64) (px, py float64) {
157	bCosEta := b * math.Cos(eta)
158	aSinEta := a * math.Sin(eta)
159	px = -aSinEta*cosTheta - bCosEta*sinTheta
160	py = -aSinEta*sinTheta + bCosEta*cosTheta
161	return
162}
163
164// ellipsePointAt gives points for parameterized elipse; a, b, radii, eta parameter, center cx, cy
165func ellipsePointAt(a, b, sinTheta, cosTheta, eta, cx, cy float64) (px, py float64) {
166	aCosEta := a * math.Cos(eta)
167	bSinEta := b * math.Sin(eta)
168	px = cx + aCosEta*cosTheta - bSinEta*sinTheta
169	py = cy + aCosEta*sinTheta + bSinEta*cosTheta
170	return
171}
172
173// FindEllipseCenter locates the center of the Ellipse if it exists. If it does not exist,
174// the radius values will be increased minimally for a solution to be possible
175// while preserving the ra to rb ratio.  ra and rb arguments are pointers that can be
176// checked after the call to see if the values changed. This method uses coordinate transformations
177// to reduce the problem to finding the center of a circle that includes the origin
178// and an arbitrary point. The center of the circle is then transformed
179// back to the original coordinates and returned.
180func FindEllipseCenter(ra, rb *float64, rotX, startX, startY, endX, endY float64, sweep, smallArc bool) (cx, cy float64) {
181	cos, sin := math.Cos(rotX), math.Sin(rotX)
182
183	// Move origin to start point
184	nx, ny := endX-startX, endY-startY
185
186	// Rotate ellipse x-axis to coordinate x-axis
187	nx, ny = nx*cos+ny*sin, -nx*sin+ny*cos
188	// Scale X dimension so that ra = rb
189	nx *= *rb / *ra // Now the ellipse is a circle radius rb; therefore foci and center coincide
190
191	midX, midY := nx/2, ny/2
192	midlenSq := midX*midX + midY*midY
193
194	var hr float64
195	if *rb**rb < midlenSq {
196		// Requested ellipse does not exist; scale ra, rb to fit. Length of
197		// span is greater than max width of ellipse, must scale *ra, *rb
198		nrb := math.Sqrt(midlenSq)
199		if *ra == *rb {
200			*ra = nrb // prevents roundoff
201		} else {
202			*ra = *ra * nrb / *rb
203		}
204		*rb = nrb
205	} else {
206		hr = math.Sqrt(*rb**rb-midlenSq) / math.Sqrt(midlenSq)
207	}
208	// Notice that if hr is zero, both answers are the same.
209	if (sweep && smallArc) || (!sweep && !smallArc) {
210		cx = midX + midY*hr
211		cy = midY - midX*hr
212	} else {
213		cx = midX - midY*hr
214		cy = midY + midX*hr
215	}
216
217	// reverse scale
218	cx *= *ra / *rb
219	//Reverse rotate and translate back to original coordinates
220	return cx*cos - cy*sin + startX, cx*sin + cy*cos + startY
221}