Transparency support!!

This commit is contained in:
Sasha Koshka 2023-08-23 19:21:28 -04:00
parent 2f0259d913
commit 94db4e8ead
7 changed files with 407 additions and 39 deletions

5
box.go
View File

@ -3,6 +3,7 @@ package x
import "image" import "image"
import "image/color" import "image/color"
import "git.tebibyte.media/tomo/tomo" 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/data"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
@ -23,7 +24,7 @@ type box struct {
padding tomo.Inset padding tomo.Inset
border []tomo.Border border []tomo.Border
color color.Color color color.Color
texture *texture texture *xcanvas.Texture
fillTransparent bool fillTransparent bool
@ -122,7 +123,7 @@ func (this *box) SetColor (c color.Color) {
} }
func (this *box) SetTexture (texture tomo.Texture) { func (this *box) SetTexture (texture tomo.Texture) {
this.texture = assertTexture(texture) this.texture = xcanvas.AssertTexture(texture)
this.determineFillTransparency() this.determineFillTransparency()
this.invalidateDraw() this.invalidateDraw()
} }

View File

@ -4,6 +4,7 @@ import "image"
import "image/color" import "image/color"
import "github.com/jezek/xgbutil" import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto" import "github.com/jezek/xgb/xproto"
import "git.tebibyte.media/tomo/tomo"
import "github.com/jezek/xgbutil/xgraphics" import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
@ -73,23 +74,34 @@ type pen struct {
align canvas.StrokeAlign align canvas.StrokeAlign
stroke xgraphics.BGRA stroke xgraphics.BGRA
fill xgraphics.BGRA fill xgraphics.BGRA
texture *Texture
} }
func (this *pen) Rectangle (bounds image.Rectangle) { func (this *pen) Rectangle (bounds image.Rectangle) {
if this.weight == 0 { if this.weight == 0 {
this.gfx.fillRectangle(bounds) if this.fill.A > 0 {
this.fillRectangle(this.fill, bounds)
}
} else { } else {
this.gfx.strokeRectangle(bounds) if this.stroke.A > 0 {
this.strokeRectangle(this.stroke, bounds)
}
} }
} }
func (this *pen) Path (points ...image.Point) { func (this *pen) Path (points ...image.Point) {
if this.weight == 0 { if this.weight == 0 {
this.fillPolygon(points...) if this.fill.A > 0 {
this.fillPolygon(this.fill, points...)
}
} else if this.closed { } else if this.closed {
this.strokePolygon(points...) if this.stroke.A > 0 {
this.strokePolygon(this.stroke, points...)
}
} else { } 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) Stroke (stroke color.Color) { this.stroke = convertColor(stroke) }
func (this *pen) Fill (fill color.Color) { this.fill = convertColor(fill) } 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 { func convertColor (c color.Color) xgraphics.BGRA {
r, g, b, a := c.RGBA() r, g, b, a := c.RGBA()

View File

@ -1,5 +1,229 @@
package xcanvas 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
View 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
View 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
}}
}
}

View File

@ -1,6 +1,7 @@
package xcanvas package xcanvas
import "image" import "image"
import "git.tebibyte.media/tomo/tomo"
// Texture is a read-only image texture that can be quickly written to a canvas. // Texture is a read-only image texture that can be quickly written to a canvas.
// It must be closed manually after use. // 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. // 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 := *this
clipped.rect = bounds clipped.rect = bounds
return &clipped 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!")
}
}

View File

@ -4,26 +4,6 @@ import "image"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/x/canvas" import "git.tebibyte.media/tomo/x/canvas"
type texture struct {
*xcanvas.Texture
}
func (backend Backend) NewTexture (source image.Image) tomo.Texture { func (backend Backend) NewTexture (source image.Image) tomo.Texture {
return texture { return xcanvas.NewTextureFrom(source)
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!")
}
} }