Merge pull request 'raw-buffer-api' (#1) from raw-buffer-api into main

Reviewed-on: sashakoshka/tomo#1
This commit is contained in:
Sasha Koshka 2023-01-15 02:04:35 +00:00
commit 972f4d3af7
20 changed files with 474 additions and 578 deletions

View File

@ -1,2 +0,0 @@
package artist

View File

@ -1,127 +1,30 @@
package artist package artist
import "image"
import "image/color" import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
// ShadingProfile contains shading information that can be used to draw chiseled // Chiseled is a pattern that has a highlight section and a shadow section.
// objects. type Chiseled struct {
type ShadingProfile struct { Highlight Pattern
Highlight tomo.Image Shadow Pattern
Shadow tomo.Image
Stroke tomo.Image
Fill tomo.Image
StrokeWeight int
ShadingWeight int
} }
// Engraved reverses the shadown and highlight colors of the ShadingProfile to // AtWhen satisfies the Pattern interface.
// produce a new ShadingProfile with an engraved appearance. func (chiseled Chiseled) AtWhen (x, y, width, height int) (c color.RGBA) {
func (profile ShadingProfile) Engraved () (reversed ShadingProfile) { var highlighted bool
reversed = profile // FIXME: this doesn't work quite right, the
reversed.Highlight = profile.Shadow // slope of the line is somewhat off.
reversed.Shadow = profile.Highlight bottomCorner :=
return float64(x) < float64(y) *
} (float64(width) / float64(height))
if bottomCorner {
// ChiseledRectangle draws a rectangle with a chiseled/embossed appearance, highlighted = float64(x) < float64(height) - float64(y)
// according to the ShadingProfile passed to it. } else {
func ChiseledRectangle ( highlighted = float64(width) - float64(x) > float64(y)
destination tomo.Canvas, }
profile ShadingProfile,
bounds image.Rectangle, if highlighted {
) ( return chiseled.Highlight.AtWhen(x, y, width, height)
updatedRegion image.Rectangle, } else {
) { return chiseled.Shadow.AtWhen(x, y, width, height)
// 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 ++
} }
return
} }

View File

