Created new patterns

This commit is contained in:
Sasha Koshka 2023-02-25 18:41:16 -05:00
parent bf2fdb5eaa
commit 81090267a6
14 changed files with 105 additions and 549 deletions

View File

@ -1,7 +1,15 @@
package theme
package artist
import "image"
// Side represents one side of a rectangle.
type Side int; const (
SideTop Side = iota
SideRight
SideBottom
SideLeft
)
// Inset represents an inset amount for all four sides of a rectangle. The top
// side is at index zero, the right at index one, the bottom at index two, and
// the left at index three. These values may be negative.

View File

@ -1,46 +0,0 @@
package artist
import "image/color"
// Beveled is a pattern that has a highlight section and a shadow section.
type Beveled [2]Pattern
// AtWhen satisfies the Pattern interface.
func (pattern Beveled) AtWhen (x, y, width, height int) (c color.RGBA) {
return QuadBeveled {
pattern[0],
pattern[1],
pattern[1],
pattern[0],
}.AtWhen(x, y, width, height)
}
// QuadBeveled is like Beveled, but with four sides. A pattern can be specified
// for each one.
type QuadBeveled [4]Pattern
// AtWhen satisfies the Pattern interface.
func (pattern QuadBeveled) AtWhen (x, y, width, height int) (c color.RGBA) {
bottom := y > height / 2
right := x > width / 2
top := !bottom
left := !right
side := 0
switch {
case top && left:
if x < y { side = 3 } else { side = 0 }
case top && right:
if width - x > y { side = 0 } else { side = 1 }
case bottom && left:
if x < height - y { side = 3 } else { side = 2 }
case bottom && right:
if width - x > height - y { side = 2 } else { side = 1 }
}
return pattern[side].AtWhen(x, y, width, height)
}

64
artist/patterns/border.go Normal file
View File

@ -0,0 +1,64 @@
package patterns
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
type Border struct {
canvas.Canvas
artist.Inset
}
func (pattern Border) Draw (destination canvas.Canvas, clip image.Rectangle) {
bounds := clip.Canon().Intersect(destination.Bounds())
if bounds.Empty() { return }
srcSections := nonasect(pattern.Bounds(), pattern.Inset)
srcTextures := [9]Texture { }
for index, section := range srcSections {
srcTextures[index] = Texture {
Canvas: canvas.Cut(pattern, section),
}
}
dstSections := nonasect(destination.Bounds(), pattern.Inset)
for index, section := range dstSections {
srcTextures[index].Draw(canvas.Cut(destination, section), clip)
}
}
func nonasect (bounds image.Rectangle, inset artist.Inset) [9]image.Rectangle {
center := inset.Apply(bounds)
return [9]image.Rectangle {
// top
image.Rectangle {
bounds.Min,
center.Min },
image.Rect (
center.Min.X, bounds.Min.Y,
center.Max.X, center.Min.Y),
image.Rect (
center.Max.X, bounds.Min.Y,
bounds.Max.X, center.Min.Y),
// center
image.Rect (
bounds.Min.X, center.Min.Y,
center.Min.X, center.Max.Y),
center,
image.Rect (
center.Max.X, center.Min.Y,
bounds.Max.X, center.Max.Y),
// bottom
image.Rect (
bounds.Min.X, center.Max.Y,
center.Min.X, bounds.Max.Y),
image.Rect (
center.Min.X, center.Max.Y,
center.Max.X, bounds.Max.Y),
image.Rect (
center.Max.X, center.Max.Y,
bounds.Max.X, bounds.Max.Y),
}
}

View File

