AI disclosure: This package and its documentation were developed with AI assistance.
cetz-fields is a pure-Typst library for drawing two-dimensional diagrams of the
three-dimensional Coulomb field with CeTZ.
It provides field-line and vector-field renderings, accepts CeTZ coordinates,
and uses bounded deterministic integration.
#import "@preview/cetz-fields:0.1.0": charge-diagram
#charge-diagram(
(
(position: (-1, 0), charge: 1),
(position: (1, 0), charge: -1),
),
mode: "field-lines",
domain: ((-3, 3), (-2, 2)),
)
API
charge-diagram(charges, ...)
Creates a self-sizing CeTZ canvas. Important options:
mode:"field-lines"(default) or"vectors".domain:((x-min, x-max), (y-min, y-max)); required by the numerical methods and used as the exact canvas extent.lines-per-charge: constant number seeded uniformly around every nonzero source. It does not vary with charge magnitude.charge-thickness-factor: field-line thickness isline-stroke × (1 + factor × abs(charge)).step,max-steps: RK4 field-line resolution and hard work bound.softening: Plummer softening length. The mathematically exact field is the default (0); a small positive value can smooth crowded diagrams.marker-radius(default0.22) sets the global minimum particle radius. A charge’sradiussets its local minimum. Visible labels are measured and the circle grows when needed to contain them.label-padding(default0.06) controls the space between a label and its circle.hit-radius,line-stroke,line-color,arrow-position, andarrow-scaleprovide further presentation controls.line-colordefaults to Typstblack, which Typst Mate maps to the active Obsidian text color.vector-samplescontrols vector-grid density.vector-length-modeselects"saturating","linear","sqrt","log", or"normalized"magnitude scaling.vector-scale,vector-min-length, andvector-max-lengthtune and clamp the result;vector-min-fieldomits near-zero vectors.vector-arrowheaddefaults to CeTZ’s filled stealth arrow (">>"), andvector-arrow-scalecontrols its size independently of field-line arrows.vector-gradientaccepts a Typst gradient, normallygradient.linear(...).vector-color-minandvector-color-maxmap the corresponding $abs(F)$ values to the gradient endpoints and clamp values outside that interval. Either endpoint may beautoto use the sampled field extrema.show-charges,labels,positive-color,negative-color.
Each charge is a dictionary:
(name: "source", position: (0, 0), charge: 1, label: [$q_1$],
color: red, opacity: 28%, radius: 0.18, line-color: black)
q is accepted as an alias for charge. Labels may be arbitrary Typst
content. Positive particles use red and negative particles blue, following the
usual charge-diagram convention. Their fills are translucent (particle-opacity: 28% globally or opacity per charge), while labels and borders use
line-color. This keeps them legible with both light and dark theme text.
A named charge exposes the standard CeTZ circle anchors, such as
"source.center", "source.north", or (name: "source", anchor: 30deg).
Named charges retain invisible circle anchors when show-charges: false.
Field-line anchors
Request named anchors for selected field lines with anchors. The nearest
uniformly seeded line to field-line is assigned the requested CeTZ path name:
draw-charge-diagram(
(
(name: "positive", position: (-1, 0), charge: 1),
(name: "negative", position: (1, 0), charge: -1),
),
domain: ((-3, 3), (-2, 2)),
anchors: (
(particle: "positive", field-line: 42deg, name: "selected-line"),
),
)
// Available after draw-charge-diagram in the same canvas:
content("selected-line.mid", [midpoint])
content((name: "selected-line", anchor: 25%), [quarter point])
The named path provides CeTZ’s standard start, mid, end, and percentage
anchors. Selection uses the field line’s seed angle around the named particle.
Path direction follows the electric field, so a negative source lies at the
path’s end, not its start.
CeTZ coordinates and anchors
draw-charge-diagram draws into an existing canvas and supports every position
resolved by cetz.coordinate.resolve, including named anchors created earlier:
#import "@preview/cetz:0.5.2"
#import "@preview/cetz-fields:0.1.0": draw-charge-diagram
#cetz.canvas({
import cetz.draw: *
rect((-2, -1), (2, 1), name: "frame")
draw-charge-diagram(
((position: "frame.west", charge: 1),
(position: "frame.east", charge: -1)),
domain: ((-3, 3), (-2, 2)),
)
})
The standalone charge-diagram also accepts a setup: block for defining
anchors before the field is processed.
electric-field(point, charges, softening: 0)
Returns the numerical field
E(r) = sum_i q_i (r-r_i) / |r-r_i|^3.
This helper requires numeric positions because no CeTZ canvas context exists. The omitted physical constant only changes the choice of units.
A magnitude-colored vector plot can be configured as follows:
#charge-diagram(
charges,
mode: "vectors",
domain: ((-3, 3), (-2, 2)),
vector-length-mode: "sqrt",
vector-scale: 0.18,
vector-min-length: 0.04,
vector-max-length: 0.32,
vector-arrowhead: ">>",
vector-arrow-scale: 0.62,
vector-gradient: gradient.linear(
rgb("440154"), rgb("21918c"), rgb("fde725"),
),
vector-color-min: 0.05,
vector-color-max: 2.5,
)
The scaling formulas before min/max clamping are: scale × |F| for linear,
scale × sqrt(|F|) for square-root, scale × ln(1 + |F|) for logarithmic,
and max-length × (1 − exp(−scale × |F|)) for saturating mode. Normalized mode
uses vector-max-length for every vector.
Numerical method
Field lines solve the normalized ODE dr/ds = ±E/|E| using classical RK4.
Positive sources integrate with the field and negative sources against it;
negative paths are reversed before drawing so arrows always indicate the
physical field direction. Traces stop at the domain, a different charge, a
field null, or max-steps. Exact rectangle intersection avoids overshooting
plot boundaries.
Development
The environment tracks unstable nixpkgs so pkgs.typst is the newest packaged
Typst release.
devenv shell
check
Dependencies use Typst’s package imports (@preview/cetz:0.5.2); no vendored
runtime or non-Typst implementation code is used.