typst
typst copied to clipboard
Suggestions for improvements to core drawing functionality
Description
With the long-term use of the Typst drawing function, some functional or performance shortcomings gradually appeared. The famous drawing libraries TikZ and PSTricks in the LaTeX world, are good reference. Suggested improvements to the core drawing functionality are detailed below. I expect some of the functionality to be difficult to implement or cause breaking changes in interfaces, but they are worth considering as a direction for future improvements.
Introduce new data types and related functions
vec2
vec2
is a pair of real numbers, which can be seen as a point or a vector on a plane, or as a complex number. The vec2
type and related operations are frequently used in various graphics processing, so the built-in implementation of them can help improve performance. Typical usage examples might be as follows:
#let v = vec2(1.5, 3.0) // create a `vec2` object
#(vec2(1, 2) + vec2(3, 4)) // addition
#(vec2(1, 2) * 4.5) // scale
#vec2(3.0, 4.0).length() // length of vector
#vec2(3.0, 4.0).angle() // angle of vector
#vec2(3.0, 4.0).dot(v) // dot of two vectors
#v.min() // minimum of two elements in vector
#v.max() // maximum of two elements in vector
transform
transform
implement affine transform in 2D geometry. Internally, it is denoted by t = (t.x, t.y, t.xx, t.xy, t.yx, t.yy)
. A point (x, y)
is transformed by the transform t to (x', y')
, where
x' = t.x + t.xx * x + t.xy * y
y' = t.y + t.yx * x + t.yy * y
Transforms can be applied to pairs, paths and graphics by multiplication on the left. And they can also be composed with one another and inverted.
Constructor for transform
object may be:
#let transform.identity() -> transform // identity transform
#let transform.translate(dx, dy) -> transform // translate transform
#let transform.scale(sx, sy) -> transform // scale transform
#let transform.rotate(angle, pt) -> transform // rotate transform by angle about point pt
#let transform.matrix(x, y, xx, xy, yx, yy) -> transform // general transform
The functions for transform
object might be as follows (suppose t1
and t2
are transform
objects):
#t1.mat() // internal matrix elements of transform
#t1.lmul(t2) // transform multiplication
#t1.inv() // inverse of transform
#vec2(1.5, 2.0).lmul(t1) // transform vector using t1
path
Currently Typst does not have an explicit path object for user, instead the path objects is tightly coupled to the drawing function (implemented as path
function). This approach is not flexible enough and introduce some troubles for future extention. Based on the idea that path definition is separated from drawing, as in mainstream drawing library. A dedicated path
type could be defined.
A path is specified as a list of points interconnected with straight line segments or cubic spline segments, and it can be closed or open. Constructor function for path
object may be:
#let path(closed: [true | false], vertices) -> path
Where vertices
is array of vertices of the path. Each vertex can be defined in 2 ways:
- A regular point (
vec2
type). Create a straight line segment with previous vertex. - An array of three points (array of
vec2
type), the first two being the control points, and the last one being the vertex. Create a cubic spline segment with previous vertex.
Several paths can be group into a path array, i.e. array of path
, as a compound path. For example, a rectangle with a hole in it.
#let p-square = path(...) // path of square
#let p-circle = path(...) // path of circle
#let ps = (p-square, p-circle) // compound path
The functions for path
object might be as follows:
#let p1 = path(
closed: true,
( vec2(0, 0), (vec2(4, 2), vec2(6, 2), vec2(10, 0)) )
) // create a closed path
#p1.closed() // whether path is closed
#p1.vertices() // array of vertices of the path
#p1.size() // the number of nodes in the path
#p1.reverse() // return reversed path
#p1.min() // pair (left,bottom) for the bounding box of path
#p1.max() // pair (right,top) for the bounding box of path
#p1.lmul(t1) // return transformed path using t1
#p1.sub-path(a, b) // return the subpath of p1 running from node a to node b
#path.merge-path(p1, p2, p3) // return merged path p1, p2, p3
Path transformation operations are computationally intensive and frequently present in graphics processing, so the built-in implementation of them can help improve performance.
More path-related functions can be implemented by the extension library.
Refine drawing functions
The following drawing functions need to be provided. They are all path-based.
#let draw(draw-pen: [`color` | ...], [`path` | array of `path`])
#let fill(fill-pen: [`color` | ...], fill-rule: ["nonzero" | "evenodd"], [`path` | array of `path`])
#let filldraw(fill-pen: [`color` | ...], draw-pen: [`color` | ...], fill-rule: ["nonzero" | "evenodd"], [`path` | array of `path`])
#let clip(clip-rule: ["nonzero" | "evenodd"], [`path` | array of `path`])
The first three functions stroke and/or fill path(compound path) using predefined color, gradient, etc. Note that both nonzero and evenodd fill rules should be supported (except in function draw
).
The last function clip
implements cliping the current drawing contents to the region bounded by the path(compound path), where both nonzero and evenodd clip rules are supported. This function may be complicated to implement, and the scope of the clipping operation needs to be more sensibly defined.
These drawing functions are already available in the library TikZ, PSTricks and in SVG specification, so Typst should consider refine these in the future.
Use Case
The following graphics would be relatively easy to draw if Typst supports the previously mentioned improvements.
A path of a circle with 5 holes in it fills the gradient, and lay on top of the texts.
Note that circles and ellipses can’t be reproduced exactly with Bézier curves, but Typst already approximates them anyway.
Indeed, circles, ellipses, arcs, and elliptical arcs can all be approximated using no more than 4 cubic Bézier curves. Further, general curves (not ill-conditioned) can also be approximated using a series of straight and cubic Bézier curve segments.
Not having a clip rule / compound path is a serious limitation, which currently prevents me from implementing the plt donuts example in https://github.com/daskol/typst-mpl-backend