@ -1,111 +0,0 @@
package artist
import "image"
import "image/color"
// Bordered is a pattern with a border and a fill.
type Bordered struct {
Fill Pattern
Stroke
}
// AtWhen satisfies the Pattern interface.
func (pattern Bordered) AtWhen (x, y, width, height int) (c color.RGBA) {
outerBounds := image.Rectangle { Max: image.Point { width, height }}
innerBounds := outerBounds.Inset(pattern.Weight)
if (image.Point { x, y }).In (innerBounds) {
return pattern.Fill.AtWhen (
x - pattern.Weight,
y - pattern.Weight,
innerBounds.Dx(), innerBounds.Dy())
} else {
return pattern.Stroke.AtWhen(x, y, width, height)
}
}
// Stroke represents a stoke that has a weight and a pattern.
type Stroke struct {
Weight int
Pattern
}
type borderInternal struct {
weight int
stroke Pattern
bounds image.Rectangle
dx, dy int
}
// MultiBordered is a pattern that allows multiple borders of different lengths
// to be inset within one another. The final border is treated as a fill color,
// and its weight does not matter.
type MultiBordered struct {
borders []borderInternal
lastWidth, lastHeight int
maxBorder int
}
// NewMultiBordered creates a new MultiBordered pattern from the given list of
// borders.
func NewMultiBordered (borders ...Stroke) (multi *MultiBordered) {
internalBorders := make([]borderInternal, len(borders))
for index, border := range borders {
internalBorders[index].weight = border.Weight
internalBorders[index].stroke = border.Pattern
}
return &MultiBordered { borders: internalBorders }
}
// AtWhen satisfies the Pattern interface.
func (multi *MultiBordered) AtWhen (x, y, width, height int) (c color.RGBA) {
if multi.lastWidth != width || multi.lastHeight != height {
multi.recalculate(width, height)
}
point := image.Point { x, y }
for index := multi.maxBorder; index >= 0; index -- {
border := multi.borders[index]
if point.In(border.bounds) {
return border.stroke.AtWhen (
point.X - border.bounds.Min.X,
point.Y - border.bounds.Min.Y,
border.dx, border.dy)
}
}
return
}
func (multi *MultiBordered) recalculate (width, height int) {
bounds := image.Rect (0, 0, width, height)
multi.maxBorder = 0
for index, border := range multi.borders {
multi.maxBorder = index
multi.borders[index].bounds = bounds
multi.borders[index].dx = bounds.Dx()
multi.borders[index].dy = bounds.Dy()
bounds = bounds.Inset(border.weight)
if bounds.Empty() { break }
}
}
// Padded is a pattern that surrounds a central fill pattern with a border that
// can have a different width for each side.
type Padded struct {
Fill Pattern
Stroke Pattern
Sides []int
}
// AtWhen satisfies the Pattern interface.
func (pattern Padded) AtWhen (x, y, width, height int) (c color.RGBA) {
innerBounds := image.Rect (
pattern.Sides[3], pattern.Sides[0],
width - pattern.Sides[1], height - pattern.Sides[2])
if (image.Point { x, y }).In (innerBounds) {
return pattern.Fill.AtWhen (
x - pattern.Sides[3],
y - pattern.Sides[0],
innerBounds.Dx(), innerBounds.Dy())
} else {
return pattern.Stroke.AtWhen(x, y, width, height)
}
}

View File

@ -1,51 +0,0 @@
package artist
import "image/color"
// Checkered is a pattern that produces a grid of two alternating colors.
type Checkered struct {
First Pattern
Second Pattern
CellWidth, CellHeight int
}
// AtWhen satisfies the Pattern interface.
func (pattern Checkered) AtWhen (x, y, width, height int) (c color.RGBA) {
twidth := pattern.CellWidth * 2
theight := pattern.CellHeight * 2
x %= twidth
y %= theight
if x < 0 { x += twidth }
if y < 0 { x += theight }
n := 0
if x >= pattern.CellWidth { n ++ }
if y >= pattern.CellHeight { n ++ }
x %= pattern.CellWidth
y %= pattern.CellHeight
if n % 2 == 0 {
return pattern.First.AtWhen (
x, y, pattern.CellWidth, pattern.CellHeight)
} else {
return pattern.Second.AtWhen (
x, y, pattern.CellWidth, pattern.CellHeight)
}
}
// Tiled is a pattern that tiles another pattern accross a grid.
type Tiled struct {
Pattern
CellWidth, CellHeight int
}
// AtWhen satisfies the Pattern interface.
func (pattern Tiled) AtWhen (x, y, width, height int) (c color.RGBA) {
x %= pattern.CellWidth
y %= pattern.CellHeight
if x < 0 { x += pattern.CellWidth }
if y < 0 { y += pattern.CellHeight }
return pattern.Pattern.AtWhen (
x, y, pattern.CellWidth, pattern.CellHeight)
}

