diff --git a/assets/sprites/ellipse-tool.png b/assets/sprites/ellipse-tool.png new file mode 100644 index 0000000..044de12 Binary files /dev/null and b/assets/sprites/ellipse-tool.png differ diff --git a/lib/render/canvas/draw.go b/lib/render/canvas/draw.go index 1f7b8be..f16381c 100644 --- a/lib/render/canvas/draw.go +++ b/lib/render/canvas/draw.go @@ -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), diff --git a/lib/render/interface.go b/lib/render/interface.go index 17883f9..c30948a 100644 --- a/lib/render/interface.go +++ b/lib/render/interface.go @@ -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 -} diff --git a/lib/render/shapes.go b/lib/render/shapes.go new file mode 100644 index 0000000..44caefe --- /dev/null +++ b/lib/render/shapes.go @@ -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) +} diff --git a/pkg/balance/debug.go b/pkg/balance/debug.go index 66349c4..561b3b2 100644 --- a/pkg/balance/debug.go +++ b/pkg/balance/debug.go @@ -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 diff --git a/pkg/collision/collide_level.go b/pkg/collision/collide_level.go index 1f5acf5..5d2a0e1 100644 --- a/pkg/collision/collide_level.go +++ b/pkg/collision/collide_level.go @@ -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 diff --git a/pkg/drawtool/shapes.go b/pkg/drawtool/shapes.go index 52cd740..9de4b18 100644 --- a/pkg/drawtool/shapes.go +++ b/pkg/drawtool/shapes.go @@ -8,5 +8,6 @@ const ( Freehand Shape = iota Line Rectangle + Ellipse Eraser // not really a shape but communicates the intention ) diff --git a/pkg/drawtool/stroke.go b/pkg/drawtool/stroke.go index 0c577f1..602b32c 100644 --- a/pkg/drawtool/stroke.go +++ b/pkg/drawtool/stroke.go @@ -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) }() diff --git a/pkg/drawtool/tools.go b/pkg/drawtool/tools.go index ddda5a7..0e0a320 100644 --- a/pkg/drawtool/tools.go +++ b/pkg/drawtool/tools.go @@ -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", diff --git a/pkg/editor_ui_toolbar.go b/pkg/editor_ui_toolbar.go index 8e563b1..a97397c 100644 --- a/pkg/editor_ui_toolbar.go +++ b/pkg/editor_ui_toolbar.go @@ -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", diff --git a/pkg/level/palette.go b/pkg/level/palette.go index 55c609b..232250f 100644 --- a/pkg/level/palette.go +++ b/pkg/level/palette.go @@ -22,7 +22,7 @@ func DefaultPalette() *Palette { }, &Swatch{ Name: "water", - Color: render.Blue, + Color: render.RGBA(0, 0, 255, 180), Water: true, }, }, diff --git a/pkg/uix/actor_collision.go b/pkg/uix/actor_collision.go index 7664865..0ef63f8 100644 --- a/pkg/uix/actor_collision.go +++ b/pkg/uix/actor_collision.go @@ -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, ) { diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index fd0027a..ebf78e2 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -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) } } diff --git a/wasm/main_wasm.go b/wasm/main_wasm.go index 0ce6181..e165aeb 100644 --- a/wasm/main_wasm.go +++ b/wasm/main_wasm.go @@ -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) }