diff --git a/theme/inset.go b/artist/inset.go similarity index 89% rename from theme/inset.go rename to artist/inset.go index 1cace51..d552eda 100644 --- a/theme/inset.go +++ b/artist/inset.go @@ -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. diff --git a/artist/patterns/bevel.go b/artist/patterns/bevel.go deleted file mode 100644 index 2e7ec91..0000000 --- a/artist/patterns/bevel.go +++ /dev/null @@ -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) -} diff --git a/artist/patterns/border.go b/artist/patterns/border.go new file mode 100644 index 0000000..0b23370 --- /dev/null +++ b/artist/patterns/border.go @@ -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), + } +} diff --git a/artist/patterns/bordered.go b/artist/patterns/bordered.go deleted file mode 100644 index 08a2a39..0000000 --- a/artist/patterns/bordered.go +++ /dev/null @@ -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) - } -} diff --git a/artist/patterns/checkered.go b/artist/patterns/checkered.go deleted file mode 100644 index c02f795..0000000 --- a/artist/patterns/checkered.go +++ /dev/null @@ -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) -} diff --git a/artist/patterns/circlebordered.go b/artist/patterns/circlebordered.go deleted file mode 100644 index 6727477..0000000 --- a/artist/patterns/circlebordered.go +++ /dev/null @@ -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) - } -} diff --git a/artist/patterns/dotted.go b/artist/patterns/dotted.go deleted file mode 100644 index 90a3bea..0000000 --- a/artist/patterns/dotted.go +++ /dev/null @@ -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) - } -} diff --git a/artist/patterns/gradient.go b/artist/patterns/gradient.go deleted file mode 100644 index c89ff29..0000000 --- a/artist/patterns/gradient.go +++ /dev/null @@ -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)), - } -} diff --git a/artist/patterns/noise.go b/artist/patterns/noise.go deleted file mode 100644 index aa31126..0000000 --- a/artist/patterns/noise.go +++ /dev/null @@ -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) - } -} diff --git a/artist/patterns/split.go b/artist/patterns/split.go deleted file mode 100644 index 353c4b5..0000000 --- a/artist/patterns/split.go +++ /dev/null @@ -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) - } -} diff --git a/artist/patterns/striped.go b/artist/patterns/striped.go deleted file mode 100644 index 24fe0dc..0000000 --- a/artist/patterns/striped.go +++ /dev/null @@ -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) - } -} diff --git a/artist/patterns/texture.go b/artist/patterns/texture.go index f2d0ae2..5a97b8b 100644 --- a/artist/patterns/texture.go +++ b/artist/patterns/texture.go @@ -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 } diff --git a/artist/patterns/uniform.go b/artist/patterns/uniform.go index 74c1d0b..0aedbca 100644 --- a/artist/patterns/uniform.go +++ b/artist/patterns/uniform.go @@ -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) } diff --git a/artist/patterns/wrap.go b/artist/patterns/wrap.go deleted file mode 100644 index 5cf0520..0000000 --- a/artist/patterns/wrap.go +++ /dev/null @@ -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 -}