Skip to main content

Coordinate Systems

A coordinate is a position on the canvas on which the picture is drawn. They take the form of dictionaries and the following sub-sections define the key value pairs for each system. Some systems have a more implicit form as an array of values and CeTZ attempts to infer the system based on the element types.

XYZ

Defines a point x units right, y units upward and z units away.

Default: 0

The number of units in the x direction.

Default: 0

The number of units in the y direction.

Default: 0

The number of units in the z direction.

The implicit form can be given as an array of two or three numbers, as in (x, y) or (x, y, z)

line((0,0), (x: 1))
line((0,0), (y: 1))
line((0,0), (z: 1))

// Implicit form
line((2, 0), (3, 0))
line((2, 0), (2, 1, 0))
line((2, 0), (2, 0, 1))

Previous

Use this to reference the position of the previous coordinate passed to a draw function. It takes the form of an empty array () and the previous position initially will be (0, 0, 0). This will never reference the position of a coordinate used to define another coordinate.

line((0,0), (1, 1))

// Draws a circle at (1,1)
circle(())

Relative

Places the given coordinate relative to the previous coordinate. Or in other words, for the given coordinate, the previous coordinate will be used as the origin. Another coordinate can be given to act as the previous coordinate instead.

The coordinate to place relative to the previous coordinate.

update:

Default: true

When false the previous the previous position will not be updated.

Default: ()

The coordinate to treat as the previous coordinate.

In the example below, the red circle is placed one unit to the right of the blue circle. If the blue circle was to be moved a different position, the red circle will move with the blue circle to stay one unit to the right.

circle((0, 0), stroke: blue)
circle((rel: (1, 0)), stroke: red)

Polar

Defines a point that is radius distance away from the origin at the given angle.

angle:

The angle of the coordinate. An angle of 0deg is to the right, a degree of 90deg is upward.

radius:

The distance from the origin. An array of number can be given, in the form (x, y), to define the x and y radii of an ellipse instead of a circle.

line((0, 0), (angle: 30deg, radius: 1))

The implicit form is an array of the angle then the radius (angle, radius) or (angle, (x, y)).

line(
(0, 0),
(30deg, 1),
(60deg, 1),
(90deg, 1),
(120deg, 1),
(150deg, 1),
(180deg, 1)
)

Barycentric

In the barycentric coordinate system a point is expressed as the linear combination of multiple vectors. The idea is that you specify vectors v1,v2,...,vnv_1, v_2, ..., v_n and numbers α1,α2,...,αn\alpha_1, \alpha_2, ..., \alpha_n. Then the barycentric coordinate specified by these vectors and numbers is

α1v1+α2v2++αnvnα1+α2++αn\frac{\alpha_1 v_1 + \alpha_2 v_2 + \cdots + \alpha_n v_n}{\alpha_1 + \alpha_2 + \cdots + \alpha_n}

A dictionary where the key is a named element and the value is a float. The center anchor of the named element is used as vv and the value is used as α\alpha

circle((90deg, 3), radius: 0, name: "content")
circle((210deg, 3), radius: 0, name: "structure")
circle((-30deg, 3), radius: 0, name: "form")

for (c, a) in (
("content", "south"),
("structure", "north"),
("form", "north")
) {
content(c, align(center, c + [\ oriented]), padding: .1, anchor: a)
}

stroke(gray + 1.2pt)
line("content", "structure", "form", close: true)

for (c, s, f, cont) in (
(0.5, 0.1, 1, "PostScript"),
(1, 0, 0.4, "DVI"),
(0.5, 0.5, 1, "PDF"),
(0, 0.25, 1, "CSS"),
(0.5, 1, 0, "XML"),
(0.5, 1, 0.4, "HTML"),
(1, 0.2, 0.8, "LaTeX"),
(1, 0.6, 0.8, "TeX"),
(0.8, 0.8, 1, "Word"),
(1, 0.05, 0.05, "ASCII")
) {
content(
(bary: (
content: c,
structure: s,
form: f
)),
cont,
fill: rgb(50, 50, 255, 100),
stroke: none,
frame: "circle"
)
}

Anchor

Defines a point relative to a named element using anchors, see Anchors.

name:

The name of the element that you wish to use to specify a coordinate.

anchor:

str or angle or number or ratio or none
Default: none

The anchor of the element. Strings are named anchors, angles are border anchors and numbers and ratios are path anchors. If not given, the default anchor will be used, on most elements this is center but it can be different or not exist at all!

circle((0,0), name: "circle")
// Anchor at 30 degree
content((name: "circle", anchor: 30deg), box(fill: white, $ 30 degree $))
// Anchor at 30% of the path length
content((name: "circle", anchor: 30%), box(fill: white, $ 30 % $))
// Anchor at 3.14 of the path
content((name: "circle", anchor: 3.14), box(fill: white, $ p = 3.14 $))

You can also use implicit syntax of a dot separated string in the form "name.anchor" for all anchors.

line((0, 0), (4, 3), name: "line")
circle("line.75%", name: "circle")
rect("line.start", "circle.east")

Tangent

