Lemon-shaped Ellipse Tool (WIP)

* Add initial Ellipse Tool to the Editor Mode. Currently there's
  something wrong with the algorithm and the ellipses have a sort of
  'lemon shape' to them.
* Refactor the IterLine/IterLine2 functions to be more consistent.
  IterLine used to be the raw algorithm that took a bunch of coordinate
  numbers and IterLine2 took two render.Point's and was the main one
  used throughout the app. Now, IterLine takes the two Points and the
  raw algorithm function removed.
physics
Noah 2019-07-14 14:18:44 -07:00
parent cc1e441232
commit 0c6c77a423
14 changed files with 243 additions and 97 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

View File

@ -44,7 +44,7 @@ func (e *Engine) DrawPoint(color render.Color, point render.Point) {
// DrawLine draws a line between two points.
func (e *Engine) DrawLine(color render.Color, a, b render.Point) {
e.canvas.ctx2d.Set("fillStyle", RGBA(color))
for pt := range render.IterLine2(a, b) {
for pt := range render.IterLine(a, b) {
e.canvas.ctx2d.Call("fillRect",
int(pt.X),
int(pt.Y),

View File

@ -3,7 +3,6 @@ package render
import (
"fmt"
"image"
"math"
"git.kirsle.net/apps/doodle/lib/events"
)
@ -195,89 +194,3 @@ var (
Purple = RGBA(153, 0, 153, 255)
Pink = RGBA(255, 153, 255, 255)
)
// IterLine is a generator that returns the X,Y coordinates to draw a line.
// https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm)
func IterLine(x1, y1, x2, y2 int32) chan Point {
generator := make(chan Point)
go func() {
var (
dx = float64(x2 - x1)
dy = float64(y2 - y1)
)
var step float64
if math.Abs(dx) >= math.Abs(dy) {
step = math.Abs(dx)
} else {
step = math.Abs(dy)
}
dx = dx / step
dy = dy / step
x := float64(x1)
y := float64(y1)
for i := 0; i <= int(step); i++ {
generator <- Point{
X: int32(x),
Y: int32(y),
}
x += dx
y += dy
}
close(generator)
}()
return generator
}
// IterLine2 works with two Points rather than four coordinates.
func IterLine2(p1 Point, p2 Point) chan Point {
return IterLine(p1.X, p1.Y, p2.X, p2.Y)
}
// IterRect loops through all the points forming a rectangle between the
// top-left point and the bottom-right point.
func IterRect(p1, p2 Point) chan Point {
generator := make(chan Point)
go func() {
var (
TopLeft = p1
BottomRight = p2
TopRight = Point{
X: BottomRight.X,
Y: TopLeft.Y,
}
BottomLeft = Point{
X: TopLeft.X,
Y: BottomRight.Y,
}
dedupe = map[Point]interface{}{}
)
// Trace all four edges and yield it.
var edges = []struct {
A Point
B Point
}{
{TopLeft, TopRight},
{TopLeft, BottomLeft},
{BottomLeft, BottomRight},
{TopRight, BottomRight},
}
for _, edge := range edges {
for pt := range IterLine2(edge.A, edge.B) {
if _, ok := dedupe[pt]; !ok {
generator <- pt
dedupe[pt] = nil
}
}
}
close(generator)
}()
return generator
}

198
lib/render/shapes.go Normal file
View File

@ -0,0 +1,198 @@
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.
// https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm)
func IterLine(p1 Point, p2 Point) chan Point {
var (
x1 = p1.X
y1 = p1.Y
x2 = p2.X
y2 = p2.Y
)
generator := make(chan Point)
go func() {
var (
dx = float64(x2 - x1)
dy = float64(y2 - y1)
)
var step float64
if math.Abs(dx) >= math.Abs(dy) {
step = math.Abs(dx)
} else {
step = math.Abs(dy)
}
dx = dx / step
dy = dy / step
x := float64(x1)
y := float64(y1)
for i := 0; i <= int(step); i++ {
generator <- Point{
X: int32(x),
Y: int32(y),
}
x += dx
y += dy
}
close(generator)
}()
return generator
}
// IterRect loops through all the points forming a rectangle between the
// top-left point and the bottom-right point.
func IterRect(p1, p2 Point) chan Point {
generator := make(chan Point)
go func() {
var (
TopLeft = p1
BottomRight = p2
TopRight = Point{
X: BottomRight.X,
Y: TopLeft.Y,
}
BottomLeft = Point{
X: TopLeft.X,
Y: BottomRight.Y,
}
dedupe = map[Point]interface{}{}
)
// Trace all four edges and yield it.
var edges = []struct {
A Point
B Point
}{
{TopLeft, TopRight},
{TopLeft, BottomLeft},
{BottomLeft, BottomRight},
{TopRight, BottomRight},
}
for _, edge := range edges {
for pt := range IterLine(edge.A, edge.B) {
if _, ok := dedupe[pt]; !ok {
generator <- pt
dedupe[pt] = nil
}
}
}
close(generator)
}()
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
// bottom-right corners of a rectangle that encompasses the ellipse.
func IterEllipse2(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)
)
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)
}

View File

@ -25,7 +25,7 @@ var (
// Background color to use when exporting a drawing Chunk as a bitmap image
// on disk. Default is white. Setting this to translucent yellow is a great
// way to visualize the chunks loaded from cache on your screen.
DebugChunkBitmapBackground = render.White // XXX: export $DEBUG_CHUNK_COLOR
DebugChunkBitmapBackground = render.Invisible // XXX: export $DEBUG_CHUNK_COLOR
// Put a border around all Canvas widgets.
DebugCanvasBorder = render.Invisible

View File

@ -140,7 +140,7 @@ func CollidesWithGrid(d doodads.Actor, grid *level.Chunker, target render.Point)
// Trace a line from where we are to where we wanna go.
result.Reset()
result.MoveTo = P
for point := range render.IterLine2(P, target) {
for point := range render.IterLine(P, target) {
if has := result.ScanBoundingBox(render.Rect{
X: point.X,
Y: point.Y,
@ -239,7 +239,7 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool {
// for any pixels to be set, implying a collision between level geometry and the
// bounding boxes of the doodad.
func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) {
for point := range render.IterLine2(p1, p2) {
for point := range render.IterLine(p1, p2) {
if swatch, err := grid.Get(point); err == nil {
// We're intersecting a pixel! If it's a solid one we'll return it
// in our result. If non-solid, we'll collect attributes from it

View File

@ -8,5 +8,6 @@ const (
Freehand Shape = iota
Line
Rectangle
Ellipse
Eraser // not really a shape but communicates the intention
)

View File

@ -88,13 +88,17 @@ func (s *Stroke) IterPoints() chan render.Point {
ch <- point
}
case Line:
for point := range render.IterLine2(s.PointA, s.PointB) {
for point := range render.IterLine(s.PointA, s.PointB) {
ch <- point
}
case Rectangle:
for point := range render.IterRect(s.PointA, s.PointB) {
ch <- point
}
case Ellipse:
for point := range render.IterEllipse2(s.PointA, s.PointB) {
ch <- point
}
}
close(ch)
}()

View File

@ -8,6 +8,7 @@ const (
PencilTool Tool = iota // draw pixels where the mouse clicks
LineTool
RectTool
EllipseTool
ActorTool // drag and move actors
LinkTool
EraserTool
@ -17,6 +18,7 @@ var toolNames = []string{
"Pencil",
"Line",
"Rectangle",
"Ellipse",
"Doodad", // readable name for ActorTool
"Link",
"Eraser",

View File

@ -75,6 +75,16 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
},
},
{
Value: drawtool.EllipseTool.String(),
Icon: "assets/sprites/ellipse-tool.png",
Click: func() {
u.Canvas.Tool = drawtool.EllipseTool
showSwatchPalette()
d.Flash("Ellipse Tool selected.")
},
},
{
Value: drawtool.ActorTool.String(),
Icon: "assets/sprites/actor-tool.png",

View File

@ -22,7 +22,7 @@ func DefaultPalette() *Palette {
},
&Swatch{
Name: "water",
Color: render.Blue,
Color: render.RGBA(0, 0, 255, 180),
Water: true,
},
},

View File

@ -117,7 +117,7 @@ func (w *Canvas) loopActorCollision() error {
rect = doodads.GetBoundingRect(b)
hitbox = a.Hitbox()
)
for point := range render.IterLine2(
for point := range render.IterLine(
b.Position(),
origPoint,
) {

View File

@ -152,7 +152,7 @@ func (w *Canvas) loopEditable(ev *events.State) error {
if lastPixel != nil || lastPixel != pixel {
// Draw the pixels in between.
if lastPixel != nil && lastPixel != pixel {
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
for point := range render.IterLine(lastPixel.Point(), pixel.Point()) {
w.currentStroke.AddPoint(point)
}
}
@ -206,6 +206,24 @@ func (w *Canvas) loopEditable(ev *events.State) error {
w.AddStroke(w.currentStroke)
}
w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y)
} else {
w.commitStroke(w.Tool, true)
}
case drawtool.EllipseTool:
if w.Palette.ActiveSwatch == nil {
return nil
}
if ev.Button1.Now {
if w.currentStroke == nil {
w.currentStroke = drawtool.NewStroke(drawtool.Ellipse, w.Palette.ActiveSwatch.Color)
w.currentStroke.Thickness = w.BrushSize
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
w.AddStroke(w.currentStroke)
}
w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y)
} else {
w.commitStroke(w.Tool, true)
@ -241,7 +259,7 @@ func (w *Canvas) loopEditable(ev *events.State) error {
// Append unique new pixels.
if lastPixel == nil || lastPixel != pixel {
if lastPixel != nil && lastPixel != pixel {
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
for point := range render.IterLine(lastPixel.Point(), pixel.Point()) {
w.currentStroke.AddPoint(point)
}
}

View File

@ -51,7 +51,7 @@ func testRawCanvas() {
fmt.Printf("Got engine: %+v\n", engine)
engine.Clear(render.Green)
for pt := range render.IterLine2(render.NewPoint(20, 20), render.NewPoint(300, 300)) {
for pt := range render.IterLine(render.NewPoint(20, 20), render.NewPoint(300, 300)) {
engine.DrawPoint(render.Red, pt)
}