View File

@ -1,33 +0,0 @@
package artist
import "math"
import "image/color"
// EllipticallyBordered is a pattern with a border and a fill that is elliptical
// in shape.
type EllipticallyBordered struct {
Fill Pattern
Stroke
}
// AtWhen satisfies the Pattern interface.
func (pattern EllipticallyBordered) AtWhen (x, y, width, height int) (c color.RGBA) {
xf := (float64(x) + 0.5) / float64(width ) * 2 - 1
yf := (float64(y) + 0.5) / float64(height) * 2 - 1
distance := math.Sqrt(xf * xf + yf * yf)
var radius float64
if width < height {
// vertical
radius = 1 - float64(pattern.Weight * 2) / float64(width)
} else {
// horizontal
radius = 1 - float64(pattern.Weight * 2) / float64(height)
}
if distance < radius {
return pattern.Fill.AtWhen(x, y, width, height)
} else {
return pattern.Stroke.AtWhen(x, y, width, height)
}
}

View File

@ -1,30 +0,0 @@
package artist
import "math"
import "image/color"
// Dotted is a pattern that produces a grid of circles.
type Dotted struct {
Background Pattern
Foreground Pattern
Size int
Spacing int
}
// AtWhen satisfies the Pattern interface.
func (pattern Dotted) AtWhen (x, y, width, height int) (c color.RGBA) {
xm := x % pattern.Spacing
ym := y % pattern.Spacing
if xm < 0 { xm += pattern.Spacing }
if ym < 0 { xm += pattern.Spacing }
radius := float64(pattern.Size) / 2
spacing := float64(pattern.Spacing) / 2 - 0.5
xf := float64(xm) - spacing
yf := float64(ym) - spacing
if math.Sqrt(xf * xf + yf * yf) > radius {
return pattern.Background.AtWhen(x, y, width, height)
} else {
return pattern.Foreground.AtWhen(x, y, width, height)
}
}

View File

@ -1,45 +0,0 @@
package artist
import "image/color"
// Gradient is a pattern that interpolates between two colors.
type Gradient struct {
First Pattern
Second Pattern
Orientation
}
// AtWhen satisfies the Pattern interface.
func (pattern Gradient) AtWhen (x, y, width, height int) (c color.RGBA) {
var position float64
switch pattern.Orientation {
case OrientationVertical:
position = float64(y) / float64(height)
case OrientationDiagonalRight:
position = (float64(width - x) / float64(width) +
float64(y) / float64(height)) / 2
case OrientationHorizontal:
position = float64(x) / float64(width)
case OrientationDiagonalLeft:
position = (float64(x) / float64(width) +
float64(y) / float64(height)) / 2
}
firstColor := pattern.First.AtWhen(x, y, width, height)
secondColor := pattern.Second.AtWhen(x, y, width, height)
return LerpRGBA(firstColor, secondColor, position)
}
// Lerp linearally interpolates between two integer values.
func Lerp (first, second int, fac float64) (n int) {
return int(float64(first) * (1 - fac) + float64(second) * fac)
}
// LerpRGBA linearally interpolates between two color.RGBA values.
func LerpRGBA (first, second color.RGBA, fac float64) (c color.RGBA) {
return color.RGBA {
R: uint8(Lerp(int(first.R), int(second.R), fac)),
G: uint8(Lerp(int(first.G), int(second.G), fac)),
B: uint8(Lerp(int(first.G), int(second.B), fac)),
}
}

View File