@ -3,9 +3,11 @@ package artist
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
// Line draws a line from one point to another with the specified weight and
// pattern.
func Line ( func Line (
destination tomo.Canvas, destination tomo.Canvas,
source tomo.Image, source Pattern,
weight int, weight int,
min image.Point, min image.Point,
max image.Point, max image.Point,
@ -17,6 +19,8 @@ func Line (
updatedRegion = image.Rectangle { Min: min, Max: max }.Canon() updatedRegion = image.Rectangle { Min: min, Max: max }.Canon()
updatedRegion.Max.X ++ updatedRegion.Max.X ++
updatedRegion.Max.Y ++ updatedRegion.Max.Y ++
width := updatedRegion.Dx()
height := updatedRegion.Dy()
if abs(max.Y - min.Y) < if abs(max.Y - min.Y) <
abs(max.X - min.X) { abs(max.X - min.X) {
@ -26,7 +30,7 @@ func Line (
min = max min = max
max = temp max = temp
} }
lineLow(destination, source, weight, min, max) lineLow(destination, source, weight, min, max, width, height)
} else { } else {
if max.Y < min.Y { if max.Y < min.Y {
@ -34,18 +38,22 @@ func Line (
min = max min = max
max = temp max = temp
} }
lineHigh(destination, source, weight, min, max) lineHigh(destination, source, weight, min, max, width, height)
} }
return return
} }
func lineLow ( func lineLow (
destination tomo.Canvas, destination tomo.Canvas,
source tomo.Image, source Pattern,
weight int, weight int,
min image.Point, min image.Point,
max image.Point, max image.Point,
width, height int,
) { ) {
data, stride := destination.Buffer()
bounds := destination.Bounds()
deltaX := max.X - min.X deltaX := max.X - min.X
deltaY := max.Y - min.Y deltaY := max.Y - min.Y
yi := 1 yi := 1
@ -59,7 +67,8 @@ func lineLow (
y := min.Y y := min.Y
for x := min.X; x < max.X; x ++ { 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 { if D > 0 {
y += yi y += yi
D += 2 * (deltaY - deltaX) D += 2 * (deltaY - deltaX)
@ -71,11 +80,15 @@ func lineLow (
func lineHigh ( func lineHigh (
destination tomo.Canvas, destination tomo.Canvas,
source tomo.Image, source Pattern,
weight int, weight int,
min image.Point, min image.Point,
max image.Point, max image.Point,
width, height int,
) { ) {
data, stride := destination.Buffer()
bounds := destination.Bounds()
deltaX := max.X - min.X deltaX := max.X - min.X
deltaY := max.Y - min.Y deltaY := max.Y - min.Y
xi := 1 xi := 1
@ -89,7 +102,8 @@ func lineHigh (
x := min.X x := min.X
for y := min.Y; y < max.Y; y ++ { 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 { if D > 0 {
x += xi x += xi
D += 2 * (deltaX - deltaY) D += 2 * (deltaX - deltaY)

58
artist/multiborder.go Normal file
View File

@ -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 }
}
}

12
artist/pattern.go Normal file
View File

@ -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)
}

View File

@ -1,105 +1,93 @@
package artist package artist
import "image" import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo" 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 ( func Paste (
destination tomo.Canvas, destination tomo.Canvas,
source tomo.Image, source tomo.Canvas,
offset image.Point, offset image.Point,
) ( ) (
updatedRegion image.Rectangle, 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) updatedRegion = sourceBounds.Add(offset)
for y := sourceBounds.Min.Y; y < sourceBounds.Max.Y; y ++ { for y := sourceBounds.Min.Y; y < sourceBounds.Max.Y; y ++ {
for x := sourceBounds.Min.X; x < sourceBounds.Max.X; x ++ { for x := sourceBounds.Min.X; x < sourceBounds.Max.X; x ++ {
destination.SetRGBA ( dstData[x + offset.X + (y + offset.Y) * dstStride] =
x + offset.X, y + offset.Y, srcData[x + y * srcStride]
source.RGBAAt(x, y))
}} }}
return return
} }
// Rectangle draws a rectangle with an inset border. If the border image is nil, // FillRectangle draws a filled rectangle with the specified pattern.
// no border will be drawn. Likewise, if the fill image is nil, the rectangle func FillRectangle (
// will have no fill.
func Rectangle (
destination tomo.Canvas, destination tomo.Canvas,
fill tomo.Image, source Pattern,
stroke tomo.Image,
weight int,
bounds image.Rectangle, bounds image.Rectangle,
) ( ) (
updatedRegion 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 updatedRegion = bounds
fillBounds := bounds width, height := bounds.Dx(), bounds.Dy()
fillBounds.Min = fillBounds.Min.Add(image.Point { weight, weight }) for y := 0; y < height; y ++ {
fillBounds.Max = fillBounds.Max.Sub(image.Point { weight, weight }) for x := 0; x < width; x ++ {
fillBounds = fillBounds.Canon() data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] =
source.AtWhen(x, y, width, height)
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)
}} }}
return 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 // StrokeRectangle draws the outline of a rectangle with the specified line
// the fill. // weight and pattern.
func OffsetRectangle ( func StrokeRectangle (
destination tomo.Canvas, destination tomo.Canvas,
fill tomo.Image, source Pattern,
stroke tomo.Image,
weight int, weight int,
bounds image.Rectangle, bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) { ) {
bounds = bounds.Canon() bounds = bounds.Canon()
updatedRegion = bounds insetBounds := bounds.Inset(weight)
if insetBounds.Empty() {
fillBounds := bounds FillRectangle(destination, source, bounds)
fillBounds.Min = fillBounds.Min.Add(image.Point { weight, weight }) return
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 // 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

View File

@ -95,12 +95,20 @@ func (drawer *TextDrawer) SetAlignment (align Align) {
// Draw draws the drawer's text onto the specified canvas at the given offset. // Draw draws the drawer's text onto the specified canvas at the given offset.
func (drawer *TextDrawer) Draw ( func (drawer *TextDrawer) Draw (
destination tomo.Canvas, destination tomo.Canvas,
source tomo.Image, source Pattern,
offset image.Point, offset image.Point,
) ( ) (
updatedRegion image.Rectangle, updatedRegion image.Rectangle,
) { ) {
wrappedSource := WrappedPattern {
Pattern: source,
Width: 0,
Height: 0, // TODO: choose a better width and height
}
if !drawer.layoutClean { drawer.recalculate() } 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 _, word := range drawer.layout {
for _, character := range word.text { for _, character := range word.text {
destinationRectangle, destinationRectangle,
@ -117,7 +125,7 @@ func (drawer *TextDrawer) Draw (
draw.DrawMask ( draw.DrawMask (
destination, destination,
destinationRectangle, destinationRectangle,
source, image.Point { }, wrappedSource, image.Point { },
mask, maskPoint, mask, maskPoint,
draw.Over) draw.Over)

43
artist/texture.go Normal file
View File

@ -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]
}

View File

@ -3,69 +3,53 @@ package artist
import "image" import "image"
import "image/color" import "image/color"
// Uniform is an infinite-sized Image of uniform color. It implements the // Uniform is an infinite-sized pattern of uniform color. It implements the
// color.Color, color.Model, and tomo.Image interfaces. // Pattern, color.Color, color.Model, and image.Image interfaces.
type Uniform struct { type Uniform color.RGBA
C color.RGBA
}
// NewUniform returns a new Uniform image of the given color. // NewUniform returns a new Uniform image of the given color.
func NewUniform (c color.Color) (uniform *Uniform) { func NewUniform (c color.Color) (uniform Uniform) {
uniform = &Uniform { }
r, g, b, a := c.RGBA() r, g, b, a := c.RGBA()
uniform.C.R = uint8(r >> 8) uniform.R = uint8(r >> 8)
uniform.C.G = uint8(g >> 8) uniform.G = uint8(g >> 8)
uniform.C.B = uint8(b >> 8) uniform.B = uint8(b >> 8)
uniform.C.A = uint8(a >> 8) uniform.A = uint8(a >> 8)
return return
} }
func (uniform *Uniform) RGBA () (r, g, b, a uint32) { // ColorModel satisfies the image.Image interface.
r = uint32(uniform.C.R) << 8 | uint32(uniform.C.R) func (uniform Uniform) ColorModel () (model color.Model) {
g = uint32(uniform.C.G) << 8 | uint32(uniform.C.G) return uniform
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) { // Convert satisfies the color.Model interface.
model = uniform func (uniform Uniform) Convert (in color.Color) (c color.Color) {
return return color.RGBA(uniform)
} }
func (uniform *Uniform) Convert (in color.Color) (out color.Color) { // Bounds satisfies the image.Image interface.
out = uniform.C func (uniform Uniform) Bounds () (rectangle image.Rectangle) {
return
}
func (uniform *Uniform) Bounds () (rectangle image.Rectangle) {
rectangle.Min = image.Point { -1e9, -1e9 } rectangle.Min = image.Point { -1e9, -1e9 }
rectangle.Max = image.Point { 1e9, 1e9 } rectangle.Max = image.Point { 1e9, 1e9 }
return return
} }
func (uniform *Uniform) At (x, y int) (c color.Color) { // At satisfies the image.Image interface.
c = uniform.C func (uniform Uniform) At (x, y int) (c color.Color) {
return return color.RGBA(uniform)
} }
func (uniform *Uniform) RGBAAt (x, y int) (c color.RGBA) { // AtWhen satisfies the Pattern interface.
c = uniform.C func (uniform Uniform) AtWhen (x, y, width, height int) (c color.RGBA) {
return return color.RGBA(uniform)
} }
func (uniform *Uniform) RGBA64At (x, y int) (c color.RGBA64) { // RGBA satisfies the color.Color interface.
r := uint16(uniform.C.R) << 8 | uint16(uniform.C.R) func (uniform Uniform) RGBA () (r, g, b, a uint32) {
g := uint16(uniform.C.G) << 8 | uint16(uniform.C.G) return color.RGBA(uniform).RGBA()
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
} }
// Opaque scans the entire image and reports whether it is fully opaque. // Opaque scans the entire image and reports whether it is fully opaque.
func (uniform *Uniform) Opaque () (opaque bool) { func (uniform Uniform) Opaque () (opaque bool) {
opaque = uniform.C.A == 0xFF return uniform.A == 0xFF
return
} }

View File

@ -1,99 +1,27 @@
package artist package artist
import "git.tebibyte.media/sashakoshka/tomo"
import "image" import "image"
import "image/draw"
import "image/color" import "image/color"
// WrappedImage wraps an image.Image and allows it to satisfy tomo.Image. // WrappedPattern is a pattern that is able to behave like an image.Image.
type WrappedImage struct { Underlying image.Image } type WrappedPattern struct {
Pattern
Width, Height int
}
// WrapImage wraps a generic image.Image and allows it to satisfy tomo.Image. // At satisfies the image.Image interface.
// Do not use this function to wrap images that already satisfy tomo.Image, func (pattern WrappedPattern) At (x, y int) (c color.Color) {
// because the resulting wrapped image will be rather slow in comparison. return pattern.Pattern.AtWhen(x, y, pattern.Width, pattern.Height)
func WrapImage (underlying image.Image) (wrapped tomo.Image) { }
wrapped = WrappedImage { Underlying: underlying }
// 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 return
} }
func (wrapped WrappedImage) Bounds () (bounds image.Rectangle) { // ColorModel satisfies the image.Image interface.
bounds = wrapped.Underlying.Bounds() func (pattern WrappedPattern) ColorModel () (model color.Model) {
return return color.RGBAModel
}
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
} }

View File

@ -187,8 +187,9 @@ func (window *Window) reallocateCanvas () {
} }
func (window *Window) redrawChildEntirely () { func (window *Window) redrawChildEntirely () {
data, stride := window.child.Buffer()
window.xCanvas.For (func (x, y int) (c xgraphics.BGRA) { 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 c.R, c.G, c.B, c.A = rgba.R, rgba.G, rgba.B, rgba.A
return return
}) })
@ -206,13 +207,14 @@ func (window *Window) resizeChildToFit () {
window.redrawChildEntirely() window.redrawChildEntirely()
} }
func (window *Window) childDrawCallback (region tomo.Image) { func (window *Window) childDrawCallback (region tomo.Canvas) {
if window.skipChildDrawCallback { return } if window.skipChildDrawCallback { return }
data, stride := region.Buffer()
bounds := region.Bounds() bounds := region.Bounds()
for x := bounds.Min.X; x < bounds.Max.X; x ++ { for x := bounds.Min.X; x < bounds.Max.X; x ++ {
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { 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 { window.xCanvas.SetBGRA (x, y, xgraphics.BGRA {
R: rgba.R, R: rgba.R,
G: rgba.G, G: rgba.G,

70
canvas.go Normal file
View File

@ -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
}

View File

@ -150,12 +150,12 @@ func (element *Button) SetText (text string) {
func (element *Button) draw () { func (element *Button) draw () {
bounds := element.core.Bounds() bounds := element.core.Bounds()
artist.ChiseledRectangle ( artist.FillRectangle (
element.core, element.core,
theme.RaisedProfile ( theme.ButtonPattern (
element.pressed,
element.enabled, element.enabled,
element.Selected()), element.Selected(),
element.pressed),
bounds) bounds)
innerBounds := bounds innerBounds := bounds
@ -179,10 +179,6 @@ func (element *Button) draw () {
offset = offset.Add(theme.SinkOffsetVector()) offset = offset.Add(theme.SinkOffsetVector())
} }
foreground := theme.ForegroundImage() foreground := theme.ForegroundPattern(element.enabled)
if !element.enabled {
foreground = theme.DisabledForegroundImage()
}
element.drawer.Draw(element.core, foreground, offset) element.drawer.Draw(element.core, foreground, offset)
} }

View File

@ -57,7 +57,7 @@ func (element *Container) Adopt (child tomo.Element, expand bool) {
return return
}, },
Draw: func (region tomo.Image) { Draw: func (region tomo.Canvas) {
element.drawChildRegion(child, region) element.drawChildRegion(child, region)
}, },
}) })
@ -318,10 +318,9 @@ func (element *Container) recalculate () {
func (element *Container) draw () { func (element *Container) draw () {
bounds := element.core.Bounds() bounds := element.core.Bounds()
artist.Rectangle ( artist.FillRectangle (
element.core, element.core,
theme.BackgroundImage(), theme.BackgroundPattern(),
nil, 0,
bounds) bounds)
for _, entry := range element.children { 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 } if element.warping { return }
for _, entry := range element.children { for _, entry := range element.children {
if entry.Element == child { if entry.Element == child {

View File

@ -93,15 +93,14 @@ func (element *Label) updateMinimumSize () {
func (element *Label) draw () { func (element *Label) draw () {
bounds := element.core.Bounds() bounds := element.core.Bounds()
artist.Rectangle ( artist.FillRectangle (
element.core, element.core,
theme.BackgroundImage(), theme.BackgroundPattern(),
nil, 0,
bounds) bounds)
textBounds := element.drawer.LayoutBounds() textBounds := element.drawer.LayoutBounds()
foreground := theme.ForegroundImage() foreground := theme.ForegroundPattern(true)
element.drawer.Draw (element.core, foreground, image.Point { element.drawer.Draw (element.core, foreground, image.Point {
X: 0 - textBounds.Min.X, X: 0 - textBounds.Min.X,
Y: 0 - textBounds.Min.Y, Y: 0 - textBounds.Min.Y,

View File

@ -7,7 +7,7 @@ import "git.tebibyte.media/sashakoshka/tomo"
// Core is a struct that implements some core functionality common to most // Core is a struct that implements some core functionality common to most
// widgets. It is meant to be embedded directly into a struct. // widgets. It is meant to be embedded directly into a struct.
type Core struct { type Core struct {
canvas *image.RGBA canvas tomo.BasicCanvas
parent tomo.Element parent tomo.Element
metrics struct { metrics struct {
@ -32,20 +32,19 @@ func (core Core) ColorModel () (model color.Model) {
} }
func (core Core) At (x, y int) (pixel color.Color) { func (core Core) At (x, y int) (pixel color.Color) {
if core.canvas == nil { return color.RGBA { } } return core.canvas.At(x, y)
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
} }
func (core Core) Bounds () (bounds image.Rectangle) { func (core Core) Bounds () (bounds image.Rectangle) {
if core.canvas != nil { bounds = core.canvas.Bounds() } return core.canvas.Bounds()
return }
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) { 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 // be used as a canvas. It must not be directly embedded into an element, but
// instead kept as a private member. // instead kept as a private member.
type CoreControl struct { type CoreControl struct {
*image.RGBA tomo.BasicCanvas
core *Core core *Core
} }
func (control CoreControl) HasImage () (has bool) { func (control CoreControl) HasImage () (empty bool) {
has = control.RGBA != nil return !control.Bounds().Empty()
return
} }
func (control CoreControl) Select () (granted bool) { func (control CoreControl) Select () (granted bool) {
@ -98,7 +96,7 @@ func (control CoreControl) SetSelectable (selectable bool) {
} }
func (control CoreControl) PushRegion (bounds image.Rectangle) { 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 () { func (control CoreControl) PushAll () {
@ -108,8 +106,8 @@ func (control CoreControl) PushAll () {
func (control *CoreControl) AllocateCanvas (width, height int) { func (control *CoreControl) AllocateCanvas (width, height int) {
core := control.core core := control.core
width, height, _ = control.ConstrainSize(width, height) width, height, _ = control.ConstrainSize(width, height)
core.canvas = image.NewRGBA(image.Rect (0, 0, width, height)) core.canvas = tomo.NewBasicCanvas(width, height)
control.RGBA = core.canvas control.BasicCanvas = core.canvas
} }
func (control CoreControl) SetMinimumSize (width, height int) { 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 // if there is an image buffer, and the current size is less
// than this new minimum size, send core.parent a resize event. // than this new minimum size, send core.parent a resize event.
if control.HasImage() { bounds := control.Bounds()
bounds := control.Bounds() imageWidth,
imageWidth, imageHeight,
imageHeight, constrained := control.ConstrainSize (
constrained := control.ConstrainSize ( bounds.Dx(),
bounds.Dx(), bounds.Dy())
bounds.Dy()) if constrained {
if constrained { core.parent.Handle (tomo.EventResize {
core.parent.Handle (tomo.EventResize { Width: imageWidth,
Width: imageWidth, Height: imageHeight,
Height: imageHeight, })
})
}
} }
} }

View File

@ -44,14 +44,14 @@ func (element *AnalogClock) SetTime (newTime time.Time) {
func (element *AnalogClock) draw () { func (element *AnalogClock) draw () {
bounds := element.core.Bounds() bounds := element.core.Bounds()
artist.ChiseledRectangle ( artist.FillRectangle (
element.core, element.core,
theme.BackgroundProfile(true), theme.SunkenPattern(),
bounds) bounds)
for hour := 0; hour < 12; hour ++ { for hour := 0; hour < 12; hour ++ {
element.radialLine ( element.radialLine (
theme.ForegroundImage(), theme.ForegroundPattern(true),
0.8, 0.9, float64(hour) / 6 * math.Pi) 0.8, 0.9, float64(hour) / 6 * math.Pi)
} }
@ -60,18 +60,18 @@ func (element *AnalogClock) draw () {
hour := float64(element.time.Hour()) + minute / 60 hour := float64(element.time.Hour()) + minute / 60
element.radialLine ( element.radialLine (
theme.ForegroundImage(), theme.ForegroundPattern(true),
0, 0.5, (hour - 3) / 6 * math.Pi) 0, 0.5, (hour - 3) / 6 * math.Pi)
element.radialLine ( element.radialLine (
theme.ForegroundImage(), theme.ForegroundPattern(true),
0, 0.7, (minute - 15) / 30 * math.Pi) 0, 0.7, (minute - 15) / 30 * math.Pi)
element.radialLine ( element.radialLine (
theme.AccentImage(), theme.AccentPattern(),
0, 0.7, (second - 15) / 30 * math.Pi) 0, 0.7, (second - 15) / 30 * math.Pi)
} }
func (element *AnalogClock) radialLine ( func (element *AnalogClock) radialLine (
source tomo.Image, source artist.Pattern,
inner float64, inner float64,
outer float64, outer float64,
radian float64, radian float64,

View File

@ -13,7 +13,7 @@ type Mouse struct {
*core.Core *core.Core
core core.CoreControl core core.CoreControl
drawing bool drawing bool
color tomo.Image color artist.Pattern
lastMousePos image.Point lastMousePos image.Point
} }
@ -33,13 +33,16 @@ func (element *Mouse) Handle (event tomo.Event) {
element.core.AllocateCanvas ( element.core.AllocateCanvas (
resizeEvent.Width, resizeEvent.Width,
resizeEvent.Height) resizeEvent.Height)
artist.Rectangle ( artist.FillRectangle (
element.core, element.core,
theme.AccentImage(), theme.AccentPattern(),
artist.NewUniform(color.Black), element.Bounds())
1, element.Bounds()) artist.StrokeRectangle (
element.core,
artist.NewUniform(color.Black), 1,
element.Bounds())
artist.Line ( artist.Line (
element.core, artist.NewUniform(color.White), 1, element.core, artist.NewUniform(color.White), 3,
image.Pt(1, 1), image.Pt(1, 1),
image.Pt(resizeEvent.Width - 2, resizeEvent.Height - 2)) image.Pt(resizeEvent.Width - 2, resizeEvent.Height - 2))
artist.Line ( artist.Line (
@ -66,6 +69,7 @@ func (element *Mouse) Handle (event tomo.Event) {
element.lastMousePos = mousePos element.lastMousePos = mousePos
case tomo.EventMouseMove: case tomo.EventMouseMove:
if !element.drawing { return }
mouseMoveEvent := event.(tomo.EventMouseMove) mouseMoveEvent := event.(tomo.EventMouseMove)
mousePos := image.Pt ( mousePos := image.Pt (
mouseMoveEvent.X, mouseMoveEvent.X,

View File

@ -3,186 +3,99 @@ package theme
import "image" import "image"
import "image/color" import "image/color"
import "golang.org/x/image/font" 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/artist"
import "git.tebibyte.media/sashakoshka/tomo/defaultfont" import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
// none of these colors are final! TODO: generate these values from a theme // none of these colors are final! TODO: generate these values from a theme
// file at startup. // file at startup.
var foregroundImage = artist.NewUniform(color.Gray16 { 0x0000}) func hex (color uint32) (c color.RGBA) {
var disabledForegroundImage = artist.NewUniform(color.Gray16 { 0x5555}) c.A = uint8(color)
var accentImage = artist.NewUniform(color.RGBA { 0x40, 0x80, 0x90, 0xFF}) c.B = uint8(color >> 8)
var highlightImage = artist.NewUniform(color.Gray16 { 0xEEEE }) c.G = uint8(color >> 16)
var shadowImage = artist.NewUniform(color.Gray16 { 0x3333 }) c.R = uint8(color >> 24)
var weakShadeImage = artist.NewUniform(color.Gray16 { 0x7777 }) return
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,
} }
var inputImage = artist.NewUniform(color.Gray16 { 0xFFFF }) var accentPattern = artist.NewUniform(hex(0x408090FF))
var inputProfile = artist.ShadingProfile { var backgroundPattern = artist.NewUniform(color.Gray16 { 0xAAAA })
Highlight: insetShadowImage, var foregroundPattern = artist.NewUniform(color.Gray16 { 0x0000 })
Shadow: inputImage, var weakForegroundPattern = artist.NewUniform(color.Gray16 { 0x4444 })
Stroke: strokeImage, var strokePattern = artist.NewUniform(color.Gray16 { 0x0000 })
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,
}
// BackgroundProfile returns the shading profile to be used for backgrounds. var buttonPattern = artist.NewMultiBorder (
func BackgroundProfile (engraved bool) artist.ShadingProfile { artist.Border { Weight: 1, Stroke: strokePattern },
if engraved { artist.Border {
return engravedBackgroundProfile 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 { } else {
return backgroundProfile return weakForegroundPattern
} }
} }
func ButtonPattern (enabled, selected, pressed bool) (artist.Pattern) {
// RaisedProfile returns the shading profile to be used for raised objects such
// as buttons.
func RaisedProfile (
engraved bool,
enabled bool,
selected bool,
) (
artist.ShadingProfile,
) {
if enabled { if enabled {
if engraved { if pressed {
if selected { return pressedButtonPattern
return selectedEngravedRaisedProfile
} else {
return engravedRaisedProfile
}
} else { } else {
if selected { if selected {
return selectedRaisedProfile return selectedButtonPattern
} else { } else {
return raisedProfile return buttonPattern
} }
} }
} else { } else {
return disabledRaisedProfile return disabledButtonPattern
} }
} }
// InputProfile returns the shading profile to be used for input fields. // TODO: load fonts from an actual source instead of using defaultfont
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
// FontFaceRegular returns the font face to be used for normal text. // FontFaceRegular returns the font face to be used for normal text.
func FontFaceRegular () font.Face { func FontFaceRegular () font.Face {

29
tomo.go
View File

@ -2,25 +2,6 @@ package tomo
import "image" import "image"
import "errors" 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 // ParentHooks is a struct that contains callbacks that let child elements send
// information to their parent element without the child element knowing // information to their parent element without the child element knowing
@ -29,7 +10,7 @@ type Canvas interface {
type ParentHooks struct { type ParentHooks struct {
// Draw is called when a part of the child element's surface is updated. // 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. // 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 // MinimumSizeChange is called when the child element's minimum width
// and/or height changes. When this function is called, the element will // 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. // 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 { if hooks.Draw != nil {
hooks.Draw(region) hooks.Draw(region)
} }
@ -82,10 +63,10 @@ func (hooks ParentHooks) RunSelectabilityChange (selectable bool) {
// Element represents a basic on-screen object. // Element represents a basic on-screen object.
type Element interface { type Element interface {
// Element must implement the Image interface. Elements should start out // Element must implement the Canvas interface. Elements should start
// with a completely blank image buffer, and only set its size and draw // out with a completely blank buffer, and only allocate memory and draw
// on it for the first time when sent an EventResize event. // on it for the first time when sent an EventResize event.
Image Canvas
// Handle handles an event, propagating it to children if necessary. // Handle handles an event, propagating it to children if necessary.
Handle (event Event) Handle (event Event)