Transparency support!!
This commit is contained in:
parent
2f0259d913
commit
94db4e8ead
5
box.go
5
box.go
@ -3,6 +3,7 @@ package x
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/tomo/tomo"
|
||||
import "git.tebibyte.media/tomo/x/canvas"
|
||||
import "git.tebibyte.media/tomo/tomo/data"
|
||||
import "git.tebibyte.media/tomo/tomo/input"
|
||||
import "git.tebibyte.media/tomo/tomo/event"
|
||||
@ -23,7 +24,7 @@ type box struct {
|
||||
padding tomo.Inset
|
||||
border []tomo.Border
|
||||
color color.Color
|
||||
texture *texture
|
||||
texture *xcanvas.Texture
|
||||
|
||||
fillTransparent bool
|
||||
|
||||
@ -122,7 +123,7 @@ func (this *box) SetColor (c color.Color) {
|
||||
}
|
||||
|
||||
func (this *box) SetTexture (texture tomo.Texture) {
|
||||
this.texture = assertTexture(texture)
|
||||
this.texture = xcanvas.AssertTexture(texture)
|
||||
this.determineFillTransparency()
|
||||
this.invalidateDraw()
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import "image"
|
||||
import "image/color"
|
||||
import "github.com/jezek/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "git.tebibyte.media/tomo/tomo"
|
||||
import "github.com/jezek/xgbutil/xgraphics"
|
||||
import "git.tebibyte.media/tomo/tomo/canvas"
|
||||
|
||||
@ -73,23 +74,34 @@ type pen struct {
|
||||
align canvas.StrokeAlign
|
||||
stroke xgraphics.BGRA
|
||||
fill xgraphics.BGRA
|
||||
texture *Texture
|
||||
}
|
||||
|
||||
func (this *pen) Rectangle (bounds image.Rectangle) {
|
||||
if this.weight == 0 {
|
||||
this.gfx.fillRectangle(bounds)
|
||||
if this.fill.A > 0 {
|
||||
this.fillRectangle(this.fill, bounds)
|
||||
}
|
||||
} else {
|
||||
this.gfx.strokeRectangle(bounds)
|
||||
if this.stroke.A > 0 {
|
||||
this.strokeRectangle(this.stroke, bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *pen) Path (points ...image.Point) {
|
||||
if this.weight == 0 {
|
||||
this.fillPolygon(points...)
|
||||
if this.fill.A > 0 {
|
||||
this.fillPolygon(this.fill, points...)
|
||||
}
|
||||
} else if this.closed {
|
||||
this.strokePolygon(points...)
|
||||
if this.stroke.A > 0 {
|
||||
this.strokePolygon(this.stroke, points...)
|
||||
}
|
||||
} else {
|
||||
this.polyLine(points...)
|
||||
if this.stroke.A > 0 {
|
||||
this.polyLine(this.stroke, points...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,6 +113,7 @@ func (this *pen) StrokeAlign (align canvas.StrokeAlign) { this.align = align
|
||||
|
||||
func (this *pen) Stroke (stroke color.Color) { this.stroke = convertColor(stroke) }
|
||||
func (this *pen) Fill (fill color.Color) { this.fill = convertColor(fill) }
|
||||
func (this *pen) Texture (texture tomo.Texture) { this.texture = AssertTexture(texture) }
|
||||
|
||||
func convertColor (c color.Color) xgraphics.BGRA {
|
||||
r, g, b, a := c.RGBA()
|
||||
|
226
canvas/draw.go
226
canvas/draw.go
@ -1,5 +1,229 @@
|
||||
package xcanvas
|
||||
|
||||
func (this *pen) fillRectangle (bounds image.Rectangle) {
|
||||
import "sort"
|
||||
import "image"
|
||||
import "github.com/jezek/xgbutil/xgraphics"
|
||||
|
||||
func (this *pen) fillRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
|
||||
if c.A == 255 {
|
||||
this.fillRectangleOpaque(c, bounds)
|
||||
} else {
|
||||
this.fillRectangleTransparent(c, bounds)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *pen) fillRectangleOpaque (c xgraphics.BGRA, bounds image.Rectangle) {
|
||||
bounds = bounds.Intersect(this.image.Bounds())
|
||||
var pos image.Point
|
||||
|
||||
for pos.Y = bounds.Min.Y; pos.Y < bounds.Max.Y; pos.Y ++ {
|
||||
for pos.X = bounds.Min.X; pos.X < bounds.Max.X; pos.X ++ {
|
||||
index := this.image.PixOffset(pos.X, pos.Y)
|
||||
this.image.Pix[index + 0] = c.B
|
||||
this.image.Pix[index + 1] = c.G
|
||||
this.image.Pix[index + 2] = c.R
|
||||
this.image.Pix[index + 3] = c.A
|
||||
}}
|
||||
}
|
||||
|
||||
func (this *pen) fillRectangleTransparent (c xgraphics.BGRA, bounds image.Rectangle) {
|
||||
bounds = bounds.Intersect(this.image.Bounds())
|
||||
var pos image.Point
|
||||
|
||||
for pos.Y = bounds.Min.Y; pos.Y < bounds.Max.Y; pos.Y ++ {
|
||||
for pos.X = bounds.Min.X; pos.X < bounds.Max.X; pos.X ++ {
|
||||
index := this.image.PixOffset(pos.X, pos.Y)
|
||||
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
|
||||
B: this.image.Pix[index + 0],
|
||||
G: this.image.Pix[index + 1],
|
||||
R: this.image.Pix[index + 2],
|
||||
A: this.image.Pix[index + 3],
|
||||
}, c)
|
||||
this.image.Pix[index + 0] = pixel.B
|
||||
this.image.Pix[index + 1] = pixel.G
|
||||
this.image.Pix[index + 2] = pixel.R
|
||||
this.image.Pix[index + 3] = pixel.A
|
||||
}}
|
||||
}
|
||||
|
||||
func (this *pen) strokeRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
|
||||
if this.weight > bounds.Dx() / 2 || this.weight > bounds.Dy() / 2 {
|
||||
this.fillRectangle(c, bounds)
|
||||
return
|
||||
}
|
||||
|
||||
top := image.Rect (
|
||||
bounds.Min.X,
|
||||
bounds.Min.Y,
|
||||
bounds.Max.X,
|
||||
bounds.Min.Y + this.weight)
|
||||
bottom := image.Rect (
|
||||
bounds.Min.X,
|
||||
bounds.Max.Y - this.weight,
|
||||
bounds.Max.X,
|
||||
bounds.Max.Y)
|
||||
left := image.Rect (
|
||||
bounds.Min.X,
|
||||
bounds.Min.Y + this.weight,
|
||||
bounds.Min.X + this.weight,
|
||||
bounds.Max.Y - this.weight)
|
||||
right := image.Rect (
|
||||
bounds.Max.X - this.weight,
|
||||
bounds.Min.Y + this.weight,
|
||||
bounds.Max.X,
|
||||
bounds.Max.Y - this.weight)
|
||||
|
||||
this.fillRectangle(c, top,)
|
||||
this.fillRectangle(c, bottom,)
|
||||
this.fillRectangle(c, left,)
|
||||
this.fillRectangle(c, right,)
|
||||
}
|
||||
|
||||
// the polygon filling algorithm is adapted from:
|
||||
// https://www.alienryderflex.com/polygon_fill/
|
||||
// (if you write C like that i will disassemble you)
|
||||
|
||||
func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) {
|
||||
if len(points) < 3 { return }
|
||||
|
||||
// figure out the bounds of the polygon so we don't test empty space
|
||||
var area image.Rectangle
|
||||
area.Min = points[0]
|
||||
area.Max = points[0]
|
||||
for _, point := range points[1:] {
|
||||
if point.X < area.Min.X { area.Min.X = point.X }
|
||||
if point.Y < area.Min.Y { area.Min.Y = point.Y }
|
||||
if point.X > area.Max.X { area.Max.X = point.X }
|
||||
if point.Y > area.Max.Y { area.Max.Y = point.Y }
|
||||
}
|
||||
area = this.image.Bounds().Intersect(area)
|
||||
if area.Empty() { return }
|
||||
|
||||
context := fillingContext {
|
||||
image: this.image,
|
||||
color: this.fill,
|
||||
min: area.Min.X,
|
||||
max: area.Max.X,
|
||||
boundaries: make([]int, len(points)),
|
||||
points: points,
|
||||
}
|
||||
|
||||
for context.y = area.Min.Y; context.y < area.Max.Y; context.y ++ {
|
||||
// build boundary list
|
||||
boundaryCount := 0
|
||||
prevPoint := points[len(points) - 1]
|
||||
for _, point := range points {
|
||||
fy := float64(context.y)
|
||||
fPointX := float64(point.X)
|
||||
fPointY := float64(point.Y)
|
||||
fPrevX := float64(prevPoint.X)
|
||||
fPrevY := float64(prevPoint.Y)
|
||||
addboundary :=
|
||||
(fPointY < fy && fPrevY >= fy) ||
|
||||
(fPrevY < fy && fPointY >= fy)
|
||||
if addboundary {
|
||||
context.boundaries[boundaryCount] = int (
|
||||
fPointX +
|
||||
(fy - fPointY) /
|
||||
(fPrevY - fPointY) *
|
||||
(fPrevX - fPointX))
|
||||
boundaryCount ++
|
||||
}
|
||||
prevPoint = point
|
||||
}
|
||||
|
||||
// sort boundary list
|
||||
cutBoundaries := context.boundaries[:boundaryCount]
|
||||
sort.Ints(cutBoundaries)
|
||||
|
||||
// fill pixels between boundary pairs
|
||||
if c.A == 255 {
|
||||
context.fillPolygonHotOpaque()
|
||||
} else {
|
||||
context.fillPolygonHotTransparent()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type fillingContext struct {
|
||||
image *xgraphics.Image
|
||||
color xgraphics.BGRA
|
||||
min, max int
|
||||
y int
|
||||
boundaries []int
|
||||
points []image.Point
|
||||
}
|
||||
|
||||
func (context *fillingContext) fillPolygonHotOpaque () {
|
||||
for index := 0; index < len(context.boundaries); index += 2 {
|
||||
left := context.boundaries[index]
|
||||
right := context.boundaries[index + 1]
|
||||
|
||||
// stop if we have exited the polygon
|
||||
if left >= context.max { break }
|
||||
// begin filling if we are within the polygon
|
||||
if right > context.min {
|
||||
// constrain boundaries to image size
|
||||
if left < context.min { left = context.min }
|
||||
if right > context.max { right = context.max }
|
||||
|
||||
// fill pixels in between
|
||||
for x := left; x < right; x ++ {
|
||||
index := context.image.PixOffset(x, context.y)
|
||||
context.image.Pix[index + 0] = context.color.B
|
||||
context.image.Pix[index + 1] = context.color.G
|
||||
context.image.Pix[index + 2] = context.color.R
|
||||
context.image.Pix[index + 3] = context.color.A
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (context *fillingContext) fillPolygonHotTransparent () {
|
||||
for index := 0; index < len(context.boundaries); index += 2 {
|
||||
left := context.boundaries[index]
|
||||
right := context.boundaries[index + 1]
|
||||
|
||||
// stop if we have exited the polygon
|
||||
if left >= context.max { break }
|
||||
// begin filling if we are within the polygon
|
||||
if right > context.min {
|
||||
// constrain boundaries to image size
|
||||
if left < context.min { left = context.min }
|
||||
if right > context.max { right = context.max }
|
||||
|
||||
// fill pixels in between
|
||||
for x := left; x < right; x ++ {
|
||||
index := context.image.PixOffset(x, context.y)
|
||||
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
|
||||
B: context.image.Pix[index + 0],
|
||||
G: context.image.Pix[index + 1],
|
||||
R: context.image.Pix[index + 2],
|
||||
A: context.image.Pix[index + 3],
|
||||
}, context.color)
|
||||
context.image.Pix[index + 0] = pixel.B
|
||||
context.image.Pix[index + 1] = pixel.G
|
||||
context.image.Pix[index + 2] = pixel.R
|
||||
context.image.Pix[index + 3] = pixel.A
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *pen) strokePolygon (c xgraphics.BGRA, points ...image.Point) {
|
||||
prevPoint := points[len(points) - 1]
|
||||
for _, point := range points {
|
||||
this.line(c, prevPoint, point)
|
||||
prevPoint = point
|
||||
}
|
||||
}
|
||||
|
||||
func (this *pen) polyLine (c xgraphics.BGRA, points ...image.Point) {
|
||||
if len(points) < 2 { return }
|
||||
prevPoint := points[0]
|
||||
for _, point := range points[1:] {
|
||||
this.line(c, prevPoint, point)
|
||||
prevPoint = point
|
||||
}
|
||||
}
|
||||
|
94
canvas/line.go
Normal file
94
canvas/line.go
Normal file
@ -0,0 +1,94 @@
|
||||
package xcanvas
|
||||
|
||||
import "image"
|
||||
import "github.com/jezek/xgbutil/xgraphics"
|
||||
|
||||
func (this *pen) line (
|
||||
c xgraphics.BGRA,
|
||||
min image.Point,
|
||||
max image.Point,
|
||||
) {
|
||||
context := linePlottingContext {
|
||||
plottingContext: plottingContext {
|
||||
image: this.image,
|
||||
color: c,
|
||||
weight: this.weight,
|
||||
},
|
||||
min: min,
|
||||
max: max,
|
||||
}
|
||||
|
||||
if abs(max.Y - min.Y) < abs(max.X - min.X) {
|
||||
if max.X < min.X { context.swap() }
|
||||
context.lineLow()
|
||||
|
||||
} else {
|
||||
if max.Y < min.Y { context.swap() }
|
||||
context.lineHigh()
|
||||
}
|
||||
}
|
||||
|
||||
type linePlottingContext struct {
|
||||
plottingContext
|
||||
min image.Point
|
||||
max image.Point
|
||||
}
|
||||
|
||||
func (context *linePlottingContext) swap () {
|
||||
temp := context.max
|
||||
context.max = context.min
|
||||
context.min = temp
|
||||
}
|
||||
|
||||
func (context linePlottingContext) lineLow () {
|
||||
deltaX := context.max.X - context.min.X
|
||||
deltaY := context.max.Y - context.min.Y
|
||||
yi := 1
|
||||
|
||||
if deltaY < 0 {
|
||||
yi = -1
|
||||
deltaY *= -1
|
||||
}
|
||||
|
||||
D := (2 * deltaY) - deltaX
|
||||
point := context.min
|
||||
|
||||
for ; point.X < context.max.X; point.X ++ {
|
||||
context.plot(point)
|
||||
if D > 0 {
|
||||
D += 2 * (deltaY - deltaX)
|
||||
point.Y += yi
|
||||
} else {
|
||||
D += 2 * deltaY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (context linePlottingContext) lineHigh () {
|
||||
deltaX := context.max.X - context.min.X
|
||||
deltaY := context.max.Y - context.min.Y
|
||||
xi := 1
|
||||
|
||||
if deltaX < 0 {
|
||||
xi = -1
|
||||
deltaX *= -1
|
||||
}
|
||||
|
||||
D := (2 * deltaX) - deltaY
|
||||
point := context.min
|
||||
|
||||
for ; point.Y < context.max.Y; point.Y ++ {
|
||||
context.plot(point)
|
||||
if D > 0 {
|
||||
point.X += xi
|
||||
D += 2 * (deltaX - deltaY)
|
||||
} else {
|
||||
D += 2 * deltaX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func abs (n int) int {
|
||||
if n < 0 { n *= -1}
|
||||
return n
|
||||
}
|
47
canvas/plot.go
Normal file
47
canvas/plot.go
Normal file
@ -0,0 +1,47 @@
|
||||
package xcanvas
|
||||
|
||||
import "image"
|
||||
import "github.com/jezek/xgbutil/xgraphics"
|
||||
|
||||
type plottingContext struct {
|
||||
image *xgraphics.Image
|
||||
color xgraphics.BGRA
|
||||
weight int
|
||||
}
|
||||
|
||||
func (context plottingContext) square (center image.Point) (square image.Rectangle) {
|
||||
return image.Rect(0, 0, context.weight, context.weight).
|
||||
Sub(image.Pt(context.weight / 2, context.weight / 2)).
|
||||
Add(center).
|
||||
Intersect(context.image.Bounds())
|
||||
}
|
||||
|
||||
func (context plottingContext) plot (center image.Point) {
|
||||
square := context.square(center)
|
||||
|
||||
if context.color.A == 255 {
|
||||
for y := square.Min.Y; y < square.Max.Y; y ++ {
|
||||
for x := square.Min.X; x < square.Max.X; x ++ {
|
||||
index := context.image.PixOffset(x, y)
|
||||
context.image.Pix[index + 0] = context.color.B
|
||||
context.image.Pix[index + 1] = context.color.G
|
||||
context.image.Pix[index + 2] = context.color.R
|
||||
context.image.Pix[index + 3] = context.color.A
|
||||
}}
|
||||
} else {
|
||||
for y := square.Min.Y; y < square.Max.Y; y ++ {
|
||||
for x := square.Min.X; x < square.Max.X; x ++ {
|
||||
index := context.image.PixOffset(x, y)
|
||||
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
|
||||
B: context.image.Pix[index + 0],
|
||||
G: context.image.Pix[index + 1],
|
||||
R: context.image.Pix[index + 2],
|
||||
A: context.image.Pix[index + 3],
|
||||
}, context.color)
|
||||
context.image.Pix[index + 0] = pixel.B
|
||||
context.image.Pix[index + 1] = pixel.G
|
||||
context.image.Pix[index + 2] = pixel.R
|
||||
context.image.Pix[index + 3] = pixel.A
|
||||
}}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package xcanvas
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/tomo/tomo"
|
||||
|
||||
// Texture is a read-only image texture that can be quickly written to a canvas.
|
||||
// It must be closed manually after use.
|
||||
@ -52,9 +53,17 @@ func (this *Texture) Close () error {
|
||||
}
|
||||
|
||||
// Clip returns a subset of this texture that points to the same data.
|
||||
func (this *Texture) Clip (bounds image.Rectangle) *Texture {
|
||||
func (this *Texture) Clip (bounds image.Rectangle) tomo.Texture {
|
||||
clipped := *this
|
||||
clipped.rect = bounds
|
||||
return &clipped
|
||||
}
|
||||
|
||||
// AssertTexture checks if a given tomo.Texture is a texture from this package.
|
||||
func AssertTexture (unknown tomo.Texture) *Texture {
|
||||
if tx, ok := unknown.(*Texture); ok {
|
||||
return tx
|
||||
} else {
|
||||
panic("foregin texture implementation, i did not make this!")
|
||||
}
|
||||
}
|
||||
|
22
texture.go
22
texture.go
@ -4,26 +4,6 @@ import "image"
|
||||
import "git.tebibyte.media/tomo/tomo"
|
||||
import "git.tebibyte.media/tomo/x/canvas"
|
||||
|
||||
type texture struct {
|
||||
*xcanvas.Texture
|
||||
}
|
||||
|
||||
func (backend Backend) NewTexture (source image.Image) tomo.Texture {
|
||||
return texture {
|
||||
Texture: xcanvas.NewTextureFrom(source),
|
||||
}
|
||||
}
|
||||
|
||||
func (this texture) Clip (bounds image.Rectangle) tomo.Texture {
|
||||
return texture {
|
||||
Texture: this.Texture.Clip(bounds),
|
||||
}
|
||||
}
|
||||
|
||||
func assertTexture (unknown tomo.Texture) *texture {
|
||||
if tx, ok := unknown.(*texture); ok {
|
||||
return tx
|
||||
} else {
|
||||
panic("foregin texture implementation, i did not make this!")
|
||||
}
|
||||
return xcanvas.NewTextureFrom(source)
|
||||
}
|
||||
|
Reference in New Issue
Block a user