diff options
| author | Přemysl Janouch <p@janouch.name> | 2018-09-04 19:22:32 +0200 | 
|---|---|---|
| committer | Přemysl Janouch <p@janouch.name> | 2018-09-05 12:30:38 +0200 | 
| commit | 254ceb810c2971b8eff1a91d6fb486d6a81ce538 (patch) | |
| tree | 12c64422b817a8c96a60531df4c9504fd511720c | |
| parent | df082e1deed1d02507584b4d604e8340edf519df (diff) | |
| download | haven-254ceb810c2971b8eff1a91d6fb486d6a81ce538.tar.gz haven-254ceb810c2971b8eff1a91d6fb486d6a81ce538.tar.xz haven-254ceb810c2971b8eff1a91d6fb486d6a81ce538.zip | |
xgb-draw: proper brush stroke render
| -rw-r--r-- | prototypes/xgb-draw.go | 163 | 
1 files changed, 143 insertions, 20 deletions
| 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  			}  		} | 
