1
2
3
4
5
6
7
8
9 package 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
45
46
47 type Canvas struct {
48 gtx layout.Context
49 ctx ctxops
50
51 bkg color.Color
52 }
53
54
55
56 const DefaultDPI = 96
57
58
59
60
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
84
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
100
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
111
112 func UseBackgroundColor(c color.Color) option {
113 return func(cfg *config) {
114 cfg.bkg = c
115 }
116 }
117
118
119 func (c *Canvas) Size() (w, h vg.Length) {
120 return c.ctx.w, c.ctx.h
121 }
122
123
124 func (c *Canvas) DPI() float64 {
125 return c.ctx.dpi
126 }
127
128
129 func (c *Canvas) Paint() *op.Ops {
130 return c.gtx.Ops
131 }
132
133
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
158
159
160
161
162 func (c *Canvas) SetLineWidth(w vg.Length) {
163 c.ctx.cur().linew = w
164 }
165
166
167
168
169
170
171
172
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
180
181
182
183
184
185
186
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
195
196 func (c *Canvas) Rotate(rad float64) {
197 c.ctx.rotate(rad)
198 }
199
200
201
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
207
208 func (c *Canvas) Scale(x, y float64) {
209 c.ctx.scale(x, y)
210 }
211
212
213
214
215
216
217 func (c *Canvas) Push() {
218 c.ctx.push()
219 }
220
221
222
223 func (c *Canvas) Pop() {
224 c.ctx.pop()
225 }
226
227
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
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
380
381
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
410
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
509
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