...

Source file src/gonum.org/v1/plot/vg/vggio/vggio.go

Documentation: gonum.org/v1/plot/vg/vggio

     1  // Copyright ©2020 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package vggio provides a vg.Canvas implementation backed by Gio,
     6  // a toolkit that implements portable immediate GUI mode in Go.
     7  //
     8  // More informations about Gio can be found at https://gioui.org/.
     9  package vggio // import "gonum.org/v1/plot/vg/vggio"
    10  
    11  import (
    12  	"bytes"
    13  	"fmt"
    14  	"image"
    15  	"image/color"
    16  	"strings"
    17  	"sync"
    18  
    19  	"gioui.org/f32"
    20  	giofont "gioui.org/font"
    21  	"gioui.org/font/opentype"
    22  	"gioui.org/gpu/headless"
    23  	"gioui.org/layout"
    24  	"gioui.org/op"
    25  	"gioui.org/op/clip"
    26  	"gioui.org/op/paint"
    27  	"gioui.org/text"
    28  	"gioui.org/unit"
    29  	"gioui.org/widget/material"
    30  	"gioui.org/x/stroke"
    31  	bstroke "github.com/andybalholm/stroke"
    32  	"golang.org/x/image/draw"
    33  	"golang.org/x/image/font/sfnt"
    34  
    35  	"gonum.org/v1/plot/font"
    36  	"gonum.org/v1/plot/vg"
    37  )
    38  
    39  var (
    40  	_ vg.Canvas      = (*Canvas)(nil)
    41  	_ vg.CanvasSizer = (*Canvas)(nil)
    42  )
    43  
    44  // Canvas implements the vg.Canvas interface,
    45  // drawing to an image.Image using vgimg and painting that image
    46  // into a Gioui context.
    47  type Canvas struct {
    48  	gtx layout.Context
    49  	ctx ctxops
    50  
    51  	bkg color.Color // bkg is the background color.
    52  }
    53  
    54  // DefaultDPI is the default dot resolution for image
    55  // drawing in dots per inch.
    56  const DefaultDPI = 96
    57  
    58  // New returns a new image canvas with the provided dimensions and options.
    59  // The currently accepted options are UseDPI and UseBackgroundColor.
    60  // If the resolution or background color are not specified, defaults are used.
    61  func New(gtx layout.Context, w, h vg.Length, opts ...option) *Canvas {
    62  	cfg := &config{
    63  		dpi: DefaultDPI,
    64  		bkg: color.White,
    65  	}
    66  	for _, opt := range opts {
    67  		opt(cfg)
    68  	}
    69  	c := &Canvas{
    70  		gtx: gtx,
    71  		ctx: ctxops{
    72  			ops: gtx.Ops,
    73  			ctx: []context{
    74  				{color: color.Black},
    75  			},
    76  			w:   w,
    77  			h:   h,
    78  			dpi: cfg.dpi,
    79  		},
    80  		bkg: cfg.bkg,
    81  	}
    82  
    83  	// flip the Y-axis so that Y grows from bottom to top and
    84  	// Y=0 is at the bottom of the image.
    85  	c.ctx.invertY()
    86  
    87  	vg.Initialize(c)
    88  
    89  	return c
    90  }
    91  
    92  type config struct {
    93  	dpi float64
    94  	bkg color.Color
    95  }
    96  
    97  type option func(*config)
    98  
    99  // UseDPI sets the dots per inch of a canvas. It should only be
   100  // used as an option argument when initializing a new canvas.
   101  func UseDPI(dpi int) option {
   102  	if dpi <= 0 {
   103  		panic("DPI must be > 0.")
   104  	}
   105  	return func(c *config) {
   106  		c.dpi = float64(dpi)
   107  	}
   108  }
   109  
   110  // UseBackgroundColor specifies the image background color.
   111  // Without UseBackgroundColor, the default color is white.
   112  func UseBackgroundColor(c color.Color) option {
   113  	return func(cfg *config) {
   114  		cfg.bkg = c
   115  	}
   116  }
   117  
   118  // Size implement vg.CanvasSizer.
   119  func (c *Canvas) Size() (w, h vg.Length) {
   120  	return c.ctx.w, c.ctx.h
   121  }
   122  
   123  // DPI returns the resolution of the receiver in pixels per inch.
   124  func (c *Canvas) DPI() float64 {
   125  	return c.ctx.dpi
   126  }
   127  
   128  // Paint returns the painting operations.
   129  func (c *Canvas) Paint() *op.Ops {
   130  	return c.gtx.Ops
   131  }
   132  
   133  // Screenshot returns a screenshot of the canvas as an image.
   134  func (c *Canvas) Screenshot() (image.Image, error) {
   135  	win, err := headless.NewWindow(
   136  		int(c.ctx.w.Dots(c.ctx.dpi)),
   137  		int(c.ctx.h.Dots(c.ctx.dpi)),
   138  	)
   139  	if err != nil {
   140  		return nil, fmt.Errorf("vggio: could not create headless window: %w", err)
   141  	}
   142  
   143  	err = win.Frame(c.gtx.Ops)
   144  	if err != nil {
   145  		return nil, fmt.Errorf("vggio: could not run headless frame: %w", err)
   146  	}
   147  
   148  	img := image.NewRGBA(image.Rectangle{Max: win.Size()})
   149  	err = win.Screenshot(img)
   150  	if err != nil {
   151  		return nil, fmt.Errorf("vggio: could not create screenshot: %w", err)
   152  	}
   153  
   154  	return img, nil
   155  }
   156  
   157  // SetLineWidth sets the width of stroked paths.
   158  // If the width is not positive then stroked lines
   159  // are not drawn.
   160  //
   161  // The initial line width is 1 point.
   162  func (c *Canvas) SetLineWidth(w vg.Length) {
   163  	c.ctx.cur().linew = w
   164  }
   165  
   166  // SetLineDash sets the dash pattern for lines.
   167  // The pattern slice specifies the lengths of
   168  // alternating dashes and gaps, and the offset
   169  // specifies the distance into the dash pattern
   170  // to start the dash.
   171  //
   172  // The initial dash pattern is a solid line.
   173  func (c *Canvas) SetLineDash(pattern []vg.Length, offset vg.Length) {
   174  	cur := c.ctx.cur()
   175  	cur.pattern = pattern
   176  	cur.offset = offset
   177  }
   178  
   179  // SetColor sets the current drawing color.
   180  // Note that fill color and stroke color are
   181  // the same, so if you want different fill
   182  // and stroke colors then you must set a color,
   183  // draw fills, set a new color and then draw lines.
   184  //
   185  // The initial color is black.
   186  // If SetColor is called with a nil color then black is used.
   187  func (c *Canvas) SetColor(clr color.Color) {
   188  	if clr == nil {
   189  		clr = color.Black
   190  	}
   191  	c.ctx.cur().color = clr
   192  }
   193  
   194  // Rotate applies a rotation transform to the context.
   195  // The parameter is specified in radians.
   196  func (c *Canvas) Rotate(rad float64) {
   197  	c.ctx.rotate(rad)
   198  }
   199  
   200  // Translate applies a translational transform
   201  // to the context.
   202  func (c *Canvas) Translate(pt vg.Point) {
   203  	c.ctx.translate(pt.X.Dots(c.ctx.dpi), pt.Y.Dots(c.ctx.dpi))
   204  }
   205  
   206  // Scale applies a scaling transform to the
   207  // context.
   208  func (c *Canvas) Scale(x, y float64) {
   209  	c.ctx.scale(x, y)
   210  }
   211  
   212  // Push saves the current line width, the
   213  // current dash pattern, the current
   214  // transforms, and the current color
   215  // onto a stack so that the state can later
   216  // be restored by calling Pop().
   217  func (c *Canvas) Push() {
   218  	c.ctx.push()
   219  }
   220  
   221  // Pop restores the context saved by the
   222  // corresponding call to Push().
   223  func (c *Canvas) Pop() {
   224  	c.ctx.pop()
   225  }
   226  
   227  // Stroke strokes the given path.
   228  func (c *Canvas) Stroke(p vg.Path) {
   229  	if c.ctx.cur().linew <= 0 {
   230  		return
   231  	}
   232  	c.ctx.push()
   233  	defer c.ctx.pop()
   234  
   235  	var (
   236  		cur    = c.ctx.cur()
   237  		dashes stroke.Dashes
   238  	)
   239  	dashes.Phase = float32(cur.offset.Dots(c.ctx.dpi))
   240  	dashes.Dashes = make([]float32, len(cur.pattern))
   241  	for i, v := range cur.pattern {
   242  		dashes.Dashes[i] = float32(v.Dots(c.ctx.dpi))
   243  	}
   244  
   245  	shape := stroke.Stroke{
   246  		Path:   c.stroke(p),
   247  		Width:  float32(cur.linew.Dots(c.ctx.dpi)),
   248  		Cap:    stroke.FlatCap,
   249  		Dashes: dashes,
   250  	}.Op(c.ctx.ops)
   251  
   252  	clr := c.ctx.cur().color
   253  	paint.FillShape(c.ctx.ops, rgba(clr), shape)
   254  }
   255  
   256  // Fill fills the given path.
   257  func (c *Canvas) Fill(p vg.Path) {
   258  	c.ctx.push()
   259  	defer c.ctx.pop()
   260  
   261  	shape := clip.Outline{
   262  		Path: c.outline(p),
   263  	}.Op()
   264  
   265  	clr := c.ctx.cur().color
   266  	paint.FillShape(c.ctx.ops, rgba(clr), shape)
   267  }
   268  
   269  func rgba(c color.Color) color.NRGBA {
   270  	r, g, b, a := c.RGBA()
   271  	return color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}
   272  }
   273  
   274  func (c *Canvas) outline(p vg.Path) clip.PathSpec {
   275  	var path clip.Path
   276  	path.Begin(c.ctx.ops)
   277  	for _, comp := range p {
   278  		switch comp.Type {
   279  		case vg.MoveComp:
   280  			pt := c.ctx.pt32(comp.Pos)
   281  			path.MoveTo(pt)
   282  
   283  		case vg.LineComp:
   284  			pt := c.ctx.pt32(comp.Pos)
   285  			path.LineTo(pt)
   286  
   287  		case vg.ArcComp:
   288  			center := c.ctx.pt32(comp.Pos)
   289  			path.ArcTo(center, center, float32(comp.Angle))
   290  
   291  		case vg.CurveComp:
   292  			switch len(comp.Control) {
   293  			case 1:
   294  				ctl := c.ctx.pt32(comp.Control[0])
   295  				end := c.ctx.pt32(comp.Pos)
   296  				path.QuadTo(ctl, end)
   297  			case 2:
   298  				ctl0 := c.ctx.pt32(comp.Control[0])
   299  				ctl1 := c.ctx.pt32(comp.Control[1])
   300  				end := c.ctx.pt32(comp.Pos)
   301  				path.CubeTo(ctl0, ctl1, end)
   302  			default:
   303  				panic("vggio: invalid number of control points")
   304  			}
   305  
   306  		case vg.CloseComp:
   307  			path.Close()
   308  
   309  		default:
   310  			panic(fmt.Sprintf("vggio: unknown path component %d", comp.Type))
   311  		}
   312  	}
   313  	return path.End()
   314  }
   315  
   316  func (c *Canvas) stroke(p vg.Path) stroke.Path {
   317  	var (
   318  		path stroke.Path
   319  		add  = func(seg stroke.Segment) {
   320  			path.Segments = append(path.Segments, seg)
   321  		}
   322  		pen f32.Point
   323  		beg f32.Point
   324  	)
   325  
   326  	for i, comp := range p {
   327  		if i == 0 {
   328  			beg = c.ctx.pt32(comp.Pos)
   329  		}
   330  		switch comp.Type {
   331  		case vg.MoveComp:
   332  			pt := c.ctx.pt32(comp.Pos)
   333  			add(stroke.MoveTo(pt))
   334  			pen = pt
   335  
   336  		case vg.LineComp:
   337  			pt := c.ctx.pt32(comp.Pos)
   338  			add(stroke.LineTo(pt))
   339  			pen = pt
   340  
   341  		case vg.ArcComp:
   342  			center := c.ctx.pt32(comp.Pos)
   343  			arcs := arcTo(pen, center, center, float32(comp.Angle))
   344  			path.Segments = append(path.Segments, xStroke(arcs)...)
   345  			pen = f32.Point(arcs[len(arcs)-1].End)
   346  
   347  		case vg.CurveComp:
   348  			switch len(comp.Control) {
   349  			case 1:
   350  				var (
   351  					ctl = c.ctx.pt32(comp.Control[0])
   352  					end = c.ctx.pt32(comp.Pos)
   353  				)
   354  				add(stroke.QuadTo(ctl, end))
   355  				pen = end
   356  			case 2:
   357  				var (
   358  					ctl0 = c.ctx.pt32(comp.Control[0])
   359  					ctl1 = c.ctx.pt32(comp.Control[1])
   360  					end  = c.ctx.pt32(comp.Pos)
   361  				)
   362  				add(stroke.CubeTo(ctl0, ctl1, end))
   363  				pen = end
   364  			default:
   365  				panic("vggio: invalid number of control points")
   366  			}
   367  
   368  		case vg.CloseComp:
   369  			add(stroke.LineTo(beg))
   370  			pen = beg
   371  
   372  		default:
   373  			panic(fmt.Sprintf("vggio: unknown path component %d", comp.Type))
   374  		}
   375  	}
   376  	return path
   377  }
   378  
   379  // FillString fills in text at the specified
   380  // location using the given font.
   381  // If the font size is zero, the text is not drawn.
   382  func (c *Canvas) FillString(fnt font.Face, pt vg.Point, txt string) {
   383  	if fnt.Font.Size == 0 {
   384  		return
   385  	}
   386  	c.ctx.push()
   387  	defer c.ctx.pop()
   388  
   389  	e := fnt.Extents()
   390  	x := pt.X.Dots(c.ctx.dpi)
   391  	y := pt.Y.Dots(c.ctx.dpi) - e.Descent.Dots(c.ctx.dpi)
   392  	h := c.ctx.h.Dots(c.ctx.dpi)
   393  
   394  	c.ctx.invertY()
   395  	c.ctx.translate(x, h-y-fnt.Font.Size.Dots(c.ctx.dpi))
   396  
   397  	th := material.NewTheme()
   398  	th.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(collectionFor(fnt)))
   399  	lbl := material.Label(
   400  		th,
   401  		unit.Sp(float32(fnt.Font.Size.Dots(c.ctx.dpi))),
   402  		txt,
   403  	)
   404  	lbl.Color = rgba(c.ctx.cur().color)
   405  	lbl.Alignment = text.Start
   406  	lbl.Layout(c.gtx)
   407  }
   408  
   409  // DrawImage draws the image, scaled to fit
   410  // the destination rectangle.
   411  func (c *Canvas) DrawImage(rect vg.Rectangle, img image.Image) {
   412  	c.ctx.push()
   413  	defer c.ctx.pop()
   414  
   415  	var (
   416  		ops    = c.ctx.ops
   417  		dpi    = c.DPI()
   418  		min    = rect.Min
   419  		xmin   = min.X.Dots(dpi)
   420  		ymin   = min.Y.Dots(dpi)
   421  		rsz    = rect.Size()
   422  		width  = rsz.X.Dots(dpi)
   423  		height = rsz.Y.Dots(dpi)
   424  		dst    = image.NewRGBA(image.Rect(0, 0, int(width), int(height)))
   425  	)
   426  
   427  	draw.NearestNeighbor.Scale(dst, dst.Rect, img, img.Bounds(), draw.Src, nil)
   428  
   429  	c.ctx.scale(1, -1)
   430  	c.ctx.translate(xmin, -ymin-height)
   431  	paint.NewImageOp(dst).Add(ops)
   432  	paint.PaintOp{}.Add(ops)
   433  }
   434  
   435  var dbfonts = &gioFontsCache{
   436  	cache: make(map[string][]giofont.FontFace),
   437  	fonts: make(map[string]struct{}),
   438  }
   439  
   440  type gioFontsCache struct {
   441  	sync.RWMutex
   442  	cache map[string][]giofont.FontFace
   443  	fonts map[string]struct{}
   444  	buf   sfnt.Buffer
   445  }
   446  
   447  func (cache *gioFontsCache) get(fnt font.Face) ([]giofont.FontFace, bool) {
   448  	cache.RLock()
   449  	defer cache.RUnlock()
   450  
   451  	_, ok := cache.fonts[fnt.Name()]
   452  	if !ok {
   453  		return nil, false
   454  	}
   455  	name := collectionName(fnt.Name())
   456  	return cache.cache[name], ok
   457  }
   458  
   459  func (cache *gioFontsCache) add(fnt font.Face) []giofont.FontFace {
   460  	cache.Lock()
   461  	defer cache.Unlock()
   462  
   463  	name := fnt.Name()
   464  	if fnt.Face == nil {
   465  		panic(fmt.Errorf("vggio: nil plot/font.Face %q", name))
   466  	}
   467  	buf := new(bytes.Buffer)
   468  	_, err := fnt.Face.WriteSourceTo(&cache.buf, buf)
   469  	if err != nil {
   470  		panic(fmt.Errorf("vggio: could not load font %q: %+v", name, err))
   471  	}
   472  
   473  	gioFace, err := opentype.Parse(buf.Bytes())
   474  	if err != nil {
   475  		panic(fmt.Errorf("vggio: could not parse font %q: %+v", name, err))
   476  	}
   477  
   478  	gioFnt := gonumToGioFont(fnt.Font)
   479  
   480  	colName := collectionName(fnt.Name())
   481  	cache.cache[colName] = append(cache.cache[colName], giofont.FontFace{
   482  		Font: gioFnt,
   483  		Face: gioFace,
   484  	})
   485  	cache.fonts[name] = struct{}{}
   486  
   487  	return cache.cache[colName]
   488  }
   489  
   490  func gonumToGioFont(fnt font.Font) giofont.Font {
   491  	o := giofont.Font{
   492  		Typeface: giofont.Typeface(fnt.Typeface),
   493  		Style:    giofont.Style(fnt.Style),
   494  		Weight:   giofont.Weight(fnt.Weight),
   495  	}
   496  	return o
   497  }
   498  
   499  func collectionFor(fnt font.Face) []giofont.FontFace {
   500  	coll, ok := dbfonts.get(fnt)
   501  	if !ok {
   502  		coll = dbfonts.add(fnt)
   503  	}
   504  	return coll
   505  }
   506  
   507  func collectionName(name string) string {
   508  	// regroup fonts with name "Liberation-Italic", "Liberation-Bold", ...
   509  	// under the same collection "Liberation".
   510  	if strings.Contains(name, "-") {
   511  		i := strings.Index(name, "-")
   512  		name = name[:i]
   513  	}
   514  	return name
   515  }
   516  
   517  func arcTo(start, f1, f2 f32.Point, angle float32) []bstroke.Segment {
   518  	if f1 == f2 {
   519  		return bstroke.AppendArc(nil, bstroke.Pt(start.X, start.Y), bstroke.Pt(f1.X, f1.Y), angle)
   520  	}
   521  	return bstroke.AppendEllipticalArc(nil, bstroke.Pt(start.X, start.Y), bstroke.Pt(f1.X, f1.Y), bstroke.Pt(f2.X, f2.Y), angle)
   522  }
   523  
   524  func xStroke(bs []bstroke.Segment) []stroke.Segment {
   525  	vs := make([]stroke.Segment, len(bs))
   526  	for i, b := range bs {
   527  		vs[i] = stroke.CubeTo(f32.Point(b.CP1), f32.Point(b.CP2), f32.Point(b.End))
   528  	}
   529  	return vs
   530  }
   531  

View as plain text