diff --git a/artist/artist.go b/artist/artist.go deleted file mode 100644 index f956174..0000000 --- a/artist/artist.go +++ /dev/null @@ -1,2 +0,0 @@ -package artist - diff --git a/artist/chisel.go b/artist/chisel.go index 1fe1a16..b172ddb 100644 --- a/artist/chisel.go +++ b/artist/chisel.go @@ -1,127 +1,30 @@ package artist -import "image" import "image/color" -import "git.tebibyte.media/sashakoshka/tomo" -// ShadingProfile contains shading information that can be used to draw chiseled -// objects. -type ShadingProfile struct { - Highlight tomo.Image - Shadow tomo.Image - Stroke tomo.Image - Fill tomo.Image - StrokeWeight int - ShadingWeight int +// Chiseled is a pattern that has a highlight section and a shadow section. +type Chiseled struct { + Highlight Pattern + Shadow Pattern } -// Engraved reverses the shadown and highlight colors of the ShadingProfile to -// produce a new ShadingProfile with an engraved appearance. -func (profile ShadingProfile) Engraved () (reversed ShadingProfile) { - reversed = profile - reversed.Highlight = profile.Shadow - reversed.Shadow = profile.Highlight - return -} - -// ChiseledRectangle draws a rectangle with a chiseled/embossed appearance, -// according to the ShadingProfile passed to it. -func ChiseledRectangle ( - destination tomo.Canvas, - profile ShadingProfile, - bounds image.Rectangle, -) ( - updatedRegion image.Rectangle, -) { - // FIXME: this breaks when the bounds are smaller than the border or - // shading weight - - stroke := profile.Stroke - highlight := profile.Highlight - shadow := profile.Shadow - fill := profile.Fill - strokeWeight := profile.StrokeWeight - shadingWeight := profile.ShadingWeight - - bounds = bounds.Canon() - updatedRegion = bounds - - strokeWeightVector := image.Point { strokeWeight, strokeWeight } - shadingWeightVector := image.Point { shadingWeight, shadingWeight } - - shadingBounds := bounds - shadingBounds.Min = shadingBounds.Min.Add(strokeWeightVector) - shadingBounds.Max = shadingBounds.Max.Sub(strokeWeightVector) - shadingBounds = shadingBounds.Canon() - - fillBounds := shadingBounds - fillBounds.Min = fillBounds.Min.Add(shadingWeightVector) - fillBounds.Max = fillBounds.Max.Sub(shadingWeightVector) - fillBounds = fillBounds.Canon() - - strokeImageMin := stroke.Bounds().Min - highlightImageMin := highlight.Bounds().Min - shadowImageMin := shadow.Bounds().Min - fillImageMin := fill.Bounds().Min - - width := float64(bounds.Dx()) - height := float64(bounds.Dy()) - - yy := 0 - for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { - xx := 0 - for x := bounds.Min.X; x < bounds.Max.X; x ++ { - var pixel color.RGBA - point := image.Point { x, y } - switch { - case point.In(fillBounds): - pixel = fill.RGBAAt ( - xx - strokeWeight - shadingWeight + - fillImageMin.X, - yy - strokeWeight - shadingWeight + - fillImageMin.Y) - - case point.In(shadingBounds): - var highlighted bool - // FIXME: this doesn't work quite right, the - // slope of the line is somewhat off. - bottomCorner := - float64(xx) < float64(yy) * - (width / height) - if bottomCorner { - highlighted = - float64(xx) < - height - float64(yy) - } else { - highlighted = - width - float64(xx) > - float64(yy) - } - - if highlighted { - pixel = highlight.RGBAAt ( - xx - strokeWeight + - highlightImageMin.X, - yy - strokeWeight + - highlightImageMin.Y) - } else { - pixel = shadow.RGBAAt ( - xx - strokeWeight + - shadowImageMin.X, - yy - strokeWeight + - shadowImageMin.Y) - } - - default: - pixel = stroke.RGBAAt ( - xx + strokeImageMin.X, - yy + strokeImageMin.Y) - } - destination.SetRGBA(x, y, pixel) - xx ++ - } - yy ++ +// AtWhen satisfies the Pattern interface. +func (chiseled Chiseled) AtWhen (x, y, width, height int) (c color.RGBA) { + var highlighted bool + // FIXME: this doesn't work quite right, the + // slope of the line is somewhat off. + bottomCorner := + float64(x) < float64(y) * + (float64(width) / float64(height)) + if bottomCorner { + highlighted = float64(x) < float64(height) - float64(y) + } else { + highlighted = float64(width) - float64(x) > float64(y) + } + + if highlighted { + return chiseled.Highlight.AtWhen(x, y, width, height) + } else { + return chiseled.Shadow.AtWhen(x, y, width, height) } - - return } diff --git a/artist/line.go b/artist/line.go index 7370520..698dfe8 100644 --- a/artist/line.go +++ b/artist/line.go @@ -3,9 +3,11 @@ package artist import "image" import "git.tebibyte.media/sashakoshka/tomo" +// Line draws a line from one point to another with the specified weight and +// pattern. func Line ( destination tomo.Canvas, - source tomo.Image, + source Pattern, weight int, min image.Point, max image.Point, @@ -17,6 +19,8 @@ func Line ( 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) { @@ -26,7 +30,7 @@ func Line ( min = max max = temp } - lineLow(destination, source, weight, min, max) + lineLow(destination, source, weight, min, max, width, height) } else { if max.Y < min.Y { @@ -34,18 +38,22 @@ func Line ( min = max max = temp } - lineHigh(destination, source, weight, min, max) + lineHigh(destination, source, weight, min, max, width, height) } return } func lineLow ( destination tomo.Canvas, - source tomo.Image, + 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 @@ -59,7 +67,8 @@ func lineLow ( y := min.Y for x := min.X; x < max.X; x ++ { - destination.SetRGBA(x, y, source.RGBAAt(x, y)) + if !(image.Point { x, y }).In(bounds) { break } + data[x + y * stride] = source.AtWhen(x, y, width, height) if D > 0 { y += yi D += 2 * (deltaY - deltaX) @@ -71,11 +80,15 @@ func lineLow ( func lineHigh ( destination tomo.Canvas, - source tomo.Image, + 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 @@ -89,7 +102,8 @@ func lineHigh ( x := min.X for y := min.Y; y < max.Y; y ++ { - destination.SetRGBA(x, y, source.RGBAAt(x, y)) + if !(image.Point { x, y }).In(bounds) { break } + data[x + y * stride] = source.AtWhen(x, y, width, height) if D > 0 { x += xi D += 2 * (deltaX - deltaY) diff --git a/artist/multiborder.go b/artist/multiborder.go new file mode 100644 index 0000000..da5f42d --- /dev/null +++ b/artist/multiborder.go @@ -0,0 +1,58 @@ +package artist + +import "image" +import "image/color" + +// Border represents a border that can be fed to MultiBorder. +type Border struct { + Weight int + Stroke Pattern + bounds image.Rectangle + dx, dy int +} + +// MultiBorder 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 MultiBorder struct { + borders []Border + lastWidth, lastHeight int + maxBorder int +} + +// NewMultiBorder creates a new MultiBorder pattern from the given list of +// borders. +func NewMultiBorder (borders ...Border) (multi *MultiBorder) { + return &MultiBorder { borders: borders } +} + +// AtWhen satisfies the Pattern interface. +func (multi *MultiBorder) 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 *MultiBorder) 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 } + } +} diff --git a/artist/pattern.go b/artist/pattern.go new file mode 100644 index 0000000..ef87caa --- /dev/null +++ b/artist/pattern.go @@ -0,0 +1,12 @@ +package artist + +import "image/color" + +// Pattern is capable of generating a pattern pixel by pixel. +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) +} diff --git a/artist/rectangle.go b/artist/rectangle.go index 3a56bdb..6534f8b 100644 --- a/artist/rectangle.go +++ b/artist/rectangle.go @@ -1,105 +1,93 @@ package artist import "image" -import "image/color" import "git.tebibyte.media/sashakoshka/tomo" -// Paste transfers one image onto another, offset by the specified point. +// Paste transfers one canvas onto another, offset by the specified point. func Paste ( destination tomo.Canvas, - source tomo.Image, + source tomo.Canvas, offset image.Point, ) ( updatedRegion image.Rectangle, ) { - sourceBounds := source.Bounds().Canon() + 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 ++ { - destination.SetRGBA ( - x + offset.X, y + offset.Y, - source.RGBAAt(x, y)) + dstData[x + offset.X + (y + offset.Y) * dstStride] = + srcData[x + y * srcStride] }} return } -// Rectangle draws a rectangle with an inset border. If the border image is nil, -// no border will be drawn. Likewise, if the fill image is nil, the rectangle -// will have no fill. -func Rectangle ( +// FillRectangle draws a filled rectangle with the specified pattern. +func FillRectangle ( destination tomo.Canvas, - fill tomo.Image, - stroke tomo.Image, - weight int, + source Pattern, bounds image.Rectangle, ) ( updatedRegion image.Rectangle, ) { - bounds = bounds.Canon() + data, stride := destination.Buffer() + bounds = bounds.Canon().Intersect(destination.Bounds()).Canon() + if bounds.Empty() { return } updatedRegion = bounds - fillBounds := bounds - fillBounds.Min = fillBounds.Min.Add(image.Point { weight, weight }) - fillBounds.Max = fillBounds.Max.Sub(image.Point { weight, weight }) - fillBounds = fillBounds.Canon() - - for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { - for x := bounds.Min.X; x < bounds.Max.X; x ++ { - var pixel color.RGBA - if (image.Point { x, y }).In(fillBounds) { - pixel = fill.RGBAAt(x, y) - } else { - pixel = stroke.RGBAAt(x, y) - } - destination.SetRGBA(x, y, pixel) + 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, y, width, height) }} - return } -// OffsetRectangle is the same as Rectangle, but offsets the border image to the -// top left corner of the border and the fill image to the top left corner of -// the fill. -func OffsetRectangle ( + +// StrokeRectangle draws the outline of a rectangle with the specified line +// weight and pattern. +func StrokeRectangle ( destination tomo.Canvas, - fill tomo.Image, - stroke tomo.Image, + source Pattern, weight int, bounds image.Rectangle, -) ( - updatedRegion image.Rectangle, ) { bounds = bounds.Canon() - updatedRegion = bounds - - fillBounds := bounds - fillBounds.Min = fillBounds.Min.Add(image.Point { weight, weight }) - fillBounds.Max = fillBounds.Max.Sub(image.Point { weight, weight }) - fillBounds = fillBounds.Canon() - - strokeImageMin := stroke.Bounds().Min - fillImageMin := fill.Bounds().Min - - yy := 0 - for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { - xx := 0 - for x := bounds.Min.X; x < bounds.Max.X; x ++ { - var pixel color.RGBA - if (image.Point { x, y }).In(fillBounds) { - pixel = fill.RGBAAt ( - xx - weight + fillImageMin.X, - yy - weight + fillImageMin.Y) - } else { - pixel = stroke.RGBAAt ( - xx + strokeImageMin.X, - yy + strokeImageMin.Y) - } - destination.SetRGBA(x, y, pixel) - xx ++ - } - yy ++ + insetBounds := bounds.Inset(weight) + if insetBounds.Empty() { + FillRectangle(destination, source, bounds) + return } - - 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)) } + +// TODO: FillEllipse + +// TODO: StrokeEllipse diff --git a/artist/text.go b/artist/text.go index 1afd741..59bd4e6 100644 --- a/artist/text.go +++ b/artist/text.go @@ -95,12 +95,20 @@ func (drawer *TextDrawer) SetAlignment (align Align) { // Draw draws the drawer's text onto the specified canvas at the given offset. func (drawer *TextDrawer) Draw ( destination tomo.Canvas, - source tomo.Image, + source Pattern, offset image.Point, ) ( updatedRegion image.Rectangle, ) { + wrappedSource := WrappedPattern { + Pattern: source, + Width: 0, + Height: 0, // TODO: choose a better width and height + } + if !drawer.layoutClean { drawer.recalculate() } + // TODO: reimplement a version of draw mask that takes in a pattern and + // only draws to a tomo.Canvas. for _, word := range drawer.layout { for _, character := range word.text { destinationRectangle, @@ -117,7 +125,7 @@ func (drawer *TextDrawer) Draw ( draw.DrawMask ( destination, destinationRectangle, - source, image.Point { }, + wrappedSource, image.Point { }, mask, maskPoint, draw.Over) diff --git a/artist/texture.go b/artist/texture.go new file mode 100644 index 0000000..f2d0ae2 --- /dev/null +++ b/artist/texture.go @@ -0,0 +1,43 @@ +package artist + +import "image" +import "image/color" + +// Texture is a struct that allows an image to be converted into a tiling +// texture pattern. +type Texture struct { + data []color.RGBA + width, height int +} + +// NewTexture converts an image into a texture. +func NewTexture (source image.Image) (texture Texture) { + bounds := source.Bounds() + texture.width = bounds.Dx() + texture.height = bounds.Dy() + texture.data = make([]color.RGBA, texture.width * texture.height) + + index := 0 + for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { + for x := bounds.Min.X; x < bounds.Max.X; x ++ { + r, g, b, a := source.At(x, y).RGBA() + texture.data[index] = color.RGBA { + uint8(r >> 8), + uint8(g >> 8), + uint8(b >> 8), + uint8(a >> 8), + } + index ++ + }} + return +} + +// AtWhen returns the color at the specified x and y coordinates, wrapped to the +// image's width. the width and height are ignored. +func (texture Texture) AtWhen (x, y, width, height int) (pixel color.RGBA) { + x %= texture.width + y %= texture.height + if x < 0 { x += texture.width } + if y < 0 { y += texture.height } + return texture.data[x + y * texture.width] +} diff --git a/artist/uniform.go b/artist/uniform.go index 2f99385..4fa3013 100644 --- a/artist/uniform.go +++ b/artist/uniform.go @@ -3,69 +3,53 @@ package artist import "image" import "image/color" -// Uniform is an infinite-sized Image of uniform color. It implements the -// color.Color, color.Model, and tomo.Image interfaces. -type Uniform struct { - C color.RGBA -} +// 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 image of the given color. -func NewUniform (c color.Color) (uniform *Uniform) { - uniform = &Uniform { } +func NewUniform (c color.Color) (uniform Uniform) { r, g, b, a := c.RGBA() - uniform.C.R = uint8(r >> 8) - uniform.C.G = uint8(g >> 8) - uniform.C.B = uint8(b >> 8) - uniform.C.A = uint8(a >> 8) + uniform.R = uint8(r >> 8) + uniform.G = uint8(g >> 8) + uniform.B = uint8(b >> 8) + uniform.A = uint8(a >> 8) return } -func (uniform *Uniform) RGBA () (r, g, b, a uint32) { - r = uint32(uniform.C.R) << 8 | uint32(uniform.C.R) - g = uint32(uniform.C.G) << 8 | uint32(uniform.C.G) - b = uint32(uniform.C.B) << 8 | uint32(uniform.C.B) - a = uint32(uniform.C.A) << 8 | uint32(uniform.C.A) - return +// ColorModel satisfies the image.Image interface. +func (uniform Uniform) ColorModel () (model color.Model) { + return uniform } -func (uniform *Uniform) ColorModel () (model color.Model) { - model = uniform - return +// Convert satisfies the color.Model interface. +func (uniform Uniform) Convert (in color.Color) (c color.Color) { + return color.RGBA(uniform) } -func (uniform *Uniform) Convert (in color.Color) (out color.Color) { - out = uniform.C - return -} - -func (uniform *Uniform) Bounds () (rectangle image.Rectangle) { +// 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 } -func (uniform *Uniform) At (x, y int) (c color.Color) { - c = uniform.C - return +// At satisfies the image.Image interface. +func (uniform Uniform) At (x, y int) (c color.Color) { + return color.RGBA(uniform) } -func (uniform *Uniform) RGBAAt (x, y int) (c color.RGBA) { - c = uniform.C - return +// AtWhen satisfies the Pattern interface. +func (uniform Uniform) AtWhen (x, y, width, height int) (c color.RGBA) { + return color.RGBA(uniform) } -func (uniform *Uniform) RGBA64At (x, y int) (c color.RGBA64) { - r := uint16(uniform.C.R) << 8 | uint16(uniform.C.R) - g := uint16(uniform.C.G) << 8 | uint16(uniform.C.G) - b := uint16(uniform.C.B) << 8 | uint16(uniform.C.B) - a := uint16(uniform.C.A) << 8 | uint16(uniform.C.A) - - c = color.RGBA64 { R: r, G: g, B: b, A: a } - return +// 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) { - opaque = uniform.C.A == 0xFF - return +func (uniform Uniform) Opaque () (opaque bool) { + return uniform.A == 0xFF } diff --git a/artist/wrap.go b/artist/wrap.go index e6a4ed5..5cf0520 100644 --- a/artist/wrap.go +++ b/artist/wrap.go @@ -1,99 +1,27 @@ package artist -import "git.tebibyte.media/sashakoshka/tomo" - import "image" -import "image/draw" import "image/color" -// WrappedImage wraps an image.Image and allows it to satisfy tomo.Image. -type WrappedImage struct { Underlying image.Image } +// WrappedPattern is a pattern that is able to behave like an image.Image. +type WrappedPattern struct { + Pattern + Width, Height int +} -// WrapImage wraps a generic image.Image and allows it to satisfy tomo.Image. -// Do not use this function to wrap images that already satisfy tomo.Image, -// because the resulting wrapped image will be rather slow in comparison. -func WrapImage (underlying image.Image) (wrapped tomo.Image) { - wrapped = WrappedImage { Underlying: underlying } +// 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 } -func (wrapped WrappedImage) Bounds () (bounds image.Rectangle) { - bounds = wrapped.Underlying.Bounds() - return -} - -func (wrapped WrappedImage) ColorModel () (model color.Model) { - model = wrapped.Underlying.ColorModel() - return -} - -func (wrapped WrappedImage) At (x, y int) (pixel color.Color) { - pixel = wrapped.Underlying.At(x, y) - return -} - -func (wrapped WrappedImage) RGBAAt (x, y int) (pixel color.RGBA) { - r, g, b, a := wrapped.Underlying.At(x, y).RGBA() - pixel.R = uint8(r >> 8) - pixel.G = uint8(g >> 8) - pixel.B = uint8(b >> 8) - pixel.A = uint8(a >> 8) - return -} - -// WrappedCanvas wraps a draw.Image and allows it to satisfy tomo.Canvas. -type WrappedCanvas struct { Underlying draw.Image } - -// WrapCanvas wraps a generic draw.Image and allows it to satisfy tomo.Canvas. -// Do not use this function to wrap images that already satisfy tomo.Canvas, -// because the resulting wrapped image will be rather slow in comparison. -func WrapCanvas (underlying draw.Image) (wrapped tomo.Canvas) { - wrapped = WrappedCanvas { Underlying: underlying } - return -} - -func (wrapped WrappedCanvas) Bounds () (bounds image.Rectangle) { - bounds = wrapped.Underlying.Bounds() - return -} - -func (wrapped WrappedCanvas) ColorModel () (model color.Model) { - model = wrapped.Underlying.ColorModel() - return -} - -func (wrapped WrappedCanvas) At (x, y int) (pixel color.Color) { - pixel = wrapped.Underlying.At(x, y) - return -} - -func (wrapped WrappedCanvas) RGBAAt (x, y int) (pixel color.RGBA) { - r, g, b, a := wrapped.Underlying.At(x, y).RGBA() - pixel.R = uint8(r >> 8) - pixel.G = uint8(g >> 8) - pixel.B = uint8(b >> 8) - pixel.A = uint8(a >> 8) - return -} - -func (wrapped WrappedCanvas) Set (x, y int, pixel color.Color) { - wrapped.Underlying.Set(x, y, pixel) -} - -func (wrapped WrappedCanvas) SetRGBA (x, y int, pixel color.RGBA) { - wrapped.Underlying.Set(x, y, pixel) -} - -// ToRGBA clones an existing image.Image into an image.RGBA struct, which -// directly satisfies tomo.Image. This is useful for things like icons and -// textures. -func ToRGBA (input image.Image) (output *image.RGBA) { - bounds := input.Bounds() - output = image.NewRGBA(bounds) - - for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { - for x := bounds.Min.X; x < bounds.Max.X; x ++ { - output.Set(x, y, input.At(x, y)) - }} - return +// ColorModel satisfies the image.Image interface. +func (pattern WrappedPattern) ColorModel () (model color.Model) { + return color.RGBAModel } diff --git a/backends/x/window.go b/backends/x/window.go index 7e59760..b2f3f7b 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -187,8 +187,9 @@ func (window *Window) reallocateCanvas () { } func (window *Window) redrawChildEntirely () { + data, stride := window.child.Buffer() window.xCanvas.For (func (x, y int) (c xgraphics.BGRA) { - rgba := window.child.RGBAAt(x, y) + rgba := data[x + y * stride] c.R, c.G, c.B, c.A = rgba.R, rgba.G, rgba.B, rgba.A return }) @@ -206,13 +207,14 @@ func (window *Window) resizeChildToFit () { window.redrawChildEntirely() } -func (window *Window) childDrawCallback (region tomo.Image) { +func (window *Window) childDrawCallback (region tomo.Canvas) { if window.skipChildDrawCallback { return } + data, stride := region.Buffer() bounds := region.Bounds() for x := bounds.Min.X; x < bounds.Max.X; x ++ { for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { - rgba := region.RGBAAt(x, y) + rgba := data[x + y * stride] window.xCanvas.SetBGRA (x, y, xgraphics.BGRA { R: rgba.R, G: rgba.G, diff --git a/canvas.go b/canvas.go new file mode 100644 index 0000000..f51c74a --- /dev/null +++ b/canvas.go @@ -0,0 +1,70 @@ +package tomo + +import "image" +import "image/draw" +import "image/color" + +// Canvas is like Image but also requires Set and SetRGBA methods. This +// interface can be easily satisfied using an image.RGBA struct. +type Canvas interface { + draw.Image + Buffer () (data []color.RGBA, stride int) +} + +// BasicCanvas is a general purpose implementation of tomo.Canvas. +type BasicCanvas struct { + pix []color.RGBA + stride int + rect image.Rectangle +} + +// NewBasicCanvas creates a new basic canvas with the specified width and +// height, allocating a buffer for it. +func NewBasicCanvas (width, height int) (canvas BasicCanvas) { + canvas.pix = make([]color.RGBA, height * width) + canvas.stride = width + canvas.rect = image.Rect(0, 0, width, height) + return +} + +// you know what it do +func (canvas BasicCanvas) Bounds () (bounds image.Rectangle) { + return canvas.rect +} + +// you know what it do +func (canvas BasicCanvas) At (x, y int) (color.Color) { + if !image.Pt(x, y).In(canvas.rect) { return nil } + return canvas.pix[x + y * canvas.stride] +} + +// you know what it do +func (canvas BasicCanvas) ColorModel () (model color.Model) { + return color.RGBAModel +} + +// you know what it do +func (canvas BasicCanvas) Set (x, y int, c color.Color) { + if !image.Pt(x, y).In(canvas.rect) { return } + r, g, b, a := c.RGBA() + canvas.pix[x + y * canvas.stride] = color.RGBA { + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + } +} + +// you know what it do +func (canvas BasicCanvas) Buffer () (data []color.RGBA, stride int) { + return canvas.pix, canvas.stride +} + +// Cut returns a sub-canvas of a given canvas. +func Cut (canvas Canvas, bounds image.Rectangle) (reduced BasicCanvas) { + bounds = bounds.Intersect(canvas.Bounds()) + if bounds.Empty() { return } + reduced.rect = bounds + reduced.pix, reduced.stride = canvas.Buffer() + return +} diff --git a/elements/basic/button.go b/elements/basic/button.go index fe7d87a..d7e80e7 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -150,12 +150,12 @@ func (element *Button) SetText (text string) { func (element *Button) draw () { bounds := element.core.Bounds() - artist.ChiseledRectangle ( + artist.FillRectangle ( element.core, - theme.RaisedProfile ( - element.pressed, + theme.ButtonPattern ( element.enabled, - element.Selected()), + element.Selected(), + element.pressed), bounds) innerBounds := bounds @@ -179,10 +179,6 @@ func (element *Button) draw () { offset = offset.Add(theme.SinkOffsetVector()) } - foreground := theme.ForegroundImage() - if !element.enabled { - foreground = theme.DisabledForegroundImage() - } - + foreground := theme.ForegroundPattern(element.enabled) element.drawer.Draw(element.core, foreground, offset) } diff --git a/elements/basic/container.go b/elements/basic/container.go index 0ae02eb..d3eb74b 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -57,7 +57,7 @@ func (element *Container) Adopt (child tomo.Element, expand bool) { return }, - Draw: func (region tomo.Image) { + Draw: func (region tomo.Canvas) { element.drawChildRegion(child, region) }, }) @@ -318,10 +318,9 @@ func (element *Container) recalculate () { func (element *Container) draw () { bounds := element.core.Bounds() - artist.Rectangle ( + artist.FillRectangle ( element.core, - theme.BackgroundImage(), - nil, 0, + theme.BackgroundPattern(), bounds) for _, entry := range element.children { @@ -329,7 +328,7 @@ func (element *Container) draw () { } } -func (element *Container) drawChildRegion (child tomo.Element, region tomo.Image) { +func (element *Container) drawChildRegion (child tomo.Element, region tomo.Canvas) { if element.warping { return } for _, entry := range element.children { if entry.Element == child { diff --git a/elements/basic/label.go b/elements/basic/label.go index f9b9186..d80f781 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -93,15 +93,14 @@ func (element *Label) updateMinimumSize () { func (element *Label) draw () { bounds := element.core.Bounds() - artist.Rectangle ( + artist.FillRectangle ( element.core, - theme.BackgroundImage(), - nil, 0, + theme.BackgroundPattern(), bounds) textBounds := element.drawer.LayoutBounds() - foreground := theme.ForegroundImage() + foreground := theme.ForegroundPattern(true) element.drawer.Draw (element.core, foreground, image.Point { X: 0 - textBounds.Min.X, Y: 0 - textBounds.Min.Y, diff --git a/elements/core/core.go b/elements/core/core.go index e678227..cb12959 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -7,7 +7,7 @@ import "git.tebibyte.media/sashakoshka/tomo" // Core is a struct that implements some core functionality common to most // widgets. It is meant to be embedded directly into a struct. type Core struct { - canvas *image.RGBA + canvas tomo.BasicCanvas parent tomo.Element metrics struct { @@ -32,20 +32,19 @@ func (core Core) ColorModel () (model color.Model) { } func (core Core) At (x, y int) (pixel color.Color) { - if core.canvas == nil { return color.RGBA { } } - pixel = core.canvas.At(x, y) - return -} - -func (core Core) RGBAAt (x, y int) (pixel color.RGBA) { - if core.canvas == nil { return color.RGBA { } } - pixel = core.canvas.RGBAAt(x, y) - return + return core.canvas.At(x, y) } func (core Core) Bounds () (bounds image.Rectangle) { - if core.canvas != nil { bounds = core.canvas.Bounds() } - return + return core.canvas.Bounds() +} + +func (core Core) Set (x, y int, c color.Color) () { + core.canvas.Set(x, y, c) +} + +func (core Core) Buffer () (data []color.RGBA, stride int) { + return core.canvas.Buffer() } func (core Core) Selectable () (selectable bool) { @@ -72,13 +71,12 @@ func (core Core) MinimumSize () (width, height int) { // be used as a canvas. It must not be directly embedded into an element, but // instead kept as a private member. type CoreControl struct { - *image.RGBA + tomo.BasicCanvas core *Core } -func (control CoreControl) HasImage () (has bool) { - has = control.RGBA != nil - return +func (control CoreControl) HasImage () (empty bool) { + return !control.Bounds().Empty() } func (control CoreControl) Select () (granted bool) { @@ -98,7 +96,7 @@ func (control CoreControl) SetSelectable (selectable bool) { } func (control CoreControl) PushRegion (bounds image.Rectangle) { - control.core.hooks.RunDraw(control.SubImage(bounds).(*image.RGBA)) + control.core.hooks.RunDraw(tomo.Cut(control, bounds)) } func (control CoreControl) PushAll () { @@ -108,8 +106,8 @@ func (control CoreControl) PushAll () { func (control *CoreControl) AllocateCanvas (width, height int) { core := control.core width, height, _ = control.ConstrainSize(width, height) - core.canvas = image.NewRGBA(image.Rect (0, 0, width, height)) - control.RGBA = core.canvas + core.canvas = tomo.NewBasicCanvas(width, height) + control.BasicCanvas = core.canvas } func (control CoreControl) SetMinimumSize (width, height int) { @@ -125,19 +123,17 @@ func (control CoreControl) SetMinimumSize (width, height int) { // if there is an image buffer, and the current size is less // than this new minimum size, send core.parent a resize event. - if control.HasImage() { - bounds := control.Bounds() - imageWidth, - imageHeight, - constrained := control.ConstrainSize ( - bounds.Dx(), - bounds.Dy()) - if constrained { - core.parent.Handle (tomo.EventResize { - Width: imageWidth, - Height: imageHeight, - }) - } + bounds := control.Bounds() + imageWidth, + imageHeight, + constrained := control.ConstrainSize ( + bounds.Dx(), + bounds.Dy()) + if constrained { + core.parent.Handle (tomo.EventResize { + Width: imageWidth, + Height: imageHeight, + }) } } diff --git a/elements/fun/clock.go b/elements/fun/clock.go index d4f7f0a..4e532bd 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -44,14 +44,14 @@ func (element *AnalogClock) SetTime (newTime time.Time) { func (element *AnalogClock) draw () { bounds := element.core.Bounds() - artist.ChiseledRectangle ( + artist.FillRectangle ( element.core, - theme.BackgroundProfile(true), + theme.SunkenPattern(), bounds) for hour := 0; hour < 12; hour ++ { element.radialLine ( - theme.ForegroundImage(), + theme.ForegroundPattern(true), 0.8, 0.9, float64(hour) / 6 * math.Pi) } @@ -60,18 +60,18 @@ func (element *AnalogClock) draw () { hour := float64(element.time.Hour()) + minute / 60 element.radialLine ( - theme.ForegroundImage(), + theme.ForegroundPattern(true), 0, 0.5, (hour - 3) / 6 * math.Pi) element.radialLine ( - theme.ForegroundImage(), + theme.ForegroundPattern(true), 0, 0.7, (minute - 15) / 30 * math.Pi) element.radialLine ( - theme.AccentImage(), + theme.AccentPattern(), 0, 0.7, (second - 15) / 30 * math.Pi) } func (element *AnalogClock) radialLine ( - source tomo.Image, + source artist.Pattern, inner float64, outer float64, radian float64, diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 30a7c3b..2c3bfa6 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -13,7 +13,7 @@ type Mouse struct { *core.Core core core.CoreControl drawing bool - color tomo.Image + color artist.Pattern lastMousePos image.Point } @@ -33,13 +33,16 @@ func (element *Mouse) Handle (event tomo.Event) { element.core.AllocateCanvas ( resizeEvent.Width, resizeEvent.Height) - artist.Rectangle ( + artist.FillRectangle ( element.core, - theme.AccentImage(), - artist.NewUniform(color.Black), - 1, element.Bounds()) + theme.AccentPattern(), + element.Bounds()) + artist.StrokeRectangle ( + element.core, + artist.NewUniform(color.Black), 1, + element.Bounds()) artist.Line ( - element.core, artist.NewUniform(color.White), 1, + element.core, artist.NewUniform(color.White), 3, image.Pt(1, 1), image.Pt(resizeEvent.Width - 2, resizeEvent.Height - 2)) artist.Line ( @@ -66,6 +69,7 @@ func (element *Mouse) Handle (event tomo.Event) { element.lastMousePos = mousePos case tomo.EventMouseMove: + if !element.drawing { return } mouseMoveEvent := event.(tomo.EventMouseMove) mousePos := image.Pt ( mouseMoveEvent.X, diff --git a/theme/theme.go b/theme/theme.go index b638a4e..1305ac3 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -3,186 +3,99 @@ package theme import "image" import "image/color" import "golang.org/x/image/font" -import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/defaultfont" // none of these colors are final! TODO: generate these values from a theme // file at startup. -var foregroundImage = artist.NewUniform(color.Gray16 { 0x0000}) -var disabledForegroundImage = artist.NewUniform(color.Gray16 { 0x5555}) -var accentImage = artist.NewUniform(color.RGBA { 0x40, 0x80, 0x90, 0xFF}) -var highlightImage = artist.NewUniform(color.Gray16 { 0xEEEE }) -var shadowImage = artist.NewUniform(color.Gray16 { 0x3333 }) -var weakShadeImage = artist.NewUniform(color.Gray16 { 0x7777 }) -var strokeImage = artist.NewUniform(color.Gray16 { 0x0000 }) -var weakStrokeImage = artist.NewUniform(color.Gray16 { 0x3333 }) -var insetShadowImage = artist.NewUniform(color.Gray16 { 0x7777 }) - -var backgroundImage = artist.NewUniform(color.Gray16 { 0xAAAA}) -var backgroundProfile = artist.ShadingProfile { - Highlight: highlightImage, - Shadow: shadowImage, - Stroke: strokeImage, - Fill: backgroundImage, - StrokeWeight: 1, - ShadingWeight: 1, -} -var engravedBackgroundProfile = backgroundProfile.Engraved() - -var raisedImage = artist.NewUniform(color.RGBA { 0x8D, 0x98, 0x94, 0xFF}) -var raisedProfile = artist.ShadingProfile { - Highlight: highlightImage, - Shadow: shadowImage, - Stroke: strokeImage, - Fill: raisedImage, - StrokeWeight: 1, - ShadingWeight: 1, -} -var selectedRaisedProfile = artist.ShadingProfile { - Highlight: highlightImage, - Shadow: shadowImage, - Stroke: accentImage, - Fill: raisedImage, - StrokeWeight: 1, - ShadingWeight: 1, -} -var engravedRaisedProfile = artist.ShadingProfile { - Highlight: weakShadeImage, - Shadow: raisedImage, - Stroke: strokeImage, - Fill: raisedImage, - StrokeWeight: 1, - ShadingWeight: 1, -} -var selectedEngravedRaisedProfile = artist.ShadingProfile { - Highlight: insetShadowImage, - Shadow: raisedImage, - Stroke: accentImage, - Fill: raisedImage, - StrokeWeight: 1, - ShadingWeight: 1, -} -var disabledRaisedProfile = artist.ShadingProfile { - Highlight: weakShadeImage, - Shadow: weakShadeImage, - Stroke: weakStrokeImage, - Fill: backgroundImage, - StrokeWeight: 1, - ShadingWeight: 0, +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 } -var inputImage = artist.NewUniform(color.Gray16 { 0xFFFF }) -var inputProfile = artist.ShadingProfile { - Highlight: insetShadowImage, - Shadow: inputImage, - Stroke: strokeImage, - Fill: inputImage, - StrokeWeight: 1, - ShadingWeight: 1, -} -var selectedInputProfile = artist.ShadingProfile { - Highlight: insetShadowImage, - Shadow: inputImage, - Stroke: accentImage, - Fill: inputImage, - StrokeWeight: 1, - ShadingWeight: 1, -} -var disabledInputProfile = artist.ShadingProfile { - Highlight: weakShadeImage, - Shadow: backgroundImage, - Stroke: accentImage, - Fill: backgroundImage, - StrokeWeight: 1, - ShadingWeight: 0, -} +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 }) -// BackgroundProfile returns the shading profile to be used for backgrounds. -func BackgroundProfile (engraved bool) artist.ShadingProfile { - if engraved { - return engravedBackgroundProfile +var buttonPattern = artist.NewMultiBorder ( + artist.Border { Weight: 1, Stroke: strokePattern }, + artist.Border { + Weight: 1, + Stroke: artist.Chiseled { + Highlight: artist.NewUniform(hex(0xCCD5D2FF)), + Shadow: artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Border { Stroke: artist.NewUniform(hex(0x8D9894FF)) }) +var selectedButtonPattern = artist.NewMultiBorder ( + artist.Border { Weight: 1, Stroke: strokePattern }, + artist.Border { + Weight: 1, + Stroke: artist.Chiseled { + Highlight: artist.NewUniform(hex(0xCCD5D2FF)), + Shadow: artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Border { Weight: 1, Stroke: accentPattern }, + artist.Border { Stroke: artist.NewUniform(hex(0x8D9894FF)) }) +var pressedButtonPattern = artist.NewMultiBorder ( + artist.Border { Weight: 1, Stroke: strokePattern }, + artist.Border { + Weight: 1, + Stroke: artist.Chiseled { + Highlight: artist.NewUniform(hex(0x4B5B59FF)), + Shadow: artist.NewUniform(hex(0x8D9894FF)), + }, + }, + artist.Border { Stroke: artist.NewUniform(hex(0x8D9894FF)) }) +var disabledButtonPattern = artist.NewMultiBorder ( + artist.Border { Weight: 1, Stroke: weakForegroundPattern }, + artist.Border { Stroke: backgroundPattern }) + +var sunkenPattern = artist.NewMultiBorder ( + artist.Border { Weight: 1, Stroke: strokePattern }, + artist.Border { + Weight: 1, + Stroke: artist.Chiseled { + Highlight: artist.NewUniform(hex(0x373C3AFF)), + Shadow: artist.NewUniform(hex(0xDBDBDBFF)), + }, + }, + artist.Border { Stroke: backgroundPattern }) + +func AccentPattern () (artist.Pattern) { return accentPattern } +func BackgroundPattern () (artist.Pattern) { return backgroundPattern } +func SunkenPattern () (artist.Pattern) { return sunkenPattern} +func ForegroundPattern (enabled bool) (artist.Pattern) { + if enabled { + return foregroundPattern } else { - return backgroundProfile + return weakForegroundPattern } } - -// RaisedProfile returns the shading profile to be used for raised objects such -// as buttons. -func RaisedProfile ( - engraved bool, - enabled bool, - selected bool, -) ( - artist.ShadingProfile, -) { +func ButtonPattern (enabled, selected, pressed bool) (artist.Pattern) { if enabled { - if engraved { - if selected { - return selectedEngravedRaisedProfile - } else { - return engravedRaisedProfile - } + if pressed { + return pressedButtonPattern } else { if selected { - return selectedRaisedProfile + return selectedButtonPattern } else { - return raisedProfile + return buttonPattern } } } else { - return disabledRaisedProfile + return disabledButtonPattern } } -// InputProfile returns the shading profile to be used for input fields. -func InputProfile (enabled bool, selected bool) artist.ShadingProfile { - if enabled { - if selected { - return selectedInputProfile - } else { - return inputProfile - } - } else { - return disabledInputProfile - } -} - -// BackgroundImage returns the texture/color used for the fill of -// BackgroundProfile. -func BackgroundImage () tomo.Image { - return backgroundImage -} - -// RaisedImage returns the texture/color used for the fill of RaisedProfile. -func RaisedImage () tomo.Image { - return raisedImage -} - -// InputImage returns the texture/color used for the fill of InputProfile. -func InputImage () tomo.Image { - return inputImage -} - -// ForegroundImage returns the texture/color text and monochromatic icons should -// be drawn with. -func ForegroundImage () tomo.Image { - return foregroundImage -} - -// DisabledForegroundImage returns the texture/color text and monochromatic -// icons should be drawn with if they are disabled. -func DisabledForegroundImage () tomo.Image { - return disabledForegroundImage -} - -// AccentImage returns the accent texture/color. -func AccentImage () tomo.Image { - return accentImage -} - -// TODO: load fonts from an actual source instead of using basicfont +// TODO: load fonts from an actual source instead of using defaultfont // FontFaceRegular returns the font face to be used for normal text. func FontFaceRegular () font.Face { diff --git a/tomo.go b/tomo.go index 8005841..134eccc 100644 --- a/tomo.go +++ b/tomo.go @@ -2,25 +2,6 @@ package tomo import "image" import "errors" -import "image/draw" -import "image/color" - -// Image represents a simple image buffer that fulfills the image.Image -// interface while also having methods that do away with the use of the -// color.Color interface to facilitate more efficient drawing. This interface -// can be easily satisfied using an image.RGBA struct. -type Image interface { - image.Image - RGBAAt (x, y int) (c color.RGBA) -} - -// Canvas is like Image but also requires Set and SetRGBA methods. This -// interface can be easily satisfied using an image.RGBA struct. -type Canvas interface { - draw.Image - RGBAAt (x, y int) (c color.RGBA) - SetRGBA (x, y int, c color.RGBA) -} // ParentHooks is a struct that contains callbacks that let child elements send // information to their parent element without the child element knowing @@ -29,7 +10,7 @@ type Canvas interface { type ParentHooks struct { // Draw is called when a part of the child element's surface is updated. // The updated region will be passed to the callback as a sub-image. - Draw func (region Image) + Draw func (region Canvas) // MinimumSizeChange is called when the child element's minimum width // and/or height changes. When this function is called, the element will @@ -49,7 +30,7 @@ type ParentHooks struct { } // RunDraw runs the Draw hook if it is not nil. If it is nil, it does nothing. -func (hooks ParentHooks) RunDraw (region Image) { +func (hooks ParentHooks) RunDraw (region Canvas) { if hooks.Draw != nil { hooks.Draw(region) } @@ -82,10 +63,10 @@ func (hooks ParentHooks) RunSelectabilityChange (selectable bool) { // Element represents a basic on-screen object. type Element interface { - // Element must implement the Image interface. Elements should start out - // with a completely blank image buffer, and only set its size and draw + // Element must implement the Canvas interface. Elements should start + // out with a completely blank buffer, and only allocate memory and draw // on it for the first time when sent an EventResize event. - Image + Canvas // Handle handles an event, propagating it to children if necessary. Handle (event Event)