data-oriented-patterns #9
							
								
								
									
										12
									
								
								artist/color.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								artist/color.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
package artist
 | 
			
		||||
 | 
			
		||||
import "image/color"
 | 
			
		||||
 | 
			
		||||
// Hex creates a color.RGBA value from an RGBA integer value.
 | 
			
		||||
func Hex (color uint32) (c color.RGBA) {
 | 
			
		||||
	c.A = uint8(color)
 | 
			
		||||
	c.B = uint8(color >>  8)
 | 
			
		||||
	c.G = uint8(color >> 16)
 | 
			
		||||
	c.R = uint8(color >> 24)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
@ -3,6 +3,7 @@ package patterns
 | 
			
		||||
import "image"
 | 
			
		||||
import "image/color"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
 | 
			
		||||
 | 
			
		||||
// Uniform is a pattern that draws a solid color.
 | 
			
		||||
@ -15,13 +16,5 @@ func (pattern Uniform) Draw (destination canvas.Canvas, clip image.Rectangle) {
 | 
			
		||||
 | 
			
		||||
// Uhex creates a new Uniform pattern from an RGBA integer value.
 | 
			
		||||
func Uhex (color uint32) (uniform Uniform) {
 | 
			
		||||
	return Uniform(hex(color))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func hex (color uint32) (c color.RGBA) {
 | 
			
		||||
	c.A = uint8(color)
 | 
			
		||||
	c.B = uint8(color >>  8)
 | 
			
		||||
	c.G = uint8(color >> 16)
 | 
			
		||||
	c.R = uint8(color >> 24)
 | 
			
		||||
	return
 | 
			
		||||
	return Uniform(artist.Hex(color))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,23 +5,23 @@ import "image"
 | 
			
		||||
import "image/color"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
 | 
			
		||||
 | 
			
		||||
// 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().
 | 
			
		||||
// TODO: redo fill ellipse, stroke ellipse, etc. so that it only takes in
 | 
			
		||||
// destination and source, using the bounds of destination as the bounds of the
 | 
			
		||||
// ellipse and the bounds of source as the "clipping rectangle". Line up the Min
 | 
			
		||||
// of both canvases.
 | 
			
		||||
 | 
			
		||||
func FillEllipse (
 | 
			
		||||
	destination canvas.Canvas,
 | 
			
		||||
	source      canvas.Canvas,
 | 
			
		||||
	offset      image.Point,
 | 
			
		||||
) (
 | 
			
		||||
	updatedRegion image.Rectangle,
 | 
			
		||||
) {
 | 
			
		||||
	dstData, dstStride := destination.Buffer()
 | 
			
		||||
	srcData, srcStride := source.Buffer()
 | 
			
		||||
	
 | 
			
		||||
	bounds     := source.Bounds().Intersect(destination.Bounds()).Canon()
 | 
			
		||||
	realBounds := source.Bounds()
 | 
			
		||||
	offset := source.Bounds().Min.Sub(destination.Bounds().Min)
 | 
			
		||||
	bounds     := source.Bounds().Sub(offset).Intersect(destination.Bounds())
 | 
			
		||||
	realBounds := destination.Bounds()
 | 
			
		||||
	if bounds.Empty() { return }
 | 
			
		||||
	updatedRegion = bounds
 | 
			
		||||
 | 
			
		||||
@ -30,22 +30,17 @@ func FillEllipse (
 | 
			
		||||
	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
 | 
			
		||||
			dstIndex := point.X       + point.Y       * dstStride
 | 
			
		||||
			srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
 | 
			
		||||
			dstData[dstIndex] = srcData[srcIndex]
 | 
			
		||||
		}
 | 
			
		||||
	}}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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().
 | 
			
		||||
func StrokeEllipse (
 | 
			
		||||
	destination canvas.Canvas,
 | 
			
		||||
	source      canvas.Canvas,
 | 
			
		||||
	offset      image.Point,
 | 
			
		||||
	weight      int,
 | 
			
		||||
) {
 | 
			
		||||
	if weight < 1 { return }
 | 
			
		||||
@ -53,7 +48,10 @@ func StrokeEllipse (
 | 
			
		||||
	dstData, dstStride := destination.Buffer()
 | 
			
		||||
	srcData, srcStride := source.Buffer()
 | 
			
		||||
	
 | 
			
		||||
	bounds := source.Bounds().Inset(weight - 1)
 | 
			
		||||
	bounds := destination.Bounds().Inset(weight - 1)
 | 
			
		||||
	offset := source.Bounds().Min.Sub(destination.Bounds().Min)
 | 
			
		||||
	realBounds := destination.Bounds()
 | 
			
		||||
	if bounds.Empty() { return }
 | 
			
		||||
 | 
			
		||||
	context := ellipsePlottingContext {
 | 
			
		||||
		plottingContext: plottingContext {
 | 
			
		||||
@ -63,9 +61,9 @@ func StrokeEllipse (
 | 
			
		||||
			srcStride: srcStride,
 | 
			
		||||
			weight:    weight,
 | 
			
		||||
			offset:    offset,
 | 
			
		||||
			bounds:    bounds.Intersect(destination.Bounds()),
 | 
			
		||||
			bounds:    realBounds,
 | 
			
		||||
		},
 | 
			
		||||
		radii:  image.Pt(bounds.Dx() / 2 - 1, bounds.Dy() / 2 - 1),
 | 
			
		||||
		radii:  image.Pt(bounds.Dx() / 2, bounds.Dy() / 2),
 | 
			
		||||
	}
 | 
			
		||||
	context.center = bounds.Min.Add(context.radii)
 | 
			
		||||
	context.plotEllipse()
 | 
			
		||||
@ -205,7 +203,7 @@ func StrokeColorEllipse (
 | 
			
		||||
	if weight < 1 { return }
 | 
			
		||||
 | 
			
		||||
	dstData, dstStride := destination.Buffer()
 | 
			
		||||
	bounds = bounds.Inset(weight - 1)
 | 
			
		||||
	insetBounds := bounds.Inset(weight - 1)
 | 
			
		||||
 | 
			
		||||
	context := ellipsePlottingContext {
 | 
			
		||||
		plottingContext: plottingContext {
 | 
			
		||||
@ -215,9 +213,9 @@ func StrokeColorEllipse (
 | 
			
		||||
			weight:    weight,
 | 
			
		||||
			bounds:    bounds.Intersect(destination.Bounds()),
 | 
			
		||||
		},
 | 
			
		||||
		radii:  image.Pt(bounds.Dx() / 2 - 1, bounds.Dy() / 2 - 1),
 | 
			
		||||
		radii: image.Pt(insetBounds.Dx() / 2, insetBounds.Dy() / 2),
 | 
			
		||||
	}
 | 
			
		||||
	context.center = bounds.Min.Add(context.radii)
 | 
			
		||||
	context.center = insetBounds.Min.Add(context.radii)
 | 
			
		||||
	context.plotEllipse()
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,32 +16,31 @@ type plottingContext struct {
 | 
			
		||||
	bounds    image.Rectangle
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (context plottingContext) square (center image.Point) image.Rectangle {
 | 
			
		||||
func (context plottingContext) square (center image.Point) (square 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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (context plottingContext) plotColor (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 ++ {
 | 
			
		||||
	for y := square.Min.Y; y < square.Max.Y; y ++ {
 | 
			
		||||
	for x := square.Min.X; x < square.Max.X; x ++ {
 | 
			
		||||
		context.dstData[x + y * context.dstStride] = context.color
 | 
			
		||||
	}}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 ++ {
 | 
			
		||||
	for y := square.Min.Y; y < square.Max.Y; y ++ {
 | 
			
		||||
	for x := square.Min.X; x < square.Max.X; x ++ {
 | 
			
		||||
		// 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
 | 
			
		||||
			x + context.offset.X +
 | 
			
		||||
			(y + context.offset.Y) * context.dstStride
 | 
			
		||||
		dstIndex := x + y * context.dstStride
 | 
			
		||||
		context.dstData[dstIndex] = context.srcData [srcIndex]
 | 
			
		||||
	}}
 | 
			
		||||
 | 
			
		||||
@ -7,51 +7,44 @@ 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
 | 
			
		||||
// canvas from being drawn, it must be cut with canvas.Cut().
 | 
			
		||||
func FillRectangle (
 | 
			
		||||
	destination canvas.Canvas,
 | 
			
		||||
	source      canvas.Canvas,
 | 
			
		||||
	offset      image.Point,
 | 
			
		||||
) (
 | 
			
		||||
	updatedRegion image.Rectangle,
 | 
			
		||||
) {
 | 
			
		||||
	dstData, dstStride := destination.Buffer()
 | 
			
		||||
	srcData, srcStride := source.Buffer()
 | 
			
		||||
 | 
			
		||||
	sourceBounds :=
 | 
			
		||||
		source.Bounds().Canon().
 | 
			
		||||
		Intersect(destination.Bounds().Sub(offset))
 | 
			
		||||
	if sourceBounds.Empty() { return }
 | 
			
		||||
	offset := source.Bounds().Min.Sub(destination.Bounds().Min)
 | 
			
		||||
	bounds     := source.Bounds().Sub(offset).Intersect(destination.Bounds())
 | 
			
		||||
	if bounds.Empty() { return }
 | 
			
		||||
	updatedRegion = bounds
 | 
			
		||||
	
 | 
			
		||||
	updatedRegion = sourceBounds.Add(offset)
 | 
			
		||||
	for y := sourceBounds.Min.Y; y < sourceBounds.Max.Y; y ++ {
 | 
			
		||||
	for x := sourceBounds.Min.X; x < sourceBounds.Max.X; x ++ {
 | 
			
		||||
		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 ++ {
 | 
			
		||||
		offsetPoint := point.Add(offset)
 | 
			
		||||
		dstIndex := point.X       + point.Y       * dstStride
 | 
			
		||||
		srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
 | 
			
		||||
		dstData[dstIndex] = srcData[srcIndex]
 | 
			
		||||
	}}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StrokeRectangle is similar to FillRectangle, but it draws an 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 StrokeRectangle (
 | 
			
		||||
	destination canvas.Canvas,
 | 
			
		||||
	source      canvas.Canvas,
 | 
			
		||||
	offset      image.Point,
 | 
			
		||||
	weight      int,
 | 
			
		||||
) {
 | 
			
		||||
	bounds := source.Bounds()
 | 
			
		||||
	bounds := destination.Bounds()
 | 
			
		||||
	insetBounds := bounds.Inset(weight)
 | 
			
		||||
	if insetBounds.Empty() {
 | 
			
		||||
		FillRectangle(destination, source, offset)
 | 
			
		||||
		FillRectangle(destination, source)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	FillRectangleShatter(destination, source, offset, insetBounds)
 | 
			
		||||
	FillRectangleShatter(destination, source, insetBounds)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FillRectangleShatter is like FillRectangle, but it does not draw in areas
 | 
			
		||||
@ -59,12 +52,14 @@ func StrokeRectangle (
 | 
			
		||||
func FillRectangleShatter (
 | 
			
		||||
	destination canvas.Canvas,
 | 
			
		||||
	source      canvas.Canvas,
 | 
			
		||||
	offset      image.Point,
 | 
			
		||||
	rocks       ...image.Rectangle,
 | 
			
		||||
) {
 | 
			
		||||
	tiles := shatter.Shatter(source.Bounds().Sub(offset), rocks...)
 | 
			
		||||
	tiles  := shatter.Shatter(destination.Bounds(), rocks...)
 | 
			
		||||
	offset := source.Bounds().Min.Sub(destination.Bounds().Min)
 | 
			
		||||
	for _, tile := range tiles {
 | 
			
		||||
		FillRectangle(destination, canvas.Cut(source, tile), offset)
 | 
			
		||||
		FillRectangle (
 | 
			
		||||
			canvas.Cut(destination, tile),
 | 
			
		||||
			canvas.Cut(source, tile.Add(offset)))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,11 +4,14 @@ import "fmt"
 | 
			
		||||
import "time"
 | 
			
		||||
import "image"
 | 
			
		||||
import "image/color"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/shatter"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
 | 
			
		||||
 | 
			
		||||
// Artist is an element that displays shapes and patterns drawn by the artist
 | 
			
		||||
// package in order to test it.
 | 
			
		||||
@ -21,103 +24,52 @@ type Artist struct {
 | 
			
		||||
func NewArtist () (element *Artist) {
 | 
			
		||||
	element = &Artist { }
 | 
			
		||||
	element.Core, element.core = core.NewCore(element.draw)
 | 
			
		||||
	element.core.SetMinimumSize(240, 360)
 | 
			
		||||
	element.core.SetMinimumSize(240, 240)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (element *Artist) draw () {
 | 
			
		||||
	bounds := element.Bounds()
 | 
			
		||||
	artist.FillRectangle(element.core, artist.NewUniform(hex(0)), bounds)
 | 
			
		||||
	patterns.Uhex(0x000000FF).Draw(element.core, bounds)
 | 
			
		||||
 | 
			
		||||
	drawStart := time.Now()
 | 
			
		||||
 | 
			
		||||
	// 0, 0
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.Beveled {
 | 
			
		||||
			artist.NewUniform(hex(0xFF0000FF)),
 | 
			
		||||
			artist.NewUniform(hex(0x0000FFFF)),
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(0, 0))
 | 
			
		||||
 | 
			
		||||
	// 1, 0
 | 
			
		||||
	artist.StrokeRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.NewUniform(hex(0x00FF00FF)), 3,
 | 
			
		||||
		element.cellAt(1, 0))
 | 
			
		||||
 | 
			
		||||
	// 2, 0
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.NewMultiBordered (
 | 
			
		||||
			artist.Stroke { Pattern: uhex(0xFF0000FF), Weight: 1 },
 | 
			
		||||
			artist.Stroke { Pattern: uhex(0x888800FF), Weight: 2 },
 | 
			
		||||
			artist.Stroke { Pattern: uhex(0x00FF00FF), Weight: 3 },
 | 
			
		||||
			artist.Stroke { Pattern: uhex(0x008888FF), Weight: 4 },
 | 
			
		||||
			artist.Stroke { Pattern: uhex(0x0000FFFF), Weight: 5 },
 | 
			
		||||
			),
 | 
			
		||||
		element.cellAt(2, 0))
 | 
			
		||||
 | 
			
		||||
	// 3, 0
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.Bordered {
 | 
			
		||||
			Stroke: artist.Stroke { Pattern: uhex(0x0000FFFF), Weight: 5 },
 | 
			
		||||
			Fill: uhex(0xFF0000FF),
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(3, 0))
 | 
			
		||||
	// 0, 0 - 3, 0
 | 
			
		||||
	for x := 0; x < 4; x ++ {
 | 
			
		||||
		element.colorLines(x + 1, element.cellAt(x, 0).Bounds())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 4, 0
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.Padded {
 | 
			
		||||
			Stroke: uhex(0xFFFFFFFF),
 | 
			
		||||
			Fill: uhex(0x666666FF),
 | 
			
		||||
			Sides: []int { 4, 13, 2, 0 },
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(4, 0))
 | 
			
		||||
	c40 := element.cellAt(4, 0)
 | 
			
		||||
	shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1)
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		c40, artist.Hex(0xFF0000FF), 1,
 | 
			
		||||
		c40.Bounds().Min, c40.Bounds().Max)
 | 
			
		||||
 | 
			
		||||
	// 0, 1 - 3, 1
 | 
			
		||||
	for x := 0; x < 4; x ++ {
 | 
			
		||||
		artist.FillRectangle (
 | 
			
		||||
			element.core,
 | 
			
		||||
			artist.Striped {
 | 
			
		||||
				First:  artist.Stroke { Pattern: uhex(0xFF8800FF), Weight: 7 },
 | 
			
		||||
				Second: artist.Stroke { Pattern: uhex(0x0088FFFF), Weight: 2 },
 | 
			
		||||
				Orientation: artist.Orientation(x),
 | 
			
		||||
	// 0, 1
 | 
			
		||||
	c01 := element.cellAt(0, 1)
 | 
			
		||||
	shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1)
 | 
			
		||||
	shapes.FillColorEllipse(element.core, artist.Hex(0x00FF00FF), c01.Bounds())
 | 
			
		||||
 | 
			
		||||
			},
 | 
			
		||||
			element.cellAt(x, 1))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 0, 2 - 3, 2
 | 
			
		||||
	for x := 0; x < 4; x ++ {
 | 
			
		||||
		element.lines(x + 1, element.cellAt(x, 2))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 0, 3
 | 
			
		||||
	artist.StrokeRectangle (
 | 
			
		||||
		element.core, uhex(0x888888FF), 1,
 | 
			
		||||
		element.cellAt(0, 3))
 | 
			
		||||
	artist.FillEllipse(element.core, uhex(0x00FF00FF), element.cellAt(0, 3))
 | 
			
		||||
 | 
			
		||||
	// 1, 3 - 3, 3
 | 
			
		||||
	// 1, 1 - 3, 1
 | 
			
		||||
	for x := 1; x < 4; x ++ {
 | 
			
		||||
		artist.StrokeRectangle (
 | 
			
		||||
			element.core,uhex(0x888888FF), 1,
 | 
			
		||||
			element.cellAt(x, 3))
 | 
			
		||||
		artist.StrokeEllipse (
 | 
			
		||||
		c := element.cellAt(x, 1)
 | 
			
		||||
		shapes.StrokeColorRectangle (
 | 
			
		||||
			element.core, artist.Hex(0x888888FF),
 | 
			
		||||
			c.Bounds(), 1)
 | 
			
		||||
		shapes.StrokeColorEllipse (
 | 
			
		||||
			element.core,
 | 
			
		||||
			[]artist.Pattern {
 | 
			
		||||
				uhex(0xFF0000FF),
 | 
			
		||||
				uhex(0x00FF00FF),
 | 
			
		||||
				uhex(0xFF00FFFF),
 | 
			
		||||
			[]color.RGBA {
 | 
			
		||||
				artist.Hex(0xFF0000FF),
 | 
			
		||||
				artist.Hex(0x00FF00FF),
 | 
			
		||||
				artist.Hex(0xFF00FFFF),
 | 
			
		||||
			} [x - 1],
 | 
			
		||||
			x, element.cellAt(x, 3))
 | 
			
		||||
			c.Bounds(), x)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 4, 3
 | 
			
		||||
	shatterPos := element.cellAt(4, 3).Min
 | 
			
		||||
	// 4, 1
 | 
			
		||||
	c41 := element.cellAt(4, 1)
 | 
			
		||||
	shatterPos := c41.Bounds().Min
 | 
			
		||||
	rocks := []image.Rectangle {
 | 
			
		||||
		image.Rect(3, 12, 13, 23).Add(shatterPos),
 | 
			
		||||
		// image.Rect(30, 10, 40, 23).Add(shatterPos),
 | 
			
		||||
@ -125,159 +77,36 @@ func (element *Artist) draw () {
 | 
			
		||||
		image.Rect(30, -10, 40, 43).Add(shatterPos),
 | 
			
		||||
		image.Rect(80, 30, 90, 45).Add(shatterPos),
 | 
			
		||||
	}
 | 
			
		||||
	tiles := shatter.Shatter(element.cellAt(4, 3), rocks...)
 | 
			
		||||
	for _, tile := range tiles {
 | 
			
		||||
		artist.FillRectangle (
 | 
			
		||||
			element.core,
 | 
			
		||||
			artist.Bordered {
 | 
			
		||||
				Fill: uhex(0x888888FF),
 | 
			
		||||
				Stroke: artist.Stroke {
 | 
			
		||||
					Pattern: artist.Beveled {
 | 
			
		||||
						uhex(0xCCCCCCFF),
 | 
			
		||||
						uhex(0x444444FF),
 | 
			
		||||
					},
 | 
			
		||||
					Weight: 1,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			tile)
 | 
			
		||||
	tiles := shatter.Shatter(c41.Bounds(), rocks...)
 | 
			
		||||
	for index, tile := range tiles {
 | 
			
		||||
		artist.DrawBounds (
 | 
			
		||||
			element.core, tile,
 | 
			
		||||
			[]artist.Pattern {
 | 
			
		||||
				patterns.Uhex(0xFF0000FF),
 | 
			
		||||
				patterns.Uhex(0x00FF00FF),
 | 
			
		||||
				patterns.Uhex(0xFF00FFFF),
 | 
			
		||||
				patterns.Uhex(0xFFFF00FF),
 | 
			
		||||
				patterns.Uhex(0x00FFFFFF),
 | 
			
		||||
			} [index % 5], tile)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 0, 4 - 3, 4
 | 
			
		||||
	for x := 0; x < 4; x ++ {
 | 
			
		||||
		artist.FillEllipse (
 | 
			
		||||
			element.core, 
 | 
			
		||||
			artist.Split {
 | 
			
		||||
				First:  uhex(0xFF0000FF),
 | 
			
		||||
				Second: uhex(0x0000FFFF),
 | 
			
		||||
				Orientation: artist.Orientation(x),
 | 
			
		||||
			},
 | 
			
		||||
			element.cellAt(x, 4))
 | 
			
		||||
	}
 | 
			
		||||
	// 0, 2
 | 
			
		||||
	c02 := element.cellAt(0, 2)
 | 
			
		||||
	shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
 | 
			
		||||
	shapes.FillEllipse(c02, c41)
 | 
			
		||||
 | 
			
		||||
	// 0, 5
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.QuadBeveled {
 | 
			
		||||
			uhex(0x880000FF),
 | 
			
		||||
			uhex(0x00FF00FF),
 | 
			
		||||
			uhex(0x0000FFFF),
 | 
			
		||||
			uhex(0xFF00FFFF),
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(0, 5))
 | 
			
		||||
	// 1, 2
 | 
			
		||||
	c12 := element.cellAt(1, 2)
 | 
			
		||||
	shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
 | 
			
		||||
	shapes.StrokeEllipse(c12, c41, 5)
 | 
			
		||||
	
 | 
			
		||||
	// 1, 5
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.Checkered {
 | 
			
		||||
			First: artist.QuadBeveled {
 | 
			
		||||
				uhex(0x880000FF),
 | 
			
		||||
				uhex(0x00FF00FF),
 | 
			
		||||
				uhex(0x0000FFFF),
 | 
			
		||||
				uhex(0xFF00FFFF),
 | 
			
		||||
			},
 | 
			
		||||
			Second: artist.Striped {
 | 
			
		||||
				First:  artist.Stroke { Pattern: uhex(0xFF8800FF), Weight: 1 },
 | 
			
		||||
				Second: artist.Stroke { Pattern: uhex(0x0088FFFF), Weight: 1 },
 | 
			
		||||
				Orientation: artist.OrientationVertical,
 | 
			
		||||
			},
 | 
			
		||||
			CellWidth: 32,
 | 
			
		||||
			CellHeight: 16,
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(1, 5))
 | 
			
		||||
	// 2, 2
 | 
			
		||||
	c22 := element.cellAt(2, 2)
 | 
			
		||||
	shapes.FillRectangle(c22, c41)
 | 
			
		||||
 | 
			
		||||
	// 2, 5
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.Dotted {
 | 
			
		||||
			Foreground: uhex(0x00FF00FF),
 | 
			
		||||
			Background: artist.Checkered {
 | 
			
		||||
				First:  uhex(0x444444FF),
 | 
			
		||||
				Second: uhex(0x888888FF),
 | 
			
		||||
				CellWidth: 16,
 | 
			
		||||
				CellHeight: 16,
 | 
			
		||||
			},
 | 
			
		||||
			Size: 8,
 | 
			
		||||
			Spacing: 16,
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(2, 5))
 | 
			
		||||
		
 | 
			
		||||
	// 3, 5
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.Tiled {
 | 
			
		||||
			Pattern: artist.QuadBeveled {
 | 
			
		||||
				uhex(0x880000FF),
 | 
			
		||||
				uhex(0x00FF00FF),
 | 
			
		||||
				uhex(0x0000FFFF),
 | 
			
		||||
				uhex(0xFF00FFFF),
 | 
			
		||||
			},
 | 
			
		||||
			CellWidth: 17,
 | 
			
		||||
			CellHeight: 23,
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(3, 5))
 | 
			
		||||
		
 | 
			
		||||
	// 0, 6 - 3, 6
 | 
			
		||||
	for x := 0; x < 4; x ++ {
 | 
			
		||||
		artist.FillRectangle (
 | 
			
		||||
			element.core, 
 | 
			
		||||
			artist.Gradient {
 | 
			
		||||
				First:  uhex(0xFF0000FF),
 | 
			
		||||
				Second: uhex(0x0000FFFF),
 | 
			
		||||
				Orientation: artist.Orientation(x),
 | 
			
		||||
			},
 | 
			
		||||
			element.cellAt(x, 6))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 0, 7
 | 
			
		||||
	artist.FillEllipse (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.EllipticallyBordered {
 | 
			
		||||
			Fill: artist.Gradient {
 | 
			
		||||
				First:  uhex(0x00FF00FF),
 | 
			
		||||
				Second: uhex(0x0000FFFF),
 | 
			
		||||
				Orientation: artist.OrientationVertical,
 | 
			
		||||
			},
 | 
			
		||||
			Stroke: artist.Stroke { Pattern: uhex(0x00FF00), Weight: 5 },
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(0, 7))
 | 
			
		||||
 | 
			
		||||
	// 1, 7
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.Noisy {
 | 
			
		||||
			Low:  uhex(0x000000FF),
 | 
			
		||||
			High: uhex(0xFFFFFFFF),
 | 
			
		||||
			Seed: 0,
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(1, 7),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 2, 7
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.Noisy {
 | 
			
		||||
			Low:  uhex(0x000000FF),
 | 
			
		||||
			High: artist.Gradient {
 | 
			
		||||
				First:  uhex(0x000000FF),
 | 
			
		||||
				Second: uhex(0xFFFFFFFF),
 | 
			
		||||
				Orientation: artist.OrientationVertical,
 | 
			
		||||
			},
 | 
			
		||||
			Seed: 0,
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(2, 7),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 3, 7
 | 
			
		||||
	artist.FillRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.Noisy {
 | 
			
		||||
			Low:  uhex(0x000000FF),
 | 
			
		||||
			High: uhex(0xFFFFFFFF),
 | 
			
		||||
			Seed: 0,
 | 
			
		||||
			Harsh: true,
 | 
			
		||||
		},
 | 
			
		||||
		element.cellAt(3, 7),
 | 
			
		||||
	)
 | 
			
		||||
	// 3, 2
 | 
			
		||||
	c32 := element.cellAt(3, 2)
 | 
			
		||||
	shapes.StrokeRectangle(c32, c41, 5)
 | 
			
		||||
	
 | 
			
		||||
	// how long did that take to render?
 | 
			
		||||
	drawTime := time.Since(drawStart)
 | 
			
		||||
@ -288,68 +117,51 @@ func (element *Artist) draw () {
 | 
			
		||||
		drawTime.Milliseconds(),
 | 
			
		||||
		drawTime.Microseconds())))
 | 
			
		||||
	textDrawer.Draw (
 | 
			
		||||
		element.core, uhex(0xFFFFFFFF),
 | 
			
		||||
		element.core, artist.Hex(0xFFFFFFFF),
 | 
			
		||||
		image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (element *Artist) lines (weight int, bounds image.Rectangle) {
 | 
			
		||||
func (element *Artist) colorLines (weight int, bounds image.Rectangle) {
 | 
			
		||||
	bounds = bounds.Inset(4)
 | 
			
		||||
	c := uhex(0xFFFFFFFF)
 | 
			
		||||
	artist.Line(element.core, c, weight, bounds.Min, bounds.Max)
 | 
			
		||||
	artist.Line (
 | 
			
		||||
	c := artist.Hex(0xFFFFFFFF)
 | 
			
		||||
	shapes.ColorLine(element.core, c, weight, bounds.Min, bounds.Max)
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		element.core, c, weight,
 | 
			
		||||
		image.Pt(bounds.Max.X, bounds.Min.Y),
 | 
			
		||||
		image.Pt(bounds.Min.X, bounds.Max.Y))
 | 
			
		||||
	artist.Line (
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		element.core, c, weight,
 | 
			
		||||
		image.Pt(bounds.Max.X, bounds.Min.Y + 16),
 | 
			
		||||
		image.Pt(bounds.Min.X, bounds.Max.Y - 16))
 | 
			
		||||
	artist.Line (
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		element.core, c, weight,
 | 
			
		||||
		image.Pt(bounds.Min.X, bounds.Min.Y + 16),
 | 
			
		||||
		image.Pt(bounds.Max.X, bounds.Max.Y - 16))
 | 
			
		||||
	artist.Line (
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		element.core, c, weight,
 | 
			
		||||
		image.Pt(bounds.Min.X + 20, bounds.Min.Y),
 | 
			
		||||
		image.Pt(bounds.Max.X - 20, bounds.Max.Y))
 | 
			
		||||
	artist.Line (
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		element.core, c, weight,
 | 
			
		||||
		image.Pt(bounds.Max.X - 20, bounds.Min.Y),
 | 
			
		||||
		image.Pt(bounds.Min.X + 20, bounds.Max.Y))
 | 
			
		||||
	artist.Line (
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		element.core, c, weight,
 | 
			
		||||
		image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
 | 
			
		||||
		image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2))
 | 
			
		||||
	artist.Line (
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		element.core, c, weight,
 | 
			
		||||
		image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y),
 | 
			
		||||
		image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (element *Artist) cellAt (x, y int) (image.Rectangle) {
 | 
			
		||||
func (element *Artist) cellAt (x, y int) (canvas.Canvas) {
 | 
			
		||||
	bounds := element.Bounds()
 | 
			
		||||
	cellBounds := image.Rectangle { }
 | 
			
		||||
	cellBounds.Min = bounds.Min
 | 
			
		||||
	cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
 | 
			
		||||
	cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 8
 | 
			
		||||
	return cellBounds.Add (image.Pt (
 | 
			
		||||
	cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4
 | 
			
		||||
	return canvas.Cut (element.core, cellBounds.Add (image.Pt (
 | 
			
		||||
		x * cellBounds.Dx(),
 | 
			
		||||
		y * cellBounds.Dy()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func hex (n uint32) (c color.RGBA) {
 | 
			
		||||
	c.A = uint8(n)
 | 
			
		||||
	c.B = uint8(n >>  8)
 | 
			
		||||
	c.G = uint8(n >> 16)
 | 
			
		||||
	c.R = uint8(n >> 24)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func uhex (n uint32) (artist.Pattern) {
 | 
			
		||||
	return artist.NewUniform (color.RGBA {
 | 
			
		||||
		A: uint8(n),
 | 
			
		||||
		B: uint8(n >>  8),
 | 
			
		||||
		G: uint8(n >> 16),
 | 
			
		||||
		R: uint8(n >> 24),
 | 
			
		||||
	})
 | 
			
		||||
		y * cellBounds.Dy())))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
package testing
 | 
			
		||||
 | 
			
		||||
import "image"
 | 
			
		||||
import "image/color"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/input"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/config"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/config"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
 | 
			
		||||
 | 
			
		||||
// Mouse is an element capable of testing mouse input. When the mouse is clicked
 | 
			
		||||
@ -14,7 +14,6 @@ type Mouse struct {
 | 
			
		||||
	*core.Core
 | 
			
		||||
	core core.CoreControl
 | 
			
		||||
	drawing      bool
 | 
			
		||||
	color        artist.Pattern
 | 
			
		||||
	lastMousePos image.Point
 | 
			
		||||
	
 | 
			
		||||
	config config.Config
 | 
			
		||||
@ -27,7 +26,6 @@ func NewMouse () (element *Mouse) {
 | 
			
		||||
	element = &Mouse { c: theme.C("testing", "mouse") }
 | 
			
		||||
	element.Core, element.core = core.NewCore(element.draw)
 | 
			
		||||
	element.core.SetMinimumSize(32, 32)
 | 
			
		||||
	element.color = artist.NewUniform(color.Black)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -55,17 +53,17 @@ func (element *Mouse) draw () {
 | 
			
		||||
		theme.PatternAccent,
 | 
			
		||||
		theme.PatternState { },
 | 
			
		||||
		element.c)
 | 
			
		||||
	artist.FillRectangle(element.core, pattern, bounds)
 | 
			
		||||
	artist.StrokeRectangle (
 | 
			
		||||
	pattern.Draw(element.core, bounds)
 | 
			
		||||
	shapes.StrokeColorRectangle (
 | 
			
		||||
		element.core,
 | 
			
		||||
		artist.NewUniform(color.Black), 1,
 | 
			
		||||
		bounds)
 | 
			
		||||
	artist.Line (
 | 
			
		||||
		element.core, artist.NewUniform(color.White), 1,
 | 
			
		||||
		artist.Hex(0x000000FF),
 | 
			
		||||
		bounds, 1)
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		element.core, artist.Hex(0xFFFFFFFF), 1,
 | 
			
		||||
		bounds.Min.Add(image.Pt(1, 1)),
 | 
			
		||||
		bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
 | 
			
		||||
	artist.Line (
 | 
			
		||||
		element.core, artist.NewUniform(color.White), 1,
 | 
			
		||||
	shapes.ColorLine (
 | 
			
		||||
		element.core, artist.Hex(0xFFFFFFFF), 1,
 | 
			
		||||
		bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
 | 
			
		||||
		bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
 | 
			
		||||
}
 | 
			
		||||
@ -78,8 +76,8 @@ func (element *Mouse) HandleMouseDown (x, y int, button input.Button) {
 | 
			
		||||
func (element *Mouse) HandleMouseUp (x, y int, button input.Button) {
 | 
			
		||||
	element.drawing = false
 | 
			
		||||
	mousePos := image.Pt(x, y)
 | 
			
		||||
	element.core.DamageRegion (artist.Line (
 | 
			
		||||
		element.core, element.color, 1,
 | 
			
		||||
	element.core.DamageRegion (shapes.ColorLine (
 | 
			
		||||
		element.core, artist.Hex(0x000000FF), 1,
 | 
			
		||||
		element.lastMousePos, mousePos))
 | 
			
		||||
	element.lastMousePos = mousePos
 | 
			
		||||
}
 | 
			
		||||
@ -87,8 +85,8 @@ func (element *Mouse) HandleMouseUp (x, y int, button input.Button) {
 | 
			
		||||
func (element *Mouse) HandleMouseMove (x, y int) {
 | 
			
		||||
	if !element.drawing { return }
 | 
			
		||||
	mousePos := image.Pt(x, y)
 | 
			
		||||
	element.core.DamageRegion (artist.Line (
 | 
			
		||||
		element.core, element.color, 1,
 | 
			
		||||
	element.core.DamageRegion (shapes.ColorLine (
 | 
			
		||||
		element.core, artist.Hex(0x000000FF), 1,
 | 
			
		||||
		element.lastMousePos, mousePos))
 | 
			
		||||
	element.lastMousePos = mousePos
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,12 +11,12 @@ func main () {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func run () {
 | 
			
		||||
	window, _ := tomo.NewWindow(128, 128)
 | 
			
		||||
	window, _ := tomo.NewWindow(480, 360)
 | 
			
		||||
	window.SetTitle("Draw Test")
 | 
			
		||||
	window.Adopt(testing.NewArtist())
 | 
			
		||||
	window.OnClose(tomo.Stop)
 | 
			
		||||
	window.Show()
 | 
			
		||||
	go func () {
 | 
			
		||||
		http.ListenAndServe("localhost:6060", nil)
 | 
			
		||||
		http.ListenAndServe("localhost:9090", nil)
 | 
			
		||||
	} ()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,9 +3,9 @@ package textdraw
 | 
			
		||||
import "image"
 | 
			
		||||
import "unicode"
 | 
			
		||||
import "image/draw"
 | 
			
		||||
import "image/color"
 | 
			
		||||
import "golang.org/x/image/math/fixed"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
 | 
			
		||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
 | 
			
		||||
 | 
			
		||||
// Drawer is an extended TypeSetter that is able to draw text. Much like
 | 
			
		||||
// TypeSetter, It has no constructor and its zero value can be used safely.
 | 
			
		||||
@ -14,17 +14,11 @@ type Drawer struct { TypeSetter }
 | 
			
		||||
// Draw draws the drawer's text onto the specified canvas at the given offset.
 | 
			
		||||
func (drawer Drawer) Draw (
 | 
			
		||||
	destination canvas.Canvas,
 | 
			
		||||
	source      artist.Pattern,
 | 
			
		||||
	color       color.RGBA,
 | 
			
		||||
	offset      image.Point,
 | 
			
		||||
) (
 | 
			
		||||
	updatedRegion image.Rectangle,
 | 
			
		||||
) {
 | 
			
		||||
	wrappedSource := artist.WrappedPattern {
 | 
			
		||||
		Pattern: source,
 | 
			
		||||
		Width:  0,
 | 
			
		||||
		Height: 0, // TODO: choose a better width and height
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	drawer.For (func (
 | 
			
		||||
		index    int,
 | 
			
		||||
		char     rune,
 | 
			
		||||
@ -46,7 +40,7 @@ func (drawer Drawer) Draw (
 | 
			
		||||
		draw.DrawMask (
 | 
			
		||||
			destination,
 | 
			
		||||
			destinationRectangle,
 | 
			
		||||
			wrappedSource, image.Point { },
 | 
			
		||||
			image.NewUniform(color), image.Point { },
 | 
			
		||||
			mask, maskPoint,
 | 
			
		||||
			draw.Over)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user