@ -1,33 +0,0 @@
package artist
import "image/color"
// Noisy is a pattern that randomly interpolates between two patterns in a
// deterministic fashion.
type Noisy struct {
Low Pattern
High Pattern
Seed uint32
Harsh bool
}
// AtWhen satisfies the pattern interface.
func (pattern Noisy) AtWhen (x, y, width, height int) (c color.RGBA) {
// FIXME: this will occasionally generate "clumps"
special := uint32(x + y * 348905)
special += (pattern.Seed + 1) * 15485863
random := (special * special * special % 2038074743)
fac := float64(random) / 2038074743.0
if pattern.Harsh {
if fac > 0.5 {
return pattern.High.AtWhen(x, y, width, height)
} else {
return pattern.Low.AtWhen(x, y, width, height)
}
} else {
return LerpRGBA (
pattern.Low.AtWhen(x, y, width, height),
pattern.High.AtWhen(x, y, width, height), fac)
}
}

View File

@ -1,43 +0,0 @@
package artist
import "image/color"
// Orientation specifies an eight-way pattern orientation.
type Orientation int
const (
OrientationVertical Orientation = iota
OrientationDiagonalRight
OrientationHorizontal
OrientationDiagonalLeft
)
// Split is a pattern that is divided in half between two sub-patterns.
type Split struct {
First Pattern
Second Pattern
Orientation
}
// AtWhen satisfies the Pattern interface.
func (pattern Split) AtWhen (x, y, width, height int) (c color.RGBA) {
var first bool
switch pattern.Orientation {
case OrientationVertical:
first = x < width / 2
case OrientationDiagonalRight:
first = float64(x) / float64(width) +
float64(y) / float64(height) < 1
case OrientationHorizontal:
first = y < height / 2
case OrientationDiagonalLeft:
first = float64(width - x) / float64(width) +
float64(y) / float64(height) < 1
}
if first {
return pattern.First.AtWhen(x, y, width, height)
} else {
return pattern.Second.AtWhen(x, y, width, height)
}
}

View File

@ -1,37 +0,0 @@
package artist
import "image/color"
// Striped is a pattern that produces stripes of two alternating colors.
type Striped struct {
First Stroke
Second Stroke
Orientation
}
// AtWhen satisfies the Pattern interface.
func (pattern Striped) AtWhen (x, y, width, height int) (c color.RGBA) {
position := 0
switch pattern.Orientation {
case OrientationVertical:
position = x
case OrientationDiagonalRight:
position = x + y
case OrientationHorizontal:
position = y
case OrientationDiagonalLeft:
position = x - y
}
phase := pattern.First.Weight + pattern.Second.Weight
position %= phase
if position < 0 {
position += phase
}
if position < pattern.First.Weight {
return pattern.First.AtWhen(x, y, width, height)
} else {
return pattern.Second.AtWhen(x, y, width, height)
}
}

View File

