diff --git a/artist/bevel.go b/artist/bevel.go deleted file mode 100644 index 2e7ec91..0000000 --- a/artist/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/bordered.go b/artist/bordered.go deleted file mode 100644 index 08a2a39..0000000 --- a/artist/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/checkered.go b/artist/checkered.go deleted file mode 100644 index c02f795..0000000 --- a/artist/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/circlebordered.go b/artist/circlebordered.go deleted file mode 100644 index 6727477..0000000 --- a/artist/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/color.go b/artist/color.go new file mode 100644 index 0000000..1729447 --- /dev/null +++ b/artist/color.go @@ -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 +} diff --git a/artist/doc.go b/artist/doc.go index 388f29e..c0a0a6f 100644 --- a/artist/doc.go +++ b/artist/doc.go @@ -1,5 +1,2 @@ // Package artist provides a simple 2D drawing library for canvas.Canvas. -// Artist's drawing functions take in things called patterns, which are sampled -// as a source in order to color and texture drawn shapes. Patterns can be -// mixed together and composited to create new, more complex patterns. package artist diff --git a/artist/dotted.go b/artist/dotted.go deleted file mode 100644 index 90a3bea..0000000 --- a/artist/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/ellipse.go b/artist/ellipse.go deleted file mode 100644 index 7dc9d4c..0000000 --- a/artist/ellipse.go +++ /dev/null @@ -1,145 +0,0 @@ -package artist - -import "math" -import "image" -import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/canvas" - -// FillEllipse draws a filled ellipse with the specified pattern. -func FillEllipse ( - destination canvas.Canvas, - source Pattern, - bounds image.Rectangle, -) ( - updatedRegion image.Rectangle, -) { - bounds = bounds.Canon() - data, stride := destination.Buffer() - realWidth, realHeight := bounds.Dx(), bounds.Dy() - bounds = bounds.Intersect(destination.Bounds()).Canon() - if bounds.Empty() { return } - updatedRegion = bounds - - width, height := bounds.Dx(), bounds.Dy() - for y := 0; y < height; y ++ { - for x := 0; x < width; x ++ { - xf := (float64(x) + 0.5) / float64(realWidth) - 0.5 - yf := (float64(y) + 0.5) / float64(realHeight) - 0.5 - if math.Sqrt(xf * xf + yf * yf) <= 0.5 { - data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] = - source.AtWhen(x, y, realWidth, realHeight) - } - }} - return -} - -// StrokeEllipse draws the outline of an ellipse with the specified line weight -// and pattern. -func StrokeEllipse ( - destination canvas.Canvas, - source Pattern, - weight int, - bounds image.Rectangle, -) { - if weight < 1 { return } - - data, stride := destination.Buffer() - bounds = bounds.Canon().Inset(weight - 1) - width, height := bounds.Dx(), bounds.Dy() - - context := ellipsePlottingContext { - data: data, - stride: stride, - source: source, - width: width, - height: height, - weight: weight, - bounds: bounds, - } - - bounds.Max.X -= 1 - bounds.Max.Y -= 1 - - radii := image.Pt ( - bounds.Dx() / 2, - bounds.Dy() / 2) - center := bounds.Min.Add(radii) - - x := float64(0) - y := float64(radii.Y) - - // region 1 decision parameter - decision1 := - float64(radii.Y * radii.Y) - - float64(radii.X * radii.X * radii.Y) + - (0.25 * float64(radii.X) * float64(radii.X)) - decisionX := float64(2 * radii.Y * radii.Y * int(x)) - decisionY := float64(2 * radii.X * radii.X * int(y)) - - // draw region 1 - for decisionX < decisionY { - context.plot( int(x) + center.X, int(y) + center.Y) - context.plot(-int(x) + center.X, int(y) + center.Y) - context.plot( int(x) + center.X, -int(y) + center.Y) - context.plot(-int(x) + center.X, -int(y) + center.Y) - - if (decision1 < 0) { - x ++ - decisionX += float64(2 * radii.Y * radii.Y) - decision1 += decisionX + float64(radii.Y * radii.Y) - } else { - x ++ - y -- - decisionX += float64(2 * radii.Y * radii.Y) - decisionY -= float64(2 * radii.X * radii.X) - decision1 += - decisionX - decisionY + - float64(radii.Y * radii.Y) - } - } - - // region 2 decision parameter - decision2 := - float64(radii.Y * radii.Y) * (x + 0.5) * (x + 0.5) + - float64(radii.X * radii.X) * (y - 1) * (y - 1) - - float64(radii.X * radii.X * radii.Y * radii.Y) - - // draw region 2 - for y >= 0 { - context.plot( int(x) + center.X, int(y) + center.Y) - context.plot(-int(x) + center.X, int(y) + center.Y) - context.plot( int(x) + center.X, -int(y) + center.Y) - context.plot(-int(x) + center.X, -int(y) + center.Y) - - if decision2 > 0 { - y -- - decisionY -= float64(2 * radii.X * radii.X) - decision2 += float64(radii.X * radii.X) - decisionY - } else { - y -- - x ++ - decisionX += float64(2 * radii.Y * radii.Y) - decisionY -= float64(2 * radii.X * radii.X) - decision2 += - decisionX - decisionY + - float64(radii.X * radii.X) - } - } -} - -type ellipsePlottingContext struct { - data []color.RGBA - stride int - source Pattern - width, height int - weight int - bounds image.Rectangle -} - -func (context ellipsePlottingContext) plot (x, y int) { - if (image.Point { x, y }).In(context.bounds) { - squareAround ( - context.data, context.stride, context.source, x, y, - context.width, context.height, context.weight) - } -} diff --git a/artist/gradient.go b/artist/gradient.go deleted file mode 100644 index c89ff29..0000000 --- a/artist/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/theme/inset.go b/artist/inset.go similarity index 74% rename from theme/inset.go rename to artist/inset.go index 1cace51..196b29b 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. @@ -40,3 +48,13 @@ func (inset Inset) Inverse () (prime Inset) { inset[3] * -1, } } + +// Horizontal returns the sum of SideRight and SideLeft. +func (inset Inset) Horizontal () int { + return inset[SideRight] + inset[SideLeft] +} + +// Vertical returns the sum of SideTop and SideBottom. +func (inset Inset) Vertical () int { + return inset[SideTop] + inset[SideBottom] +} diff --git a/artist/line.go b/artist/line.go deleted file mode 100644 index 733c626..0000000 --- a/artist/line.go +++ /dev/null @@ -1,143 +0,0 @@ -package artist - -import "image" -import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/canvas" - -// TODO: draw thick lines more efficiently - -// Line draws a line from one point to another with the specified weight and -// pattern. -func Line ( - destination canvas.Canvas, - source Pattern, - weight int, - min image.Point, - max image.Point, -) ( - updatedRegion image.Rectangle, -) { - - updatedRegion = image.Rectangle { Min: min, Max: max }.Canon() - updatedRegion.Max.X ++ - updatedRegion.Max.Y ++ - width := updatedRegion.Dx() - height := updatedRegion.Dy() - - if abs(max.Y - min.Y) < - abs(max.X - min.X) { - - if max.X < min.X { - temp := min - min = max - max = temp - } - lineLow(destination, source, weight, min, max, width, height) - } else { - - if max.Y < min.Y { - temp := min - min = max - max = temp - } - lineHigh(destination, source, weight, min, max, width, height) - } - return -} - -func lineLow ( - destination canvas.Canvas, - source Pattern, - weight int, - min image.Point, - max image.Point, - width, height int, -) { - data, stride := destination.Buffer() - bounds := destination.Bounds() - - deltaX := max.X - min.X - deltaY := max.Y - min.Y - yi := 1 - - if deltaY < 0 { - yi = -1 - deltaY *= -1 - } - - D := (2 * deltaY) - deltaX - y := min.Y - - for x := min.X; x < max.X; x ++ { - if !(image.Point { x, y }).In(bounds) { break } - squareAround(data, stride, source, x, y, width, height, weight) - // data[x + y * stride] = source.AtWhen(x, y, width, height) - if D > 0 { - y += yi - D += 2 * (deltaY - deltaX) - } else { - D += 2 * deltaY - } - } -} - -func lineHigh ( - destination canvas.Canvas, - source Pattern, - weight int, - min image.Point, - max image.Point, - width, height int, -) { - data, stride := destination.Buffer() - bounds := destination.Bounds() - - deltaX := max.X - min.X - deltaY := max.Y - min.Y - xi := 1 - - if deltaX < 0 { - xi = -1 - deltaX *= -1 - } - - D := (2 * deltaX) - deltaY - x := min.X - - for y := min.Y; y < max.Y; y ++ { - if !(image.Point { x, y }).In(bounds) { break } - squareAround(data, stride, source, x, y, width, height, weight) - // data[x + y * stride] = source.AtWhen(x, y, width, height) - if D > 0 { - x += xi - D += 2 * (deltaX - deltaY) - } else { - D += 2 * deltaX - } - } -} - -func abs (in int) (out int) { - if in < 0 { in *= -1} - out = in - return -} - -// TODO: this method of doing things sucks and can cause a segfault. we should -// not be doing it this way -func squareAround ( - data []color.RGBA, - stride int, - source Pattern, - x, y, patternWidth, patternHeight, diameter int, -) { - minY := y - diameter + 1 - minX := x - diameter + 1 - maxY := y + diameter - maxX := x + diameter - for y = minY; y < maxY; y ++ { - for x = minX; x < maxX; x ++ { - data[x + y * stride] = - source.AtWhen(x, y, patternWidth, patternHeight) - }} -} diff --git a/artist/noise.go b/artist/noise.go deleted file mode 100644 index aa31126..0000000 --- a/artist/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/pattern.go b/artist/pattern.go index ef87caa..558bbf6 100644 --- a/artist/pattern.go +++ b/artist/pattern.go @@ -1,12 +1,67 @@ package artist -import "image/color" +import "image" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/shatter" -// Pattern is capable of generating a pattern pixel by pixel. +// Pattern is capable of drawing to a canvas within the bounds of a given +// clipping rectangle. type Pattern interface { - // AtWhen returns the color of the pixel located at (x, y) relative to - // the origin point of the pattern (0, 0), when the pattern has the - // specified width and height. Patterns may ignore the width and height - // parameters, but it may be useful for some patterns such as gradients. - AtWhen (x, y, width, height int) (color.RGBA) + // Draw draws to destination, using the bounds of destination as a width + // and height for things like gradients, bevels, etc. The pattern may + // not draw outside the union of destination.Bounds() and clip. The + // clipping rectangle effectively takes a subset of the pattern. To + // change the bounds of the pattern itself, use canvas.Cut() on the + // destination before passing it to Draw(). + Draw (destination canvas.Canvas, clip image.Rectangle) } + +// Draw lets you use several clipping rectangles to draw a pattern. +func Draw ( + destination canvas.Canvas, + source Pattern, + clips ...image.Rectangle, +) ( + updatedRegion image.Rectangle, +) { + for _, clip := range clips { + source.Draw(destination, clip) + updatedRegion = updatedRegion.Union(clip) + } + return +} + +// DrawBounds lets you specify an overall bounding rectangle for drawing a +// pattern. The destination is cut to this rectangle. +func DrawBounds ( + destination canvas.Canvas, + source Pattern, + bounds image.Rectangle, +) ( + updatedRegion image.Rectangle, +) { + return Draw(canvas.Cut(destination, bounds), source, bounds) +} + +// DrawShatter is like an inverse of Draw, drawing nothing in the areas +// specified in "rocks". +func DrawShatter ( + destination canvas.Canvas, + source Pattern, + rocks ...image.Rectangle, +) ( + updatedRegion image.Rectangle, +) { + tiles := shatter.Shatter(destination.Bounds(), rocks...) + return Draw(destination, source, tiles...) +} + +// AllocateSample returns a new canvas containing the result of a pattern. The +// resulting canvas can be sourced from shape drawing functions. I beg of you +// please do not call this every time you need to draw a shape with a pattern on +// it because that is horrible and cruel to the computer. +func AllocateSample (source Pattern, width, height int) (allocated canvas.Canvas) { + allocated = canvas.NewBasicCanvas(width, height) + source.Draw(allocated, allocated.Bounds()) + return +} diff --git a/artist/patterns/border.go b/artist/patterns/border.go new file mode 100644 index 0000000..3202a0b --- /dev/null +++ b/artist/patterns/border.go @@ -0,0 +1,90 @@ +package patterns + +import "image" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" + +// Border is a pattern that behaves similarly to border-image in CSS. It divides +// a source canvas into nine sections... +// +// Inset[1] +// ┌──┴──┐ +// ┌─┌─────┬─────┬─────┐ +// Inset[0]─┤ │ 0 │ 1 │ 2 │ +// └─├─────┼─────┼─────┤ +// │ 3 │ 4 │ 5 │ +// ├─────┼─────┼─────┤─┐ +// │ 6 │ 7 │ 8 │ ├─Inset[2] +// └─────┴─────┴─────┘─┘ +// └──┬──┘ +// Inset[3] +// +// ... Where the bounds of section 4 are defined as the application of the +// pattern's inset to the canvas's bounds. The bounds of the other eight +// sections are automatically sized around it. +// +// When drawn to a destination canvas, the bounds of sections 1, 3, 4, 5, and 7 +// are expanded or contracted to fit the destination's bounds. All sections +// are rendered as if they are Texture patterns, meaning these flexible sections +// will repeat to fill in any empty space. +// +// This pattern can be used to make a static image texture into something that +// responds well to being resized. +type Border struct { + canvas.Canvas + artist.Inset +} + +// Draw draws the border pattern onto the destination canvas within the clipping +// bounds. +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].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/doc.go b/artist/patterns/doc.go new file mode 100644 index 0000000..c36fb42 --- /dev/null +++ b/artist/patterns/doc.go @@ -0,0 +1,3 @@ +// Package patterns provides a basic set of types that satisfy the +// artist.Pattern interface. +package patterns diff --git a/artist/patterns/texture.go b/artist/patterns/texture.go new file mode 100644 index 0000000..897417e --- /dev/null +++ b/artist/patterns/texture.go @@ -0,0 +1,41 @@ +package patterns + +import "image" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +// Texture is a pattern that tiles the content of a canvas both horizontally and +// vertically. +type Texture struct { + canvas.Canvas +} + +// 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) { + realBounds := destination.Bounds() + bounds := clip.Canon().Intersect(realBounds) + if bounds.Empty() { return } + + dstData, dstStride := destination.Buffer() + srcData, srcStride := pattern.Buffer() + srcBounds := pattern.Bounds() + + point := image.Point { } + for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { + for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { + srcPoint := point.Sub(realBounds.Min).Add(srcBounds.Min) + + dstIndex := point.X + point.Y * dstStride + srcIndex := + wrap(srcPoint.X, srcBounds.Min.X, srcBounds.Max.X) + + wrap(srcPoint.Y, srcBounds.Min.Y, srcBounds.Max.Y) * srcStride + dstData[dstIndex] = srcData[srcIndex] + }} +} + +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 new file mode 100644 index 0000000..8efb8be --- /dev/null +++ b/artist/patterns/uniform.go @@ -0,0 +1,20 @@ +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. +type Uniform color.RGBA + +// 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) +} + +// Uhex creates a new Uniform pattern from an RGBA integer value. +func Uhex (color uint32) (uniform Uniform) { + return Uniform(artist.Hex(color)) +} diff --git a/artist/rectangle.go b/artist/rectangle.go deleted file mode 100644 index 5adbe72..0000000 --- a/artist/rectangle.go +++ /dev/null @@ -1,130 +0,0 @@ -package artist - -import "image" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/shatter" - -// Paste transfers one canvas onto another, offset by the specified point. -func Paste ( - 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 } - - 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] - }} - - return -} - -// FillRectangle draws a filled rectangle with the specified pattern. -func FillRectangle ( - destination canvas.Canvas, - source Pattern, - bounds image.Rectangle, -) ( - updatedRegion image.Rectangle, -) { - return FillRectangleClip(destination, source, bounds, bounds) -} - -// FillRectangleClip is similar to FillRectangle, but it clips the pattern to -// a specified rectangle mask. That is—the pattern will be queried as if it -// were drawn without the mask, but only the area specified by the intersection -// of bounds and mask will be drawn to. -func FillRectangleClip ( - destination canvas.Canvas, - source Pattern, - bounds image.Rectangle, - mask image.Rectangle, -) ( - updatedRegion image.Rectangle, -) { - data, stride := destination.Buffer() - realBounds := bounds - bounds = - bounds.Canon(). - Intersect(mask.Canon()). - Intersect(destination.Bounds()) - if bounds.Empty() { return } - updatedRegion = bounds - - realWidth, realHeight := realBounds.Dx(), realBounds.Dy() - patternOffset := realBounds.Min.Sub(bounds.Min) - - width, height := bounds.Dx(), bounds.Dy() - for y := 0; y < height; y ++ { - for x := 0; x < width; x ++ { - data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] = - source.AtWhen ( - x - patternOffset.X, y - patternOffset.Y, - realWidth, realHeight) - }} - return -} - -// FillRectangleShatter shatters a bounding rectangle and draws its tiles in one -// fell swoop. -func FillRectangleShatter ( - destination canvas.Canvas, - source Pattern, - glass image.Rectangle, - rocks ...image.Rectangle, -) ( - updatedRegions []image.Rectangle, -) { - tiles := shatter.Shatter(glass, rocks...) - for _, tile := range tiles { - FillRectangleClip(destination, source, glass, tile) - } - return tiles -} - -// StrokeRectangle draws the outline of a rectangle with the specified line -// weight and pattern. -func StrokeRectangle ( - destination canvas.Canvas, - source Pattern, - weight int, - bounds image.Rectangle, -) { - bounds = bounds.Canon() - insetBounds := bounds.Inset(weight) - if insetBounds.Empty() { - FillRectangle(destination, source, bounds) - return - } - - // top - FillRectangle (destination, source, image.Rect ( - bounds.Min.X, bounds.Min.Y, - bounds.Max.X, insetBounds.Min.Y)) - - // bottom - FillRectangle (destination, source, image.Rect ( - bounds.Min.X, insetBounds.Max.Y, - bounds.Max.X, bounds.Max.Y)) - - // left - FillRectangle (destination, source, image.Rect ( - bounds.Min.X, insetBounds.Min.Y, - insetBounds.Min.X, insetBounds.Max.Y)) - - // right - FillRectangle (destination, source, image.Rect ( - insetBounds.Max.X, insetBounds.Min.Y, - bounds.Max.X, insetBounds.Max.Y)) -} diff --git a/artist/shapes/doc.go b/artist/shapes/doc.go new file mode 100644 index 0000000..98eb8f6 --- /dev/null +++ b/artist/shapes/doc.go @@ -0,0 +1,11 @@ +// Package shapes provides some basic shape drawing routines. +// +// A word about using patterns with shape routines: +// +// Most drawing routines have a version that samples from other canvases, and a +// version that samples from a solid color. None of these routines can use +// patterns directly, but it is entirely possible to have a pattern draw to an +// off-screen canvas and then draw a shape based on that canvas. As a little +// bonus, you can save the canvas for later so you don't have to render the +// pattern again when you need to redraw the shape. +package shapes diff --git a/artist/shapes/ellipse.go b/artist/shapes/ellipse.go new file mode 100644 index 0000000..0e5f0c5 --- /dev/null +++ b/artist/shapes/ellipse.go @@ -0,0 +1,228 @@ +package shapes + +import "math" +import "image" +import "image/color" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +// 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, +) ( + updatedRegion image.Rectangle, +) { + dstData, dstStride := destination.Buffer() + srcData, srcStride := source.Buffer() + + 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 + + point := image.Point { } + for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { + for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { + if inEllipse(point, realBounds) { + offsetPoint := point.Add(offset) + dstIndex := point.X + point.Y * dstStride + srcIndex := offsetPoint.X + offsetPoint.Y * srcStride + dstData[dstIndex] = srcData[srcIndex] + } + }} + return +} + +func StrokeEllipse ( + destination canvas.Canvas, + source canvas.Canvas, + weight int, +) { + if weight < 1 { return } + + dstData, dstStride := destination.Buffer() + srcData, srcStride := source.Buffer() + + 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 { + dstData: dstData, + dstStride: dstStride, + srcData: srcData, + srcStride: srcStride, + weight: weight, + offset: offset, + bounds: realBounds, + }, + radii: image.Pt(bounds.Dx() / 2, bounds.Dy() / 2), + } + context.center = bounds.Min.Add(context.radii) + context.plotEllipse() +} + +type ellipsePlottingContext struct { + plottingContext + radii image.Point + center image.Point +} + +func (context ellipsePlottingContext) plotEllipse () { + x := float64(0) + y := float64(context.radii.Y) + + // region 1 decision parameter + decision1 := + float64(context.radii.Y * context.radii.Y) - + float64(context.radii.X * context.radii.X * context.radii.Y) + + (0.25 * float64(context.radii.X) * float64(context.radii.X)) + decisionX := float64(2 * context.radii.Y * context.radii.Y * int(x)) + decisionY := float64(2 * context.radii.X * context.radii.X * int(y)) + + // draw region 1 + for decisionX < decisionY { + points := []image.Point { + image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y), + image.Pt( int(x) + context.center.X, -int(y) + context.center.Y), + image.Pt(-int(x) + context.center.X, int(y) + context.center.Y), + image.Pt( int(x) + context.center.X, int(y) + context.center.Y), + } + if context.srcData == nil { + context.plotColor(points[0]) + context.plotColor(points[1]) + context.plotColor(points[2]) + context.plotColor(points[3]) + } else { + context.plotSource(points[0]) + context.plotSource(points[1]) + context.plotSource(points[2]) + context.plotSource(points[3]) + } + + if (decision1 < 0) { + x ++ + decisionX += float64(2 * context.radii.Y * context.radii.Y) + decision1 += decisionX + float64(context.radii.Y * context.radii.Y) + } else { + x ++ + y -- + decisionX += float64(2 * context.radii.Y * context.radii.Y) + decisionY -= float64(2 * context.radii.X * context.radii.X) + decision1 += + decisionX - decisionY + + float64(context.radii.Y * context.radii.Y) + } + } + + // region 2 decision parameter + decision2 := + float64(context.radii.Y * context.radii.Y) * (x + 0.5) * (x + 0.5) + + float64(context.radii.X * context.radii.X) * (y - 1) * (y - 1) - + float64(context.radii.X * context.radii.X * context.radii.Y * context.radii.Y) + + // draw region 2 + for y >= 0 { + points := []image.Point { + image.Pt( int(x) + context.center.X, int(y) + context.center.Y), + image.Pt(-int(x) + context.center.X, int(y) + context.center.Y), + image.Pt( int(x) + context.center.X, -int(y) + context.center.Y), + image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y), + } + if context.srcData == nil { + context.plotColor(points[0]) + context.plotColor(points[1]) + context.plotColor(points[2]) + context.plotColor(points[3]) + } else { + context.plotSource(points[0]) + context.plotSource(points[1]) + context.plotSource(points[2]) + context.plotSource(points[3]) + } + + if decision2 > 0 { + y -- + decisionY -= float64(2 * context.radii.X * context.radii.X) + decision2 += float64(context.radii.X * context.radii.X) - decisionY + } else { + y -- + x ++ + decisionX += float64(2 * context.radii.Y * context.radii.Y) + decisionY -= float64(2 * context.radii.X * context.radii.X) + decision2 += + decisionX - decisionY + + float64(context.radii.X * context.radii.X) + } + } +} + +// FillColorEllipse fills an ellipse within the destination canvas with a solid +// color. +func FillColorEllipse ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, +) ( + updatedRegion image.Rectangle, +) { + dstData, dstStride := destination.Buffer() + + realBounds := bounds + bounds = bounds.Intersect(destination.Bounds()).Canon() + if bounds.Empty() { return } + updatedRegion = bounds + + point := image.Point { } + for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { + for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { + if inEllipse(point, realBounds) { + dstData[point.X + point.Y * dstStride] = color + } + }} + return +} + +// StrokeColorEllipse is similar to FillColorEllipse, but it draws an inset +// outline of an ellipse instead. +func StrokeColorEllipse ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, + weight int, +) ( + updatedRegion image.Rectangle, +) { + if weight < 1 { return } + + dstData, dstStride := destination.Buffer() + insetBounds := bounds.Inset(weight - 1) + + context := ellipsePlottingContext { + plottingContext: plottingContext { + dstData: dstData, + dstStride: dstStride, + color: color, + weight: weight, + bounds: bounds.Intersect(destination.Bounds()), + }, + radii: image.Pt(insetBounds.Dx() / 2, insetBounds.Dy() / 2), + } + context.center = insetBounds.Min.Add(context.radii) + context.plotEllipse() + return +} + +func inEllipse (point image.Point, bounds image.Rectangle) bool { + point = point.Sub(bounds.Min) + x := (float64(point.X) + 0.5) / float64(bounds.Dx()) - 0.5 + y := (float64(point.Y) + 0.5) / float64(bounds.Dy()) - 0.5 + return math.Hypot(x, y) <= 0.5 +} diff --git a/artist/shapes/line.go b/artist/shapes/line.go new file mode 100644 index 0000000..1ebfbbb --- /dev/null +++ b/artist/shapes/line.go @@ -0,0 +1,112 @@ +package shapes + +import "image" +import "image/color" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +// ColorLine draws a line from one point to another with the specified weight +// and color. +func ColorLine ( + destination canvas.Canvas, + color color.RGBA, + weight int, + min image.Point, + max image.Point, +) ( + updatedRegion image.Rectangle, +) { + updatedRegion = image.Rectangle { Min: min, Max: max }.Canon() + updatedRegion.Max.X ++ + updatedRegion.Max.Y ++ + + data, stride := destination.Buffer() + bounds := destination.Bounds() + context := linePlottingContext { + plottingContext: plottingContext { + dstData: data, + dstStride: stride, + color: color, + weight: weight, + bounds: bounds, + }, + min: min, + max: max, + } + + if abs(max.Y - min.Y) < abs(max.X - min.X) { + if max.X < min.X { context.swap() } + context.lineLow() + + } else { + if max.Y < min.Y { context.swap() } + context.lineHigh() + } + return +} + +type linePlottingContext struct { + plottingContext + min image.Point + max image.Point +} + +func (context *linePlottingContext) swap () { + temp := context.max + context.max = context.min + context.min = temp +} + +func (context linePlottingContext) lineLow () { + deltaX := context.max.X - context.min.X + deltaY := context.max.Y - context.min.Y + yi := 1 + + if deltaY < 0 { + yi = -1 + deltaY *= -1 + } + + D := (2 * deltaY) - deltaX + point := context.min + + for ; point.X < context.max.X; point.X ++ { + if !point.In(context.bounds) { break } + context.plotColor(point) + if D > 0 { + D += 2 * (deltaY - deltaX) + point.Y += yi + } else { + D += 2 * deltaY + } + } +} + +func (context linePlottingContext) lineHigh () { + deltaX := context.max.X - context.min.X + deltaY := context.max.Y - context.min.Y + xi := 1 + + if deltaX < 0 { + xi = -1 + deltaX *= -1 + } + + D := (2 * deltaX) - deltaY + point := context.min + + for ; point.Y < context.max.Y; point.Y ++ { + if !point.In(context.bounds) { break } + context.plotColor(point) + if D > 0 { + point.X += xi + D += 2 * (deltaX - deltaY) + } else { + D += 2 * deltaX + } + } +} + +func abs (n int) int { + if n < 0 { n *= -1} + return n +} diff --git a/artist/shapes/plot.go b/artist/shapes/plot.go new file mode 100644 index 0000000..6749768 --- /dev/null +++ b/artist/shapes/plot.go @@ -0,0 +1,47 @@ +package shapes + +import "image" +import "image/color" + +// FIXME? drawing a ton of overlapping squares might be a bit wasteful. + +type plottingContext struct { + dstData []color.RGBA + dstStride int + srcData []color.RGBA + srcStride int + color color.RGBA + weight int + offset image.Point + bounds 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). + Intersect(context.bounds) +} + +func (context plottingContext) plotColor (center image.Point) { + square := context.square(center) + 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.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 + dstIndex := x + y * context.dstStride + context.dstData[dstIndex] = context.srcData [srcIndex] + }} +} diff --git a/artist/shapes/rectangle.go b/artist/shapes/rectangle.go new file mode 100644 index 0000000..8912ade --- /dev/null +++ b/artist/shapes/rectangle.go @@ -0,0 +1,116 @@ +package shapes + +import "image" +import "image/color" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/shatter" + +// TODO: return updatedRegion for all routines in this package + +func FillRectangle ( + destination canvas.Canvas, + source canvas.Canvas, +) ( + updatedRegion image.Rectangle, +) { + dstData, dstStride := destination.Buffer() + srcData, srcStride := source.Buffer() + + offset := source.Bounds().Min.Sub(destination.Bounds().Min) + bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds()) + if bounds.Empty() { return } + updatedRegion = bounds + + point := image.Point { } + for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { + for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { + offsetPoint := point.Add(offset) + dstIndex := point.X + point.Y * dstStride + srcIndex := offsetPoint.X + offsetPoint.Y * srcStride + dstData[dstIndex] = srcData[srcIndex] + }} + + return +} + +func StrokeRectangle ( + destination canvas.Canvas, + source canvas.Canvas, + weight int, +) { + bounds := destination.Bounds() + insetBounds := bounds.Inset(weight) + if insetBounds.Empty() { + FillRectangle(destination, source) + return + } + FillRectangleShatter(destination, source, insetBounds) +} + +// FillRectangleShatter is like FillRectangle, but it does not draw in areas +// specified in "rocks". +func FillRectangleShatter ( + destination canvas.Canvas, + source canvas.Canvas, + rocks ...image.Rectangle, +) { + tiles := shatter.Shatter(destination.Bounds(), rocks...) + offset := source.Bounds().Min.Sub(destination.Bounds().Min) + for _, tile := range tiles { + FillRectangle ( + canvas.Cut(destination, tile), + canvas.Cut(source, tile.Add(offset))) + } +} + +// FillColorRectangle fills a rectangle within the destination canvas with a +// solid color. +func FillColorRectangle ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, +) ( + updatedRegion image.Rectangle, +) { + dstData, dstStride := destination.Buffer() + bounds = bounds.Canon().Intersect(destination.Bounds()) + if bounds.Empty() { return } + + updatedRegion = bounds + for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { + for x := bounds.Min.X; x < bounds.Max.X; x ++ { + dstData[x + y * dstStride] = color + }} + + return +} + +// FillColorRectangleShatter is like FillColorRectangle, but it does not draw in +// areas specified in "rocks". +func FillColorRectangleShatter ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, + rocks ...image.Rectangle, +) { + tiles := shatter.Shatter(bounds, rocks...) + for _, tile := range tiles { + FillColorRectangle(destination, color, tile) + } +} + +// StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset +// outline of the given rectangle instead. +func StrokeColorRectangle ( + destination canvas.Canvas, + color color.RGBA, + bounds image.Rectangle, + weight int, +) { + insetBounds := bounds.Inset(weight) + if insetBounds.Empty() { + FillColorRectangle(destination, color, bounds) + return + } + FillColorRectangleShatter(destination, color, bounds, insetBounds) +} diff --git a/artist/split.go b/artist/split.go deleted file mode 100644 index 353c4b5..0000000 --- a/artist/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/striped.go b/artist/striped.go deleted file mode 100644 index 24fe0dc..0000000 --- a/artist/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/texture.go b/artist/texture.go deleted file mode 100644 index f2d0ae2..0000000 --- a/artist/texture.go +++ /dev/null @@ -1,43 +0,0 @@ -package artist - -import "image" -import "image/color" - -// Texture is a struct that allows an image to be converted into a tiling -// texture pattern. -type Texture struct { - data []color.RGBA - width, height int -} - -// 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 - 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 ++ - }} - 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] -} diff --git a/artist/uniform.go b/artist/uniform.go deleted file mode 100644 index 74c1d0b..0000000 --- a/artist/uniform.go +++ /dev/null @@ -1,68 +0,0 @@ -package artist - -import "image" -import "image/color" - -// Uniform is an infinite-sized pattern of uniform color. It implements the -// Pattern, color.Color, color.Model, and image.Image interfaces. -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 -} diff --git a/artist/wrap.go b/artist/wrap.go deleted file mode 100644 index 5cf0520..0000000 --- a/artist/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 -} diff --git a/canvas/canvas.go b/canvas/canvas.go index 436dcd1..1661b9c 100644 --- a/canvas/canvas.go +++ b/canvas/canvas.go @@ -34,6 +34,21 @@ func NewBasicCanvas (width, height int) (canvas BasicCanvas) { return } +// FromImage creates a new BasicCanvas from an image.Image. +func FromImage (img image.Image) (canvas BasicCanvas) { + bounds := img.Bounds() + canvas = NewBasicCanvas(bounds.Dx(), bounds.Dy()) + 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 ++ { + canvasPoint := point.Sub(bounds.Min) + canvas.Set ( + canvasPoint.X, canvasPoint.Y, + img.At(point.X, point.Y)) + }} + return +} + // you know what it do func (canvas BasicCanvas) Bounds () (bounds image.Rectangle) { return canvas.rect diff --git a/config/config.go b/config/config.go index 2937e0c..a7e2a66 100644 --- a/config/config.go +++ b/config/config.go @@ -2,15 +2,6 @@ package config // Config can return global configuration parameters. type Config interface { - // Padding returns the amount of internal padding elements should have. - // An element's inner content (such as text) should be inset by this - // amount, in addition to the inset returned by the pattern of its - // background. - Padding () int - - // Margin returns how much space should be put in between elements. - Margin () int - // HandleWidth returns how large grab handles should typically be. This // is important for accessibility reasons. HandleWidth () int @@ -26,15 +17,6 @@ type Config interface { // Default specifies default configuration values. type Default struct { } -// Padding returns the default padding value. -func (Default) Padding () int { - return 7 -} - -// Margin returns the default margin value. -func (Default) Margin () int { - return 8 -} // HandleWidth returns the default handle width value. func (Default) HandleWidth () int { @@ -56,19 +38,6 @@ type Wrapped struct { Config } -// Padding returns the amount of internal padding elements should have. -// An element's inner content (such as text) should be inset by this -// amount, in addition to the inset returned by the pattern of its -// background. -func (wrapped Wrapped) Padding () int { - return wrapped.ensure().Padding() -} - -// Margin returns how much space should be put in between elements. -func (wrapped Wrapped) Margin () int { - return wrapped.ensure().Margin() -} - // HandleWidth returns how large grab handles should typically be. This // is important for accessibility reasons. func (wrapped Wrapped) HandleWidth () int { diff --git a/elements/basic/button.go b/elements/basic/button.go index 69ed715..e11a827 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -6,6 +6,7 @@ import "git.tebibyte.media/sashakoshka/tomo/input" 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/shatter" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -121,7 +122,8 @@ func (element *Button) SetConfig (new config.Config) { func (element *Button) updateMinimumSize () { textBounds := element.drawer.LayoutBounds() - minimumSize := textBounds.Inset(-element.config.Padding()) + padding := element.theme.Padding(theme.PatternButton) + minimumSize := padding.Inverse().Apply(textBounds) element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) } @@ -138,8 +140,8 @@ func (element *Button) drawAndPush (partial bool) { } } -func (element *Button) state () theme.PatternState { - return theme.PatternState { +func (element *Button) state () theme.State { + return theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), Pressed: element.pressed, @@ -152,20 +154,20 @@ func (element *Button) drawBackground (partial bool) []image.Rectangle { pattern := element.theme.Pattern(theme.PatternButton, state) static := element.theme.Hints(theme.PatternButton).StaticInset - if partial && static != (theme.Inset { }) { - return artist.FillRectangleShatter ( - element.core, pattern, bounds, static.Apply(bounds)) + if partial && static != (artist.Inset { }) { + tiles := shatter.Shatter(bounds, static.Apply(bounds)) + artist.Draw(element.core, pattern, tiles...) + return tiles } else { - return []image.Rectangle { - artist.FillRectangle(element.core, pattern, bounds), - } + pattern.Draw(element.core, bounds) + return []image.Rectangle { bounds } } } func (element *Button) drawText (partial bool) image.Rectangle { state := element.state() bounds := element.Bounds() - foreground := element.theme.Pattern(theme.PatternForeground, state) + foreground := element.theme.Color(theme.ColorForeground, state) sink := element.theme.Sink(theme.PatternButton) textBounds := element.drawer.LayoutBounds() @@ -183,8 +185,7 @@ func (element *Button) drawText (partial bool) image.Rectangle { if partial { pattern := element.theme.Pattern(theme.PatternButton, state) - artist.FillRectangleClip ( - element.core, pattern, bounds, region) + pattern.Draw(element.core, region) } element.drawer.Draw(element.core, foreground, offset) diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index 89eaabc..ab2217e 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -146,8 +146,9 @@ func (element *Checkbox) updateMinimumSize () { if element.text == "" { element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) } else { + margin := element.theme.Margin(theme.PatternBackground) element.core.SetMinimumSize ( - textBounds.Dy() + element.config.Padding() + textBounds.Dx(), + textBounds.Dy() + margin.X + textBounds.Dx(), textBounds.Dy()) } } @@ -163,7 +164,7 @@ func (element *Checkbox) draw () { bounds := element.Bounds() boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) - state := theme.PatternState { + state := theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), Pressed: element.pressed, @@ -172,19 +173,20 @@ func (element *Checkbox) draw () { backgroundPattern := element.theme.Pattern ( theme.PatternBackground, state) - artist.FillRectangle(element.core, backgroundPattern, bounds) + backgroundPattern.Draw(element.core, bounds) pattern := element.theme.Pattern(theme.PatternButton, state) - artist.FillRectangle(element.core, pattern, boxBounds) + artist.DrawBounds(element.core, pattern, boxBounds) textBounds := element.drawer.LayoutBounds() + margin := element.theme.Margin(theme.PatternBackground) offset := bounds.Min.Add(image.Point { - X: bounds.Dy() + element.config.Padding(), + X: bounds.Dy() + margin.X, }) offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Pattern(theme.PatternForeground, state) + foreground := element.theme.Color(theme.ColorForeground, state) element.drawer.Draw(element.core, foreground, offset) } diff --git a/elements/basic/container.go b/elements/basic/container.go index b4bb5b0..0347def 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -226,9 +226,9 @@ func (element *Container) redoAll () { } pattern := element.theme.Pattern ( theme.PatternBackground, - theme.PatternState { }) - artist.FillRectangleShatter ( - element.core, pattern, element.Bounds(), rocks...) + theme.State { }) + artist.DrawShatter ( + element.core, pattern, rocks...) // cut our canvas up and give peices to child elements for _, entry := range element.children { @@ -311,9 +311,11 @@ func (element *Container) HandleKeyUp (key input.Key, modifiers input.Modifiers) } func (element *Container) FlexibleHeightFor (width int) (height int) { + margin := element.theme.Margin(theme.PatternBackground) + // TODO: have layouts take in x and y margins return element.layout.FlexibleHeightFor ( element.children, - element.config.Margin(), width) + margin.X, width) } func (element *Container) OnFlexibleHeightChange (callback func ()) { @@ -515,16 +517,20 @@ func (element *Container) childFocusRequestCallback ( } func (element *Container) updateMinimumSize () { - width, height := element.layout.MinimumSize ( - element.children, element.config.Margin()) + margin := element.theme.Margin(theme.PatternBackground) + // TODO: have layouts take in x and y margins + width, height := element.layout.MinimumSize(element.children, margin.X) if element.flexible { height = element.layout.FlexibleHeightFor ( - element.children, element.config.Margin(), width) + element.children, + margin.X, width) } element.core.SetMinimumSize(width, height) } func (element *Container) doLayout () { + margin := element.theme.Margin(theme.PatternBackground) + // TODO: have layouts take in x and y margins element.layout.Arrange ( - element.children, element.config.Margin(), element.Bounds()) + element.children, margin.X, element.Bounds()) } diff --git a/elements/basic/image.go b/elements/basic/image.go index 46482a3..2e5d4fd 100644 --- a/elements/basic/image.go +++ b/elements/basic/image.go @@ -1,17 +1,18 @@ package basicElements import "image" -import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" type Image struct { *core.Core core core.CoreControl - buffer artist.Pattern + buffer canvas.Canvas } func NewImage (image image.Image) (element *Image) { - element = &Image { buffer: artist.NewTexture(image) } + element = &Image { buffer: canvas.FromImage(image) } element.Core, element.core = core.NewCore(element.draw) bounds := image.Bounds() element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) @@ -19,5 +20,6 @@ func NewImage (image image.Image) (element *Image) { } func (element *Image) draw () { - artist.FillRectangle(element.core, element.buffer, element.Bounds()) + (patterns.Texture { Canvas: element.buffer }). + Draw(element.core, element.Bounds()) } diff --git a/elements/basic/label.go b/elements/basic/label.go index e82fea9..2674715 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -2,7 +2,6 @@ package basicElements 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/textdraw" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -137,7 +136,9 @@ func (element *Label) SetConfig (new config.Config) { func (element *Label) updateMinimumSize () { if element.wrap { em := element.drawer.Em().Round() - if em < 1 { em = element.config.Padding() } + if em < 1 { + em = element.theme.Padding(theme.PatternBackground)[0] + } element.core.SetMinimumSize ( em, element.drawer.LineHeight().Round()) if element.onFlexibleHeightChange != nil { @@ -154,13 +155,13 @@ func (element *Label) draw () { pattern := element.theme.Pattern ( theme.PatternBackground, - theme.PatternState { }) - artist.FillRectangle(element.core, pattern, bounds) + theme.State { }) + pattern.Draw(element.core, bounds) textBounds := element.drawer.LayoutBounds() - foreground := element.theme.Pattern ( - theme.PatternForeground, - theme.PatternState { }) + foreground := element.theme.Color ( + theme.ColorForeground, + theme.State { }) element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min)) } diff --git a/elements/basic/list.go b/elements/basic/list.go index 7fdaa87..f6386b1 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -221,8 +221,8 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) { } func (element *List) scrollViewportHeight () (height int) { - inset := element.theme.Inset(theme.PatternSunken) - return element.Bounds().Dy() - inset[0] - inset[2] + padding := element.theme.Padding(theme.PatternSunken) + return element.Bounds().Dy() - padding[0] - padding[2] } func (element *List) maxScrollHeight () (height int) { @@ -355,8 +355,8 @@ func (element *List) Select (index int) { } func (element *List) selectUnderMouse (x, y int) (updated bool) { - inset := element.theme.Inset(theme.PatternSunken) - bounds := inset.Apply(element.Bounds()) + padding := element.theme.Padding(theme.PatternSunken) + bounds := padding.Apply(element.Bounds()) mousePoint := image.Pt(x, y) dot := image.Pt ( bounds.Min.X, @@ -398,8 +398,8 @@ func (element *List) changeSelectionBy (delta int) (updated bool) { func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { bounds := element.Bounds() - inset := element.theme.Inset(theme.PatternSunken) - entry.Resize(inset.Apply(bounds).Dx()) + padding := element.theme.Padding(theme.PatternSunken) + entry.Resize(padding.Apply(bounds).Dx()) return entry } @@ -425,17 +425,17 @@ func (element *List) updateMinimumSize () { minimumHeight = element.contentHeight } - inset := element.theme.Inset(theme.PatternSunken) - minimumHeight += inset[0] + inset[2] + padding := element.theme.Padding(theme.PatternSunken) + minimumHeight += padding[0] + padding[2] element.core.SetMinimumSize(minimumWidth, minimumHeight) } func (element *List) draw () { bounds := element.Bounds() - inset := element.theme.Inset(theme.PatternSunken) - innerBounds := inset.Apply(bounds) - state := theme.PatternState { + padding := element.theme.Padding(theme.PatternSunken) + innerBounds := padding.Apply(bounds) + state := theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), } @@ -460,6 +460,6 @@ func (element *List) draw () { innerBounds.Dx(), element.contentHeight, ).Add(innerBounds.Min).Intersect(innerBounds) pattern := element.theme.Pattern(theme.PatternSunken, state) - artist.FillRectangleShatter ( - element.core, pattern, bounds, covered) + artist.DrawShatter ( + element.core, pattern, covered) } diff --git a/elements/basic/listentry.go b/elements/basic/listentry.go index 439ce90..d1020c2 100644 --- a/elements/basic/listentry.go +++ b/elements/basic/listentry.go @@ -47,8 +47,8 @@ func (entry *ListEntry) SetConfig (new config.Config) { } func (entry *ListEntry) updateBounds () { - inset := entry.theme.Inset(theme.PatternRaised) - entry.bounds = inset.Inverse().Apply(entry.drawer.LayoutBounds()) + padding := entry.theme.Padding(theme.PatternRaised) + entry.bounds = padding.Inverse().Apply(entry.drawer.LayoutBounds()) entry.bounds = entry.bounds.Sub(entry.bounds.Min) entry.minimumWidth = entry.bounds.Dx() entry.bounds.Max.X = entry.width @@ -62,23 +62,21 @@ func (entry *ListEntry) Draw ( ) ( updatedRegion image.Rectangle, ) { - state := theme.PatternState { + state := theme.State { Focused: focused, On: on, } - pattern := entry.theme.Pattern (theme.PatternRaised, state) - inset := entry.theme.Inset(theme.PatternRaised) - artist.FillRectangle ( - destination, - pattern, - entry.Bounds().Add(offset)) + pattern := entry.theme.Pattern(theme.PatternRaised, state) + padding := entry.theme.Padding(theme.PatternRaised) + bounds := entry.Bounds().Add(offset) + artist.DrawBounds(destination, pattern, bounds) - foreground := entry.theme.Pattern (theme.PatternForeground, state) + foreground := entry.theme.Color (theme.ColorForeground, state) return entry.drawer.Draw ( destination, foreground, - offset.Add(image.Pt(inset[3], inset[0])). + offset.Add(image.Pt(padding[artist.SideLeft], padding[artist.SideTop])). Sub(entry.drawer.LayoutBounds().Min)) } diff --git a/elements/basic/progressbar.go b/elements/basic/progressbar.go index 626b215..8e5ce32 100644 --- a/elements/basic/progressbar.go +++ b/elements/basic/progressbar.go @@ -52,9 +52,11 @@ func (element *ProgressBar) SetConfig (new config.Config) { } func (element (ProgressBar)) updateMinimumSize() { + padding := element.theme.Padding(theme.PatternSunken) + innerPadding := element.theme.Padding(theme.PatternMercury) element.core.SetMinimumSize ( - element.config.Padding() * 2, - element.config.Padding() * 2) + padding.Horizontal() + innerPadding.Horizontal(), + padding.Vertical() + innerPadding.Vertical()) } func (element *ProgressBar) redo () { @@ -67,18 +69,14 @@ func (element *ProgressBar) redo () { func (element *ProgressBar) draw () { bounds := element.Bounds() - pattern := element.theme.Pattern ( - theme.PatternSunken, - theme.PatternState { }) - inset := element.theme.Inset(theme.PatternSunken) - artist.FillRectangle(element.core, pattern, bounds) - bounds = inset.Apply(bounds) + pattern := element.theme.Pattern(theme.PatternSunken, theme.State { }) + padding := element.theme.Padding(theme.PatternSunken) + pattern.Draw(element.core, bounds) + bounds = padding.Apply(bounds) meterBounds := image.Rect ( bounds.Min.X, bounds.Min.Y, bounds.Min.X + int(float64(bounds.Dx()) * element.progress), bounds.Max.Y) - accent := element.theme.Pattern ( - theme.PatternAccent, - theme.PatternState { }) - artist.FillRectangle(element.core, accent, meterBounds) + mercury := element.theme.Pattern(theme.PatternMercury, theme.State { }) + artist.DrawBounds(element.core, mercury, meterBounds) } diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index 719eff2..c686878 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -302,7 +302,7 @@ func (element *ScrollContainer) OnFocusMotionRequest ( } func (element *ScrollContainer) childDamageCallback (region canvas.Canvas) { - element.core.DamageRegion(artist.Paste(element.core, region, image.Point { })) + element.core.DamageRegion(region.Bounds()) } func (element *ScrollContainer) childFocusRequestCallback () (granted bool) { @@ -352,8 +352,8 @@ func (element *ScrollContainer) recalculate () { horizontal := &element.horizontal vertical := &element.vertical - gutterInsetHorizontal := horizontal.theme.Inset(theme.PatternGutter) - gutterInsetVertical := vertical.theme.Inset(theme.PatternGutter) + gutterInsetHorizontal := horizontal.theme.Padding(theme.PatternGutter) + gutterInsetVertical := vertical.theme.Padding(theme.PatternGutter) bounds := element.Bounds() thicknessHorizontal := @@ -438,8 +438,8 @@ func (element *ScrollContainer) recalculate () { func (element *ScrollContainer) draw () { deadPattern := element.theme.Pattern ( - theme.PatternDead, theme.PatternState { }) - artist.FillRectangle ( + theme.PatternDead, theme.State { }) + artist.DrawBounds ( element.core, deadPattern, image.Rect ( element.vertical.gutter.Min.X, @@ -451,27 +451,27 @@ func (element *ScrollContainer) draw () { } func (element *ScrollContainer) drawHorizontalBar () { - state := theme.PatternState { + state := theme.State { Disabled: !element.horizontal.enabled, Pressed: element.horizontal.dragging, } gutterPattern := element.horizontal.theme.Pattern(theme.PatternGutter, state) - artist.FillRectangle(element.core, gutterPattern, element.horizontal.gutter) + artist.DrawBounds(element.core, gutterPattern, element.horizontal.gutter) handlePattern := element.horizontal.theme.Pattern(theme.PatternHandle, state) - artist.FillRectangle(element.core, handlePattern, element.horizontal.bar) + artist.DrawBounds(element.core, handlePattern, element.horizontal.bar) } func (element *ScrollContainer) drawVerticalBar () { - state := theme.PatternState { + state := theme.State { Disabled: !element.vertical.enabled, Pressed: element.vertical.dragging, } gutterPattern := element.vertical.theme.Pattern(theme.PatternGutter, state) - artist.FillRectangle(element.core, gutterPattern, element.vertical.gutter) + artist.DrawBounds(element.core, gutterPattern, element.vertical.gutter) handlePattern := element.vertical.theme.Pattern(theme.PatternHandle, state) - artist.FillRectangle(element.core, handlePattern, element.vertical.bar) + artist.DrawBounds(element.core, handlePattern, element.vertical.bar) } func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) { @@ -493,8 +493,8 @@ func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) { } func (element *ScrollContainer) updateMinimumSize () { - gutterInsetHorizontal := element.horizontal.theme.Inset(theme.PatternGutter) - gutterInsetVertical := element.vertical.theme.Inset(theme.PatternGutter) + gutterInsetHorizontal := element.horizontal.theme.Padding(theme.PatternGutter) + gutterInsetVertical := element.vertical.theme.Padding(theme.PatternGutter) thicknessHorizontal := element.config.HandleWidth() + diff --git a/elements/basic/slider.go b/elements/basic/slider.go index 63121bb..bf6efaa 100644 --- a/elements/basic/slider.go +++ b/elements/basic/slider.go @@ -173,7 +173,7 @@ func (element *Slider) redo () { func (element *Slider) draw () { bounds := element.Bounds() - element.track = element.theme.Inset(theme.PatternGutter).Apply(bounds) + element.track = element.theme.Padding(theme.PatternGutter).Apply(bounds) if element.vertical { barSize := element.track.Dx() element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) @@ -190,16 +190,16 @@ func (element *Slider) draw () { element.bar = element.bar.Add(image.Pt(int(barOffset), 0)) } - state := theme.PatternState { + state := theme.State { Focused: element.Focused(), Disabled: !element.Enabled(), Pressed: element.dragging, } - artist.FillRectangle ( + artist.DrawBounds ( element.core, element.theme.Pattern(theme.PatternGutter, state), bounds) - artist.FillRectangle ( + artist.DrawBounds ( element.core, element.theme.Pattern(theme.PatternHandle, state), element.bar) diff --git a/elements/basic/spacer.go b/elements/basic/spacer.go index cb77206..4d710b3 100644 --- a/elements/basic/spacer.go +++ b/elements/basic/spacer.go @@ -2,7 +2,6 @@ package basicElements 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/elements/core" // Spacer can be used to put space between two elements.. @@ -22,7 +21,7 @@ func NewSpacer (line bool) (element *Spacer) { element = &Spacer { line: line } element.theme.Case = theme.C("basic", "spacer") element.Core, element.core = core.NewCore(element.draw) - element.core.SetMinimumSize(1, 1) + element.updateMinimumSize() return } @@ -30,6 +29,7 @@ func NewSpacer (line bool) (element *Spacer) { func (element *Spacer) SetLine (line bool) { if element.line == line { return } element.line = line + element.updateMinimumSize() if element.core.HasImage() { element.draw() element.core.DamageAll() @@ -50,6 +50,17 @@ func (element *Spacer) SetConfig (new config.Config) { element.redo() } +func (element *Spacer) updateMinimumSize () { + if element.line { + padding := element.theme.Padding(theme.PatternLine) + element.core.SetMinimumSize ( + padding.Horizontal(), + padding.Vertical()) + } else { + element.core.SetMinimumSize(1, 1) + } +} + func (element *Spacer) redo () { if !element.core.HasImage() { element.draw() @@ -62,13 +73,13 @@ func (element *Spacer) draw () { if element.line { pattern := element.theme.Pattern ( - theme.PatternForeground, - theme.PatternState { }) - artist.FillRectangle(element.core, pattern, bounds) + theme.PatternLine, + theme.State { }) + pattern.Draw(element.core, bounds) } else { pattern := element.theme.Pattern ( theme.PatternBackground, - theme.PatternState { }) - artist.FillRectangle(element.core, pattern, bounds) + theme.State { }) + pattern.Draw(element.core, bounds) } } diff --git a/elements/basic/switch.go b/elements/basic/switch.go index 1c06b80..4eeb9ad 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -149,7 +149,7 @@ func (element *Switch) updateMinimumSize () { } else { element.core.SetMinimumSize ( lineHeight * 2 + - element.config.Padding() + + element.theme.Margin(theme.PatternBackground).X + textBounds.Dx(), lineHeight) } @@ -160,14 +160,14 @@ func (element *Switch) draw () { handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min) - state := theme.PatternState { + state := theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), Pressed: element.pressed, } backgroundPattern := element.theme.Pattern ( theme.PatternBackground, state) - artist.FillRectangle (element.core, backgroundPattern, bounds) + backgroundPattern.Draw(element.core, bounds) if element.checked { handleBounds.Min.X += bounds.Dy() @@ -185,21 +185,22 @@ func (element *Switch) draw () { gutterPattern := element.theme.Pattern ( theme.PatternGutter, state) - artist.FillRectangle(element.core, gutterPattern, gutterBounds) + artist.DrawBounds(element.core, gutterPattern, gutterBounds) handlePattern := element.theme.Pattern ( theme.PatternHandle, state) - artist.FillRectangle(element.core, handlePattern, handleBounds) + artist.DrawBounds(element.core, handlePattern, handleBounds) textBounds := element.drawer.LayoutBounds() offset := bounds.Min.Add(image.Point { - X: bounds.Dy() * 2 + element.config.Padding(), + X: bounds.Dy() * 2 + + element.theme.Margin(theme.PatternBackground).X, }) offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Pattern ( - theme.PatternForeground, state) + foreground := element.theme.Color ( + theme.ColorForeground, state) element.drawer.Draw(element.core, foreground, offset) } diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index bcc004b..1f99460 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -9,6 +9,7 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/textmanip" import "git.tebibyte.media/sashakoshka/tomo/fixedutil" +import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/elements/core" // TextBox is a single-line text input. @@ -92,9 +93,10 @@ func (element *TextBox) HandleMouseMove (x, y int) { } func (element *TextBox) atPosition (position image.Point) int { + padding := element.theme.Padding(theme.PatternInput) offset := element.Bounds().Min.Add (image.Pt ( - element.config.Padding() - element.scroll, - element.config.Padding())) + padding[artist.SideLeft] - element.scroll, + padding[artist.SideTop])) textBoundsMin := element.valueDrawer.LayoutBounds().Min return element.valueDrawer.AtPosition ( fixedutil.Pt(position.Sub(offset).Add(textBoundsMin))) @@ -251,7 +253,8 @@ func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) { } func (element *TextBox) scrollViewportWidth () (width int) { - return element.Bounds().Inset(element.config.Padding()).Dx() + padding := element.theme.Padding(theme.PatternInput) + return padding.Apply(element.Bounds()).Dx() } // ScrollTo scrolls the viewport to the specified point relative to @@ -290,7 +293,8 @@ func (element *TextBox) runOnChange () { func (element *TextBox) scrollToCursor () { if !element.core.HasImage() { return } - bounds := element.Bounds().Inset(element.config.Padding()) + padding := element.theme.Padding(theme.PatternInput) + bounds := padding.Apply(element.Bounds()) bounds = bounds.Sub(bounds.Min) bounds.Max.X -= element.valueDrawer.Em().Round() cursorPosition := fixedutil.RoundPt ( @@ -329,11 +333,11 @@ func (element *TextBox) SetConfig (new config.Config) { func (element *TextBox) updateMinimumSize () { textBounds := element.placeholderDrawer.LayoutBounds() + padding := element.theme.Padding(theme.PatternInput) element.core.SetMinimumSize ( - textBounds.Dx() + - element.config.Padding() * 2, - element.placeholderDrawer.LineHeight().Round() + - element.config.Padding() * 2) + padding.Horizontal() + textBounds.Dx(), + padding.Vertical() + + element.placeholderDrawer.LineHeight().Round()) } func (element *TextBox) redo () { @@ -346,30 +350,29 @@ func (element *TextBox) redo () { func (element *TextBox) draw () { bounds := element.Bounds() - state := theme.PatternState { + state := theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), } - pattern := element.theme.Pattern(theme.PatternSunken, state) - inset := element.theme.Inset(theme.PatternSunken) - innerCanvas := canvas.Cut(element.core, inset.Apply(bounds)) - artist.FillRectangle(element.core, pattern, bounds) + pattern := element.theme.Pattern(theme.PatternInput, state) + padding := element.theme.Padding(theme.PatternInput) + innerCanvas := canvas.Cut(element.core, padding.Apply(bounds)) + pattern.Draw(element.core, bounds) offset := bounds.Min.Add (image.Point { - X: element.config.Padding() - element.scroll, - Y: element.config.Padding(), + X: padding[artist.SideLeft] - element.scroll, + Y: padding[artist.SideTop], }) if element.Focused() && !element.dot.Empty() { // draw selection bounds - accent := element.theme.Pattern ( - theme.PatternAccent, state) + accent := element.theme.Color(theme.ColorAccent, state) canon := element.dot.Canon() foff := fixedutil.Pt(offset) start := element.valueDrawer.PositionAt(canon.Start).Add(foff) end := element.valueDrawer.PositionAt(canon.End).Add(foff) end.Y += element.valueDrawer.LineHeight() - artist.FillRectangle ( + shapes.FillColorRectangle ( innerCanvas, accent, image.Rectangle { @@ -381,9 +384,9 @@ func (element *TextBox) draw () { if len(element.text) == 0 { // draw placeholder textBounds := element.placeholderDrawer.LayoutBounds() - foreground := element.theme.Pattern ( - theme.PatternForeground, - theme.PatternState { Disabled: true }) + foreground := element.theme.Color ( + theme.ColorForeground, + theme.State { Disabled: true }) element.placeholderDrawer.Draw ( innerCanvas, foreground, @@ -391,8 +394,7 @@ func (element *TextBox) draw () { } else { // draw input value textBounds := element.valueDrawer.LayoutBounds() - foreground := element.theme.Pattern ( - theme.PatternForeground, state) + foreground := element.theme.Color(theme.ColorForeground, state) element.valueDrawer.Draw ( innerCanvas, foreground, @@ -401,11 +403,10 @@ func (element *TextBox) draw () { if element.Focused() && element.dot.Empty() { // draw cursor - foreground := element.theme.Pattern ( - theme.PatternForeground, state) + foreground := element.theme.Color(theme.ColorForeground, state) cursorPosition := fixedutil.RoundPt ( element.valueDrawer.PositionAt(element.dot.End)) - artist.Line ( + shapes.ColorLine ( innerCanvas, foreground, 1, cursorPosition.Add(offset), diff --git a/elements/fun/clock.go b/elements/fun/clock.go index 066af12..00861ab 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -3,9 +3,10 @@ package fun import "time" import "math" import "image" +import "image/color" 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" // AnalogClock can display the time of day in an analog format. @@ -58,15 +59,15 @@ func (element *AnalogClock) redo () { func (element *AnalogClock) draw () { bounds := element.Bounds() - state := theme.PatternState { } + state := theme.State { } pattern := element.theme.Pattern(theme.PatternSunken, state) - inset := element.theme.Inset(theme.PatternSunken) - artist.FillRectangle(element.core, pattern, bounds) + padding := element.theme.Padding(theme.PatternSunken) + pattern.Draw(element.core, bounds) - bounds = inset.Apply(bounds) + bounds = padding.Apply(bounds) - foreground := element.theme.Pattern(theme.PatternForeground, state) - accent := element.theme.Pattern(theme.PatternAccent, state) + foreground := element.theme.Color(theme.ColorForeground, state) + accent := element.theme.Color(theme.ColorAccent, state) for hour := 0; hour < 12; hour ++ { element.radialLine ( @@ -93,7 +94,7 @@ func (element *AnalogClock) FlexibleHeightFor (width int) (height int) { func (element *AnalogClock) OnFlexibleHeightChange (func ()) { } func (element *AnalogClock) radialLine ( - source artist.Pattern, + source color.RGBA, inner float64, outer float64, radian float64, @@ -107,5 +108,5 @@ func (element *AnalogClock) radialLine ( max := element.Bounds().Min.Add(image.Pt ( int(math.Cos(radian) * outer * width + width), int(math.Sin(radian) * outer * height + height))) - artist.Line(element.core, source, 1, min, max) + shapes.ColorLine(element.core, source, 1, min, max) } diff --git a/elements/fun/piano.go b/elements/fun/piano.go index e116c1d..4cba308 100644 --- a/elements/fun/piano.go +++ b/elements/fun/piano.go @@ -218,10 +218,11 @@ func (element *Piano) SetConfig (new config.Config) { } func (element *Piano) updateMinimumSize () { - inset := element.theme.Inset(theme.PatternSunken) + padding := element.theme.Padding(theme.PatternPinboard) element.core.SetMinimumSize ( - pianoKeyWidth * 7 * element.countOctaves() + inset[1] + inset[3], - 64 + inset[0] + inset[2]) + pianoKeyWidth * 7 * element.countOctaves() + + padding.Horizontal(), + 64 + padding.Vertical()) } func (element *Piano) countOctaves () int { @@ -247,8 +248,8 @@ func (element *Piano) recalculate () { element.flatKeys = make([]pianoKey, element.countFlats()) element.sharpKeys = make([]pianoKey, element.countSharps()) - inset := element.theme.Inset(theme.PatternPinboard) - bounds := inset.Apply(element.Bounds()) + padding := element.theme.Padding(theme.PatternPinboard) + bounds := padding.Apply(element.Bounds()) dot := bounds.Min note := element.low.Note(0) @@ -280,7 +281,7 @@ func (element *Piano) recalculate () { } func (element *Piano) draw () { - state := theme.PatternState { + state := theme.State { Focused: element.Focused(), Disabled: !element.Enabled(), } @@ -303,28 +304,28 @@ func (element *Piano) draw () { } pattern := element.theme.Pattern(theme.PatternPinboard, state) - artist.FillRectangleShatter ( - element.core, pattern, element.Bounds(), element.contentBounds) + artist.DrawShatter ( + element.core, pattern, element.contentBounds) } func (element *Piano) drawFlat ( bounds image.Rectangle, pressed bool, - state theme.PatternState, + state theme.State, ) { state.Pressed = pressed pattern := element.theme.Theme.Pattern ( theme.PatternButton, state, theme.C("fun", "flatKey")) - artist.FillRectangle(element.core, pattern, bounds) + artist.DrawBounds(element.core, pattern, bounds) } func (element *Piano) drawSharp ( bounds image.Rectangle, pressed bool, - state theme.PatternState, + state theme.State, ) { state.Pressed = pressed pattern := element.theme.Theme.Pattern ( theme.PatternButton, state, theme.C("fun", "sharpKey")) - artist.FillRectangle(element.core, pattern, bounds) + artist.DrawBounds(element.core, pattern, bounds) } diff --git a/elements/testing/artist.go b/elements/testing/artist.go index 7db6f3e..bb42bfa 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -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), - - }, - element.cellAt(x, 1)) - } + // 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()) - // 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,53 @@ 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 ( + tiles := shatter.Shatter(c41.Bounds(), rocks...) + for index, tile := range tiles { + artist.DrawBounds ( element.core, - artist.Bordered { - Fill: uhex(0x888888FF), - Stroke: artist.Stroke { - Pattern: artist.Beveled { - uhex(0xCCCCCCFF), - uhex(0x444444FF), - }, - Weight: 1, - }, - }, - 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) + + // 1, 2 + c12 := element.cellAt(1, 2) + shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1) + shapes.StrokeEllipse(c12, c41, 5) - // 0, 5 - artist.FillRectangle ( - element.core, - 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, 2 + c22 := element.cellAt(2, 2) + shapes.FillRectangle(c22, c41) + + // 3, 2 + c32 := element.cellAt(3, 2) + shapes.StrokeRectangle(c32, c41, 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 - 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), - ) + // 4, 2 + c42 := element.cellAt(4, 2) + + // 0, 3 + c03 := element.cellAt(0, 3) + patterns.Border { + Canvas: element.thingy(c42), + Inset: artist.Inset { 8, 8, 8, 8 }, + }.Draw(c03, c03.Bounds()) + + // 1, 3 + c13 := element.cellAt(1, 3) + patterns.Border { + Canvas: element.thingy(c42), + Inset: artist.Inset { 8, 8, 8, 8 }, + }.Draw(c13, c13.Bounds().Inset(10)) // how long did that take to render? drawTime := time.Since(drawStart) @@ -288,68 +134,62 @@ 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())) + 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), - }) +func (element *Artist) thingy (destination canvas.Canvas) (result canvas.Canvas) { + bounds := destination.Bounds() + bounds = image.Rect(0, 0, 32, 32).Add(bounds.Min) + shapes.FillColorRectangle(destination, artist.Hex(0x440000FF), bounds) + shapes.StrokeColorRectangle(destination, artist.Hex(0xFF0000FF), bounds, 1) + shapes.StrokeColorRectangle(destination, artist.Hex(0x004400FF), bounds.Inset(4), 1) + shapes.FillColorRectangle(destination, artist.Hex(0x004444FF), bounds.Inset(12)) + shapes.StrokeColorRectangle(destination, artist.Hex(0x888888FF), bounds.Inset(8), 1) + return canvas.Cut(destination, bounds) } diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 63695c0..194518a 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -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 } @@ -51,21 +49,21 @@ func (element *Mouse) redo () { func (element *Mouse) draw () { bounds := element.Bounds() - pattern := element.theme.Pattern ( - theme.PatternAccent, - theme.PatternState { }, + accent := element.theme.Color ( + theme.ColorAccent, + theme.State { }, element.c) - artist.FillRectangle(element.core, pattern, bounds) - artist.StrokeRectangle ( + shapes.FillColorRectangle(element.core, accent, 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 } diff --git a/examples/artist/main.go b/examples/artist/main.go index b09932b..3b4517f 100644 --- a/examples/artist/main.go +++ b/examples/artist/main.go @@ -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) } () } diff --git a/examples/raycaster/raycaster.go b/examples/raycaster/raycaster.go index f4bfa7b..3ccd703 100644 --- a/examples/raycaster/raycaster.go +++ b/examples/raycaster/raycaster.go @@ -7,6 +7,7 @@ import "image/color" import "git.tebibyte.media/sashakoshka/tomo/input" 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" type ControlState struct { @@ -202,9 +203,9 @@ func (element *Raycaster) drawMinimap () { if cell > 0 { cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF } } - artist.FillRectangle ( + shapes.FillColorRectangle ( element.core, - artist.NewUniform(cellColor), + cellColor, cellBounds.Inset(1)) }} @@ -217,18 +218,18 @@ func (element *Raycaster) drawMinimap () { hitPt := hit.Mul(float64(scale)).Point().Add(bounds.Min) playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8) - artist.FillEllipse ( + shapes.FillColorEllipse ( element.core, - artist.Uhex(0xFFFFFFFF), + artist.Hex(0xFFFFFFFF), playerBounds) - artist.Line ( + shapes.ColorLine ( element.core, - artist.Uhex(0xFFFFFFFF), 1, + artist.Hex(0xFFFFFFFF), 1, playerPt, playerAnglePt) - artist.Line ( + shapes.ColorLine ( element.core, - artist.Uhex(0x00FF00FF), 1, + artist.Hex(0x00FF00FF), 1, playerPt, hitPt) } diff --git a/textdraw/drawer.go b/textdraw/drawer.go index 09fe749..3ef4011 100644 --- a/textdraw/drawer.go +++ b/textdraw/drawer.go @@ -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) diff --git a/theme/assets/wintergreen.png b/theme/assets/wintergreen.png new file mode 100644 index 0000000..154a6d8 Binary files /dev/null and b/theme/assets/wintergreen.png differ diff --git a/theme/default.go b/theme/default.go index e211037..061db58 100644 --- a/theme/default.go +++ b/theme/default.go @@ -1,10 +1,69 @@ package theme import "image" +import "bytes" +import _ "embed" +import _ "image/png" +import "image/color" import "golang.org/x/image/font" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/defaultfont" +import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" + +//go:embed assets/wintergreen.png +var defaultAtlasBytes []byte +var defaultAtlas canvas.Canvas +var defaultTextures [14][9]artist.Pattern + +func atlasCell (col, row int, border artist.Inset) { + bounds := image.Rect(0, 0, 16, 16).Add(image.Pt(col, row).Mul(16)) + defaultTextures[col][row] = patterns.Border { + Canvas: canvas.Cut(defaultAtlas, bounds), + Inset: border, + } +} + +func atlasCol (col int, border artist.Inset) { + for index, _ := range defaultTextures[col] { + atlasCell(col, index, border) + } +} + +func init () { + defaultAtlasImage, _, _ := image.Decode(bytes.NewReader(defaultAtlasBytes)) + defaultAtlas = canvas.FromImage(defaultAtlasImage) + + // PatternDead + atlasCol(0, artist.Inset { }) + // PatternRaised + atlasCol(1, artist.Inset { 6, 6, 6, 6 }) + // PatternSunken + atlasCol(2, artist.Inset { 4, 4, 4, 4 }) + // PatternPinboard + atlasCol(3, artist.Inset { 2, 2, 2, 2 }) + // PatternButton + atlasCol(4, artist.Inset { 6, 6, 6, 6 }) + // PatternInput + atlasCol(5, artist.Inset { 4, 4, 4, 4 }) + // PatternGutter + atlasCol(6, artist.Inset { 7, 7, 7, 7 }) + // PatternHandle + atlasCol(7, artist.Inset { 3, 3, 3, 3 }) + // PatternLine + atlasCol(8, artist.Inset { 1, 1, 1, 1 }) + // PatternMercury + atlasCol(13, artist.Inset { 2, 2, 2, 2 }) + + // PatternButton: basic.checkbox + atlasCol(9, artist.Inset { 3, 3, 3, 3 }) + // PatternRaised: basic.listEntry + atlasCol(10, artist.Inset { 3, 3, 3, 3 }) + // PatternRaised: fun.flatKey + atlasCol(11, artist.Inset { 3, 3, 5, 3 }) + // PatternRaised: fun.sharpKey + atlasCol(12, artist.Inset { 3, 3, 4, 3 }) +} // Default is the default theme. type Default struct { } @@ -31,211 +90,96 @@ func (Default) Icon (string, IconSize, Case) canvas.Image { // Pattern returns a pattern from the default theme corresponding to the given // pattern ID. -func (Default) Pattern ( - pattern Pattern, - state PatternState, - c Case, -) artist.Pattern { - switch pattern { - case PatternAccent: - return accentPattern - case PatternBackground: - return backgroundPattern - case PatternForeground: - if state.Disabled || c == C("basic", "spacer") { - return weakForegroundPattern - } else { - return foregroundPattern - } - case PatternDead: - return deadPattern +func (Default) Pattern (id Pattern, state State, c Case) artist.Pattern { + offset := 0; switch { + case state.Disabled: offset = 1 + case state.Pressed && state.On: offset = 4 + case state.Focused && state.On: offset = 7 + case state.Invalid && state.On: offset = 8 + case state.On: offset = 2 + case state.Pressed: offset = 3 + case state.Focused: offset = 5 + case state.Invalid: offset = 6 + } + + switch id { + case PatternBackground: return patterns.Uhex(0xaaaaaaFF) + case PatternDead: return defaultTextures[0][offset] case PatternRaised: if c == C("basic", "listEntry") { - if state.Focused { - if state.On { - return focusedOnListEntryPattern - } else { - return focusedListEntryPattern - } - } else { - if state.On { - return onListEntryPattern - } else { - return listEntryPattern - } - } + return defaultTextures[10][offset] } else { - if state.Focused { - return selectedRaisedPattern - } else { - return raisedPattern - } - } - case PatternSunken: - if c == C("basic", "list") { - if state.Focused { - return focusedListPattern - } else { - return listPattern - } - } else if c == C("basic", "textBox") { - if state.Disabled { - return disabledInputPattern - } else { - if state.Focused { - return selectedInputPattern - } else { - return inputPattern - } - } - } else { - if state.Focused { - return focusedSunkenPattern - } else { - return sunkenPattern - } - } - case PatternPinboard: - if state.Focused { - return focusedTexturedSunkenPattern - } else { - return texturedSunkenPattern + return defaultTextures[1][offset] } + case PatternSunken: return defaultTextures[2][offset] + case PatternPinboard: return defaultTextures[3][offset] case PatternButton: - if state.Disabled { - return disabledButtonPattern - } else { - if c == C("fun", "sharpKey") { - if state.Pressed { - return pressedDarkButtonPattern - } else { - return darkButtonPattern - } - } else if c == C("fun", "flatKey") { - if state.Pressed { - return pressedButtonPattern - } else { - return buttonPattern - } - } else { - if state.Pressed || state.On && c == C("basic", "checkbox") { - if state.Focused { - return pressedSelectedButtonPattern - } else { - return pressedButtonPattern - } - } else { - if state.Focused { - return selectedButtonPattern - } else { - return buttonPattern - } - } - } + switch c { + case C("basic", "checkbox"): return defaultTextures[9][offset] + case C("fun", "flatKey"): return defaultTextures[11][offset] + case C("fun", "sharpKey"): return defaultTextures[12][offset] + default: return defaultTextures[4][offset] } - case PatternInput: - if state.Disabled { - return disabledInputPattern - } else { - if state.Focused { - return selectedInputPattern - } else { - return inputPattern - } - } - case PatternGutter: - if c == C("basic", "sliderVertical") || c == C("basic", "sliderHorizontal") { - if state.Disabled { - return disabledThinScrollGutterPattern - } else { - return thinScrollGutterPattern - } - } else { - if state.Disabled { - return disabledScrollGutterPattern - } else { - return scrollGutterPattern - } - } - case PatternHandle: - if state.Disabled { - return disabledScrollBarPattern - } else { - if state.Focused { - if state.Pressed { - return pressedSelectedScrollBarPattern - } else { - return selectedScrollBarPattern - } - } else { - if state.Pressed { - return pressedScrollBarPattern - } else { - return scrollBarPattern - } - } - } - default: - return uhex(0) + case PatternInput: return defaultTextures[5][offset] + case PatternGutter: return defaultTextures[6][offset] + case PatternHandle: return defaultTextures[7][offset] + case PatternLine: return defaultTextures[8][offset] + case PatternMercury: return defaultTextures[13][offset] + default: return patterns.Uhex(0xFF00FFFF) } } -// Inset returns the default inset value for the given pattern. -func (Default) Inset (pattern Pattern, c Case) Inset { - switch pattern { +func (Default) Color (id Color, state State, c Case) color.RGBA { + if state.Disabled { + return artist.Hex(0x444444FF) + } else { + switch id { + case ColorAccent: return artist.Hex(0x408090FF) + case ColorForeground: return artist.Hex(0x000000FF) + default: return artist.Hex(0x888888FF) + } + } +} + +// Padding returns the default padding value for the given pattern. +func (Default) Padding (id Pattern, c Case) artist.Inset { + switch id { case PatternRaised: if c == C("basic", "listEntry") { - return Inset { 4, 6, 4, 6 } + return artist.Inset { 4, 8, 4, 8 } } else { - return Inset { 2, 2, 2, 2 } + return artist.Inset { 8, 8, 8, 8 } } - case PatternSunken: if c == C("basic", "list") { - return Inset { 2, 1, 2, 1 } + return artist.Inset { 4, 0, 3, 0 } } else if c == C("basic", "progressBar") { - return Inset { 2, 1, 1, 2 } + return artist.Inset { 2, 1, 1, 2 } } else { - return Inset { 2, 2, 2, 2 } + return artist.Inset { 8, 8, 8, 8 } } - case PatternPinboard: - return Inset { 2, 2, 2, 2 } - - case PatternInput, PatternButton, PatternHandle: - return Inset { 2, 2, 2, 2} - - default: return Inset { } + if c == C("fun", "piano") { + return artist.Inset { 2, 2, 2, 2 } + } else { + return artist.Inset { 8, 8, 8, 8 } + } + case PatternGutter: return artist.Inset { } + case PatternLine: return artist.Inset { 1, 1, 1, 1 } + case PatternMercury: return artist.Inset { 5, 5, 5, 5 } + default: return artist.Inset { 8, 8, 8, 8 } } } +// Margin returns the default margin value for the given pattern. +func (Default) Margin (id Pattern, c Case) image.Point { + return image.Pt(8, 8) +} + // Hints returns rendering optimization hints for a particular pattern. // These are optional, but following them may result in improved // performance. func (Default) Hints (pattern Pattern, c Case) (hints Hints) { - switch pattern { - case PatternRaised: - if c == C("basic", "listEntry") { - hints.StaticInset = Inset { 0, 1, 0, 1 } - } else { - hints.StaticInset = Inset { 3, 3, 3, 3 } - } - - case PatternSunken: - if c == C("basic", "list") { - hints.StaticInset = Inset { 2, 1, 2, 1 } - } else { - hints.StaticInset = Inset { 3, 3, 3, 3 } - } - - case - PatternPinboard, - PatternInput, - PatternButton, - PatternHandle: - - hints.StaticInset = Inset { 3, 3, 3, 3 } - } return } diff --git a/theme/defaultpatterns.go b/theme/defaultpatterns.go deleted file mode 100644 index 82afd47..0000000 --- a/theme/defaultpatterns.go +++ /dev/null @@ -1,272 +0,0 @@ -package theme - -import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/artist" - -// var backgroundPattern = artist.Gradient { - // First: uhex(0xFF0000FF), - // Second: uhex(0x00FF00FF), -// } -var accentPattern = artist.NewUniform(hex(0x408090FF)) -var backgroundPattern = artist.NewUniform(color.Gray16 { 0xAAAA }) -var foregroundPattern = artist.NewUniform(color.Gray16 { 0x0000 }) -var weakForegroundPattern = artist.NewUniform(color.Gray16 { 0x4444 }) -var strokePattern = artist.NewUniform(color.Gray16 { 0x0000 }) - -var sunkenPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x3b534eFF)), - artist.NewUniform(hex(0x97a09cFF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) }) -var focusedSunkenPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) }) - -var texturedSunkenPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x3b534eFF)), - artist.NewUniform(hex(0x97a09cFF)), - }, - }, - - artist.Stroke { Pattern: artist.Noisy { - Low: artist.NewUniform(hex(0x97a09cFF)), - High: artist.NewUniform(hex(0x6e8079FF)), - }}) -var focusedTexturedSunkenPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.Noisy { - Low: artist.NewUniform(hex(0x97a09cFF)), - High: artist.NewUniform(hex(0x6e8079FF)), - }}) - -var raisedPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xDBDBDBFF)), - artist.NewUniform(hex(0x383C3AFF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) }) - -var selectedRaisedPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xDBDBDBFF)), - artist.NewUniform(hex(0x383C3AFF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) }) - -var deadPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) }) - -var buttonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var selectedButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var pressedButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x4B5B59FF)), - artist.NewUniform(hex(0x8D9894FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var pressedSelectedButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x4B5B59FF)), - artist.NewUniform(hex(0x8D9894FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var disabledButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, - artist.Stroke { Pattern: backgroundPattern }) - -var darkButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xaebdb9FF)), - artist.NewUniform(hex(0x3b4947FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x6b7a75FF)) }) -var pressedDarkButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x3b4947FF)), - artist.NewUniform(hex(0x6b7a75FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x6b7a75FF)) }) - -var inputPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x89925AFF)), - artist.NewUniform(hex(0xD2CB9AFF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) }) -var selectedInputPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) }) -var disabledInputPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, - artist.Stroke { Pattern: backgroundPattern }) - -var listPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - uhex(0x383C3AFF), - uhex(0x999C99FF), - }, - }, - artist.Stroke { Pattern: uhex(0x999C99FF) }) - -var focusedListPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: uhex(0x999C99FF) }) - -var listEntryPattern = artist.Padded { - Stroke: uhex(0x383C3AFF), - Fill: uhex(0x999C99FF), - Sides: []int { 0, 0, 0, 1 }, -} - -var onListEntryPattern = artist.Padded { - Stroke: uhex(0x383C3AFF), - Fill: uhex(0x6e8079FF), - Sides: []int { 0, 0, 0, 1 }, -} - -var focusedListEntryPattern = artist.Padded { - Stroke: accentPattern, - Fill: uhex(0x999C99FF), - Sides: []int { 0, 1, 0, 1 }, -} - -var focusedOnListEntryPattern = artist.Padded { - Stroke: accentPattern, - Fill: uhex(0x6e8079FF), - Sides: []int { 0, 1, 0, 1 }, -} - -var scrollGutterPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x3b534eFF)), - artist.NewUniform(hex(0x6e8079FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x6e8079FF)) }) -var thinScrollGutterPattern = artist.Padded { - Fill: scrollGutterPattern, - Stroke: sunkenPattern, - Sides: []int{ 6, 6, 6, 6 }, -} -var disabledScrollGutterPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, - artist.Stroke { Pattern: backgroundPattern }) -var disabledThinScrollGutterPattern = artist.Padded { - Fill: disabledScrollGutterPattern, - Stroke: disabledButtonPattern, - Sides: []int{ 6, 6, 6, 6}, -} -var scrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var selectedScrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var pressedScrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: artist.NewUniform(hex(0x8D9894FF)) }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) }) -var pressedSelectedScrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) }) -var disabledScrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, - artist.Stroke { Pattern: backgroundPattern }) diff --git a/theme/state.go b/theme/state.go index cd46e75..57f96a4 100644 --- a/theme/state.go +++ b/theme/state.go @@ -8,7 +8,7 @@ package theme // specific elements. type Case struct { Namespace, Element string } -// C can be used as shorthand to generate a case struct as used in PatternState. +// C can be used as shorthand to generate a case struct as used in State. func C (namespace, element string) (c Case) { return Case { Namespace: namespace, @@ -16,14 +16,14 @@ func C (namespace, element string) (c Case) { } } -// PatternState lists parameters which can change the appearance of some -// patterns. For example, passing a PatternState with Selected set to true may -// result in a pattern that has a colored border within it. -type PatternState struct { +// State lists parameters which can change the appearance of some patterns and +// colors. For example, passing a State with Selected set to true may result in +// a pattern that has a colored border within it. +type State struct { // On should be set to true if the element that is using this pattern is - // in some sort of "on" state, such as if a checkbox is checked or a - // switch is toggled on. This is only necessary if the element in - // question is capable of being toggled. + // in some sort of selected or "on" state, such as if a checkbox is + // checked or a switch is toggled on. This is only necessary if the + // element in question is capable of being toggled or selected. On bool // Focused should be set to true if the element that is using this diff --git a/theme/theme.go b/theme/theme.go index e88e028..840c599 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -18,17 +18,9 @@ const ( // This allows custom elements to follow themes, even those that do not // explicitly support them. type Pattern int; const ( - // PatternAccent is the accent color of the theme. It is safe to assume - // that this is, by default, a solid color. - PatternAccent Pattern = iota - - // PatternBackground is the background color of the theme. It is safe to - // assume that this is, by default, a solid color. - PatternBackground - - // PatternForeground is the foreground text color of the theme. It is - // safe to assume that this is, by default, a solid color. - PatternForeground + // PatternBackground is the window background of the theme. It appears + // in things like containers and behind text. + PatternBackground Pattern = iota // PatternDead is a pattern that is displayed on a "dead area" where no // controls exist, but there still must be some indication of visual @@ -55,6 +47,20 @@ type Pattern int; const ( // PatternHandle is a handle that slides along a gutter. PatternHandle + + // PatternLine is an engraved line that separates things. + PatternLine + + // PatternMercury is a fill pattern for progress bars, meters, etc. + PatternMercury +) + +type Color int; const ( + // ColorAccent is the accent color of the theme. + ColorAccent Color = iota + + // ColorForeground is the text/icon color of the theme. + ColorForeground ) // Hints specifies rendering hints for a particular pattern. Elements can take @@ -63,7 +69,7 @@ type Hints struct { // StaticInset defines an inset rectangular area in the middle of the // pattern that does not change between PatternStates. If the inset is // zero on all sides, this hint does not apply. - StaticInset Inset + StaticInset artist.Inset // Uniform specifies a singular color for the entire pattern. If the // alpha channel is zero, this hint does not apply. @@ -80,11 +86,20 @@ type Theme interface { // Pattern returns an appropriate pattern given a pattern name, case, // and state. - Pattern (Pattern, PatternState, Case) artist.Pattern + Pattern (Pattern, State, Case) artist.Pattern - // Inset returns the area on all sides of a given pattern that is not - // meant to be drawn on. - Inset (Pattern, Case) Inset + // Color returns an appropriate pattern given a color name, case, and + // state. + Color (Color, State, Case) color.RGBA + + // Padding returns how much space should be between the bounds of a + // pattern whatever an element draws inside of it. + Padding (Pattern, Case) artist.Inset + + // Margin returns the left/right (x) and top/bottom (y) margins that + // should be put between any self-contained objects drawn within this + // pattern (if applicable). + Margin (Pattern, Case) image.Point // Sink returns a vector that should be added to an element's inner // content when it is pressed down (if applicable) to simulate a 3D @@ -96,57 +111,3 @@ type Theme interface { // performance. Hints (Pattern, Case) Hints } - -// Wrapped wraps any theme and injects a case into it automatically so that it -// doesn't need to be specified for each query. Additionally, if the underlying -// theme is nil, it just uses the default theme instead. -type Wrapped struct { - Theme - Case -} - -// FontFace returns the proper font for a given style and size. -func (wrapped Wrapped) FontFace (style FontStyle, size FontSize) font.Face { - real := wrapped.ensure() - return real.FontFace(style, size, wrapped.Case) -} - -// Icon returns an appropriate icon given an icon name. -func (wrapped Wrapped) Icon (name string, size IconSize) canvas.Image { - real := wrapped.ensure() - return real.Icon(name, size, wrapped.Case) -} - -// Pattern returns an appropriate pattern given a pattern name and state. -func (wrapped Wrapped) Pattern (id Pattern, state PatternState) artist.Pattern { - real := wrapped.ensure() - return real.Pattern(id, state, wrapped.Case) -} - -// Inset returns the area on all sides of a given pattern that is not meant to -// be drawn on. -func (wrapped Wrapped) Inset (id Pattern) Inset { - real := wrapped.ensure() - return real.Inset(id, wrapped.Case) -} - -// Sink returns a vector that should be added to an element's inner content when -// it is pressed down (if applicable) to simulate a 3D sinking effect. -func (wrapped Wrapped) Sink (id Pattern) image.Point { - real := wrapped.ensure() - return real.Sink(id, wrapped.Case) -} - -// Hints returns rendering optimization hints for a particular pattern. -// These are optional, but following them may result in improved -// performance. -func (wrapped Wrapped) Hints (id Pattern) Hints { - real := wrapped.ensure() - return real.Hints(id, wrapped.Case) -} - -func (wrapped Wrapped) ensure () (real Theme) { - real = wrapped.Theme - if real == nil { real = Default { } } - return -} diff --git a/theme/util.go b/theme/util.go deleted file mode 100644 index 2e9723e..0000000 --- a/theme/util.go +++ /dev/null @@ -1,16 +0,0 @@ -package theme - -import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/artist" - -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 -} - -func uhex (color uint32) (pattern artist.Pattern) { - return artist.NewUniform(hex(color)) -} diff --git a/theme/wrapped.go b/theme/wrapped.go new file mode 100644 index 0000000..731c6fc --- /dev/null +++ b/theme/wrapped.go @@ -0,0 +1,75 @@ +package theme + +import "image" +import "image/color" +import "golang.org/x/image/font" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +// Wrapped wraps any theme and injects a case into it automatically so that it +// doesn't need to be specified for each query. Additionally, if the underlying +// theme is nil, it just uses the default theme instead. +type Wrapped struct { + Theme + Case +} + +// FontFace returns the proper font for a given style and size. +func (wrapped Wrapped) FontFace (style FontStyle, size FontSize) font.Face { + real := wrapped.ensure() + return real.FontFace(style, size, wrapped.Case) +} + +// Icon returns an appropriate icon given an icon name. +func (wrapped Wrapped) Icon (name string, size IconSize) canvas.Image { + real := wrapped.ensure() + return real.Icon(name, size, wrapped.Case) +} + +// Pattern returns an appropriate pattern given a pattern name and state. +func (wrapped Wrapped) Pattern (id Pattern, state State) artist.Pattern { + real := wrapped.ensure() + return real.Pattern(id, state, wrapped.Case) +} + +// Color returns an appropriate color given a color name and state. +func (wrapped Wrapped) Color (id Color, state State) color.RGBA { + real := wrapped.ensure() + return real.Color(id, state, wrapped.Case) +} + +// Padding returns how much space should be between the bounds of a +// pattern whatever an element draws inside of it. +func (wrapped Wrapped) Padding (id Pattern) artist.Inset { + real := wrapped.ensure() + return real.Padding(id, wrapped.Case) +} + +// Margin returns the left/right (x) and top/bottom (y) margins that +// should be put between any self-contained objects drawn within this +// pattern (if applicable). +func (wrapped Wrapped) Margin (id Pattern) image.Point { + real := wrapped.ensure() + return real.Margin(id, wrapped.Case) +} + +// Sink returns a vector that should be added to an element's inner content when +// it is pressed down (if applicable) to simulate a 3D sinking effect. +func (wrapped Wrapped) Sink (id Pattern) image.Point { + real := wrapped.ensure() + return real.Sink(id, wrapped.Case) +} + +// Hints returns rendering optimization hints for a particular pattern. +// These are optional, but following them may result in improved +// performance. +func (wrapped Wrapped) Hints (id Pattern) Hints { + real := wrapped.ensure() + return real.Hints(id, wrapped.Case) +} + +func (wrapped Wrapped) ensure () (real Theme) { + real = wrapped.Theme + if real == nil { real = Default { } } + return +}