From bf2fdb5eaa3a510b4ef1b4ccbd1469f50181c8e5 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 24 Feb 2023 16:31:42 -0500 Subject: [PATCH] Ellipse and rectangle have both color and source routines --- artist/shapes/ellipse.go | 202 +++++++++++++++++++++++++++---------- artist/shapes/line.go | 2 - artist/shapes/plot.go | 14 ++- artist/shapes/rectangle.go | 57 ++++++++++- 4 files changed, 215 insertions(+), 60 deletions(-) diff --git a/artist/shapes/ellipse.go b/artist/shapes/ellipse.go index e4b46a5..3acc2e2 100644 --- a/artist/shapes/ellipse.go +++ b/artist/shapes/ellipse.go @@ -2,6 +2,7 @@ package shapes import "math" import "image" +import "image/color" import "git.tebibyte.media/sashakoshka/tomo/canvas" // FillEllipse draws the content of one canvas onto another, clipped by an @@ -19,26 +20,25 @@ func FillEllipse ( dstData, dstStride := destination.Buffer() srcData, srcStride := source.Buffer() - bounds := source.Bounds() - realWidth, realHeight := bounds.Dx(), bounds.Dy() - bounds = bounds.Intersect(destination.Bounds()).Canon() + bounds := source.Bounds().Intersect(destination.Bounds()).Canon() + realBounds := source.Bounds() if bounds.Empty() { return } updatedRegion = bounds - width, height := bounds.Dx(), bounds.Dy() - for y := 0; y < height; y ++ { - for x := 0; x < width; x ++ { - xf := (float64(x) + 0.5) / float64(realWidth) - 0.5 - yf := (float64(y) + 0.5) / float64(realHeight) - 0.5 - if math.Sqrt(xf * xf + yf * yf) <= 0.5 { - dstData[x + offset.X + (y + offset.Y) * dstStride] = - srcData[x + y * srcStride] + point := image.Point { } + for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { + for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { + if inEllipse(point, realBounds) { + offsetPoint := point.Add(offset) + dstIndex := offsetPoint.X + (offsetPoint.Y) * dstStride + srcIndex := point.X + point.Y * srcStride + dstData[dstIndex] = srcData[srcIndex] } }} return } -// StrokeRectangle is similar to FillEllipse, but it draws an elliptical inset +// StrokeEllipse is similar to FillEllipse, but it draws an elliptical inset // outline of the source canvas onto the destination canvas. To prevent the // entire source canvas's bounds from being used, it must be cut with // canvas.Cut(). @@ -55,82 +55,176 @@ func StrokeEllipse ( bounds := source.Bounds().Inset(weight - 1) - context := plottingContext { - dstData: dstData, - dstStride: dstStride, - srcData: srcData, - srcStride: srcStride, - weight: weight, - offset: offset, - bounds: bounds.Intersect(destination.Bounds()), + context := ellipsePlottingContext { + plottingContext: plottingContext { + dstData: dstData, + dstStride: dstStride, + srcData: srcData, + srcStride: srcStride, + weight: weight, + offset: offset, + bounds: bounds.Intersect(destination.Bounds()), + }, + radii: image.Pt(bounds.Dx() / 2 - 1, bounds.Dy() / 2 - 1), } - - bounds.Max.X -= 1 - bounds.Max.Y -= 1 + context.center = bounds.Min.Add(context.radii) + context.plotEllipse() +} - radii := image.Pt ( - bounds.Dx() / 2, - bounds.Dy() / 2) - center := bounds.Min.Add(radii) +type ellipsePlottingContext struct { + plottingContext + radii image.Point + center image.Point +} +func (context ellipsePlottingContext) plotEllipse () { x := float64(0) - y := float64(radii.Y) + y := float64(context.radii.Y) // region 1 decision parameter decision1 := - float64(radii.Y * radii.Y) - - float64(radii.X * radii.X * radii.Y) + - (0.25 * float64(radii.X) * float64(radii.X)) - decisionX := float64(2 * radii.Y * radii.Y * int(x)) - decisionY := float64(2 * radii.X * radii.X * int(y)) + float64(context.radii.Y * context.radii.Y) - + float64(context.radii.X * context.radii.X * context.radii.Y) + + (0.25 * float64(context.radii.X) * float64(context.radii.X)) + decisionX := float64(2 * context.radii.Y * context.radii.Y * int(x)) + decisionY := float64(2 * context.radii.X * context.radii.X * int(y)) // draw region 1 for decisionX < decisionY { - context.plotSource(image.Pt( int(x) + center.X, int(y) + center.Y)) - context.plotSource(image.Pt(-int(x) + center.X, int(y) + center.Y)) - context.plotSource(image.Pt( int(x) + center.X, -int(y) + center.Y)) - context.plotSource(image.Pt(-int(x) + center.X, -int(y) + center.Y)) + points := []image.Point { + image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y), + image.Pt( int(x) + context.center.X, -int(y) + context.center.Y), + image.Pt(-int(x) + context.center.X, int(y) + context.center.Y), + image.Pt( int(x) + context.center.X, int(y) + context.center.Y), + } + if context.srcData == nil { + context.plotColor(points[0]) + context.plotColor(points[1]) + context.plotColor(points[2]) + context.plotColor(points[3]) + } else { + context.plotSource(points[0]) + context.plotSource(points[1]) + context.plotSource(points[2]) + context.plotSource(points[3]) + } if (decision1 < 0) { x ++ - decisionX += float64(2 * radii.Y * radii.Y) - decision1 += decisionX + float64(radii.Y * radii.Y) + decisionX += float64(2 * context.radii.Y * context.radii.Y) + decision1 += decisionX + float64(context.radii.Y * context.radii.Y) } else { x ++ y -- - decisionX += float64(2 * radii.Y * radii.Y) - decisionY -= float64(2 * radii.X * radii.X) + decisionX += float64(2 * context.radii.Y * context.radii.Y) + decisionY -= float64(2 * context.radii.X * context.radii.X) decision1 += decisionX - decisionY + - float64(radii.Y * radii.Y) + float64(context.radii.Y * context.radii.Y) } } // region 2 decision parameter decision2 := - float64(radii.Y * radii.Y) * (x + 0.5) * (x + 0.5) + - float64(radii.X * radii.X) * (y - 1) * (y - 1) - - float64(radii.X * radii.X * radii.Y * radii.Y) + float64(context.radii.Y * context.radii.Y) * (x + 0.5) * (x + 0.5) + + float64(context.radii.X * context.radii.X) * (y - 1) * (y - 1) - + float64(context.radii.X * context.radii.X * context.radii.Y * context.radii.Y) // draw region 2 for y >= 0 { - context.plotSource(image.Pt( int(x) + center.X, int(y) + center.Y)) - context.plotSource(image.Pt(-int(x) + center.X, int(y) + center.Y)) - context.plotSource(image.Pt( int(x) + center.X, -int(y) + center.Y)) - context.plotSource(image.Pt(-int(x) + center.X, -int(y) + center.Y)) + points := []image.Point { + image.Pt( int(x) + context.center.X, int(y) + context.center.Y), + image.Pt(-int(x) + context.center.X, int(y) + context.center.Y), + image.Pt( int(x) + context.center.X, -int(y) + context.center.Y), + image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y), + } + if context.srcData == nil { + context.plotColor(points[0]) + context.plotColor(points[1]) + context.plotColor(points[2]) + context.plotColor(points[3]) + } else { + context.plotSource(points[0]) + context.plotSource(points[1]) + context.plotSource(points[2]) + context.plotSource(points[3]) + } if decision2 > 0 { y -- - decisionY -= float64(2 * radii.X * radii.X) - decision2 += float64(radii.X * radii.X) - decisionY + decisionY -= float64(2 * context.radii.X * context.radii.X) + decision2 += float64(context.radii.X * context.radii.X) - decisionY } else { y -- x ++ - decisionX += float64(2 * radii.Y * radii.Y) - decisionY -= float64(2 * radii.X * radii.X) + decisionX += float64(2 * context.radii.Y * context.radii.Y) + decisionY -= float64(2 * context.radii.X * context.radii.X) decision2 += decisionX - decisionY + - float64(radii.X * radii.X) + float64(context.radii.X * context.radii.X) } } } + +// FillColorEllipse fills an ellipse within the destination canvas with a solid +// color. +func FillColorEllipse ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, +) ( + updatedRegion image.Rectangle, +) { + dstData, dstStride := destination.Buffer() + + realBounds := bounds + bounds = bounds.Intersect(destination.Bounds()).Canon() + if bounds.Empty() { return } + updatedRegion = bounds + + point := image.Point { } + for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { + for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { + if inEllipse(point, realBounds) { + dstData[point.X + point.Y * dstStride] = color + } + }} + return +} + +// StrokeColorEllipse is similar to FillColorEllipse, but it draws an inset +// outline of an ellipse instead. +func StrokeColorEllipse ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, + weight int, +) ( + updatedRegion image.Rectangle, +) { + if weight < 1 { return } + + dstData, dstStride := destination.Buffer() + bounds = bounds.Inset(weight - 1) + + context := ellipsePlottingContext { + plottingContext: plottingContext { + dstData: dstData, + dstStride: dstStride, + color: color, + weight: weight, + bounds: bounds.Intersect(destination.Bounds()), + }, + radii: image.Pt(bounds.Dx() / 2 - 1, bounds.Dy() / 2 - 1), + } + context.center = bounds.Min.Add(context.radii) + context.plotEllipse() + return +} + +func inEllipse (point image.Point, bounds image.Rectangle) bool { + point = point.Sub(bounds.Min) + x := (float64(point.X) + 0.5) / float64(bounds.Dx()) - 0.5 + y := (float64(point.Y) + 0.5) / float64(bounds.Dy()) - 0.5 + return math.Hypot(x, y) <= 0.5 +} diff --git a/artist/shapes/line.go b/artist/shapes/line.go index 6d4050e..1ebfbbb 100644 --- a/artist/shapes/line.go +++ b/artist/shapes/line.go @@ -4,8 +4,6 @@ import "image" import "image/color" import "git.tebibyte.media/sashakoshka/tomo/canvas" -// TODO: draw thick lines more efficiently - // ColorLine draws a line from one point to another with the specified weight // and color. func ColorLine ( diff --git a/artist/shapes/plot.go b/artist/shapes/plot.go index a35751a..b629bd9 100644 --- a/artist/shapes/plot.go +++ b/artist/shapes/plot.go @@ -3,6 +3,8 @@ package shapes import "image" import "image/color" +// FIXME? drawing a ton of overlapping squares might be a bit wasteful. + type plottingContext struct { dstData []color.RGBA dstStride int @@ -18,6 +20,7 @@ func (context plottingContext) square (center image.Point) image.Rectangle { return image.Rect(0, 0, context.weight, context.weight). Sub(image.Pt(context.weight / 2, context.weight / 2)). Add(center). + Add(context.offset). Intersect(context.bounds) } @@ -33,8 +36,13 @@ func (context plottingContext) plotSource (center image.Point) { square := context.square(center) for y := square.Min.Y; y < square.Min.Y; y ++ { for x := square.Min.X; x < square.Min.X; x ++ { - context.dstData[x + y * context.dstStride] = - context.srcData [ - x + y * context.dstStride] + // we offset srcIndex here because we have already applied the + // offset to the square, and we need to reverse that to get the + // proper source coordinates. + srcIndex := + x - context.offset.X + + (y - context.offset.Y) * context.dstStride + dstIndex := x + y * context.dstStride + context.dstData[dstIndex] = context.srcData [srcIndex] }} } diff --git a/artist/shapes/rectangle.go b/artist/shapes/rectangle.go index 2b59a59..9e6f86f 100644 --- a/artist/shapes/rectangle.go +++ b/artist/shapes/rectangle.go @@ -1,9 +1,12 @@ package shapes import "image" +import "image/color" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/shatter" +// TODO: return updatedRegion for all routines in this package + // FillRectangle draws the content of one canvas onto another. The offset point // defines where the origin point of the source canvas is positioned in relation // to the origin point of the destination canvas. To prevent the entire source @@ -59,8 +62,60 @@ func FillRectangleShatter ( offset image.Point, rocks ...image.Rectangle, ) { - tiles := shatter.Shatter(source.Bounds(), rocks...) + tiles := shatter.Shatter(source.Bounds().Sub(offset), rocks...) for _, tile := range tiles { FillRectangle(destination, canvas.Cut(source, tile), offset) } } + +// FillColorRectangle fills a rectangle within the destination canvas with a +// solid color. +func FillColorRectangle ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, +) ( + updatedRegion image.Rectangle, +) { + dstData, dstStride := destination.Buffer() + bounds = bounds.Canon().Intersect(destination.Bounds()) + if bounds.Empty() { return } + + updatedRegion = bounds + for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { + for x := bounds.Min.X; x < bounds.Max.X; x ++ { + dstData[x + y * dstStride] = color + }} + + return +} + +// FillColorRectangleShatter is like FillColorRectangle, but it does not draw in +// areas specified in "rocks". +func FillColorRectangleShatter ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, + rocks ...image.Rectangle, +) { + tiles := shatter.Shatter(bounds, rocks...) + for _, tile := range tiles { + FillColorRectangle(destination, color, tile) + } +} + +// StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset +// outline of the given rectangle instead. +func StrokeColorRectangle ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, + weight int, +) { + insetBounds := bounds.Inset(weight) + if insetBounds.Empty() { + FillColorRectangle(destination, color, bounds) + return + } + FillColorRectangleShatter(destination, color, bounds, insetBounds) +}