Canvas is a common vector drawing target that can output SVG, PDF, EPS, raster images (PNG, JPG, GIF, ...), HTML Canvas through WASM, and OpenGL. It has a wide range of path manipulation functionality such as flattening, stroking and dashing implemented. Additionally, it has a good text formatter and embeds fonts (TTF, OTF, WOFF, or WOFF2) or converts them to outlines. It can be considered a Cairo or node-canvas alternative in Go. See the example below in Fig. 1 and Fig. 2 for an overview of the functionality.
Figure 1: top-left you can see text being fitted into a box and their bounding box (orange-red), the spaces between the words on the first row are being stretched to fill the whole width. You can see all the possible styles and text decorations applied. Also note the typographic substitutions (the quotes) and ligature support (fi, ffi, ffl, ...). Below the text box, the word "stroke" is being stroked and drawn as a path. Top-right we see a LaTeX formula that has been converted to a path. Left of that we see ellipse support showcasing precise dashing, notably the length of e.g. the short dash is equal wherever it is (approximated through arc length parametrization) on the curve. It also shows support for alternating dash lengths, in this case (2.0, 4.0, 2.0) for dashes and for spaces. Note that the dashes themselves are elliptical arcs as well (thus exactly precise even if magnified greatly). In the bottom-right we see a closed polygon of four points being smoothed by cubic Béziers that are smooth along the whole path, and next to it on the left an open path. In the middle you can see a rasterized image painted.
Figure 2abc: Three examples of what is possible with this library, for example the plotting of graphs, maps and documents.
Live WASM HTML Canvas example
Terminology: a path is a sequence of drawing commands (MoveTo, LineTo, QuadTo, CubeTo, ArcTo, Close) that completely describe a path. QuadTo and CubeTo are quadratic and cubic Béziers respectively, ArcTo is an elliptical arc, and Close is a LineTo to the last MoveTo command and closes the path (sometimes this has a special meaning such as when stroking). A path can consist of several subpaths by having more than one MoveTo or Close command. A subpath consists of path segments which are defined by a command and some values or coordinates.
Flattening is the act of converting the QuadTo, CubeTo and ArcTo segments into LineTos so that all path segments are linear.
Getting Started
With modules enabled, add the following imports and run the project with go get
import (
"github.com/tdewolff/canvas"
)
Examples
Preview: canvas preview (as shown above) showing most of the functionality and exporting as PNG, SVG, PDF and EPS. It shows image and text rendering as well as LaTeX support and path functionality.
Map: data is loaded from Open Street Map of the city centre of Amsterdam and rendered to a PNG.
Graph: a simple graph is being plotted using the CO2 data from the Mauna Loa observatory.
Text document: a simple text document is rendered to PNG.
HTML Canvas: using WASM, a HTML Canvas is used as target. Live demo.
TeX/PGF: using the PGF (TikZ) LaTeX package, the output can be directly included in the main TeX file.
OpenGL: rendering example to an OpenGL target (WIP).
go-chart: using the go-chart library a financial graph is plotted.
gonum/plot: using the gonum/plot library an example is plotted.
Articles
My own
Papers
- M. Walter, A. Fournier, Approximate Arc Length Parametrization, Anais do IX SIBGRAPHI (1996), p. 143--150
- T.F. Hain, et al., Fast, precise flattening of cubic Bézier path and offset curves, Computers & Graphics 29 (2005). p. 656--666
- M. Goldapp, Approximation of circular arcs by cubic polynomials, Computer Aided Geometric Design 8 (1991), p. 227--238
- L. Maisonobe, Drawing and elliptical arc using polylines, quadratic or cubic Bézier curves (2003)
- S.H. Kim and Y.J. Ahn, An approximation of circular arcs by quartic Bezier curves, Computer-Aided Design 39 (2007, p. 490--493)
Status
Targets
Feature | Image | SVG | PDF | EPS | WASM Canvas | OpenGL |
---|
Draw path fill | yes | yes | yes | yes | yes | no |
Draw path stroke | yes | yes | yes | no | yes | no |
Draw path dash | yes | yes | yes | no | yes | no |
Embed fonts | | yes | yes | no | no | no |
Draw text | path | yes | yes | path | path | path |
Draw image | yes | yes | yes | no | yes | no |
EvenOdd fill rule | no | yes | yes | no | no | no |
- EPS does not support transparency
- PDF and EPS do not support line joins for last and first dash for closed dashed path
- OpenGL proper tessellation is missing
Path
Command | Flatten | Stroke | Length | SplitAt |
---|
LineTo | yes | yes | yes | yes |
QuadTo | yes (CubeTo) | yes (CubeTo) | yes | yes (GL5 + Chebyshev10) |
CubeTo | yes | yes | yes (GL5) | yes (GL5 + Chebyshev10) |
ArcTo | yes | yes | yes (GL5) | yes (GL5 + Chebyshev10) |
- Ellipse => Cubic Bézier: used by rasterizer and PDF targets (see Maisonobe paper)
NB: GL5 means a Gauss-Legendre n=5, which is an numerical approximation as there is no analytical solution. Chebyshev is a converging way to approximate a function by an n=10 degree polynomial. It uses the bisection method as well to determine the polynomial points.
Planning
Features that are planned to be implemented in the future, with important issues in bold. Also see the TODOs in the code.
General
- Fix slowness in the rasterizer (text_example.go is slow! use rasterized cache for each glyph/path)
- Use general span placement algorithm (like CSS flexbox) that replace the current Text placer, to allow for text, image, path elements (e.g. inline formulas, inline icons or emoticons, ...)
- Use word breaking algorithm from Knuth & Plass, implemented in JS in typeset. Use letter stretching and shrinking, shrinking by using ligatures, space shrinking and stretching (depending if space is between words or after comma or dot), and spacing or shrinking between glyphs. Use a point system of how ugly breaks are on a paragraph basis. Also see Justify Just or Just Justify.
- Load in Markdown/HTML formatting and turn into text
- Add OpenGL target, needs tessellation (see Delaunay triangulation). See Resolution independent NURBS curves rendering using programmable graphics pipeline and poly2tri-go. Use rational quadratic Beziérs to represent quadratic Beziérs and elliptic arcs exactly, and reduce degree of cubic Beziérs. Using a fragment shader we can draw all curves exactly. Or use rational cubic Beziérs to represent them all exactly?
Fonts
- Compressing fonts and embedding only used characters
- Use ligature and OS/2 tables
- Support EOT font format
- Font embedding for EPS
- Support font hinting (for the rasterizer)?
Paths
- Avoid overlapping paths when offsetting in corners
- Get position and derivative/normal at length L along the path
- Simplify polygons using the Ramer-Douglas-Peucker algorithm
- Intersection function between line, Bézier and ellipse and between themselves (for path merge, overlap/mask, clipping, etc.)
- Implement Bentley-Ottmann algorithm to find all line intersections (clipping)
Far future
- Support fill gradients and patterns (hard)
- Load in PDF, SVG and EPS and turn to paths/text
- Generate TeX-like formulas in pure Go, use OpenType math font such as STIX or TeX Gyre
Canvas
c := canvas.New(width, height float64)
ctx := canvas.NewContext(c)
ctx.Push()
ctx.Pop()
ctx.SetView(Matrix)
ctx.ComposeView(Matrix)
ctx.ResetView()
ctx.SetFillColor(color.Color)
ctx.SetStrokeColor(color.Color)
ctx.SetStrokeCapper(Capper)
ctx.SetStrokeJoiner(Joiner)
ctx.SetStrokeWidth(width float64)
ctx.SetDashes(offset float64, lengths ...float64)
ctx.DrawPath(x, y float64, *Path)
ctx.DrawText(x, y float64, *Text)
ctx.DrawImage(x, y float64, image.Image, dpm float64)
c.Fit(margin float64)
c.WriteFile(filename string, svg.Writer)
c.WriteFile(filename string, pdf.Writer)
c.WriteFile(filename string, eps.Writer)
c.WriteFile(filename string, rasterizer.PNGWriter(resolution DPMM))
c.WriteFile(filename string, rasterizer.JPGWriter(resolution DPMM, opts *jpeg.Options))
c.WriteFile(filename string, rasterizer.GIFWriter(resolution DPMM, opts *gif.Options))
rasterizer.Draw(c *Canvas, resolution DPMM) *image.RGBA
Canvas allows to draw either paths, text or images. All positions and sizes are given in millimeters.
Text
dejaVuSerif := NewFontFamily("dejavu-serif")
err := dejaVuSerif.LoadFontFile("DejaVuSerif.ttf", canvas.FontRegular)
ff := dejaVuSerif.Face(size float64, color.Color, FontStyle, FontVariant, ...FontDecorator)
text = NewTextLine(ff, "string\nsecond line", halign)
text = NewTextBox(ff, "string", width, height, halign, valign, indent, lineStretch)
richText := NewRichText()
richText.Add(ff, "string")
text = richText.ToText(width, height, halign, valign, indent, lineStretch)
ctx.DrawText(0.0, 0.0, text)
Note that the LoadLocalFont
function will use fc-match "font name"
to find the closest matching font.
Paths
A large deal of this library implements functionality for building paths. Any path can be constructed from a few basic commands, see below. Successive commands build up segments that start from the current pen position (which is the previous segments's end point) and are drawn towards a new end point. A path can consist of multiple subpaths which each start with a MoveTo command (there is an implicit MoveTo after each Close command), but be aware that overlapping paths can cancel each other depending on the FillRule.
p := &Path{}
p.MoveTo(x, y float64)
p.LineTo(x, y float64)
p.QuadTo(cpx, cpy, x, y float64)
p.CubeTo(cp1x, cp1y, cp2x, cp2y, x, y float64)
p.ArcTo(rx, ry, rot float64, largeArc, sweep bool, x, y float64)
p.Arc(rx, ry, rot float64, theta0, theta1 float64)
p.Close()
p = Rectangle(w, h float64)
p = RoundedRectangle(w, h, r float64)
p = BeveledRectangle(w, h, r float64)
p = Circle(r float64)
p = Ellipse(rx, ry float64)
p = RegularPolygon(n int, r float64, up bool)
p = RegularStarPolygon(n, d int, r float64, up bool)
p = StarPolygon(n int, R, r float64, up bool)
We can extract information from these paths using:
p.Empty() bool
p.Pos() (x, y float64)
p.StartPos() (x, y float64)
p.Coords() []Point
p.CCW() bool
p.Interior(x, y float64) bool
p.Filling() []bool
p.Bounds() Rect
p.Length() float64
These paths can be manipulated and transformed with the following commands. Each will return a pointer to the path.
p = p.Copy()
p = p.Append(q *Path)
p = p.Join(q *Path)
p = p.Reverse()
ps = p.Split() []*Path
ps = p.SplitAt(d ...float64) []*Path
p = p.Transform(Matrix)
p = p.Translate(x, y float64)
p = p.Flatten()
p = p.Offset(width float64)
p = p.Stroke(width float64, capper Capper, joiner Joiner)
p = p.Dash(offset float64, d ...float64)
Polylines
Some operations on paths only work when it consists of linear segments only. We can either flatten an existing path or use the start/end coordinates of the segments to create a polyline.
polyline := PolylineFromPath(p)
polyline = PolylineFromPathCoords(p)
polyline.Smoothen()
polyline.FillCount() int
polyline.Interior(x, y float64)
Path stroke
Below is an illustration of the different types of Cappers and Joiners you can use when creating a stroke of a path:
LaTeX
To generate outlines generated by LaTeX, you need latex
and dvisvgm
installed on your system.
p, err := ParseLaTeX(`$y=\sin\(\frac{x}{180}\pi\)$`)
if err != nil {
panic(err)
}
Where the provided string gets inserted into the following document template:
\documentclass{article}
\begin{document}
\thispagestyle{empty}
{{input}}
\end{document}
Examples
See https://github.com/tdewolff/canvas/tree/master/examples for a working examples.
License
Released under the MIT license.