diff --git a/artist/shapes/doc.go b/artist/shapes/doc.go index ff03446..98eb8f6 100644 --- a/artist/shapes/doc.go +++ b/artist/shapes/doc.go @@ -1,6 +1,6 @@ // Package shapes provides some basic shape drawing routines. // -// A word about patterns: +// A word about using patterns with shape routines: // // Most drawing routines have a version that samples from other canvases, and a // version that samples from a solid color. None of these routines can use diff --git a/artist/shapes/ellipse.go b/artist/shapes/ellipse.go index 39b6f13..654b905 100644 --- a/artist/shapes/ellipse.go +++ b/artist/shapes/ellipse.go @@ -3,19 +3,25 @@ package shapes import "math" import "image" import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/artist" +// import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/canvas" -// FillEllipse draws a filled ellipse with the specified pattern. +// FillEllipse draws the content of one canvas onto another, clipped by an +// ellipse stretched to the bounds of the source canvas. 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 +// canvas's bounds from being used, it must be cut with canvas.Cut(). func FillEllipse ( destination canvas.Canvas, - source artist.Pattern, - bounds image.Rectangle, + source canvas.Canvas, + offset image.Point, ) ( updatedRegion image.Rectangle, ) { - bounds = bounds.Canon() - data, stride := destination.Buffer() + dstData, dstStride := destination.Buffer() + srcData, srcStride := source.Buffer() + + bounds := source.Bounds() realWidth, realHeight := bounds.Dx(), bounds.Dy() bounds = bounds.Intersect(destination.Bounds()).Canon() if bounds.Empty() { return } @@ -27,35 +33,38 @@ func FillEllipse ( 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 { - data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] = - source.AtWhen(x, y, realWidth, realHeight) + dstData[x + offset.X + (y + offset.Y) * dstStride] = + srcData[x + y * srcStride] } }} return } -// StrokeEllipse draws the outline of an ellipse with the specified line weight -// and pattern. +// StrokeRectangle 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(). func StrokeEllipse ( destination canvas.Canvas, - source artist.Pattern, - weight int, - bounds image.Rectangle, + source canvas.Canvas, + offset image.Point, + weight int, ) { if weight < 1 { return } - data, stride := destination.Buffer() - bounds = bounds.Canon().Inset(weight - 1) - width, height := bounds.Dx(), bounds.Dy() + dstData, dstStride := destination.Buffer() + srcData, srcStride := source.Buffer() + + bounds := source.Bounds().Inset(weight - 1) context := ellipsePlottingContext { - data: data, - stride: stride, - source: source, - width: width, - height: height, - weight: weight, - bounds: bounds, + dstData: dstData, + dstStride: dstStride, + srcData: srcData, + srcStride: srcStride, + weight: weight, + offset: offset, + bounds: bounds.Intersect(destination.Bounds()), } bounds.Max.X -= 1 @@ -129,18 +138,26 @@ func StrokeEllipse ( } type ellipsePlottingContext struct { - data []color.RGBA - stride int - source artist.Pattern - width, height int - weight int - bounds image.Rectangle + dstData []color.RGBA + dstStride int + srcData []color.RGBA + srcStride int + weight int + offset image.Point + bounds image.Rectangle } func (context ellipsePlottingContext) plot (x, y int) { - if (image.Point { x, y }).In(context.bounds) { - squareAround ( - context.data, context.stride, context.source, x, y, - context.width, context.height, context.weight) - } + square := + image.Rect(0, 0, context.weight, context.weight). + Sub(image.Pt(context.weight / 2, context.weight / 2)). + Add(image.Pt(x, y)). + Intersect(context.bounds) + + 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] + }} } diff --git a/artist/shapes/line.go b/artist/shapes/line.go index 2b92c59..85ed642 100644 --- a/artist/shapes/line.go +++ b/artist/shapes/line.go @@ -6,14 +6,14 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" // TODO: draw thick lines more efficiently -// Line draws a line from one point to another with the specified weight and -// pattern. -func Line ( +// ColorLine draws a line from one point to another with the specified weight +// and color. +func ColorLine ( destination canvas.Canvas, - source canvas.Canvas, - weight int, - min image.Point, - max image.Point, + color color.RGBA, + weight int, + min image.Point, + max image.Point, ) ( updatedRegion image.Rectangle, ) { @@ -21,43 +21,49 @@ func Line ( updatedRegion = image.Rectangle { Min: min, Max: max }.Canon() updatedRegion.Max.X ++ updatedRegion.Max.Y ++ - width := updatedRegion.Dx() - height := updatedRegion.Dy() - if abs(max.Y - min.Y) < - abs(max.X - min.X) { + data, stride := destination.Buffer() + bounds := destination.Bounds() + context := linePlottingContext { + dstData: data, + dstStride: stride, + color: color, + weight: weight, + bounds: bounds, + min: min, + max: max, + } + + if abs(max.Y - min.Y) < abs(max.X - min.X) { + if max.X < min.X { context.swap() } + context.lineLow() - if max.X < min.X { - temp := min - min = max - max = temp - } - lineLow(destination, source, weight, min, max, width, height) } else { - - if max.Y < min.Y { - temp := min - min = max - max = temp - } - lineHigh(destination, source, weight, min, max, width, height) + if max.Y < min.Y { context.swap() } + context.lineHigh() } return } -func lineLow ( - destination canvas.Canvas, - source Pattern, - weight int, - min image.Point, - max image.Point, - width, height int, -) { - data, stride := destination.Buffer() - bounds := destination.Bounds() +type linePlottingContext struct { + dstData []color.RGBA + dstStride int + color color.RGBA + weight int + bounds image.Rectangle + min image.Point + max image.Point +} - deltaX := max.X - min.X - deltaY := max.Y - min.Y +func (context *linePlottingContext) swap () { + temp := context.max + context.max = context.min + context.min = temp +} + +func (context linePlottingContext) lineLow () { + deltaX := context.max.X - context.min.X + deltaY := context.max.Y - context.min.Y yi := 1 if deltaY < 0 { @@ -66,34 +72,23 @@ func lineLow ( } D := (2 * deltaY) - deltaX - y := min.Y + point := context.min - for x := min.X; x < max.X; x ++ { - if !(image.Point { x, y }).In(bounds) { break } - squareAround(data, stride, source, x, y, width, height, weight) - // data[x + y * stride] = source.AtWhen(x, y, width, height) + for ; point.X < context.max.X; point.X ++ { + if !point.In(context.bounds) { break } + context.plot(point) if D > 0 { - y += yi D += 2 * (deltaY - deltaX) + point.Y += yi } else { D += 2 * deltaY } } } -func lineHigh ( - destination canvas.Canvas, - source Pattern, - weight int, - min image.Point, - max image.Point, - width, height int, -) { - data, stride := destination.Buffer() - bounds := destination.Bounds() - - deltaX := max.X - min.X - deltaY := max.Y - min.Y +func (context linePlottingContext) lineHigh () { + deltaX := context.max.X - context.min.X + deltaY := context.max.Y - context.min.Y xi := 1 if deltaX < 0 { @@ -102,14 +97,13 @@ func lineHigh ( } D := (2 * deltaX) - deltaY - x := min.X + point := context.min - for y := min.Y; y < max.Y; y ++ { - if !(image.Point { x, y }).In(bounds) { break } - squareAround(data, stride, source, x, y, width, height, weight) - // data[x + y * stride] = source.AtWhen(x, y, width, height) + for ; point.Y < context.max.Y; point.Y ++ { + if !point.In(context.bounds) { break } + context.plot(point) if D > 0 { - x += xi + point.X += xi D += 2 * (deltaX - deltaY) } else { D += 2 * deltaX @@ -117,27 +111,20 @@ func lineHigh ( } } -func abs (in int) (out int) { - if in < 0 { in *= -1} - out = in - return +func abs (n int) int { + if n < 0 { n *= -1} + return n } -// TODO: this method of doing things sucks and can cause a segfault. we should -// not be doing it this way -func squareAround ( - data []color.RGBA, - stride int, - source Pattern, - x, y, patternWidth, patternHeight, diameter int, -) { - minY := y - diameter + 1 - minX := x - diameter + 1 - maxY := y + diameter - maxX := x + diameter - for y = minY; y < maxY; y ++ { - for x = minX; x < maxX; x ++ { - data[x + y * stride] = - source.AtWhen(x, y, patternWidth, patternHeight) +func (context linePlottingContext) plot (center image.Point) { + square := + image.Rect(0, 0, context.weight, context.weight). + Sub(image.Pt(context.weight / 2, context.weight / 2)). + Add(center). + Intersect(context.bounds) + + 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.color }} } diff --git a/artist/shapes/rectangle.go b/artist/shapes/rectangle.go index 3569b19..2b59a59 100644 --- a/artist/shapes/rectangle.go +++ b/artist/shapes/rectangle.go @@ -4,11 +4,10 @@ import "image" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/shatter" -// FillRectangle draws a rectangular subset of one canvas onto the other. 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 canvas from being drawn, it must be cut with -// canvas.Cut(). +// 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 +// canvas from being drawn, it must be cut with canvas.Cut(). func FillRectangle ( destination canvas.Canvas, source canvas.Canvas, @@ -49,24 +48,7 @@ func StrokeRectangle ( FillRectangle(destination, source, offset) return } - - top := image.Rect ( - bounds.Min.X, bounds.Min.Y, - bounds.Max.X, insetBounds.Min.Y) - bottom := image.Rect ( - bounds.Min.X, insetBounds.Max.Y, - bounds.Max.X, bounds.Max.Y) - left := image.Rect ( - bounds.Min.X, insetBounds.Min.Y, - insetBounds.Min.X, insetBounds.Max.Y) - right := image.Rect ( - insetBounds.Max.X, insetBounds.Min.Y, - bounds.Max.X, insetBounds.Max.Y) - - FillRectangle (destination, canvas.Cut(source, top), offset) - FillRectangle (destination, canvas.Cut(source, bottom), offset) - FillRectangle (destination, canvas.Cut(source, left), offset) - FillRectangle (destination, canvas.Cut(source, right), offset) + FillRectangleShatter(destination, source, offset, insetBounds) } // FillRectangleShatter is like FillRectangle, but it does not draw in areas @@ -75,10 +57,10 @@ func FillRectangleShatter ( destination canvas.Canvas, source canvas.Canvas, offset image.Point, - rocks []image.Rectangle, + rocks ...image.Rectangle, ) { - tiles := shatter.Shatter(source.Bounds()) + tiles := shatter.Shatter(source.Bounds(), rocks...) for _, tile := range tiles { - tile + FillRectangle(destination, canvas.Cut(source, tile), offset) } }