Better Ellipse Drawing Algorithm

This commit is contained in:
Noah 2019-07-16 18:27:00 -07:00
parent 0c6c77a423
commit 17b18b8c0a
5 changed files with 96 additions and 101 deletions

16
TODO.md
View File

@ -86,3 +86,19 @@ In addition to those listed above:
* Client/server negotiation, protocol
* Client can request chunks from server for local rendering.
* Players send inputs over network sockets.
## Far Off Ideas
### Level Styles (Top-down vs Side Scrolling)
It might be cool to support multiple "styles" of level, apart from the current
2D platforming-with-gravity style.
For example a top-down perspective level would let the player freely walk in
both axes and would probably have a full set of unique Doodads drawn in that
perspective.
Migration path: a GameStyle enum would be added to Levels and Doodads as an
integer type, default 0 is the current 2D platformer style, 1 for top-down,
etc. -- so existing levels and doodads would default to 0 on upgrade and new
levels/doodads would use the new value.

64
lib/render/ellipse.go Normal file
View File

@ -0,0 +1,64 @@
package render
// MidpointEllipse implements an ellipse plotting algorithm.
func MidpointEllipse(center, radius Point) chan Point {
yield := make(chan Point)
go func() {
var (
pos = NewPoint(radius.X, 0)
delta = NewPoint(
2*radius.Y*radius.Y*pos.X,
2*radius.X*radius.X*pos.Y,
)
err = radius.X*radius.X -
radius.Y*radius.Y*radius.X +
(radius.Y*radius.Y)/4
)
for delta.Y < delta.X {
yield <- NewPoint(center.X+pos.X, center.Y+pos.Y)
yield <- NewPoint(center.X+pos.X, center.Y-pos.Y)
yield <- NewPoint(center.X-pos.X, center.Y+pos.Y)
yield <- NewPoint(center.X-pos.X, center.Y-pos.Y)
pos.Y++
if err < 0 {
delta.Y += 2 * radius.X * radius.X
err += delta.Y + radius.X*radius.X
} else {
pos.X--
delta.Y += 2 * radius.X * radius.X
delta.X -= 2 * radius.Y * radius.Y
err += delta.Y - delta.X + radius.X*radius.X
}
}
err = radius.X*radius.X*(pos.Y*pos.Y+pos.Y) +
radius.Y*radius.Y*(pos.X-1)*(pos.X-1) -
radius.Y*radius.Y*radius.X*radius.X
for pos.X >= 0 {
yield <- NewPoint(center.X+pos.X, center.Y+pos.Y)
yield <- NewPoint(center.X+pos.X, center.Y-pos.Y)
yield <- NewPoint(center.X-pos.X, center.Y+pos.Y)
yield <- NewPoint(center.X-pos.X, center.Y-pos.Y)
pos.X--
if err > 0 {
delta.X -= 2 * radius.Y * radius.Y
err += radius.Y*radius.Y - delta.X
} else {
pos.Y++
delta.Y += 2 * radius.X * radius.X
delta.X -= 2 * radius.Y * radius.Y
err += delta.Y - delta.X + radius.Y*radius.Y
}
}
close(yield)
}()
return yield
}

View File

@ -98,3 +98,11 @@ func TrimBox(src, dst *Rect, p Point, S Rect, thickness int32) {
dst.W = S.W - thickness
}
}
// AbsInt32 returns the absolute value of an int32.
func AbsInt32(v int32) int32 {
if v < 0 {
return -v
}
return v
}

View File

@ -2,8 +2,6 @@ package render
import (
"math"
"git.kirsle.net/apps/doodle/pkg/log"
)
// IterLine is a generator that returns the X,Y coordinates to draw a line.
@ -93,106 +91,15 @@ func IterRect(p1, p2 Point) chan Point {
return generator
}
// IterEllipse is a generator that draws out the pixels of an ellipse.
func IterEllipse(rx, ry, xc, yc float32) chan Point {
generator := make(chan Point)
mkPoint := func(x, y float32) Point {
return NewPoint(int32(x), int32(y))
}
go func() {
var (
dx float32
dy float32
d1 float32
d2 float32
x float32
y = ry
)
d1 = (ry * ry) - (rx * rx * ry) + (0.25 * rx * rx)
dx = 2 * ry * ry * x
dy = 2 * rx * rx * y
// For region 1
for dx < dy {
// Yields points based on 4-way symmetry.
for _, point := range []Point{
mkPoint(x+xc, y+yc),
mkPoint(-x+xc, y+yc),
mkPoint(x+xc, -y+yc),
mkPoint(-x+xc, -y+yc),
} {
generator <- point
}
if d1 < 0 {
x++
dx = dx + (2 * ry * ry)
d1 = d1 + dx + (ry * ry)
} else {
x++
y--
dx = dx + (2 * ry * ry)
dy = dy - (2 * rx * rx)
d1 = d1 + dx - dy + (ry * ry)
}
}
d2 = ((ry * ry) + ((x + 0.5) * (x + 0.5))) +
((rx * rx) * ((y - 1) * (y - 1))) -
(rx * rx * ry * ry)
// Region 2
for y >= 0 {
// Yields points based on 4-way symmetry.
for _, point := range []Point{
mkPoint(x+xc, y+yc),
mkPoint(-x+xc, y+yc),
mkPoint(x+xc, -y+yc),
mkPoint(-x+xc, -y+yc),
} {
generator <- point
}
if d2 > 0 {
y--
dy = dy - (2 * rx * rx)
d2 = d2 + (rx * rx) - dy
} else {
y--
x++
dx = dx + (2 * ry * ry)
dy = dy - (2 * rx * rx)
d2 = d2 + dx - dy + (rx * rx)
}
}
close(generator)
}()
return generator
}
// IterEllipse2 iterates an Ellipse using two Points as the top-left and
// IterEllipse iterates an Ellipse using two Points as the top-left and
// bottom-right corners of a rectangle that encompasses the ellipse.
func IterEllipse2(A, B Point) chan Point {
func IterEllipse(A, B Point) chan Point {
var (
// xc = float32(A.X+B.X) / 2
// yc = float32(A.Y+B.Y) / 2
xc = float32(B.X)
yc = float32(B.Y)
rx = float32(B.X - A.X)
ry = float32(B.Y - A.Y)
width = AbsInt32(B.X - A.X)
height = AbsInt32(B.Y - A.Y)
radius = NewPoint(width/2, height/2)
center = NewPoint(AbsInt32(B.X-radius.X), AbsInt32(B.Y-radius.Y))
)
if rx < 0 {
rx = -rx
}
if ry < 0 {
ry = -ry
}
log.Info("Ellipse btwn=%s-%s radius=%f,%f at center %f,%f", A, B, rx, ry, xc, yc)
return IterEllipse(rx, ry, xc, yc)
return MidpointEllipse(center, radius)
}

View File

@ -96,7 +96,7 @@ func (s *Stroke) IterPoints() chan render.Point {
ch <- point
}
case Ellipse:
for point := range render.IterEllipse2(s.PointA, s.PointB) {
for point := range render.IterEllipse(s.PointA, s.PointB) {
ch <- point
}
}