This system allows you to compute the point that lies tangent to a shape. In detail, consider an element and a point. Now draw a straight line from the point so that it "touches" the element (more formally, so that it is tangent to this element). The point where the line touches the shape is the point referred to by this coordinate system.

element:

The name of the element on whose border the tangent should lie.

The point through which the tangent should go.

solution:

Which solution should be used if there are more than one.

A special algorithm is needed in order to compute the tangent for a given shape. Currently it does this by assuming the distance between the center and top anchor (See Anchors) is the radius of a circle.

grid((0,0), (3,2), help-lines: true)

circle((3,2), name: "a", radius: 2pt)
circle((1,1), name: "c", radius: 0.75)
content("c", $ c $, anchor: "north-east", padding: .1)

stroke(red)
line(
// The starting point
"a",
// The tangent coordinate
(element: "c", point: "a", solution: 1),
// The center of the circle
"c",
// The other tangent coordinate
(element: "c", point: "a", solution: 2),
close: true
)

Perpendicular

Can be used to find the intersection of a vertical line going through a point pp and a horizontal line going through some other point qq.

horizontal:

The coordinate through which the horizontal line passes.

vertical:

The coordinate through which the vertical line passes.

You can use the implicit syntax of (horizontal, "-|", vertical) or (vertical, "|-", horizontal).

set-style(content: (padding: .05))
content((30deg, 1), $ p_1 $, name: "p1")
content((75deg, 1), $ p_2 $, name: "p2")

line((-0.2, 0), (1.2, 0), name: "xline")
content("xline.end", $ q_1 $, anchor: "west")

line((2, -0.2), (2, 1.2), name: "yline")
content("yline.end", $ q_2 $, anchor: "south")

line("p1.south-east", (horizontal: (), vertical: "xline.end"))
line("p2.south-east", ((), "|-", "xline.end")) // Short form
line("p1.south-east", (vertical: (), horizontal: "yline.end"))
line("p2.south-east", ((), "-|", "yline.end")) // Short form

Interpolation

Use this to linearly interpolate between two coordinates a and b with a given distance number. If number is a number the position will be at the absolute distance away from a towards b, a ratio can be given instead to be the relative distance between a and b. An angle can also be given for the general meaning: "First consider the line from a to b. Then rotate this line by angle around point a. Then the two endpoints of this line will be a and some point c. Use the point c for the subsequent computation."

The coordinate to interpolate from.

The coordinate to interpolate to.

number:

The distance between a and b. A ratio will be the relative distance between the two points, a number will be the absolute distance between the two points.

angle:

Default: 0deg

Angle between AB\vec{AB} and AP\vec{AP}, where PP is the resulting coordinate. This can be used to get the normal for a tangent between two points.

Can be used implicitly as an array in the form (a, number, b) or (a, number, angle, b).

grid((0,0), (3,3), help-lines: true)

line((0,0), (2,2), name: "a")
for i in (0%, 20%, 50%, 80%, 100%, 125%) { // Relative distance
content(("a.start", i, "a.end"),
box(fill: white, inset: 1pt, [#i]))
}

line((1,0), (3,2), name: "b")
for i in (0, 0.5, 1, 2) { // Absolute distance
content(("b.start", i, "b.end"),
box(fill: white, inset: 1pt, text(red, [#i])))
}


grid((0,0), (3,3), help-lines: true)
line((1,0), (3,2))
line((1,0), ((1, 0), 1, 10deg, (3,2)))
fill(red)
stroke(none)
circle(((1, 0), 50%, 10deg, (3, 2)), radius: 2pt)


grid((0,0), (4,4), help-lines: true)

fill(black)
stroke(none)
let n = 16
for i in range(0, n+1) {
circle(((2,2), i / 8, i * 22.5deg, (3,2)), radius: 2pt)
}

You can even chain them together!

grid((0,0), (3, 2), help-lines: true)
line((0,0), (3,2))
stroke(red)
line(((0,0), 0.3, (3,2)), (3,0))
fill(red)
stroke(none)
circle(
( // a
(((0, 0), .3, (3, 2))),
0.7,
(3,0)
),
radius: 2pt
)


grid((0,0), (3, 2), help-lines: true)
line((1,0), (3,2))
for (l, c) in ((0cm, "0cm"), (1cm, "1cm"), (15mm, "15mm")) {
content(((1,0), l, (3,2)), box(fill: white, $ #c $))
}

Interpolation coordinates can be used to get the normal on a tangent:

let (a, b) = ((0,0), (3,2))
line(a, b)
// Get normal for tangent from a to () with distance .5, at a
circle(a, radius: .1, fill: black)
line((a, .7, b), (a: (), b: a, number: .5, angle: 90deg), stroke: red)

Function

An array where the first element is a function and the rest are coordinates will cause the function to be called with the resolved coordinates. The resolved coordinates will be given as a vector that represents an xyz point in space.

The example below shows how to use this system to create an offset from an anchor, however this could easily be replaced with a relative coordinate with the to argument set.

circle((0, 0), name: "c")
fill(red)
circle((v => cetz.vector.add(v, (0, -1)), "c.west"), radius: 0.3)