// Network-friendly drawing application based on XRender. // // TODO // - use double buffering to remove flicker // (more pronounced over X11 forwarding) // - maybe keep the pixmap as large as the window package main import ( "github.com/BurntSushi/xgb" "github.com/BurntSushi/xgb/render" "github.com/BurntSushi/xgb/xproto" "log" ) 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 { log.Fatalln(err) } if err := render.Init(X); err != nil { log.Fatalln(err) } setup := xproto.Setup(X) screen := setup.DefaultScreen(X) visual, depth := screen.RootVisual, screen.RootDepth if depth < 24 { log.Fatalln("need more colors") } wid, err := xproto.NewWindowId(X) if err != nil { log.Fatalln(err) } _ = xproto.CreateWindow(X, depth, wid, screen.Root, 0, 0, 500, 500, 0, xproto.WindowClassInputOutput, visual, xproto.CwBackPixel|xproto.CwEventMask, []uint32{0xffffffff, xproto.EventMaskButtonPress | xproto.EventMaskButtonMotion | xproto.EventMaskButtonRelease | xproto.EventMaskStructureNotify | xproto.EventMaskExposure}) title := []byte("Draw") _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName, xproto.AtomString, 8, uint32(len(title)), title) _ = xproto.MapWindow(X, wid) pformats, err := render.QueryPictFormats(X).Reply() if err != nil { log.Fatalln(err) } // 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 { pformat = pv.Format } } } 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) } 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) } cFull := render.Color{0xffff, 0xffff, 0xffff, 0xffff} cTrans := render.Color{0xffff, 0xffff, 0xffff, 0} _ = render.CreateRadialGradient(X, brushid, render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)}, render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)}, F64ToFixed(0), F64ToFixed(brushRadius), 3, []render.Fixed{F64ToFixed(0), F64ToFixed(0.1), F64ToFixed(1)}, []render.Color{cFull, cFull, cTrans}) // Brush color. colorid, err := render.NewPictureId(X) if err != nil { log.Fatalln(err) } _ = render.CreateSolidFill(X, colorid, render.Color{ Red: 0xeeee, Green: 0x8888, Blue: 0x4444, Alpha: 0xffff, }) // 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. // // 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 drawPointAt := 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, pixmaskpictid, pid, 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ pixWidth, pixHeight) } // Integer version of Bresenham's line drawing algorithm drawLine := func(x0, y0, x1, y1 int16) { dx, dy := x1-x0, y1-y0 if dx < 0 { dx = -dx } if dy < 0 { dy = -dy } steep := dx < dy if steep { // Flip the coordinate system on input x0, y0 = y0, x0 x1, y1 = y1, x1 dx, dy = dy, dx } var stepX, stepY int16 = 1, 1 if x0 > x1 { stepX = -1 } if y0 > y1 { stepY = -1 } dpr := dy * 2 delta := dpr - dx dpru := delta - dx for ; dx > 0; dx-- { // Unflip the coordinate system on output if steep { drawPointAt(y0, x0) } else { drawPointAt(x0, y0) } x0 += stepX if delta > 0 { y0 += stepY delta += dpru } else { delta += dpr } } } var startX, startY int16 = 0, 0 drawing := false for { ev, xerr := X.WaitForEvent() if xerr != nil { log.Printf("Error: %s\n", xerr) return } if ev == nil { return } log.Printf("Event: %s\n", ev) switch e := ev.(type) { 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 drawPointAt(e.EventX, e.EventY) startX, startY = e.EventX, e.EventY } case xproto.MotionNotifyEvent: if drawing { drawLine(startX, startY, e.EventX, e.EventY) startX, startY = 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 } } } }