From 94db4e8ead616a6644c93e5ff9534c9895858db3 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 23 Aug 2023 19:21:28 -0400 Subject: [PATCH] Transparency support!! --- box.go | 5 +- canvas/canvas.go | 41 ++++++--- canvas/draw.go | 226 +++++++++++++++++++++++++++++++++++++++++++++- canvas/line.go | 94 +++++++++++++++++++ canvas/plot.go | 47 ++++++++++ canvas/texture.go | 11 ++- texture.go | 22 +---- 7 files changed, 407 insertions(+), 39 deletions(-) create mode 100644 canvas/line.go create mode 100644 canvas/plot.go diff --git a/box.go b/box.go index 491e52c..09401b0 100644 --- a/box.go +++ b/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() } diff --git a/canvas/canvas.go b/canvas/canvas.go index b74c06f..37dd4ce 100644 --- a/canvas/canvas.go +++ b/canvas/canvas.go @@ -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" @@ -66,30 +67,41 @@ func (this *Canvas) assert () { type pen struct { image *xgraphics.Image - closed bool - endCap canvas.Cap - joint canvas.Joint - weight int - align canvas.StrokeAlign - stroke xgraphics.BGRA - fill xgraphics.BGRA + closed bool + endCap canvas.Cap + joint canvas.Joint + weight int + 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...) + } } } @@ -99,8 +111,9 @@ func (this *pen) Joint (joint canvas.Joint) { this.joint = joint func (this *pen) StrokeWeight (weight int) { this.weight = weight } 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) 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() diff --git a/canvas/draw.go b/canvas/draw.go index 04c94bf..653dab6 100644 --- a/canvas/draw.go +++ b/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 + } } diff --git a/canvas/line.go b/canvas/line.go new file mode 100644 index 0000000..7f507cc --- /dev/null +++ b/canvas/line.go @@ -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 +} diff --git a/canvas/plot.go b/canvas/plot.go new file mode 100644 index 0000000..4e128c4 --- /dev/null +++ b/canvas/plot.go @@ -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 + }} + } +} diff --git a/canvas/texture.go b/canvas/texture.go index dec80a1..99cb6e6 100644 --- a/canvas/texture.go +++ b/canvas/texture.go @@ -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!") + } +} diff --git a/texture.go b/texture.go index 6faa883..7a5f968 100644 --- a/texture.go +++ b/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) }