// Network-friendly drawing application based on XRender. // // TODO: 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 createNewPicture(X *xgb.Conn, depth byte, drawable xproto.Drawable, width uint16, height uint16, format render.Pictformat) render.Picture { pixmapid, err := xproto.NewPixmapId(X) if err != nil { log.Fatalln(err) } _ = xproto.CreatePixmap(X, depth, pixmapid, drawable, width, height) pictid, err := render.NewPictureId(X) if err != nil { log.Fatalln(err) } _ = render.CreatePicture(X, pictid, xproto.Drawable(pixmapid), format, 0, []uint32{}) return pictid } 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: 0x4444, Green: 0x8888, Blue: 0xffff, Alpha: 0xffff, }) // Various pixmaps. const ( pixWidth = 1000 pixHeight = 1000 ) canvasid := createNewPicture(X, 24, xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB) bufferid := createNewPicture(X, 24, xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB) maskid := createNewPicture(X, 8, xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatAlpha) // Smoothing by way of blur, apparently a misguided idea. /* _ = render.SetPictureFilter(X, maskid, uint16(len("convolution")), "convolution", []render.Fixed{F64ToFixed(3), F64ToFixed(3), F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0.40), F64ToFixed(0.15), F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0)}) */ // Pixmaps come uninitialized. _ = render.FillRectangles(X, render.PictOpSrc, canvasid, 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 brush strokes without // alpha accumulation due to stamping. Though this also seems to be // misguided. Keeping it here for educational purposes. // // 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, maskid, 0, 0, 0, 0, x-brushRadius, y-brushRadius, brushRadius*2, brushRadius*2) _ = render.SetPictureClipRectangles(X, bufferid, x-brushRadius, y-brushRadius, []xproto.Rectangle{ {Width: brushRadius * 2, Height: brushRadius * 2}}) _ = render.Composite(X, render.PictOpSrc, canvasid, render.PictureNone, bufferid, 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ pixWidth, pixHeight) _ = render.Composite(X, render.PictOpOver, colorid, maskid, bufferid, 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ pixWidth, pixHeight) // Composited, now blit to window without flicker. _ = render.SetPictureClipRectangles(X, pid, x-brushRadius, y-brushRadius, []xproto.Rectangle{ {Width: brushRadius * 2, Height: brushRadius * 2}}) _ = render.Composite(X, render.PictOpSrc, bufferid, render.PictureNone, 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}}) // Not bothering to deflicker here using the buffer pixmap, // with compositing this event is rare enough. _ = render.Composite(X, render.PictOpSrc, canvasid, render.PictureNone, pid, 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ pixWidth, pixHeight) if drawing { _ = render.Composite(X, render.PictOpOver, colorid, maskid, 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, maskid, 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, maskid, canvasid, 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */ pixWidth, pixHeight) drawing = false } } } }