Initial commit
This commit is contained in:
2
artist/artist.go
Normal file
2
artist/artist.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package artist
|
||||
|
||||
127
artist/chisel.go
Normal file
127
artist/chisel.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// ShadingProfile contains shading information that can be used to draw chiseled
|
||||
// objects.
|
||||
type ShadingProfile struct {
|
||||
Highlight tomo.Image
|
||||
Shadow tomo.Image
|
||||
Stroke tomo.Image
|
||||
Fill tomo.Image
|
||||
StrokeWeight int
|
||||
ShadingWeight int
|
||||
}
|
||||
|
||||
// Engraved reverses the shadown and highlight colors of the ShadingProfile to
|
||||
// produce a new ShadingProfile with an engraved appearance.
|
||||
func (profile ShadingProfile) Engraved () (reversed ShadingProfile) {
|
||||
reversed = profile
|
||||
reversed.Highlight = profile.Shadow
|
||||
reversed.Shadow = profile.Highlight
|
||||
return
|
||||
}
|
||||
|
||||
// ChiseledRectangle draws a rectangle with a chiseled/embossed appearance,
|
||||
// according to the ShadingProfile passed to it.
|
||||
func ChiseledRectangle (
|
||||
destination tomo.Canvas,
|
||||
profile ShadingProfile,
|
||||
bounds image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
// FIXME: this breaks when the bounds are smaller than the border or
|
||||
// shading weight
|
||||
|
||||
stroke := profile.Stroke
|
||||
highlight := profile.Highlight
|
||||
shadow := profile.Shadow
|
||||
fill := profile.Fill
|
||||
strokeWeight := profile.StrokeWeight
|
||||
shadingWeight := profile.ShadingWeight
|
||||
|
||||
bounds = bounds.Canon()
|
||||
updatedRegion = bounds
|
||||
|
||||
strokeWeightVector := image.Point { strokeWeight, strokeWeight }
|
||||
shadingWeightVector := image.Point { shadingWeight, shadingWeight }
|
||||
|
||||
shadingBounds := bounds
|
||||
shadingBounds.Min = shadingBounds.Min.Add(strokeWeightVector)
|
||||
shadingBounds.Max = shadingBounds.Max.Sub(strokeWeightVector)
|
||||
shadingBounds = shadingBounds.Canon()
|
||||
|
||||
fillBounds := shadingBounds
|
||||
fillBounds.Min = fillBounds.Min.Add(shadingWeightVector)
|
||||
fillBounds.Max = fillBounds.Max.Sub(shadingWeightVector)
|
||||
fillBounds = fillBounds.Canon()
|
||||
|
||||
strokeImageMin := stroke.Bounds().Min
|
||||
highlightImageMin := highlight.Bounds().Min
|
||||
shadowImageMin := shadow.Bounds().Min
|
||||
fillImageMin := fill.Bounds().Min
|
||||
|
||||
width := float64(bounds.Dx())
|
||||
height := float64(bounds.Dy())
|
||||
|
||||
yy := 0
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
xx := 0
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
var pixel color.RGBA
|
||||
point := image.Point { x, y }
|
||||
switch {
|
||||
case point.In(fillBounds):
|
||||
pixel = fill.RGBAAt (
|
||||
xx - strokeWeight - shadingWeight +
|
||||
fillImageMin.X,
|
||||
yy - strokeWeight - shadingWeight +
|
||||
fillImageMin.Y)
|
||||
|
||||
case point.In(shadingBounds):
|
||||
var highlighted bool
|
||||
// FIXME: this doesn't work quite right, the
|
||||
// slope of the line is somewhat off.
|
||||
bottomCorner :=
|
||||
float64(xx) < float64(yy) *
|
||||
(width / height)
|
||||
if bottomCorner {
|
||||
highlighted =
|
||||
float64(xx) <
|
||||
height - float64(yy)
|
||||
} else {
|
||||
highlighted =
|
||||
width - float64(xx) >
|
||||
float64(yy)
|
||||
}
|
||||
|
||||
if highlighted {
|
||||
pixel = highlight.RGBAAt (
|
||||
xx - strokeWeight +
|
||||
highlightImageMin.X,
|
||||
yy - strokeWeight +
|
||||
highlightImageMin.Y)
|
||||
} else {
|
||||
pixel = shadow.RGBAAt (
|
||||
xx - strokeWeight +
|
||||
shadowImageMin.X,
|
||||
yy - strokeWeight +
|
||||
shadowImageMin.Y)
|
||||
}
|
||||
|
||||
default:
|
||||
pixel = stroke.RGBAAt (
|
||||
xx + strokeImageMin.X,
|
||||
yy + strokeImageMin.Y)
|
||||
}
|
||||
destination.SetRGBA(x, y, pixel)
|
||||
xx ++
|
||||
}
|
||||
yy ++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
106
artist/line.go
Normal file
106
artist/line.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package artist
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
func Line (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
weight int,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
// TODO: respect weight
|
||||
|
||||
updatedRegion = image.Rectangle { Min: min, Max: max }.Canon()
|
||||
updatedRegion.Max.X ++
|
||||
updatedRegion.Max.Y ++
|
||||
|
||||
if abs(max.Y - min.Y) <
|
||||
abs(max.X - min.X) {
|
||||
|
||||
if max.X < min.X {
|
||||
temp := min
|
||||
min = max
|
||||
max = temp
|
||||
}
|
||||
lineLow(destination, source, weight, min, max)
|
||||
} else {
|
||||
|
||||
if max.Y < min.Y {
|
||||
temp := min
|
||||
min = max
|
||||
max = temp
|
||||
}
|
||||
lineHigh(destination, source, weight, min, max)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lineLow (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
weight int,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
) {
|
||||
deltaX := max.X - min.X
|
||||
deltaY := max.Y - min.Y
|
||||
yi := 1
|
||||
|
||||
if deltaY < 0 {
|
||||
yi = -1
|
||||
deltaY *= -1
|
||||
}
|
||||
|
||||
D := (2 * deltaY) - deltaX
|
||||
y := min.Y
|
||||
|
||||
for x := min.X; x < max.X; x ++ {
|
||||
destination.SetRGBA(x, y, source.RGBAAt(x, y))
|
||||
if D > 0 {
|
||||
y += yi
|
||||
D += 2 * (deltaY - deltaX)
|
||||
} else {
|
||||
D += 2 * deltaY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lineHigh (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
weight int,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
) {
|
||||
deltaX := max.X - min.X
|
||||
deltaY := max.Y - min.Y
|
||||
xi := 1
|
||||
|
||||
if deltaX < 0 {
|
||||
xi = -1
|
||||
deltaX *= -1
|
||||
}
|
||||
|
||||
D := (2 * deltaX) - deltaY
|
||||
x := min.X
|
||||
|
||||
for y := min.Y; y < max.Y; y ++ {
|
||||
destination.SetRGBA(x, y, source.RGBAAt(x, y))
|
||||
if D > 0 {
|
||||
x += xi
|
||||
D += 2 * (deltaX - deltaY)
|
||||
} else {
|
||||
D += 2 * deltaX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func abs (in int) (out int) {
|
||||
if in < 0 { in *= -1}
|
||||
out = in
|
||||
return
|
||||
}
|
||||
105
artist/rectangle.go
Normal file
105
artist/rectangle.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// Paste transfers one image onto another, offset by the specified point.
|
||||
func Paste (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
offset image.Point,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
sourceBounds := source.Bounds().Canon()
|
||||
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))
|
||||
}}
|
||||
|
||||
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 (
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
265
artist/text.go
Normal file
265
artist/text.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package artist
|
||||
|
||||
// import "fmt"
|
||||
import "image"
|
||||
import "unicode"
|
||||
import "image/draw"
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/math/fixed"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
type characterLayout struct {
|
||||
x int
|
||||
character rune
|
||||
}
|
||||
|
||||
type wordLayout struct {
|
||||
position image.Point
|
||||
width int
|
||||
text []characterLayout
|
||||
}
|
||||
|
||||
// Align specifies a text alignment method.
|
||||
type Align int
|
||||
|
||||
const (
|
||||
// AlignLeft aligns the start of each line to the beginning point
|
||||
// of each dot.
|
||||
AlignLeft Align = iota
|
||||
AlignRight
|
||||
AlignCenter
|
||||
AlignJustify
|
||||
)
|
||||
|
||||
// TextDrawer is a struct that is capable of efficient rendering of wrapped
|
||||
// text, and calculating text bounds. It avoids doing redundant work
|
||||
// automatically.
|
||||
type TextDrawer struct {
|
||||
text string
|
||||
runes []rune
|
||||
face font.Face
|
||||
width int
|
||||
height int
|
||||
align Align
|
||||
wrap bool
|
||||
cut bool
|
||||
|
||||
layout []wordLayout
|
||||
layoutClean bool
|
||||
layoutBounds image.Rectangle
|
||||
}
|
||||
|
||||
// SetText sets the text of the text drawer.
|
||||
func (drawer *TextDrawer) SetText (text string) {
|
||||
if drawer.text == text { return }
|
||||
drawer.text = text
|
||||
drawer.runes = []rune(text)
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// SetFace sets the font face of the text drawer.
|
||||
func (drawer *TextDrawer) SetFace (face font.Face) {
|
||||
if drawer.face == face { return }
|
||||
drawer.face = face
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// SetMaxWidth sets a maximum width for the text drawer, and recalculates the
|
||||
// layout if needed. If zero is given, there will be no width limit and the text
|
||||
// will not wrap.
|
||||
func (drawer *TextDrawer) SetMaxWidth (width int) {
|
||||
if drawer.width == width { return }
|
||||
drawer.width = width
|
||||
drawer.wrap = width != 0
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// SetMaxHeight sets a maximum height for the text drawer. Lines that are
|
||||
// entirely below this height will not be drawn, and lines that are on the cusp
|
||||
// of this maximum height will be clipped at the point that they cross it.
|
||||
func (drawer *TextDrawer) SetMaxHeight (height int) {
|
||||
if drawer.height == height { return }
|
||||
drawer.height = height
|
||||
drawer.cut = height != 0
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// SetAlignment specifies how the drawer should align its text. For this to have
|
||||
// an effect, a maximum width must have been set.
|
||||
func (drawer *TextDrawer) SetAlignment (align Align) {
|
||||
if drawer.align == align { return }
|
||||
drawer.align = align
|
||||
drawer.layoutClean = false
|
||||
}
|
||||
|
||||
// Draw draws the drawer's text onto the specified canvas at the given offset.
|
||||
func (drawer *TextDrawer) Draw (
|
||||
destination tomo.Canvas,
|
||||
source tomo.Image,
|
||||
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 }
|
||||
|
||||
// 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)
|
||||
|
||||
updatedRegion = updatedRegion.Union(destinationRectangle)
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
// LayoutBounds returns a semantic bounding box for text to be used to determine
|
||||
// an offset for drawing. If a maximum width or height has been set, those will
|
||||
// be used as the width and height of the bounds respectively. The origin point
|
||||
// (0, 0) of the returned bounds will be equivalent to the baseline at the start
|
||||
// of the first line. As such, the minimum of the bounds will be negative.
|
||||
func (drawer *TextDrawer) LayoutBounds () (bounds image.Rectangle) {
|
||||
if !drawer.layoutClean { drawer.recalculate() }
|
||||
bounds = drawer.layoutBounds
|
||||
return
|
||||
}
|
||||
|
||||
func (drawer *TextDrawer) recalculate () {
|
||||
drawer.layoutClean = true
|
||||
drawer.layout = nil
|
||||
drawer.layoutBounds = image.Rectangle { }
|
||||
if drawer.runes == nil { return }
|
||||
if drawer.face == nil { return }
|
||||
|
||||
metrics := drawer.face.Metrics()
|
||||
dot := fixed.Point26_6 { 0, 0 }
|
||||
index := 0
|
||||
horizontalExtent := 0
|
||||
|
||||
previousCharacter := rune(-1)
|
||||
for index < len(drawer.runes) {
|
||||
word := wordLayout { }
|
||||
word.position.X = dot.X.Round()
|
||||
word.position.Y = dot.Y.Round()
|
||||
|
||||
// process a word
|
||||
currentCharacterX := fixed.Int26_6(0)
|
||||
wordWidth := fixed.Int26_6(0)
|
||||
for index < len(drawer.runes) && !unicode.IsSpace(drawer.runes[index]) {
|
||||
character := drawer.runes[index]
|
||||
_, advance, ok := drawer.face.GlyphBounds(character)
|
||||
index ++
|
||||
if !ok { continue }
|
||||
|
||||
word.text = append(word.text, characterLayout {
|
||||
x: currentCharacterX.Round(),
|
||||
character: character,
|
||||
})
|
||||
|
||||
dot.X += advance
|
||||
wordWidth += advance
|
||||
currentCharacterX += advance
|
||||
if dot.X.Round () > horizontalExtent {
|
||||
horizontalExtent = dot.X.Round()
|
||||
}
|
||||
if previousCharacter >= 0 {
|
||||
dot.X += drawer.face.Kern (
|
||||
previousCharacter,
|
||||
character)
|
||||
}
|
||||
previousCharacter = character
|
||||
}
|
||||
word.width = wordWidth.Round()
|
||||
|
||||
// detect if the word that was just processed goes out of
|
||||
// bounds, and if it does, wrap it
|
||||
if drawer.wrap &&
|
||||
word.width + word.position.X > drawer.width &&
|
||||
word.position.X > 0 {
|
||||
|
||||
word.position.Y += metrics.Height.Round()
|
||||
word.position.X = 0
|
||||
dot.Y += metrics.Height
|
||||
dot.X = wordWidth
|
||||
}
|
||||
|
||||
// add the word to the layout
|
||||
drawer.layout = append(drawer.layout, word)
|
||||
|
||||
// skip over whitespace, going onto a new line if there is a
|
||||
// newline character
|
||||
for index < len(drawer.runes) && unicode.IsSpace(drawer.runes[index]) {
|
||||
character := drawer.runes[index]
|
||||
if character == '\n' {
|
||||
dot.Y += metrics.Height
|
||||
dot.X = 0
|
||||
previousCharacter = character
|
||||
index ++
|
||||
} else {
|
||||
_, advance, ok := drawer.face.GlyphBounds(character)
|
||||
index ++
|
||||
if !ok { continue }
|
||||
|
||||
dot.X += advance
|
||||
if previousCharacter >= 0 {
|
||||
dot.X += drawer.face.Kern (
|
||||
previousCharacter,
|
||||
character)
|
||||
}
|
||||
previousCharacter = character
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a set maximum height, and we have crossed it,
|
||||
// stop processing more words. and remove any words that have
|
||||
// also crossed the line.
|
||||
if
|
||||
drawer.cut &&
|
||||
(dot.Y - metrics.Ascent - metrics.Descent).Round() >
|
||||
drawer.height {
|
||||
|
||||
for
|
||||
index := len(drawer.layout) - 1;
|
||||
index >= 0; index -- {
|
||||
|
||||
if drawer.layout[index].position.Y < dot.Y.Round() {
|
||||
break
|
||||
}
|
||||
drawer.layout = drawer.layout[:index]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if drawer.wrap {
|
||||
drawer.layoutBounds.Max.X = drawer.width
|
||||
} else {
|
||||
drawer.layoutBounds.Max.X = horizontalExtent
|
||||
}
|
||||
|
||||
if drawer.cut {
|
||||
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
|
||||
drawer.layoutBounds.Max.Y = drawer.height - metrics.Ascent.Round()
|
||||
} else {
|
||||
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
|
||||
drawer.layoutBounds.Max.Y = dot.Y.Round() + metrics.Descent.Round()
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// for each line, calculate the bounds as if the words are left aligned,
|
||||
// and then at the end of the process go through each line and re-align
|
||||
// everything. this will make the process far simpler.
|
||||
}
|
||||
71
artist/uniform.go
Normal file
71
artist/uniform.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
|
||||
// Uniform is an infinite-sized Image of uniform color. It implements the
|
||||
// color.Color, color.Model, and tomo.Image interfaces.
|
||||
type Uniform struct {
|
||||
C color.RGBA
|
||||
}
|
||||
|
||||
// NewUniform returns a new Uniform image of the given color.
|
||||
func NewUniform (c color.Color) (uniform *Uniform) {
|
||||
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)
|
||||
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) {
|
||||
model = uniform
|
||||
return
|
||||
}
|
||||
|
||||
func (uniform *Uniform) Convert (in color.Color) (out color.Color) {
|
||||
out = uniform.C
|
||||
return
|
||||
}
|
||||
|
||||
func (uniform *Uniform) Bounds () (rectangle image.Rectangle) {
|
||||
rectangle.Min = image.Point { -1e9, -1e9 }
|
||||
rectangle.Max = image.Point { 1e9, 1e9 }
|
||||
return
|
||||
}
|
||||
|
||||
func (uniform *Uniform) At (x, y int) (c color.Color) {
|
||||
c = uniform.C
|
||||
return
|
||||
}
|
||||
|
||||
func (uniform *Uniform) RGBAAt (x, y int) (c color.RGBA) {
|
||||
c = uniform.C
|
||||
return
|
||||
}
|
||||
|
||||
func (uniform *Uniform) RGBA64At (x, y int) (c color.RGBA64) {
|
||||
r := uint16(uniform.C.R) << 8 | uint16(uniform.C.R)
|
||||
g := uint16(uniform.C.G) << 8 | uint16(uniform.C.G)
|
||||
b := uint16(uniform.C.B) << 8 | uint16(uniform.C.B)
|
||||
a := uint16(uniform.C.A) << 8 | uint16(uniform.C.A)
|
||||
|
||||
c = color.RGBA64 { R: r, G: g, B: b, A: a }
|
||||
return
|
||||
}
|
||||
|
||||
// Opaque scans the entire image and reports whether it is fully opaque.
|
||||
func (uniform *Uniform) Opaque () (opaque bool) {
|
||||
opaque = uniform.C.A == 0xFF
|
||||
return
|
||||
}
|
||||
99
artist/wrap.go
Normal file
99
artist/wrap.go
Normal file
@@ -0,0 +1,99 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user