@ -1,43 +1,37 @@
package artist
package patterns
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// Texture is a struct that allows an image to be converted into a tiling
// texture pattern.
// Texture is a pattern that tiles the content of a canvas both horizontally and
// vertically.
type Texture struct {
data []color.RGBA
width, height int
canvas.Canvas
}
// NewTexture converts an image into a texture.
func NewTexture (source image.Image) (texture Texture) {
bounds := source.Bounds()
texture.width = bounds.Dx()
texture.height = bounds.Dy()
texture.data = make([]color.RGBA, texture.width * texture.height)
index := 0
// Draw tiles the pattern's canvas within the clipping bounds. The minimum
// points of the pattern's canvas and the destination canvas will be lined up.
func (pattern Texture) Draw (destination canvas.Canvas, clip image.Rectangle) {
bounds := clip.Canon().Intersect(destination.Bounds())
if bounds.Empty() { return }
dstData, dstStride := destination.Buffer()
srcData, srcStride := pattern.Buffer()
srcBounds := pattern.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
r, g, b, a := source.At(x, y).RGBA()
texture.data[index] = color.RGBA {
uint8(r >> 8),
uint8(g >> 8),
uint8(b >> 8),
uint8(a >> 8),
}
index ++
dstIndex := x + y * dstStride
srcIndex :=
wrap(x - bounds.Min.X, srcBounds.Min.X, srcBounds.Max.X) +
wrap(x - bounds.Min.Y, srcBounds.Min.Y, srcBounds.Max.Y) * srcStride
dstData[dstIndex] = srcData[srcIndex]
}}
return
}
// AtWhen returns the color at the specified x and y coordinates, wrapped to the
// image's width. the width and height are ignored.
func (texture Texture) AtWhen (x, y, width, height int) (pixel color.RGBA) {
x %= texture.width
y %= texture.height
if x < 0 { x += texture.width }
if y < 0 { y += texture.height }
return texture.data[x + y * texture.width]
func wrap (value, min, max int) int {
difference := max - min
value = (value - min) % difference
if value < 0 { value += difference }
return value + min
}

View File

@ -1,68 +1,14 @@
package artist
package patterns
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
// Uniform is an infinite-sized pattern of uniform color. It implements the
// Pattern, color.Color, color.Model, and image.Image interfaces.
// Uniform is a pattern that draws a solid color.
type Uniform color.RGBA
// NewUniform returns a new Uniform pattern of the given color.
func NewUniform (c color.Color) (uniform Uniform) {
r, g, b, a := c.RGBA()
uniform.R = uint8(r >> 8)
uniform.G = uint8(g >> 8)
uniform.B = uint8(b >> 8)
uniform.A = uint8(a >> 8)
return
}
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
}
// Uhex creates a new Uniform pattern from an RGBA integer value.
func Uhex (color uint32) (uniform Uniform) {
return NewUniform(hex(color))
}
// ColorModel satisfies the image.Image interface.
func (uniform Uniform) ColorModel () (model color.Model) {
return uniform
}
// Convert satisfies the color.Model interface.
func (uniform Uniform) Convert (in color.Color) (c color.Color) {
return color.RGBA(uniform)
}
// Bounds satisfies the image.Image interface.
func (uniform Uniform) Bounds () (rectangle image.Rectangle) {
rectangle.Min = image.Point { -1e9, -1e9 }
rectangle.Max = image.Point { 1e9, 1e9 }
return
}
// At satisfies the image.Image interface.
func (uniform Uniform) At (x, y int) (c color.Color) {
return color.RGBA(uniform)
}
// AtWhen satisfies the Pattern interface.
func (uniform Uniform) AtWhen (x, y, width, height int) (c color.RGBA) {
return color.RGBA(uniform)
}
// RGBA satisfies the color.Color interface.
func (uniform Uniform) RGBA () (r, g, b, a uint32) {
return color.RGBA(uniform).RGBA()
}
// Opaque scans the entire image and reports whether it is fully opaque.
func (uniform Uniform) Opaque () (opaque bool) {
return uniform.A == 0xFF
// Draw fills the clipping rectangle with the pattern's color.
func (pattern Uniform) Draw (destination canvas.Canvas, clip image.Rectangle) {
shapes.FillColorRectangle(destination, color.RGBA(pattern), clip)
}

View File

@ -1,27 +0,0 @@
package artist
import "image"
import "image/color"
// WrappedPattern is a pattern that is able to behave like an image.Image.
type WrappedPattern struct {
Pattern
Width, Height int
}
// At satisfies the image.Image interface.
func (pattern WrappedPattern) At (x, y int) (c color.Color) {
return pattern.Pattern.AtWhen(x, y, pattern.Width, pattern.Height)
}
// Bounds satisfies the image.Image interface.
func (pattern WrappedPattern) Bounds () (rectangle image.Rectangle) {
rectangle.Min = image.Point { -1e9, -1e9 }
rectangle.Max = image.Point { 1e9, 1e9 }
return
}
// ColorModel satisfies the image.Image interface.
func (pattern WrappedPattern) ColorModel () (model color.Model) {
return color.RGBAModel
}