aboutsummaryrefslogtreecommitdiff
path: root/prototypes
diff options
context:
space:
mode:
authorPřemysl Janouch <p@janouch.name>2018-09-04 19:22:32 +0200
committerPřemysl Janouch <p@janouch.name>2018-09-05 12:30:38 +0200
commit254ceb810c2971b8eff1a91d6fb486d6a81ce538 (patch)
tree12c64422b817a8c96a60531df4c9504fd511720c /prototypes
parentdf082e1deed1d02507584b4d604e8340edf519df (diff)
downloadhaven-254ceb810c2971b8eff1a91d6fb486d6a81ce538.tar.gz
haven-254ceb810c2971b8eff1a91d6fb486d6a81ce538.tar.xz
haven-254ceb810c2971b8eff1a91d6fb486d6a81ce538.zip
xgb-draw: proper brush stroke render
Diffstat (limited to 'prototypes')
-rw-r--r--prototypes/xgb-draw.go163
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
}
}