whee back in busineess
This commit is contained in:
parent
2859dc3313
commit
241c297626
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"
|
||||||
import "image/color"
|
import "image/color"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
||||||
|
|
||||||
// Uniform is a pattern that draws a solid color.
|
// 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.
|
// Uhex creates a new Uniform pattern from an RGBA integer value.
|
||||||
func Uhex (color uint32) (uniform Uniform) {
|
func Uhex (color uint32) (uniform Uniform) {
|
||||||
return Uniform(hex(color))
|
return Uniform(artist.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
|
|
||||||
}
|
}
|
||||||
|
@ -5,23 +5,23 @@ import "image"
|
|||||||
import "image/color"
|
import "image/color"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
|
||||||
// FillEllipse draws the content of one canvas onto another, clipped by an
|
// TODO: redo fill ellipse, stroke ellipse, etc. so that it only takes in
|
||||||
// ellipse stretched to the bounds of the source canvas. The offset point
|
// destination and source, using the bounds of destination as the bounds of the
|
||||||
// defines where the origin point of the source canvas is positioned in relation
|
// ellipse and the bounds of source as the "clipping rectangle". Line up the Min
|
||||||
// to the origin point of the destination canvas. To prevent the entire source
|
// of both canvases.
|
||||||
// canvas's bounds from being used, it must be cut with canvas.Cut().
|
|
||||||
func FillEllipse (
|
func FillEllipse (
|
||||||
destination canvas.Canvas,
|
destination canvas.Canvas,
|
||||||
source canvas.Canvas,
|
source canvas.Canvas,
|
||||||
offset image.Point,
|
|
||||||
) (
|
) (
|
||||||
updatedRegion image.Rectangle,
|
updatedRegion image.Rectangle,
|
||||||
) {
|
) {
|
||||||
dstData, dstStride := destination.Buffer()
|
dstData, dstStride := destination.Buffer()
|
||||||
srcData, srcStride := source.Buffer()
|
srcData, srcStride := source.Buffer()
|
||||||
|
|
||||||
bounds := source.Bounds().Intersect(destination.Bounds()).Canon()
|
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
|
||||||
realBounds := source.Bounds()
|
bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds())
|
||||||
|
realBounds := destination.Bounds()
|
||||||
if bounds.Empty() { return }
|
if bounds.Empty() { return }
|
||||||
updatedRegion = bounds
|
updatedRegion = bounds
|
||||||
|
|
||||||
@ -30,22 +30,17 @@ func FillEllipse (
|
|||||||
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
|
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
|
||||||
if inEllipse(point, realBounds) {
|
if inEllipse(point, realBounds) {
|
||||||
offsetPoint := point.Add(offset)
|
offsetPoint := point.Add(offset)
|
||||||
dstIndex := offsetPoint.X + (offsetPoint.Y) * dstStride
|
dstIndex := point.X + point.Y * dstStride
|
||||||
srcIndex := point.X + point.Y * srcStride
|
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
|
||||||
dstData[dstIndex] = srcData[srcIndex]
|
dstData[dstIndex] = srcData[srcIndex]
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
return
|
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 (
|
func StrokeEllipse (
|
||||||
destination canvas.Canvas,
|
destination canvas.Canvas,
|
||||||
source canvas.Canvas,
|
source canvas.Canvas,
|
||||||
offset image.Point,
|
|
||||||
weight int,
|
weight int,
|
||||||
) {
|
) {
|
||||||
if weight < 1 { return }
|
if weight < 1 { return }
|
||||||
@ -53,7 +48,10 @@ func StrokeEllipse (
|
|||||||
dstData, dstStride := destination.Buffer()
|
dstData, dstStride := destination.Buffer()
|
||||||
srcData, srcStride := source.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 {
|
context := ellipsePlottingContext {
|
||||||
plottingContext: plottingContext {
|
plottingContext: plottingContext {
|
||||||
@ -63,9 +61,9 @@ func StrokeEllipse (
|
|||||||
srcStride: srcStride,
|
srcStride: srcStride,
|
||||||
weight: weight,
|
weight: weight,
|
||||||
offset: offset,
|
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.center = bounds.Min.Add(context.radii)
|
||||||
context.plotEllipse()
|
context.plotEllipse()
|
||||||
@ -205,7 +203,7 @@ func StrokeColorEllipse (
|
|||||||
if weight < 1 { return }
|
if weight < 1 { return }
|
||||||
|
|
||||||
dstData, dstStride := destination.Buffer()
|
dstData, dstStride := destination.Buffer()
|
||||||
bounds = bounds.Inset(weight - 1)
|
insetBounds := bounds.Inset(weight - 1)
|
||||||
|
|
||||||
context := ellipsePlottingContext {
|
context := ellipsePlottingContext {
|
||||||
plottingContext: plottingContext {
|
plottingContext: plottingContext {
|
||||||
@ -215,9 +213,9 @@ func StrokeColorEllipse (
|
|||||||
weight: weight,
|
weight: weight,
|
||||||
bounds: bounds.Intersect(destination.Bounds()),
|
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()
|
context.plotEllipse()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -16,32 +16,31 @@ type plottingContext struct {
|
|||||||
bounds image.Rectangle
|
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).
|
return image.Rect(0, 0, context.weight, context.weight).
|
||||||
Sub(image.Pt(context.weight / 2, context.weight / 2)).
|
Sub(image.Pt(context.weight / 2, context.weight / 2)).
|
||||||
Add(center).
|
Add(center).
|
||||||
Add(context.offset).
|
|
||||||
Intersect(context.bounds)
|
Intersect(context.bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (context plottingContext) plotColor (center image.Point) {
|
func (context plottingContext) plotColor (center image.Point) {
|
||||||
square := context.square(center)
|
square := context.square(center)
|
||||||
for y := square.Min.Y; y < square.Min.Y; y ++ {
|
for y := square.Min.Y; y < square.Max.Y; y ++ {
|
||||||
for x := square.Min.X; x < square.Min.X; x ++ {
|
for x := square.Min.X; x < square.Max.X; x ++ {
|
||||||
context.dstData[x + y * context.dstStride] = context.color
|
context.dstData[x + y * context.dstStride] = context.color
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (context plottingContext) plotSource (center image.Point) {
|
func (context plottingContext) plotSource (center image.Point) {
|
||||||
square := context.square(center)
|
square := context.square(center)
|
||||||
for y := square.Min.Y; y < square.Min.Y; y ++ {
|
for y := square.Min.Y; y < square.Max.Y; y ++ {
|
||||||
for x := square.Min.X; x < square.Min.X; x ++ {
|
for x := square.Min.X; x < square.Max.X; x ++ {
|
||||||
// we offset srcIndex here because we have already applied the
|
// we offset srcIndex here because we have already applied the
|
||||||
// offset to the square, and we need to reverse that to get the
|
// offset to the square, and we need to reverse that to get the
|
||||||
// proper source coordinates.
|
// proper source coordinates.
|
||||||
srcIndex :=
|
srcIndex :=
|
||||||
x - context.offset.X +
|
x + context.offset.X +
|
||||||
(y - context.offset.Y) * context.dstStride
|
(y + context.offset.Y) * context.dstStride
|
||||||
dstIndex := x + y * context.dstStride
|
dstIndex := x + y * context.dstStride
|
||||||
context.dstData[dstIndex] = context.srcData [srcIndex]
|
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
|
// 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 (
|
func FillRectangle (
|
||||||
destination canvas.Canvas,
|
destination canvas.Canvas,
|
||||||
source canvas.Canvas,
|
source canvas.Canvas,
|
||||||
offset image.Point,
|
|
||||||
) (
|
) (
|
||||||
updatedRegion image.Rectangle,
|
updatedRegion image.Rectangle,
|
||||||
) {
|
) {
|
||||||
dstData, dstStride := destination.Buffer()
|
dstData, dstStride := destination.Buffer()
|
||||||
srcData, srcStride := source.Buffer()
|
srcData, srcStride := source.Buffer()
|
||||||
|
|
||||||
sourceBounds :=
|
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
|
||||||
source.Bounds().Canon().
|
bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds())
|
||||||
Intersect(destination.Bounds().Sub(offset))
|
if bounds.Empty() { return }
|
||||||
if sourceBounds.Empty() { return }
|
updatedRegion = bounds
|
||||||
|
|
||||||
updatedRegion = sourceBounds.Add(offset)
|
point := image.Point { }
|
||||||
for y := sourceBounds.Min.Y; y < sourceBounds.Max.Y; y ++ {
|
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
|
||||||
for x := sourceBounds.Min.X; x < sourceBounds.Max.X; x ++ {
|
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
|
||||||
dstData[x + offset.X + (y + offset.Y) * dstStride] =
|
offsetPoint := point.Add(offset)
|
||||||
srcData[x + y * srcStride]
|
dstIndex := point.X + point.Y * dstStride
|
||||||
|
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
|
||||||
|
dstData[dstIndex] = srcData[srcIndex]
|
||||||
}}
|
}}
|
||||||
|
|
||||||
return
|
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 (
|
func StrokeRectangle (
|
||||||
destination canvas.Canvas,
|
destination canvas.Canvas,
|
||||||
source canvas.Canvas,
|
source canvas.Canvas,
|
||||||
offset image.Point,
|
|
||||||
weight int,
|
weight int,
|
||||||
) {
|
) {
|
||||||
bounds := source.Bounds()
|
bounds := destination.Bounds()
|
||||||
insetBounds := bounds.Inset(weight)
|
insetBounds := bounds.Inset(weight)
|
||||||
if insetBounds.Empty() {
|
if insetBounds.Empty() {
|
||||||
FillRectangle(destination, source, offset)
|
FillRectangle(destination, source)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
FillRectangleShatter(destination, source, offset, insetBounds)
|
FillRectangleShatter(destination, source, insetBounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FillRectangleShatter is like FillRectangle, but it does not draw in areas
|
// FillRectangleShatter is like FillRectangle, but it does not draw in areas
|
||||||
@ -59,12 +52,14 @@ func StrokeRectangle (
|
|||||||
func FillRectangleShatter (
|
func FillRectangleShatter (
|
||||||
destination canvas.Canvas,
|
destination canvas.Canvas,
|
||||||
source canvas.Canvas,
|
source canvas.Canvas,
|
||||||
offset image.Point,
|
|
||||||
rocks ...image.Rectangle,
|
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 {
|
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 "time"
|
||||||
import "image"
|
import "image"
|
||||||
import "image/color"
|
import "image/color"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
|
import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
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
|
// Artist is an element that displays shapes and patterns drawn by the artist
|
||||||
// package in order to test it.
|
// package in order to test it.
|
||||||
@ -21,103 +24,52 @@ type Artist struct {
|
|||||||
func NewArtist () (element *Artist) {
|
func NewArtist () (element *Artist) {
|
||||||
element = &Artist { }
|
element = &Artist { }
|
||||||
element.Core, element.core = core.NewCore(element.draw)
|
element.Core, element.core = core.NewCore(element.draw)
|
||||||
element.core.SetMinimumSize(240, 360)
|
element.core.SetMinimumSize(240, 240)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Artist) draw () {
|
func (element *Artist) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
artist.FillRectangle(element.core, artist.NewUniform(hex(0)), bounds)
|
patterns.Uhex(0x000000FF).Draw(element.core, bounds)
|
||||||
|
|
||||||
drawStart := time.Now()
|
drawStart := time.Now()
|
||||||
|
|
||||||
// 0, 0
|
// 0, 0 - 3, 0
|
||||||
artist.FillRectangle (
|
for x := 0; x < 4; x ++ {
|
||||||
element.core,
|
element.colorLines(x + 1, element.cellAt(x, 0).Bounds())
|
||||||
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))
|
|
||||||
|
|
||||||
// 4, 0
|
// 4, 0
|
||||||
artist.FillRectangle (
|
c40 := element.cellAt(4, 0)
|
||||||
element.core,
|
shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1)
|
||||||
artist.Padded {
|
shapes.ColorLine (
|
||||||
Stroke: uhex(0xFFFFFFFF),
|
c40, artist.Hex(0xFF0000FF), 1,
|
||||||
Fill: uhex(0x666666FF),
|
c40.Bounds().Min, c40.Bounds().Max)
|
||||||
Sides: []int { 4, 13, 2, 0 },
|
|
||||||
},
|
|
||||||
element.cellAt(4, 0))
|
|
||||||
|
|
||||||
// 0, 1 - 3, 1
|
// 0, 1
|
||||||
for x := 0; x < 4; x ++ {
|
c01 := element.cellAt(0, 1)
|
||||||
artist.FillRectangle (
|
shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1)
|
||||||
element.core,
|
shapes.FillColorEllipse(element.core, artist.Hex(0x00FF00FF), c01.Bounds())
|
||||||
artist.Striped {
|
|
||||||
First: artist.Stroke { Pattern: uhex(0xFF8800FF), Weight: 7 },
|
|
||||||
Second: artist.Stroke { Pattern: uhex(0x0088FFFF), Weight: 2 },
|
|
||||||
Orientation: artist.Orientation(x),
|
|
||||||
|
|
||||||
},
|
|
||||||
element.cellAt(x, 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0, 2 - 3, 2
|
// 1, 1 - 3, 1
|
||||||
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
|
|
||||||
for x := 1; x < 4; x ++ {
|
for x := 1; x < 4; x ++ {
|
||||||
artist.StrokeRectangle (
|
c := element.cellAt(x, 1)
|
||||||
element.core,uhex(0x888888FF), 1,
|
shapes.StrokeColorRectangle (
|
||||||
element.cellAt(x, 3))
|
element.core, artist.Hex(0x888888FF),
|
||||||
artist.StrokeEllipse (
|
c.Bounds(), 1)
|
||||||
|
shapes.StrokeColorEllipse (
|
||||||
element.core,
|
element.core,
|
||||||
[]artist.Pattern {
|
[]color.RGBA {
|
||||||
uhex(0xFF0000FF),
|
artist.Hex(0xFF0000FF),
|
||||||
uhex(0x00FF00FF),
|
artist.Hex(0x00FF00FF),
|
||||||
uhex(0xFF00FFFF),
|
artist.Hex(0xFF00FFFF),
|
||||||
} [x - 1],
|
} [x - 1],
|
||||||
x, element.cellAt(x, 3))
|
c.Bounds(), x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4, 3
|
// 4, 1
|
||||||
shatterPos := element.cellAt(4, 3).Min
|
c41 := element.cellAt(4, 1)
|
||||||
|
shatterPos := c41.Bounds().Min
|
||||||
rocks := []image.Rectangle {
|
rocks := []image.Rectangle {
|
||||||
image.Rect(3, 12, 13, 23).Add(shatterPos),
|
image.Rect(3, 12, 13, 23).Add(shatterPos),
|
||||||
// image.Rect(30, 10, 40, 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(30, -10, 40, 43).Add(shatterPos),
|
||||||
image.Rect(80, 30, 90, 45).Add(shatterPos),
|
image.Rect(80, 30, 90, 45).Add(shatterPos),
|
||||||
}
|
}
|
||||||
tiles := shatter.Shatter(element.cellAt(4, 3), rocks...)
|
tiles := shatter.Shatter(c41.Bounds(), rocks...)
|
||||||
for _, tile := range tiles {
|
for index, tile := range tiles {
|
||||||
artist.FillRectangle (
|
artist.DrawBounds (
|
||||||
element.core,
|
element.core, tile,
|
||||||
artist.Bordered {
|
[]artist.Pattern {
|
||||||
Fill: uhex(0x888888FF),
|
patterns.Uhex(0xFF0000FF),
|
||||||
Stroke: artist.Stroke {
|
patterns.Uhex(0x00FF00FF),
|
||||||
Pattern: artist.Beveled {
|
patterns.Uhex(0xFF00FFFF),
|
||||||
uhex(0xCCCCCCFF),
|
patterns.Uhex(0xFFFF00FF),
|
||||||
uhex(0x444444FF),
|
patterns.Uhex(0x00FFFFFF),
|
||||||
},
|
} [index % 5], tile)
|
||||||
Weight: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0, 4 - 3, 4
|
// 0, 2
|
||||||
for x := 0; x < 4; x ++ {
|
c02 := element.cellAt(0, 2)
|
||||||
artist.FillEllipse (
|
shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
|
||||||
element.core,
|
shapes.FillEllipse(c02, c41)
|
||||||
artist.Split {
|
|
||||||
First: uhex(0xFF0000FF),
|
// 1, 2
|
||||||
Second: uhex(0x0000FFFF),
|
c12 := element.cellAt(1, 2)
|
||||||
Orientation: artist.Orientation(x),
|
shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
|
||||||
},
|
shapes.StrokeEllipse(c12, c41, 5)
|
||||||
element.cellAt(x, 4))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0, 5
|
// 2, 2
|
||||||
artist.FillRectangle (
|
c22 := element.cellAt(2, 2)
|
||||||
element.core,
|
shapes.FillRectangle(c22, c41)
|
||||||
artist.QuadBeveled {
|
|
||||||
uhex(0x880000FF),
|
|
||||||
uhex(0x00FF00FF),
|
|
||||||
uhex(0x0000FFFF),
|
|
||||||
uhex(0xFF00FFFF),
|
|
||||||
},
|
|
||||||
element.cellAt(0, 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, 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
|
// 3, 2
|
||||||
artist.FillEllipse (
|
c32 := element.cellAt(3, 2)
|
||||||
element.core,
|
shapes.StrokeRectangle(c32, c41, 5)
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
// how long did that take to render?
|
// how long did that take to render?
|
||||||
drawTime := time.Since(drawStart)
|
drawTime := time.Since(drawStart)
|
||||||
@ -288,68 +117,51 @@ func (element *Artist) draw () {
|
|||||||
drawTime.Milliseconds(),
|
drawTime.Milliseconds(),
|
||||||
drawTime.Microseconds())))
|
drawTime.Microseconds())))
|
||||||
textDrawer.Draw (
|
textDrawer.Draw (
|
||||||
element.core, uhex(0xFFFFFFFF),
|
element.core, artist.Hex(0xFFFFFFFF),
|
||||||
image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
|
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)
|
bounds = bounds.Inset(4)
|
||||||
c := uhex(0xFFFFFFFF)
|
c := artist.Hex(0xFFFFFFFF)
|
||||||
artist.Line(element.core, c, weight, bounds.Min, bounds.Max)
|
shapes.ColorLine(element.core, c, weight, bounds.Min, bounds.Max)
|
||||||
artist.Line (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
element.core, c, weight,
|
||||||
image.Pt(bounds.Max.X, bounds.Min.Y),
|
image.Pt(bounds.Max.X, bounds.Min.Y),
|
||||||
image.Pt(bounds.Min.X, bounds.Max.Y))
|
image.Pt(bounds.Min.X, bounds.Max.Y))
|
||||||
artist.Line (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
element.core, c, weight,
|
||||||
image.Pt(bounds.Max.X, bounds.Min.Y + 16),
|
image.Pt(bounds.Max.X, bounds.Min.Y + 16),
|
||||||
image.Pt(bounds.Min.X, bounds.Max.Y - 16))
|
image.Pt(bounds.Min.X, bounds.Max.Y - 16))
|
||||||
artist.Line (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
element.core, c, weight,
|
||||||
image.Pt(bounds.Min.X, bounds.Min.Y + 16),
|
image.Pt(bounds.Min.X, bounds.Min.Y + 16),
|
||||||
image.Pt(bounds.Max.X, bounds.Max.Y - 16))
|
image.Pt(bounds.Max.X, bounds.Max.Y - 16))
|
||||||
artist.Line (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
element.core, c, weight,
|
||||||
image.Pt(bounds.Min.X + 20, bounds.Min.Y),
|
image.Pt(bounds.Min.X + 20, bounds.Min.Y),
|
||||||
image.Pt(bounds.Max.X - 20, bounds.Max.Y))
|
image.Pt(bounds.Max.X - 20, bounds.Max.Y))
|
||||||
artist.Line (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
element.core, c, weight,
|
||||||
image.Pt(bounds.Max.X - 20, bounds.Min.Y),
|
image.Pt(bounds.Max.X - 20, bounds.Min.Y),
|
||||||
image.Pt(bounds.Min.X + 20, bounds.Max.Y))
|
image.Pt(bounds.Min.X + 20, bounds.Max.Y))
|
||||||
artist.Line (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
element.core, c, weight,
|
||||||
image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
|
image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
|
||||||
image.Pt(bounds.Max.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,
|
element.core, c, weight,
|
||||||
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y),
|
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y),
|
||||||
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.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()
|
bounds := element.Bounds()
|
||||||
cellBounds := image.Rectangle { }
|
cellBounds := image.Rectangle { }
|
||||||
cellBounds.Min = bounds.Min
|
cellBounds.Min = bounds.Min
|
||||||
cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
|
cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
|
||||||
cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 8
|
cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4
|
||||||
return cellBounds.Add (image.Pt (
|
return canvas.Cut (element.core, cellBounds.Add (image.Pt (
|
||||||
x * cellBounds.Dx(),
|
x * cellBounds.Dx(),
|
||||||
y * cellBounds.Dy()))
|
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),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package testing
|
package testing
|
||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
import "image/color"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
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/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
// Mouse is an element capable of testing mouse input. When the mouse is clicked
|
// 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
|
||||||
core core.CoreControl
|
core core.CoreControl
|
||||||
drawing bool
|
drawing bool
|
||||||
color artist.Pattern
|
|
||||||
lastMousePos image.Point
|
lastMousePos image.Point
|
||||||
|
|
||||||
config config.Config
|
config config.Config
|
||||||
@ -27,7 +26,6 @@ func NewMouse () (element *Mouse) {
|
|||||||
element = &Mouse { c: theme.C("testing", "mouse") }
|
element = &Mouse { c: theme.C("testing", "mouse") }
|
||||||
element.Core, element.core = core.NewCore(element.draw)
|
element.Core, element.core = core.NewCore(element.draw)
|
||||||
element.core.SetMinimumSize(32, 32)
|
element.core.SetMinimumSize(32, 32)
|
||||||
element.color = artist.NewUniform(color.Black)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,17 +53,17 @@ func (element *Mouse) draw () {
|
|||||||
theme.PatternAccent,
|
theme.PatternAccent,
|
||||||
theme.PatternState { },
|
theme.PatternState { },
|
||||||
element.c)
|
element.c)
|
||||||
artist.FillRectangle(element.core, pattern, bounds)
|
pattern.Draw(element.core, bounds)
|
||||||
artist.StrokeRectangle (
|
shapes.StrokeColorRectangle (
|
||||||
element.core,
|
element.core,
|
||||||
artist.NewUniform(color.Black), 1,
|
artist.Hex(0x000000FF),
|
||||||
bounds)
|
bounds, 1)
|
||||||
artist.Line (
|
shapes.ColorLine (
|
||||||
element.core, artist.NewUniform(color.White), 1,
|
element.core, artist.Hex(0xFFFFFFFF), 1,
|
||||||
bounds.Min.Add(image.Pt(1, 1)),
|
bounds.Min.Add(image.Pt(1, 1)),
|
||||||
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
|
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
|
||||||
artist.Line (
|
shapes.ColorLine (
|
||||||
element.core, artist.NewUniform(color.White), 1,
|
element.core, artist.Hex(0xFFFFFFFF), 1,
|
||||||
bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
|
bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
|
||||||
bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
|
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) {
|
func (element *Mouse) HandleMouseUp (x, y int, button input.Button) {
|
||||||
element.drawing = false
|
element.drawing = false
|
||||||
mousePos := image.Pt(x, y)
|
mousePos := image.Pt(x, y)
|
||||||
element.core.DamageRegion (artist.Line (
|
element.core.DamageRegion (shapes.ColorLine (
|
||||||
element.core, element.color, 1,
|
element.core, artist.Hex(0x000000FF), 1,
|
||||||
element.lastMousePos, mousePos))
|
element.lastMousePos, mousePos))
|
||||||
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) {
|
func (element *Mouse) HandleMouseMove (x, y int) {
|
||||||
if !element.drawing { return }
|
if !element.drawing { return }
|
||||||
mousePos := image.Pt(x, y)
|
mousePos := image.Pt(x, y)
|
||||||
element.core.DamageRegion (artist.Line (
|
element.core.DamageRegion (shapes.ColorLine (
|
||||||
element.core, element.color, 1,
|
element.core, artist.Hex(0x000000FF), 1,
|
||||||
element.lastMousePos, mousePos))
|
element.lastMousePos, mousePos))
|
||||||
element.lastMousePos = mousePos
|
element.lastMousePos = mousePos
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,12 @@ func main () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(128, 128)
|
window, _ := tomo.NewWindow(480, 360)
|
||||||
window.SetTitle("Draw Test")
|
window.SetTitle("Draw Test")
|
||||||
window.Adopt(testing.NewArtist())
|
window.Adopt(testing.NewArtist())
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
go func () {
|
go func () {
|
||||||
http.ListenAndServe("localhost:6060", nil)
|
http.ListenAndServe("localhost:9090", nil)
|
||||||
} ()
|
} ()
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,9 @@ package textdraw
|
|||||||
import "image"
|
import "image"
|
||||||
import "unicode"
|
import "unicode"
|
||||||
import "image/draw"
|
import "image/draw"
|
||||||
|
import "image/color"
|
||||||
import "golang.org/x/image/math/fixed"
|
import "golang.org/x/image/math/fixed"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
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
|
// 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.
|
// 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.
|
// Draw draws the drawer's text onto the specified canvas at the given offset.
|
||||||
func (drawer Drawer) Draw (
|
func (drawer Drawer) Draw (
|
||||||
destination canvas.Canvas,
|
destination canvas.Canvas,
|
||||||
source artist.Pattern,
|
color color.RGBA,
|
||||||
offset image.Point,
|
offset image.Point,
|
||||||
) (
|
) (
|
||||||
updatedRegion image.Rectangle,
|
updatedRegion image.Rectangle,
|
||||||
) {
|
) {
|
||||||
wrappedSource := artist.WrappedPattern {
|
|
||||||
Pattern: source,
|
|
||||||
Width: 0,
|
|
||||||
Height: 0, // TODO: choose a better width and height
|
|
||||||
}
|
|
||||||
|
|
||||||
drawer.For (func (
|
drawer.For (func (
|
||||||
index int,
|
index int,
|
||||||
char rune,
|
char rune,
|
||||||
@ -46,7 +40,7 @@ func (drawer Drawer) Draw (
|
|||||||
draw.DrawMask (
|
draw.DrawMask (
|
||||||
destination,
|
destination,
|
||||||
destinationRectangle,
|
destinationRectangle,
|
||||||
wrappedSource, image.Point { },
|
image.NewUniform(color), image.Point { },
|
||||||
mask, maskPoint,
|
mask, maskPoint,
|
||||||
draw.Over)
|
draw.Over)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user