Ellipse and rectangle have both color and source routines
This commit is contained in:
		
							parent
							
								
									211219eb01
								
							
						
					
					
						commit
						bf2fdb5eaa
					
				@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 (
 | 
			
		||||
 | 
			
		||||
@ -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]
 | 
			
		||||
	}}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user