data-oriented-patterns #9
@ -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)
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								artist/color.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								artist/color.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
package artist
 | 
			
		||||
 | 
			
		||||
import "image/color"
 | 
			
		||||
 | 
			
		||||
// Hex creates a color.RGBA value from an RGBA integer value.
 | 
			
		||||
func Hex (color uint32) (c color.RGBA) {
 | 
			
		||||
	c.A = uint8(color)
 | 
			
		||||
	c.B = uint8(color >>  8)
 | 
			
		||||
	c.G = uint8(color >> 16)
 | 
			
		||||
	c.R = uint8(color >> 24)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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)),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										143
									
								
								artist/line.go
									
									
									
									
									
								
							
							
						
						
									
										143
									
								
								artist/line.go
									
									
									
									
									
								
							@ -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)
 | 
			
		||||
	}}
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
} 
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										90
									
								
								artist/patterns/border.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								artist/patterns/border.go
									
									
									
									
									
										Normal file
									
								
							@ -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),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								artist/patterns/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								artist/patterns/doc.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
// Package patterns provides a basic set of types that satisfy the
 | 
			
		||||
// artist.Pattern interface.
 | 
			
		||||
package patterns
 | 
			
		||||
							
								
								
									
										41
									
								
								artist/patterns/texture.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								artist/patterns/texture.go
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								artist/patterns/uniform.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								artist/patterns/uniform.go
									
									
									
									
									
										Normal file
									
								
							@ -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))
 | 
			
		||||
}
 | 
			
		||||
@ -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))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								artist/shapes/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								artist/shapes/doc.go
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
							
								
								
									
										228
									
								
								artist/shapes/ellipse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								artist/shapes/ellipse.go
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										112
									
								
								artist/shapes/line.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								artist/shapes/line.go
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								artist/shapes/plot.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								artist/shapes/plot.go
									
									
									
									
									
										Normal file
									
								
							@ -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]
 | 
			
		||||
	}}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										116
									
								
								artist/shapes/rectangle.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								artist/shapes/rectangle.go
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -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]
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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() +
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
	} ()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								theme/assets/wintergreen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								theme/assets/wintergreen.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.9 KiB  | 
							
								
								
									
										302
									
								
								theme/default.go
									
									
									
									
									
								
							
							
						
						
									
										302
									
								
								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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 })
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										101
									
								
								theme/theme.go
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								theme/wrapped.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								theme/wrapped.go
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user