diff --git a/prototypes/xgb-draw.go b/prototypes/xgb-draw.go index 540783b..53eb727 100644 --- a/prototypes/xgb-draw.go +++ b/prototypes/xgb-draw.go @@ -1,3 +1,10 @@ +// Network-friendly drawing application based on XRender. +// +// TODO +// - interpolate motion between points +// - use double buffering to remove flicker +// (more pronounced over X11 forwarding) +// - maybe keep the pixmap as large as the window package main import ( @@ -10,6 +17,16 @@ import ( func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) } func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 } +func findPictureFormat(formats []render.Pictforminfo, + depth byte, direct render.Directformat) render.Pictformat { + for _, pf := range formats { + if pf.Depth == depth && pf.Direct == direct { + return pf.Id + } + } + return 0 +} + func main() { X, err := xgb.NewConn() if err != nil { @@ -37,7 +54,7 @@ func main() { visual, xproto.CwBackPixel|xproto.CwEventMask, []uint32{0xffffffff, xproto.EventMaskButtonPress | xproto.EventMaskButtonMotion | xproto.EventMaskButtonRelease | - xproto.EventMaskStructureNotify}) + xproto.EventMaskStructureNotify | xproto.EventMaskExposure}) title := []byte("Draw") _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName, @@ -50,7 +67,8 @@ func main() { log.Fatalln(err) } - var pformat render.Pictformat + // Find appropriate picture formats. + var pformat, pformatAlpha, pformatRGB render.Pictformat for _, pd := range pformats.Screens[X.DefaultScreen].Depths { for _, pv := range pd.Visuals { if pv.Visual == visual { @@ -58,7 +76,26 @@ func main() { } } } + if pformatAlpha = findPictureFormat(pformats.Formats, 8, + render.Directformat{ + AlphaShift: 0, + AlphaMask: 0xff, + }); pformat == 0 { + log.Fatalln("required picture format not found") + } + if pformatRGB = findPictureFormat(pformats.Formats, 24, + render.Directformat{ + RedShift: 16, + RedMask: 0xff, + GreenShift: 8, + GreenMask: 0xff, + BlueShift: 0, + BlueMask: 0xff, + }); pformatRGB == 0 { + log.Fatalln("required picture format not found") + } + // Picture for the window. pid, err := render.NewPictureId(X) if err != nil { log.Fatalln(err) @@ -66,6 +103,8 @@ func main() { render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{}) // Brush shape. + const brushRadius = 5 + brushid, err := render.NewPictureId(X) if err != nil { log.Fatalln(err) @@ -74,12 +113,12 @@ func main() { cFull := render.Color{0xffff, 0xffff, 0xffff, 0xffff} cTrans := render.Color{0xffff, 0xffff, 0xffff, 0} _ = render.CreateRadialGradient(X, brushid, - render.Pointfix{F64ToFixed(50), F64ToFixed(50)}, - render.Pointfix{F64ToFixed(50), F64ToFixed(50)}, + render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)}, + render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)}, F64ToFixed(0), - F64ToFixed(50), - 2, []render.Fixed{F64ToFixed(0), F64ToFixed(1)}, - []render.Color{cFull, cTrans}) + F64ToFixed(brushRadius), + 3, []render.Fixed{F64ToFixed(0), F64ToFixed(0.1), F64ToFixed(1)}, + []render.Color{cFull, cFull, cTrans}) // Brush color. colorid, err := render.NewPictureId(X) @@ -87,22 +126,81 @@ func main() { log.Fatalln(err) } _ = render.CreateSolidFill(X, colorid, render.Color{ - Red: 0xcccc, - Green: 0xaaaa, - Blue: 0x8888, + Red: 0xeeee, + Green: 0x8888, + Blue: 0x4444, Alpha: 0xffff, }) - // Unfortunately, XRender won't give us any /blending operators/, only - // providing basic Porter-Duff ones with useless disjoint/conjoint variants, - // but we need the "lighten" operator to draw brush strokes properly. + // Pixmaps. + const ( + pixWidth = 1000 + pixHeight = 1000 + ) + + pixid, err := xproto.NewPixmapId(X) + if err != nil { + log.Fatalln(err) + } + _ = xproto.CreatePixmap(X, 24, pixid, xproto.Drawable(screen.Root), + pixWidth, pixHeight) + + pixpictid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + render.CreatePicture(X, pixpictid, xproto.Drawable(pixid), + pformatRGB, 0, []uint32{}) + + pixmaskid, err := xproto.NewPixmapId(X) + if err != nil { + log.Fatalln(err) + } + _ = xproto.CreatePixmap(X, 8, pixmaskid, xproto.Drawable(screen.Root), + pixWidth, pixHeight) + + pixmaskpictid, err := render.NewPictureId(X) + if err != nil { + log.Fatalln(err) + } + render.CreatePicture(X, pixmaskpictid, xproto.Drawable(pixmaskid), + pformatAlpha, 0, []uint32{}) + + // Pixmaps come uninitialized. + render.FillRectangles(X, + render.PictOpSrc, pixpictid, render.Color{ + Red: 0xffff, Green: 0xffff, Blue: 0xffff, Alpha: 0xffff, + }, []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}}) + + // This is the only method we can use to render proper brush strokes. // - // https://www.cairographics.org/operators/ - // http://ssp.impulsetrain.com/porterduff.html - printAt := func(x, y int16) { + // ConjointOver is defined as: A = Aa * 1 + Ab * max(1-Aa/Ab,0) + // which basically resolves to: A = max(Aa, Ab) + // which equals "lighten" with one channel only. + // + // Resources: + // - https://www.cairographics.org/operators/ + // - http://ssp.impulsetrain.com/porterduff.html + // - https://keithp.com/~keithp/talks/renderproblems/renderproblems/render-title.html + // - https://keithp.com/~keithp/talks/cairo2003.pdf + drawLineTo := func(x, y int16) { + _ = render.Composite(X, render.PictOpConjointOver, + brushid, render.PictureNone, pixmaskpictid, + 0, 0, 0, 0, x-brushRadius, y-brushRadius, + brushRadius*2, brushRadius*2) + + _ = render.SetPictureClipRectangles(X, pid, + x-brushRadius, y-brushRadius, []xproto.Rectangle{ + {Width: brushRadius * 2, Height: brushRadius * 2}}) + + _ = render.Composite(X, render.PictOpSrc, + pixpictid, render.PictureNone, pid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) _ = render.Composite(X, render.PictOpOver, - colorid, brushid, pid, - 0, 0, 0, 0, x-50, y-50, 100, 100) + colorid, pixmaskpictid, pid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) } drawing := false @@ -121,19 +219,44 @@ func main() { case xproto.UnmapNotifyEvent: return + case xproto.ExposeEvent: + _ = render.SetPictureClipRectangles(X, pid, int16(e.X), int16(e.Y), + []xproto.Rectangle{{Width: e.Width, Height: e.Height}}) + + _ = render.Composite(X, render.PictOpSrc, + pixpictid, render.PictureNone, pid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) + + if drawing { + _ = render.Composite(X, render.PictOpOver, + colorid, pixmaskpictid, pid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) + } + case xproto.ButtonPressEvent: if e.Detail == xproto.ButtonIndex1 { + render.FillRectangles(X, + render.PictOpSrc, pixmaskpictid, render.Color{}, + []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}}) + drawing = true - printAt(e.EventX, e.EventY) + drawLineTo(e.EventX, e.EventY) } case xproto.MotionNotifyEvent: if drawing { - printAt(e.EventX, e.EventY) + drawLineTo(e.EventX, e.EventY) } case xproto.ButtonReleaseEvent: if e.Detail == xproto.ButtonIndex1 { + _ = render.Composite(X, render.PictOpOver, + colorid, pixmaskpictid, pixpictid, + 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ + pixWidth, pixHeight) + drawing = false } }