diff options
Diffstat (limited to 'prototypes/xgb-draw.go')
-rw-r--r-- | prototypes/xgb-draw.go | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/prototypes/xgb-draw.go b/prototypes/xgb-draw.go new file mode 100644 index 0000000..b893f6f --- /dev/null +++ b/prototypes/xgb-draw.go @@ -0,0 +1,329 @@ +// 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 + } + } + } +} |