From e4fdde3da1200d0fd5292d2b74b8e79ef8bd0a68 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 12 Aug 2024 18:15:15 -0400 Subject: [PATCH] Use premultiplied alpha for X canvas --- x/canvas/canvas.go | 29 +++++++++++++++++++++++++++++ x/canvas/draw.go | 6 +++--- x/canvas/plot.go | 2 +- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/x/canvas/canvas.go b/x/canvas/canvas.go index 8b6a72a..26dd0cb 100644 --- a/x/canvas/canvas.go +++ b/x/canvas/canvas.go @@ -132,3 +132,32 @@ func convertColor (c color.Color) xgraphics.BGRA { A: uint8(a >> 8), } } + +// For some reason, xgraphics.BGRA does not specify whether or not it uses +// premultiplied alpha, and information regarding this is contradictory. +// Basically: +// - BGRAModel just takes the result of c.RGBA and bit shifts it, without +// un-doing the aplha premultiplication that is required by Color.RGBA, +// suggesting that xgraphics.BGRA stores alpha-premultiplied color. +// - xgraphics.BlendBGRA lerps between dest and src using only the alpha of +// src (temporarily converting the colors to fucking floats for some reason) +// which seems to suggest that xgraphics.BGRA *does not* store alpha- +// premultiplied color. +// There is no issues page on xgbutil so we may never get an answer to this +// question. However, in this package we just use xgraphics.BGRA to store alpha- +// premultiplied color anyway because its way faster, and I would sooner eat +// spaghetti with a spoon than convert to and from float64 to blend pixels. +func blendPremultipliedBGRA (dst, src xgraphics.BGRA) xgraphics.BGRA { + // https://en.wikipedia.org/wiki/Alpha_compositing + return xgraphics.BGRA { + B: blendPremultipliedChannel(dst.B, src.B, src.A), + G: blendPremultipliedChannel(dst.G, src.G, src.A), + R: blendPremultipliedChannel(dst.R, src.R, src.A), + A: blendPremultipliedChannel(dst.A, src.A, src.A), + } +} + +func blendPremultipliedChannel (dst, src, a uint8) uint8 { + dst16, src16, a16 := uint16(dst), uint16(src), uint16(a) + return uint8(src16 + ((dst16 * (255 - a16)) >> 8)) +} diff --git a/x/canvas/draw.go b/x/canvas/draw.go index 2e54c35..3dce463 100644 --- a/x/canvas/draw.go +++ b/x/canvas/draw.go @@ -46,7 +46,7 @@ func (this *pen) textureRectangleTransparent (bounds image.Rectangle) { srcPos := pos.Add(offset) dstIndex := this.image.PixOffset(pos.X, pos.Y) srcIndex := this.texture.PixOffset(srcPos.X, srcPos.Y) - pixel := xgraphics.BlendBGRA(xgraphics.BGRA { + pixel := blendPremultipliedBGRA(xgraphics.BGRA { B: dst[dstIndex + 0], G: dst[dstIndex + 1], R: dst[dstIndex + 2], @@ -93,7 +93,7 @@ func (this *pen) fillRectangleTransparent (c xgraphics.BGRA, bounds image.Rectan 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 { + pixel := blendPremultipliedBGRA(xgraphics.BGRA { B: this.image.Pix[index + 0], G: this.image.Pix[index + 1], R: this.image.Pix[index + 2], @@ -256,7 +256,7 @@ func (context *fillingContext) fillPolygonHotTransparent () { // fill pixels in between for x := left; x < right; x ++ { index := context.image.PixOffset(x, context.y) - pixel := xgraphics.BlendBGRA(xgraphics.BGRA { + pixel := blendPremultipliedBGRA(xgraphics.BGRA { B: context.image.Pix[index + 0], G: context.image.Pix[index + 1], R: context.image.Pix[index + 2], diff --git a/x/canvas/plot.go b/x/canvas/plot.go index 4e128c4..69fd105 100644 --- a/x/canvas/plot.go +++ b/x/canvas/plot.go @@ -32,7 +32,7 @@ func (context plottingContext) plot (center image.Point) { 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 { + pixel := blendPremultipliedBGRA(xgraphics.BGRA { B: context.image.Pix[index + 0], G: context.image.Pix[index + 1], R: context.image.Pix[index + 2],