backend/x/canvas/canvas.go

164 lines
5.0 KiB
Go

package xcanvas
import "image"
import "image/color"
import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/tomo/tomo/canvas"
// Canvas satisfies the canvas.Canvas interface. It draws to an xgraphics.Image.
// It must be closed after use.
type Canvas struct {
*xgraphics.Image
}
// NewCanvas creates a new canvas from a bounding rectangle.
func NewCanvas (x *xgbutil.XUtil, bounds image.Rectangle) *Canvas {
return NewCanvasFrom(xgraphics.New(x, bounds))
}
// NewCanvasFrom creates a new canvas from an existing xgraphics.Image. Note
// that calling Close() on the resulting canvas will destroy this image.
func NewCanvasFrom (image *xgraphics.Image) *Canvas {
if image == nil { return nil }
return &Canvas { image }
}
// Pen returns a new drawing context.
func (this *Canvas) Pen () canvas.Pen {
return &pen {
image: this.Image,
}
}
// SubCanvas returns a subset of this canvas that points to the same data.
func (this *Canvas) SubCanvas (bounds image.Rectangle) canvas.Canvas {
this.assert()
subImage := this.Image.SubImage(bounds)
if subImage == nil { return nil }
xImage := subImage.(*xgraphics.Image)
return &Canvas { xImage }
}
// Push pushes this canvas to the screen.
func (this *Canvas) Push (window xproto.Window) {
this.assert()
this.XDraw()
this.XExpPaint(window, this.Bounds().Min.X, this.Bounds().Min.Y)
}
// Close frees this canvas from the X server.
func (this *Canvas) Close () error {
this.assert()
this.Image.Destroy()
return nil
}
func (this *Canvas) assert () {
if this == nil { panic("nil canvas") }
}
// TODO: we need to implement:
// - cap
// - joint
// - align
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
texture *Texture
}
func (this *pen) Rectangle (bounds image.Rectangle) {
bounds = bounds.Canon()
if this.weight == 0 {
if this.fill.A > 0 && !this.textureObscures() {
this.fillRectangle(this.fill, bounds)
}
if this.texture != nil {
this.textureRectangle(bounds)
}
} else {
if this.stroke.A > 0 {
this.strokeRectangle(this.stroke, bounds)
}
}
}
func (this *pen) Path (points ...image.Point) {
if this.weight == 0 {
if this.fill.A > 0 {
this.fillPolygon(this.fill, points...)
}
} else if this.closed && len(points) > 2 {
if this.stroke.A > 0 {
this.strokePolygon(this.stroke, points...)
}
} else {
if this.stroke.A > 0 {
this.polyLine(this.stroke, points...)
}
}
}
func (this *pen) Closed (closed bool) { this.closed = closed }
func (this *pen) Cap (endCap canvas.Cap) { this.endCap = endCap }
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) Texture (texture canvas.Texture) { this.texture = AssertTexture(texture) }
func (this *pen) textureObscures () bool {
return this.texture != nil && this.texture.Opaque()
}
func convertColor (c color.Color) xgraphics.BGRA {
r, g, b, a := c.RGBA()
return xgraphics.BGRA {
B: uint8(b >> 8),
G: uint8(g >> 8),
R: uint8(r >> 8),
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))
}