diff --git a/drawing.go b/drawing.go index ff369eb..103dc91 100644 --- a/drawing.go +++ b/drawing.go @@ -5,9 +5,11 @@ import ( "math/rand" "github.com/bit101/blgo/blmath" + "github.com/bit101/blgo/floodfill" "github.com/bit101/blgo/geom" ) +// Plot draws a single pixel. func (s *Surface) Plot(p *geom.Point) { s.Save() s.Translate(p.X, p.Y) @@ -473,6 +475,15 @@ func (s *Surface) StrokeMultiLoop(points []*geom.Point) { s.Stroke() } +//////////////////////////////////////// +// FloodFill +//////////////////////////////////////// + +// FloodFill fills adjacent pixels with a specified color +func (s *Surface) FloodFill(x, y, r, g, b, threshold float64) { + floodfill.FloodFill(s, x, y, r, g, b, threshold) +} + //////////////////////////////////////// // Grid //////////////////////////////////////// diff --git a/floodfill/coord.go b/floodfill/coord.go new file mode 100644 index 0000000..37bc1e4 --- /dev/null +++ b/floodfill/coord.go @@ -0,0 +1,13 @@ +package floodfill + +type coord struct { + index int + x int + y int +} + +func newCoord(x, y, w int) coord { + // we get width passed so we can precompute the data index + index := (y*w + x) * 4 + return coord{index: index, x: x, y: y} +} diff --git a/floodfill/floodfill.go b/floodfill/floodfill.go new file mode 100644 index 0000000..0e3ff1b --- /dev/null +++ b/floodfill/floodfill.go @@ -0,0 +1,68 @@ +package floodfill + +// Surface is an interface for a surface used for performing flood fills +// Used to avoid import cycles +type Surface interface { + GetHeight() int + GetWidth() int + SetData([]byte) + GetData() []byte +} + +// FloodFill fills a surface from the specified point with the specified color +func FloodFill(surface Surface, x, y, r, g, b float64, threshold float64) { + // surface uses float values, but pixel data is all int values. convert. + xi, yi := int(x), int(y) + w, h := surface.GetWidth(), surface.GetHeight() + + // if pixel outside of image, return + if xi < 0 || xi >= w || yi < 0 || yi >= h { + return + } + + // more int conversions + ri, gi, bi, th := int(r*255), int(g*255), int(b*255), int(threshold*255) + + data := surface.GetData() + nextCoord := newCoord(xi, yi, w) + + // color of target color and replacement color for fill + targetRGB := getRGB(data, nextCoord) + replacementRGB := newRGB(ri, gi, bi) + + // target color is already replacement color, return + if targetRGB.isEqual(replacementRGB, th) { + return + } + + // change color of first pixel to replacement color, add coord to queue + setRGB(data, nextCoord, replacementRGB) + var queue []coord + queue = append(queue, nextCoord) + + // helper func. if coord is in bounds and has target color, change to replacement color and add to queue + checkNeighbor := func(x, y int) { + neighbor := newCoord(x, y, w) + if neighbor.x < w-1 && neighbor.x > 0 && neighbor.y < h-1 && neighbor.y > 0 && + getRGB(data, neighbor).isEqual(targetRGB, th) { + + setRGB(data, neighbor, replacementRGB) + queue = append(queue, neighbor) + } + } + + // while there are items in the queue + for len(queue) > 0 { + // shift the first one off (it's already been colored + nextCoord, queue = queue[0], queue[1:] + + // check four surrounding coords + checkNeighbor(nextCoord.x+1, nextCoord.y) + checkNeighbor(nextCoord.x-1, nextCoord.y) + checkNeighbor(nextCoord.x, nextCoord.y+1) + checkNeighbor(nextCoord.x, nextCoord.y-1) + } + + // queue is empty. rewrite data + surface.SetData(data) +} diff --git a/floodfill/rgb.go b/floodfill/rgb.go new file mode 100644 index 0000000..647b182 --- /dev/null +++ b/floodfill/rgb.go @@ -0,0 +1,39 @@ +package floodfill + +type rgb struct { + r int + g int + b int +} + +func newRGB(r, g, b int) rgb { + return rgb{ + r: r, + g: g, + b: b, + } +} + +func (p rgb) isEqual(q rgb, max int) bool { + return abs(p.r, q.r) <= max && abs(p.g, q.g) <= max && abs(p.b, q.b) <= max +} + +func abs(a, b int) int { + diff := a - b + if diff < 0 { + return -diff + } + return diff +} + +func getRGB(data []byte, c coord) rgb { + // stored as b, g, r, a + return newRGB(int(data[c.index+2]), int(data[c.index+1]), int(data[c.index])) +} + +func setRGB(data []byte, c coord, replacementRGB rgb) { + // stored as b, g, r, a + data[c.index+2] = byte(replacementRGB.r) + data[c.index+1] = byte(replacementRGB.g) + data[c.index] = byte(replacementRGB.b) +} diff --git a/surface.go b/surface.go index 6653429..db1980b 100644 --- a/surface.go +++ b/surface.go @@ -71,7 +71,7 @@ func (s *Surface) SetSourceColor(color color.Color) { s.SetSourceRGBA(color.R, color.G, color.B, color.A) } -// GetPixel returns the r, g, b, a value at a given x, y location. +// GetPixel returns the b, g, r, a value at a given x, y location. func (s *Surface) GetPixel(x int, y int) (byte, byte, byte, byte) { data := s.GetData() index := (y*s.GetWidth() + x) * 4