From 34bf3038acdf41c527fdedc75e6b94a46cbe7995 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 14 Jan 2023 01:54:57 -0500 Subject: [PATCH 1/9] Replaced tomo.Image with tomo.Canvas and tomo.Pattern This is the first step in transitioning the API over to the new design. The new tomo.Canvas interface gives drawing functions direct access to data buffers and eliminates overhead associated with calling functions for every pixel. The entire artist package will be remade around this. --- artist/artist.go | 50 +++++++++++++++++++ artist/chisel.go | 51 ++++++++----------- artist/line.go | 22 ++++++--- artist/rectangle.go | 98 +++++++++---------------------------- artist/text.go | 39 ++++++++------- artist/uniform.go | 27 ++++------ artist/wrap.go | 98 ------------------------------------- backends/x/window.go | 8 +-- canvas.go | 70 ++++++++++++++++++++++++++ elements/basic/container.go | 7 ++- elements/basic/label.go | 3 +- elements/core/core.go | 60 +++++++++++------------ elements/fun/clock.go | 2 +- elements/testing/mouse.go | 8 +-- theme/theme.go | 13 +++-- tomo.go | 29 ++--------- 16 files changed, 260 insertions(+), 325 deletions(-) create mode 100644 canvas.go diff --git a/artist/artist.go b/artist/artist.go index f956174..d2f4bd4 100644 --- a/artist/artist.go +++ b/artist/artist.go @@ -1,2 +1,52 @@ package artist +import "image" +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) +} + +// 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/chisel.go b/artist/chisel.go index 1fe1a16..873046b 100644 --- a/artist/chisel.go +++ b/artist/chisel.go @@ -7,10 +7,10 @@ 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 + Highlight Pattern + Shadow Pattern + Stroke Pattern + Fill Pattern StrokeWeight int ShadingWeight int } @@ -43,6 +43,7 @@ func ChiseledRectangle ( strokeWeight := profile.StrokeWeight shadingWeight := profile.ShadingWeight + data, stride := destination.Buffer() bounds = bounds.Canon() updatedRegion = bounds @@ -59,11 +60,6 @@ func ChiseledRectangle ( 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()) @@ -75,11 +71,10 @@ func ChiseledRectangle ( point := image.Point { x, y } switch { case point.In(fillBounds): - pixel = fill.RGBAAt ( - xx - strokeWeight - shadingWeight + - fillImageMin.X, - yy - strokeWeight - shadingWeight + - fillImageMin.Y) + pixel = fill.AtWhen ( + xx - strokeWeight - shadingWeight, + yy - strokeWeight - shadingWeight, + fillBounds.Dx(), fillBounds.Dy()) case point.In(shadingBounds): var highlighted bool @@ -97,27 +92,21 @@ func ChiseledRectangle ( width - float64(xx) > float64(yy) } - + + shadingSource := shadow 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) + shadingSource = highlight } - + pixel = shadingSource.AtWhen ( + xx - strokeWeight, + yy - strokeWeight, + shadingBounds.Dx(), + shadingBounds.Dy()) default: - pixel = stroke.RGBAAt ( - xx + strokeImageMin.X, - yy + strokeImageMin.Y) + pixel = stroke.AtWhen ( + xx, yy, bounds.Dx(), bounds.Dy()) } - destination.SetRGBA(x, y, pixel) + data[x + y * stride] = pixel xx ++ } yy ++ diff --git a/artist/line.go b/artist/line.go index 7370520..51458bb 100644 --- a/artist/line.go +++ b/artist/line.go @@ -5,7 +5,7 @@ import "git.tebibyte.media/sashakoshka/tomo" func Line ( destination tomo.Canvas, - source tomo.Image, + source Pattern, weight int, min image.Point, max image.Point, @@ -17,6 +17,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 +28,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 +36,21 @@ 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() + deltaX := max.X - min.X deltaY := max.Y - min.Y yi := 1 @@ -59,7 +64,7 @@ func lineLow ( y := min.Y for x := min.X; x < max.X; x ++ { - destination.SetRGBA(x, y, source.RGBAAt(x, y)) + data[x + y * stride] = source.AtWhen(x, y, width, height) if D > 0 { y += yi D += 2 * (deltaY - deltaX) @@ -71,11 +76,14 @@ 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() + deltaX := max.X - min.X deltaY := max.Y - min.Y xi := 1 @@ -89,7 +97,7 @@ func lineHigh ( x := min.X for y := min.Y; y < max.Y; y ++ { - destination.SetRGBA(x, y, source.RGBAAt(x, y)) + data[x + y * stride] = source.AtWhen(x, y, width, height) if D > 0 { x += xi D += 2 * (deltaX - deltaY) diff --git a/artist/rectangle.go b/artist/rectangle.go index 3a56bdb..3b613be 100644 --- a/artist/rectangle.go +++ b/artist/rectangle.go @@ -1,105 +1,51 @@ 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 ( +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 ( - destination tomo.Canvas, - fill tomo.Image, - stroke tomo.Image, - 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 ++ - } - return } diff --git a/artist/text.go b/artist/text.go index 1afd741..b1c1e61 100644 --- a/artist/text.go +++ b/artist/text.go @@ -3,7 +3,7 @@ package artist // import "fmt" import "image" import "unicode" -import "image/draw" +// import "image/draw" import "golang.org/x/image/font" import "golang.org/x/image/math/fixed" import "git.tebibyte.media/sashakoshka/tomo" @@ -95,34 +95,35 @@ 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, ) { if !drawer.layoutClean { drawer.recalculate() } - for _, word := range drawer.layout { - for _, character := range word.text { - destinationRectangle, - mask, maskPoint, _, ok := drawer.face.Glyph ( - fixed.P ( - offset.X + word.position.X + character.x, - offset.Y + word.position.Y), - character.character) - if !ok { continue } + // TODO: reimplement a version of draw mask that takes in a pattern + // for _, word := range drawer.layout { + // for _, character := range word.text { + // destinationRectangle, + // mask, maskPoint, _, ok := drawer.face.Glyph ( + // fixed.P ( + // offset.X + word.position.X + character.x, + // offset.Y + word.position.Y), + // character.character) + // if !ok { continue } // FIXME: clip destination rectangle if we are on the cusp of // the maximum height. - draw.DrawMask ( - destination, - destinationRectangle, - source, image.Point { }, - mask, maskPoint, - draw.Over) + // draw.DrawMask ( + // destination, + // destinationRectangle, + // source, image.Point { }, + // mask, maskPoint, + // draw.Over) - updatedRegion = updatedRegion.Union(destinationRectangle) - }} + // updatedRegion = updatedRegion.Union(destinationRectangle) + // }} return } diff --git a/artist/uniform.go b/artist/uniform.go index 2f99385..e50923e 100644 --- a/artist/uniform.go +++ b/artist/uniform.go @@ -3,8 +3,8 @@ 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. +// Uniform is an infinite-sized pattern of uniform color. It implements the +// color.Color, color.Model, and image.Image interfaces. type Uniform struct { C color.RGBA } @@ -29,13 +29,11 @@ func (uniform *Uniform) RGBA () (r, g, b, a uint32) { } func (uniform *Uniform) ColorModel () (model color.Model) { - model = uniform - return + return uniform } -func (uniform *Uniform) Convert (in color.Color) (out color.Color) { - out = uniform.C - return +func (uniform *Uniform) Convert (in color.Color) (c color.Color) { + return uniform.C } func (uniform *Uniform) Bounds () (rectangle image.Rectangle) { @@ -45,13 +43,11 @@ func (uniform *Uniform) Bounds () (rectangle image.Rectangle) { } func (uniform *Uniform) At (x, y int) (c color.Color) { - c = uniform.C - return + return uniform.C } -func (uniform *Uniform) RGBAAt (x, y int) (c color.RGBA) { - c = uniform.C - return +func (uniform *Uniform) AtWhen (x, y, width, height int) (c color.RGBA) { + return uniform.C } func (uniform *Uniform) RGBA64At (x, y int) (c color.RGBA64) { @@ -59,13 +55,10 @@ func (uniform *Uniform) RGBA64At (x, y int) (c color.RGBA64) { 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 + return color.RGBA64 { R: r, G: g, B: b, A: a } } // Opaque scans the entire image and reports whether it is fully opaque. func (uniform *Uniform) Opaque () (opaque bool) { - opaque = uniform.C.A == 0xFF - return + return uniform.C.A == 0xFF } diff --git a/artist/wrap.go b/artist/wrap.go index e6a4ed5..530e53f 100644 --- a/artist/wrap.go +++ b/artist/wrap.go @@ -1,99 +1 @@ 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 } - -// 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 } - 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 -} 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/container.go b/elements/basic/container.go index 0ae02eb..886f98c 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, 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..6304afd 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -93,10 +93,9 @@ func (element *Label) updateMinimumSize () { func (element *Label) draw () { bounds := element.core.Bounds() - artist.Rectangle ( + artist.FillRectangle ( element.core, theme.BackgroundImage(), - nil, 0, bounds) textBounds := element.drawer.LayoutBounds() 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..6a97314 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -71,7 +71,7 @@ func (element *AnalogClock) draw () { } 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..6c4a90f 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,11 +33,11 @@ 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()) + element.Bounds()) + // TODO: draw a stroked rectangle around the edges artist.Line ( element.core, artist.NewUniform(color.White), 1, image.Pt(1, 1), diff --git a/theme/theme.go b/theme/theme.go index b638a4e..6ee9921 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -3,7 +3,6 @@ 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" @@ -151,34 +150,34 @@ func InputProfile (enabled bool, selected bool) artist.ShadingProfile { // BackgroundImage returns the texture/color used for the fill of // BackgroundProfile. -func BackgroundImage () tomo.Image { +func BackgroundImage () artist.Pattern { return backgroundImage } // RaisedImage returns the texture/color used for the fill of RaisedProfile. -func RaisedImage () tomo.Image { +func RaisedImage () artist.Pattern { return raisedImage } // InputImage returns the texture/color used for the fill of InputProfile. -func InputImage () tomo.Image { +func InputImage () artist.Pattern { return inputImage } // ForegroundImage returns the texture/color text and monochromatic icons should // be drawn with. -func ForegroundImage () tomo.Image { +func ForegroundImage () artist.Pattern { return foregroundImage } // DisabledForegroundImage returns the texture/color text and monochromatic // icons should be drawn with if they are disabled. -func DisabledForegroundImage () tomo.Image { +func DisabledForegroundImage () artist.Pattern { return disabledForegroundImage } // AccentImage returns the accent texture/color. -func AccentImage () tomo.Image { +func AccentImage () artist.Pattern { return accentImage } 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) From ec24eb7b4f040f38dd46903221489868a48aec00 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 14 Jan 2023 12:41:51 -0500 Subject: [PATCH 2/9] Text rendering is a thing again --- artist/pattern.go | 12 +++++++ artist/rectangle.go | 1 + artist/text.go | 42 +++++++++++++---------- artist/{artist.go => texture.go} | 9 ----- artist/uniform.go | 59 ++++++++++++++------------------ artist/wrap.go | 26 ++++++++++++++ 6 files changed, 88 insertions(+), 61 deletions(-) create mode 100644 artist/pattern.go rename artist/{artist.go => texture.go} (72%) 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 3b613be..4e15469 100644 --- a/artist/rectangle.go +++ b/artist/rectangle.go @@ -29,6 +29,7 @@ func Paste ( return } +// FillRectangle draws a filled rectangle with the specified pattern. func FillRectangle ( destination tomo.Canvas, source Pattern, diff --git a/artist/text.go b/artist/text.go index b1c1e61..823db44 100644 --- a/artist/text.go +++ b/artist/text.go @@ -3,7 +3,7 @@ package artist // import "fmt" import "image" import "unicode" -// import "image/draw" +import "image/draw" import "golang.org/x/image/font" import "golang.org/x/image/math/fixed" import "git.tebibyte.media/sashakoshka/tomo" @@ -100,30 +100,36 @@ func (drawer *TextDrawer) Draw ( ) ( 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 - // for _, word := range drawer.layout { - // for _, character := range word.text { - // destinationRectangle, - // mask, maskPoint, _, ok := drawer.face.Glyph ( - // fixed.P ( - // offset.X + word.position.X + character.x, - // offset.Y + word.position.Y), - // character.character) - // if !ok { continue } + for _, word := range drawer.layout { + for _, character := range word.text { + destinationRectangle, + mask, maskPoint, _, ok := drawer.face.Glyph ( + fixed.P ( + offset.X + word.position.X + character.x, + offset.Y + word.position.Y), + character.character) + if !ok { continue } // FIXME: clip destination rectangle if we are on the cusp of // the maximum height. - // draw.DrawMask ( - // destination, - // destinationRectangle, - // source, image.Point { }, - // mask, maskPoint, - // draw.Over) + draw.DrawMask ( + destination, + destinationRectangle, + wrappedSource, image.Point { }, + mask, maskPoint, + draw.Over) - // updatedRegion = updatedRegion.Union(destinationRectangle) - // }} + updatedRegion = updatedRegion.Union(destinationRectangle) + }} return } diff --git a/artist/artist.go b/artist/texture.go similarity index 72% rename from artist/artist.go rename to artist/texture.go index d2f4bd4..f2d0ae2 100644 --- a/artist/artist.go +++ b/artist/texture.go @@ -3,15 +3,6 @@ package artist import "image" 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) -} - // Texture is a struct that allows an image to be converted into a tiling // texture pattern. type Texture struct { diff --git a/artist/uniform.go b/artist/uniform.go index e50923e..4fa3013 100644 --- a/artist/uniform.go +++ b/artist/uniform.go @@ -4,61 +4,52 @@ import "image" import "image/color" // Uniform is an infinite-sized pattern of uniform color. It implements the -// color.Color, color.Model, and image.Image interfaces. -type Uniform struct { - C color.RGBA -} +// 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 -} - -func (uniform *Uniform) ColorModel () (model color.Model) { +// ColorModel satisfies the image.Image interface. +func (uniform Uniform) ColorModel () (model color.Model) { return uniform } -func (uniform *Uniform) Convert (in color.Color) (c color.Color) { - return uniform.C +// Convert satisfies the color.Model interface. +func (uniform Uniform) Convert (in color.Color) (c color.Color) { + return color.RGBA(uniform) } -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) { - return uniform.C +// At satisfies the image.Image interface. +func (uniform Uniform) At (x, y int) (c color.Color) { + return color.RGBA(uniform) } -func (uniform *Uniform) AtWhen (x, y, width, height int) (c color.RGBA) { - return uniform.C +// 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) - return color.RGBA64 { R: r, G: g, B: b, A: a } +// 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.C.A == 0xFF +func (uniform Uniform) Opaque () (opaque bool) { + return uniform.A == 0xFF } diff --git a/artist/wrap.go b/artist/wrap.go index 530e53f..5cf0520 100644 --- a/artist/wrap.go +++ b/artist/wrap.go @@ -1 +1,27 @@ 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 +} From b52696025a78e385f0a8e78fa7225f6919fb97fa Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 14 Jan 2023 13:40:05 -0500 Subject: [PATCH 3/9] Implemented StrokeRectangle --- artist/line.go | 2 ++ artist/rectangle.go | 41 +++++++++++++++++++++++++++++++++++++++++ artist/text.go | 3 ++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/artist/line.go b/artist/line.go index 51458bb..03ee97f 100644 --- a/artist/line.go +++ b/artist/line.go @@ -3,6 +3,8 @@ 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 Pattern, diff --git a/artist/rectangle.go b/artist/rectangle.go index 4e15469..36da6bb 100644 --- a/artist/rectangle.go +++ b/artist/rectangle.go @@ -50,3 +50,44 @@ func FillRectangle ( }} return } + + +// StrokeRectangle draws the outline of a rectangle with the specified line +// weight and pattern. +func StrokeRectangle ( + destination tomo.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, + insetBounds.Max.X, insetBounds.Min.Y)) + + // bottom + FillRectangle (destination, source, image.Rect ( + bounds.Min.X, insetBounds.Max.Y, + insetBounds.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 823db44..59bd4e6 100644 --- a/artist/text.go +++ b/artist/text.go @@ -107,7 +107,8 @@ func (drawer *TextDrawer) Draw ( } if !drawer.layoutClean { drawer.recalculate() } - // TODO: reimplement a version of draw mask that takes in a pattern + // 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, From 89eb88c982f2f9a30a684366204359002849cced Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 14 Jan 2023 13:57:12 -0500 Subject: [PATCH 4/9] Draw border around mouse test --- elements/testing/mouse.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 6c4a90f..350b01c 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -37,7 +37,10 @@ func (element *Mouse) Handle (event tomo.Event) { element.core, theme.AccentImage(), element.Bounds()) - // TODO: draw a stroked rectangle around the edges + artist.StrokeRectangle ( + element.core, + artist.NewUniform(color.Black), 5, + element.Bounds()) artist.Line ( element.core, artist.NewUniform(color.White), 1, image.Pt(1, 1), From d75ec12a3c4dd0cb69f4c4a4338bc7a53ae875d7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 14 Jan 2023 13:59:07 -0500 Subject: [PATCH 5/9] Fixed bug with StrokeRectangle where some areas were blank --- artist/rectangle.go | 4 ++-- elements/testing/mouse.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/artist/rectangle.go b/artist/rectangle.go index 36da6bb..6534f8b 100644 --- a/artist/rectangle.go +++ b/artist/rectangle.go @@ -70,12 +70,12 @@ func StrokeRectangle ( // top FillRectangle (destination, source, image.Rect ( bounds.Min.X, bounds.Min.Y, - insetBounds.Max.X, insetBounds.Min.Y)) + bounds.Max.X, insetBounds.Min.Y)) // bottom FillRectangle (destination, source, image.Rect ( bounds.Min.X, insetBounds.Max.Y, - insetBounds.Max.X, bounds.Max.Y)) + bounds.Max.X, bounds.Max.Y)) // left FillRectangle (destination, source, image.Rect ( diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 350b01c..c6eabfa 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -39,7 +39,7 @@ func (element *Mouse) Handle (event tomo.Event) { element.Bounds()) artist.StrokeRectangle ( element.core, - artist.NewUniform(color.Black), 5, + artist.NewUniform(color.Black), 1, element.Bounds()) artist.Line ( element.core, artist.NewUniform(color.White), 1, From e740f5385c793825641521a803717b3c2b667dc4 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 14 Jan 2023 14:00:57 -0500 Subject: [PATCH 6/9] Mouse test does not draw lines if mouse is not held down --- elements/testing/mouse.go | 1 + 1 file changed, 1 insertion(+) diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index c6eabfa..07f914a 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -69,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, From 4d609f6fa1aa95591ef4ed502b41c33a62d71cb6 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 14 Jan 2023 14:05:27 -0500 Subject: [PATCH 7/9] Line no longer segfaults --- artist/line.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/artist/line.go b/artist/line.go index 03ee97f..698dfe8 100644 --- a/artist/line.go +++ b/artist/line.go @@ -52,6 +52,7 @@ func lineLow ( width, height int, ) { data, stride := destination.Buffer() + bounds := destination.Bounds() deltaX := max.X - min.X deltaY := max.Y - min.Y @@ -66,6 +67,7 @@ func lineLow ( y := min.Y for x := min.X; x < max.X; x ++ { + if !(image.Point { x, y }).In(bounds) { break } data[x + y * stride] = source.AtWhen(x, y, width, height) if D > 0 { y += yi @@ -85,6 +87,7 @@ func lineHigh ( width, height int, ) { data, stride := destination.Buffer() + bounds := destination.Bounds() deltaX := max.X - min.X deltaY := max.Y - min.Y @@ -99,6 +102,7 @@ func lineHigh ( x := min.X for y := min.Y; y < max.Y; y ++ { + if !(image.Point { x, y }).In(bounds) { break } data[x + y * stride] = source.AtWhen(x, y, width, height) if D > 0 { x += xi From 9540812a04aa01327bac83b7f53284887271f824 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 14 Jan 2023 20:08:55 -0500 Subject: [PATCH 8/9] Implemented a multiborder pattern --- artist/multiborder.go | 51 +++++++++++++++++++++++++++++++++++++++ elements/testing/mouse.go | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 artist/multiborder.go diff --git a/artist/multiborder.go b/artist/multiborder.go new file mode 100644 index 0000000..6119a88 --- /dev/null +++ b/artist/multiborder.go @@ -0,0 +1,51 @@ +package artist + +import "image" +import "image/color" + +type Border struct { + Weight int + Stroke Pattern + bounds image.Rectangle + dx, dy int +} + +type MultiBorder struct { + borders []Border + lastWidth, lastHeight int + maxBorder int +} + +func NewMultiBorder (borders ...Border) (multi *MultiBorder) { + return &MultiBorder { borders: borders } +} + +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/elements/testing/mouse.go b/elements/testing/mouse.go index 07f914a..08680a5 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -42,7 +42,7 @@ func (element *Mouse) Handle (event tomo.Event) { 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 ( From e83dde2d21c3a72444b82202f2554b99551cac81 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 14 Jan 2023 21:01:00 -0500 Subject: [PATCH 9/9] Replaced the chiseled box with the chiseled pattern --- artist/chisel.go | 130 ++++---------------- artist/multiborder.go | 7 ++ elements/basic/button.go | 14 +-- elements/basic/container.go | 2 +- elements/basic/label.go | 4 +- elements/fun/clock.go | 12 +- elements/testing/mouse.go | 2 +- theme/theme.go | 228 +++++++++++------------------------- 8 files changed, 115 insertions(+), 284 deletions(-) diff --git a/artist/chisel.go b/artist/chisel.go index 873046b..b172ddb 100644 --- a/artist/chisel.go +++ b/artist/chisel.go @@ -1,116 +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 Pattern - Shadow Pattern - Stroke Pattern - Fill Pattern - 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 - - data, stride := destination.Buffer() - 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() - - 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.AtWhen ( - xx - strokeWeight - shadingWeight, - yy - strokeWeight - shadingWeight, - fillBounds.Dx(), fillBounds.Dy()) - - 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) - } - - shadingSource := shadow - if highlighted { - shadingSource = highlight - } - pixel = shadingSource.AtWhen ( - xx - strokeWeight, - yy - strokeWeight, - shadingBounds.Dx(), - shadingBounds.Dy()) - default: - pixel = stroke.AtWhen ( - xx, yy, bounds.Dx(), bounds.Dy()) - } - data[x + y * stride] = 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/multiborder.go b/artist/multiborder.go index 6119a88..da5f42d 100644 --- a/artist/multiborder.go +++ b/artist/multiborder.go @@ -3,6 +3,7 @@ package artist import "image" import "image/color" +// Border represents a border that can be fed to MultiBorder. type Border struct { Weight int Stroke Pattern @@ -10,16 +11,22 @@ type Border struct { 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) 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 886f98c..d3eb74b 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -320,7 +320,7 @@ func (element *Container) draw () { artist.FillRectangle ( element.core, - theme.BackgroundImage(), + theme.BackgroundPattern(), bounds) for _, entry := range element.children { diff --git a/elements/basic/label.go b/elements/basic/label.go index 6304afd..d80f781 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -95,12 +95,12 @@ func (element *Label) draw () { artist.FillRectangle ( element.core, - theme.BackgroundImage(), + 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/fun/clock.go b/elements/fun/clock.go index 6a97314..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,13 +60,13 @@ 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) } diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 08680a5..2c3bfa6 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -35,7 +35,7 @@ func (element *Mouse) Handle (event tomo.Event) { resizeEvent.Height) artist.FillRectangle ( element.core, - theme.AccentImage(), + theme.AccentPattern(), element.Bounds()) artist.StrokeRectangle ( element.core, diff --git a/theme/theme.go b/theme/theme.go index 6ee9921..1305ac3 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -9,179 +9,93 @@ 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 () artist.Pattern { - return backgroundImage -} - -// RaisedImage returns the texture/color used for the fill of RaisedProfile. -func RaisedImage () artist.Pattern { - return raisedImage -} - -// InputImage returns the texture/color used for the fill of InputProfile. -func InputImage () artist.Pattern { - return inputImage -} - -// ForegroundImage returns the texture/color text and monochromatic icons should -// be drawn with. -func ForegroundImage () artist.Pattern { - return foregroundImage -} - -// DisabledForegroundImage returns the texture/color text and monochromatic -// icons should be drawn with if they are disabled. -func DisabledForegroundImage () artist.Pattern { - return disabledForegroundImage -} - -// AccentImage returns the accent texture/color. -func AccentImage () artist.Pattern { - 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 {