mp-tikz-wasm GitHub ↗
mp-tikz-wasm · version 0.5.0 · MetaPost 2.11, pdfTeX 1.40, LuaTeX 1.21, dvisvgm 3.4.3

MetaPost and TikZ in the browser, with real LaTeX. John Hobby's MetaPost, pdfTeX, LuaTeX and dvisvgm are compiled to WebAssembly and wrapped in one TypeScript library: MetaPost source becomes SVG with real glyph outlines, EPS and structured JSON, and whole LaTeX/TikZ documents become one SVG per page — shadings, patterns, opacity, pgfplots and all. No server, no web fonts, and output that is byte-identical to TeX Live's own mpost and latex + dvisvgm.

4.8 MBof WebAssembly (2.1 MB gzipped) for MetaPost, pdfTeX and dvisvgm; LuaTeX adds 4.2 MB on demand
1181 / 1181pages of the PGF/TikZ manual identical to TeX Live 2025
~15 msfor a MetaPost figure; ~100 ms for a TikZ figure
0 serverseverything runs in the page or in Node

Get it

Two ways. The prebuilt archive is what most people want: it contains the compiled engines, the JavaScript and the font, format and package bundles, and needs neither TeX Live nor Emscripten.

1. Prebuilt release

  1. Download mp-tikz-wasm-0.5.0.tar.gz (or .zip) from the releases page and unpack it.
  2. Host the dist/ folder on any static web server (the files are plain; nothing needs special headers). To try it locally: node serve.mjs 8080 inside the unpacked folder, then open http://localhost:8080/site/.
  3. Add one line to your page — see Drop-in tags — or import { MetaPost } from '/path/to/dist/index.js'.

2. From source

git clone https://github.com/jmckalex/mp-tikz-wasm.git
cd mp-tikz-wasm
./scripts/extract-vendor.sh      # fetch and verify the pinned TeX Live 2025 source (111 MB)
./scripts/verify-pin.sh          # assert the mplib API the design relies on
npm install
make contract                    # native build + the L0 contract harness (46 checks)
./scripts/native-texlive.sh      # native web2c pass: generates pdfTeX's C (once, ~5 min)
./scripts/native-dvisvgm.sh      # native configure of dvisvgm: config.h (once)
npm run build                    # mplib.wasm, tex.wasm, dvisvgm.wasm, texmf tree, formats, bundles, TypeScript
npm test                         # unit + end-to-end tests
npm run demo                     # http://localhost:8080/site/

Prerequisites: a C compiler, Node ≥ 20, the Emscripten SDK (.emsdk-version pins 6.0.9) and a TeX Live 2025 installation, which is both the source of the bundled texmf files and the oracle the tests compare against. See Building for what each step produces.

Drop-in tags

This is the tikzjax-style integration: one script, and every diagram tag on the page is replaced by its SVG.

<script type="module" src="/path/to/dist/auto.js"></script>

<script type="text/tikz" data-libraries="arrows.meta,calc">
  \begin{tikzpicture}
    \draw[->,thick] (0,0) -- (2,1) node[right] {$x^2$};
  \end{tikzpicture}
</script>

<script type="text/metapost">
  draw fullcircle scaled 50 withpen pencircle scaled 1.2;
  label.top(btex $\int_0^1 x\,dx$ etex, (0,25));
</script>

<tikz-diagram data-libraries="shadings">\shade[ball color=red] (0,0) circle (1);</tikz-diagram>
<metapost-diagram>input boxes; boxit.a("hello"); drawboxed(a);</metapost-diagram>

Ids, classes and slides

The bundled TikZ library svg.attributes puts attributes on the SVG a scope, path or node produces. Load it with \usetikzlibrary{svg.attributes}, or data-libraries="svg.attributes" on a tag; it is in the default bundles.

The attributes land on the <g> PGF itself opens for that scope, path or node, so nothing has to be balanced by hand, and a nested scope does not inherit them. Under a driver that is not SVG (pdfTeX, dvips) the keys do nothing, so the same source still builds with pdflatex. A clip path gets no group of its own, so its attributes are dropped. Classes arrive as written; ids are prefixed per figure by the tags (id="box" becomes mpwHASH-box, so two figures cannot collide), so select by class, or by [id$="-box"].

data-replace on a <tikz-diagram> or <metapost-diagram> replaces the element, once it has rendered, by the <svg> itself; the element's id, class and style move to the SVG root. The figure is then ordinary SVG in the page, and static: it no longer re-typesets. A failed render keeps the element and its diagnostics. The mp-tikz-wasm:rendered event is dispatched from the SVG after the swap, with replaced: true.

Together they give reveal.js fragments inside a diagram:

<section>
  <tikz-diagram data-replace data-libraries="svg.attributes" class="r-stretch">
    \draw[thick] (0,0) rectangle (4,3);
    \begin{scope}[svg class=fragment]\fill[red!60] (1,1) circle (0.5);\end{scope}
    \begin{scope}[svg class={fragment fade-up}]\fill[blue!60] (3,1) circle (0.5);\end{scope}
    \node[svg class=fragment, svg attributes={data-fragment-index="0"}] at (2,2.5) {first};
  </tikz-diagram>
</section>

Checked with reveal.js 5: the three parts step in order, data-fragment-index honoured, with no Reveal.sync() — reveal looks fragments up as it navigates, and its stylesheet hides a .fragment as soon as the SVG arrives.

The engines run in a Web Worker, so the page never blocks; there is a 20 s watchdog per run.

Library API

import { MetaPost } from './dist/index.js';   // browser (a Worker) or Node (in-process)

const mp = await MetaPost.create();          // loads the engines and the bundle manifests

// MetaPost -> SVG / EPS / JSON
const r = await mp.run(`
  beginfig(1);
    draw fullcircle scaled 100;
    label.top(btex $\\int_0^\\infty e^{-x^2}dx$ etex, (0,50));
  endfig; end.`, { format: ['svg', 'eps', 'json'] });
r.status            // 'ok' | 'warning' | 'error' | 'fatal'  (MetaPost's own history)
r.figures[0].svg    // glyph outlines, self-contained
r.figures[0].eps    // what mpost writes, byte for byte
r.figures[0].json   // typed knots, pens, colours, dashes, text runs, clips
r.diagnostics       // [{ severity, source, message, help[], file, line }]
r.stats             // metapostMs, texMs, texRuns, snippetCacheHits ...

// LaTeX / TikZ -> one SVG per page
const t = await mp.latex(String.raw`
  \documentclass[tikz,border=2pt]{standalone}
  \begin{document}\begin{tikzpicture}
    \shade[ball color=blue!60] (0,0) circle (1);
  \end{tikzpicture}\end{document}`);
t.pages[0]          // SVG
t.diagnostics       // TeX errors with document line numbers, package warnings
(await mp.latex(doc, { output: 'pdf' })).pdf   // or a PDF (Uint8Array), from the engine's own PDF back end
t.stats             // texMs, dvisvgmMs, format ('latex', 'tikz', 'dvilualatex' …)

// TikZ graph drawing needs LuaTeX: engine 'auto' switches to luatex.wasm by itself
const g = await mp.latex(String.raw`
  \documentclass[tikz]{standalone}
  \usetikzlibrary{graphs,graphdrawing} \usegdlibrary{layered}
  \begin{document}\tikz\graph[layered layout]{a -> {b, c} -> d};\end{document}`, { engine: 'auto' });
OptionWhereMeaning
bundles, bundleBaseUrlcreatewhich texmf bundles to load and from where (default: DEFAULT_BUNDLES, next to the script; add 'opentype' and 'otf-fonts' for fontspec and unicode-math, 'ghostscript' for PostScript: PSTricks, EPS, graphicx's rotation and scaling)
numberSystemcreatescaled (mpost's default), double, decimal
texcreate / runlabel engine for btex: auto (LaTeX when the preamble says so), plain, etex, latex, none
deterministiccreatefixed random seed and frozen date, so two runs are byte-identical (default on)
runScript, makeText, onFindFilecreateJavaScript hooks for runscript, for overriding label typesetting, and for last-chance file lookup (in-process mode)
worker, timeoutMs, snapshot, prefetchcreateWorker vs in-process; stall watchdog (a run is killed only when nothing happens for timeoutMs, so a slow first load is not cut short); prefetch: ['latex'] fetches a first run's files in parallel; default snapshot policy
logLevel, loggercreatewhat each run writes to the console: silent, error, warn (default: errors and warnings, a MetaPost error with its help text), info (one line per run and per engine pass, with timings), debug (the engines' own terminal output, live) or trace (every file looked up or fetched); mp.logLevel changes it at any time; logger receives the records instead of the console, mp.on('record') as well
format, prologues, internals, files, jobNamerunoutputs; prologues (3 for SVG outlines); -s name=value internals; extra files placed next to the job
engine, fonts, pages, bbox, dvisvgmArgs, snapshot, fileslatexlatex | lualatex | luatex | plain | tex | auto; outlines or woff2; page selection; dvisvgm's bbox; extra arguments; the pre-warmed format
outputlatexsvg (default) or pdf: pdfTeX's or LuaTeX's own PDF back end, returned as result.pdf (a Uint8Array); fonts embedded, PNG and JPEG images included; byte-identical to TeX Live's latex -output-format=pdf apart from the version string. One pass per call: hand artifacts back as files for cross-references

Also exported: MetaPostPool for batch work across several workers, postProcessSvg (numeric compaction, id namespacing), scanTexBlocks, splitMpx, parseMetaPostLog.

Untrusted sources. MetaPost's special and dvisvgm's raw specials put arbitrary markup into the SVG, so a diagram written by someone else can carry <script> or event handlers. The tags insert SVG as is, which is right for a page's own diagrams. For anyone else's, sanitise with DOMPurify (USE_PROFILES: { svg: true, svgFilters: true }, ADD_TAGS: ['use'], then drop any <use> whose href is not #…); the README has the function. The old sanitizeSvg export is deprecated: it strips shadings, patterns and web fonts.

Node

The same API runs in-process in Node with the bundles read from disk; nothing is fetched. MetaPost.create({ texmfDir }) can point at a texmf tree in the flattened layout instead of bundles.

Command line

npx mpost-wasm figure.mp                                  # figure.1, figure.2 …  like mpost
npx mpost-wasm -s 'outputformat="svg"' -s prologues=3 figure.mp
npx mpost-wasm -tex=latex -numbersystem=double -jobname=out figure.mp
npx mpost-wasm --latex figure.tex                         # figure-1.svg, figure-2.svg …
npx mpost-wasm --latex --plain --fonts=woff2 figure.tex
npx mpost-wasm --latex --engine=lualatex graph.tex                # or --engine=auto (the default)
npx mpost-wasm -vv figure.mp                                      # the engines' output on stderr as it runs (-v timings, -vvv every file, -q silence)
npx mpost-wasm --prerender page.html                              # figures/figure-HASH.svg for every diagram tag on the page

mpost-wasm accepts the usual mpost flags (-interaction, -jobname, -tex, -s, -halt-on-error, -recorder; -troff with a warning), writes output files with mpost's outputtemplate naming, prints the transcript, and exits 1 on error. -v, -vv, -vvv and -q (or --log-level=) set how much goes to stderr.

What it does — MetaPost

Everything below was rendered by the wasm engines when this page was built; open “source” under any figure for the exact input. The output of the MetaPost side is byte-identical to native mpost on the golden corpus, because it is MetaPost: the C output is left untouched.

Paths, tension and curl — MetaPost's path syntax: .. for smooth curves solved by Hobby's algorithm, -- for straight lines, tension and curl control, explicit controls, cycles. 15 ms MetaPost
source (MetaPost)
beginfig(1);
  draw (0,0)..(30,40)..(60,0) withcolor (0.1,0.3,0.8);
  draw (0,50)..tension 2..(30,90)..(60,50);
  draw (80,0){up}..{right}(140,60);
  draw (80,80){curl 0}..(110,120)..{curl 2}(140,80) withpen pencircle scaled 1.2;
  draw (160,0)..(190,40)..(220,0)..cycle;
  draw (160,60)..controls (170,110) and (210,110)..(220,60) dashed evenly;
endfig; end.
Pens, joins, caps and dashes — Elliptical and polygonal pens are real pens: a stroke is the envelope of the pen dragged along the path, as in the original program. 3 ms MetaPost
source (MetaPost)
beginfig(1);
  draw (0,0)--(50,50) withpen pencircle scaled 6;
  draw (60,0)--(110,50) withpen pensquare scaled 6;
  draw (120,0)--(170,50) withpen pencircle xscaled 8 yscaled 2 rotated 30;
  linecap := butt; linejoin := mitered;
  draw (0,70)--(25,120)--(50,70) withpen pencircle scaled 6;
  linejoin := beveled; draw (60,70)--(85,120)--(110,70) withpen pencircle scaled 6;
  linejoin := rounded; linecap := rounded; draw (120,70)--(145,120)--(170,70) withpen pencircle scaled 6;
  draw (0,140)--(170,140) dashed evenly scaled 2 withpen pencircle scaled 2;
  draw (0,160)--(170,160) dashed withdots scaled 2 withpen pencircle scaled 3;
endfig; end.
Fills, colour models and buildcycle — Grey, RGB and CMYK colours, unfill, filldraw, and buildcycle to fill the intersection of two shapes. 2 ms MetaPost
source (MetaPost)
beginfig(1);
  fill fullcircle scaled 50 shifted (25,25) withcolor (0.9,0.2,0.2);
  fill unitsquare scaled 40 shifted (60,5) withcolor 0.5;
  fill fullcircle scaled 50 shifted (135,25) withcmykcolor (1,0,0,0);
  filldraw fullcircle scaled 40 shifted (185,25) withcolor (0.2,0.7,0.3) withpen pencircle scaled 2;
  path a, b; a := fullcircle scaled 60 shifted (30,90); b := fullcircle scaled 60 shifted (60,90);
  draw a; draw b; fill buildcycle(a,b) withcolor (0.2,0.4,0.9);
  fill fullcircle scaled 60 shifted (150,90); unfill fullcircle scaled 30 shifted (150,90);
endfig; end.
Clipping and bounds — Pictures are values: build one, clip it to a path, take its bounding box, and draw the result. 2 ms MetaPost
source (MetaPost)
beginfig(1);
  picture pic; pic := image(for i = -8 upto 8: draw (i*10,-70)--(i*10,70) withpen pencircle scaled 1.5 withcolor (0.5+i/16, 0.3, 0.8-i/16); endfor);
  clip pic to fullcircle scaled 130;
  draw pic; draw bbox pic dashed evenly withcolor 0.5;
  picture q; q := image(fill fullcircle scaled 30 withcolor (1,0.6,0););
  setbounds q to unitsquare scaled 12 shifted (-6,-6);
  draw q shifted (110,0); draw bbox q shifted (110,0);
endfig; end.
Text without TeX: Type 1 outlines — A plain label needs no TeX at all: metrics come from the TFM file and the glyphs are Computer Modern Type 1 outlines converted to SVG paths by MetaPost's own backend (prologues:=3). No web fonts are involved. 3 ms MetaPost
source (MetaPost)
prologues := 3;
beginfig(1);
  path p; p := fullcircle scaled 120;
  draw p withpen pencircle scaled 1;
  label.top("north", point 2 of p); label.bot("south", point 6 of p);
  label.lft("west", point 4 of p); label.rt("east", point 0 of p);
  label("MetaPost" infont "cmbx10" scaled 1.6, (0,12));
  label("cmti10 italic" infont "cmti10", (0,-30));
  dotlabel.lrt("origin", origin);
endfig; end.
Labels typeset by plain TeX — btex … etex blocks are typeset by a real pdfTeX (DVI mode) in one batched run per document, converted by MetaPost's own dvitomp, and cached by content hash. 3 ms MetaPost, 51 ms TeX
source (MetaPost)
prologues := 3;
beginfig(1);
  numeric a; a := 60;
  draw (-a,0)--(a,0) withcolor .6white; draw (0,-a)--(0,a) withcolor .6white;
  draw (-a, a*sind(-90)) for t=-89 upto 90: ..(t*a/90, a*sind(t)) endfor withpen pencircle scaled 1.2 withcolor (0.1,0.3,0.8);
  label.top(btex $y=\sin\theta$ etex, (a/2, a*.9));
  label.bot(btex $-\pi$ etex, (-a,0)); label.bot(btex $\pi$ etex, (a,0));
  label.rt(btex $\displaystyle\int_{-\pi}^{\pi}\sin\theta\,d\theta = 0$ etex, (a+8, -a*.8));
endfig; end.
Labels typeset by LaTeX — A verbatimtex preamble with \documentclass switches the label engine to LaTeX automatically. amsmath, amssymb, tabular, any package in the bundles. 2 ms MetaPost, 149 ms TeX
source (MetaPost)
verbatimtex
\documentclass{article}
\usepackage{amsmath,amssymb}
\begin{document}
etex
prologues := 3;
beginfig(1);
  draw fullcircle scaled 110 withpen pencircle scaled .8;
  label.top(btex $\displaystyle\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}$ etex, (0,55));
  label.bot(btex $\begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix}$ etex, (0,-55));
  label.lft(btex $\forall x \in \mathbb{R}$ etex, (-55,0));
  label.rt(btex \LaTeX\ \textit{in the browser} etex, (55,0));
endfig; end.
boxes.mp — The standard macro packages ship in the core bundle: boxes, rboxes, graph, format, sarith, string, metaobj, featpost, metauml and more. 3 ms MetaPost, 57 ms TeX
source (MetaPost)
input boxes;
prologues := 3;
beginfig(1);
  boxit.lex(btex lexer etex); boxit.parse(btex parser etex);
  boxit.ast(btex AST etex); circleit.out(btex code etex);
  parse.w = lex.e + (25,0); ast.w = parse.e + (25,0); out.w = ast.e + (25,0);
  drawboxed(lex, parse, ast, out);
  drawarrow lex.e -- parse.w; drawarrow parse.e -- ast.w; drawarrow ast.e -- out.w;
  label.top(btex \it tokens etex, .5[lex.e, parse.w] + (0,2));
  label.top(btex \it tree etex, .5[parse.e, ast.w] + (0,2));
endfig; end.
graph.mp — John Hobby's graph package with TeX-typeset tick labels; MetaPost chooses the ticks. 6 ms MetaPost, 54 ms TeX
source (MetaPost)
input graph;
prologues := 3;
beginfig(1);
  draw begingraph(7cm, 4.5cm);
    glabel.lft(btex $f(x)$ etex, OUT); glabel.bot(btex $x$ etex, OUT);
    path p; p := (0,0) for i = 1 upto 60: ..(i/6, (i/6)*(i/6)*mexp(-256*(i/6))) endfor;
    gdraw p withpen pencircle scaled 1 withcolor (0.8,0.1,0.1);
    gdraw (0,0.5) for i = 1 upto 60: ..(i/6, sind(60i)/2+0.5) endfor withcolor (0.1,0.3,0.8);
    glabel.urt(btex $x^2e^{-x}$ etex, (4,0.3));
  endgraph;
endfig; end.
A real programming language — Recursive vardefs, paths as first-class values joined with &, loops, and linear equations solved for you. 5 ms MetaPost
source (MetaPost)
vardef koch(expr a, b, n) =
  if n = 0: a--b
  else:
    save c, d, e; pair c, d, e;
    c = 1/3[a,b]; d = 2/3[a,b];
    e = 1/2[a,b] + ((b-a) rotated -90) scaled ((sqrt 3)/6);
    koch(a,c,n-1) & koch(c,e,n-1) & koch(e,d,n-1) & koch(d,b,n-1)
  fi
enddef;
beginfig(1);
  pair p, q, r; p = (-75,-43); q = (75,-43); r = (0, 87);
  path s; s := koch(p,q,4) & koch(q,r,4) & koch(r,p,4) & cycle;
  fill s withcolor (0.85,0.93,1);
  draw s withpen pencircle scaled .6 withcolor (0.1,0.3,0.7);
endfig; end.

Also supported: all number systems mplib builds without GMP (scaled, double, decimal); withprescript/withpostscript and special; TFM output; write … to files (returned as artifacts); runscript bridged to a JavaScript function when you opt in; the structured JSON figure model for canvas rendering, hit testing or export; and the classic .mpx route (extensions: false) for byte-parity with mpost's own file handling.

What it does — TikZ and LaTeX

A TikZ document is not a label but a whole page, so it takes a different road: pdfTeX writes a DVI whose specials carry PGF's drawing commands, and dvisvgm — the reference converter, with its PGF special handlers, FreeType and potrace, also compiled to WebAssembly — turns each page into SVG. The library selects PGF's dvisvgm driver automatically, so nothing needs Ghostscript.

Shadings — Axis, radial and ball shadings become SVG gradients, through PGF's own dvisvgm driver. This is where a JavaScript DVI reader gives up. 85 ms TeX, 30 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=3pt]{standalone}
\usetikzlibrary{shadings}
\begin{document}
\begin{tikzpicture}
  \shade[left color=red,right color=blue] (0,0) rectangle (2.4,1.2);
  \shade[inner color=white,outer color=orange] (3.6,0.6) circle (0.6);
  \shade[ball color=green!70!black] (5.4,0.6) circle (0.6);
  \shade[top color=cyan!60,bottom color=violet!60,middle color=white] (6.6,0) rectangle (9,1.2);
\end{tikzpicture}
\end{document}
Patterns, opacity, clipping, decorations — Fill patterns become SVG patterns; opacity, clipping scopes and path decorations all survive intact. 120 ms TeX, 15 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=3pt]{standalone}
\usetikzlibrary{patterns,decorations.pathmorphing,decorations.markings}
\begin{document}
\begin{tikzpicture}
  \fill[pattern=north east lines,pattern color=gray] (0,0) rectangle (2,1.2);
  \fill[pattern=crosshatch dots,pattern color=blue!60] (2.3,0) rectangle (4.3,1.2);
  \begin{scope}
    \clip (5.5,0.6) circle (0.7);
    \fill[orange] (4.5,0) rectangle (6.5,1.3);
    \draw[decorate,decoration=snake,thick] (4.5,0.6) -- (6.5,0.6);
  \end{scope}
  \fill[red,opacity=0.5] (7,0) rectangle (8.2,1.2);
  \fill[blue,opacity=0.5] (7.6,0.2) rectangle (8.8,1.4);
  \draw[thick,postaction={decorate},decoration={markings,mark=at position 0.5 with {\arrow{>}}}] (0,-0.5) -- (8.8,-0.5);
\end{tikzpicture}
\end{document}
Nodes, shapes, arrows — positioning, shapes.geometric and arrows.meta; bent and curved edges; every library that ships with PGF in TeX Live 2025 is available. 98 ms TeX, 34 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=3pt]{standalone}
\usetikzlibrary{positioning,shapes.geometric,arrows.meta}
\begin{document}
\begin{tikzpicture}[node distance=1.1cm and 1.5cm, every node/.style={font=\small}]
  \node[circle,draw,fill=blue!15] (a) {$a$};
  \node[circle,draw,fill=blue!15,right=of a] (b) {$b$};
  \node[diamond,draw,fill=red!15,below right=of a] (c) {$c$};
  \node[rectangle,draw,rounded corners,right=of b] (d) {$d$};
  \draw[-{Stealth}] (a) -- node[above] {1} (b);
  \draw[-{Stealth}] (a) -- node[left] {2} (c);
  \draw[-{Stealth},dashed] (b) -- (c);
  \draw[-{Stealth},bend left] (b) to node[above] {3} (d);
  \draw[-{Stealth}] (c) to[out=0,in=-90] (d);
\end{tikzpicture}
\end{document}
Commutative diagrams (tikz-cd) 142 ms TeX, 39 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[border=3pt]{standalone}
\usepackage{tikz-cd,amssymb}
\begin{document}
\begin{tikzcd}[column sep=large,row sep=large]
  A \arrow[r,"f"] \arrow[d,"g"'] \arrow[dr,dashed,"h"] & B \arrow[d,"g'"] \\
  C \arrow[r,"f'"'] & D \arrow[ul,phantom,"\lrcorner",very near start]
\end{tikzcd}
\end{document}
pgfplots — The real pgfplots package: axes, grids, legends, sampled functions, data coordinates, filled areas. 255 ms TeX, 49 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[border=3pt]{standalone}
\usepackage{amsmath,pgfplots}
\pgfplotsset{compat=1.18}
\begin{document}
\begin{tikzpicture}
  \begin{axis}[width=8cm,height=5cm,xlabel={$x$},ylabel={$f(x)$},legend pos=outer north east,grid=major,axis lines=left]
    \addplot[blue,thick,domain=0:4,samples=60] {x^2*exp(-x)};
    \addplot[red,dashed,domain=0:4,samples=60] {0.5*sin(deg(3*x))+0.5};
    \addplot[only marks,mark=*,mark size=1.5pt] coordinates {(0.5,0.3) (1.5,0.5) (2.5,0.6) (3.5,0.2)};
    \addplot[fill=blue!10,draw=none,domain=0:4,samples=60] {x^2*exp(-x)} \closedcycle;
    \legend{$x^2e^{-x}$,$\tfrac12(1+\sin 3x)$,data}
  \end{axis}
\end{tikzpicture}
\end{document}
Latin Modern, T1 encoding, amsmath — Text is glyph outlines in the SVG, so it renders identically everywhere; fonts: 'woff2' embeds web fonts instead if you prefer selectable text. 144 ms TeX, 72 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=3pt]{standalone}
\usepackage{lmodern}
\usepackage[T1]{fontenc}
\usepackage{amsmath,amssymb}
\begin{document}
\begin{tikzpicture}
  \node[draw,align=left,text width=6.2cm,inner sep=8pt,rounded corners] {Latin Modern in \textbf{T1} encoding: fi fl ffi \& \textit{italic} \texttt{mono} \textsf{sans} \textsc{Small Caps}.\\[4pt] $\displaystyle\sum_{n\ge1}\frac{1}{n^2}=\frac{\pi^2}{6}, \qquad \mathbb{R}\setminus\mathbb{Q}$};
\end{tikzpicture}
\end{document}
Trees, matrices, decorations 144 ms TeX, 39 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=3pt]{standalone}
\usetikzlibrary{trees,decorations.pathreplacing}
\begin{document}
\begin{tikzpicture}[level distance=11mm, sibling distance=18mm, every node/.style={draw,rounded corners,fill=blue!8,font=\footnotesize}, edge from parent/.style={draw,-latex}]
  \node {mp-tikz-wasm}
    child { node {mplib.wasm} child { node {SVG} } child { node {EPS} } }
    child { node {tex.wasm} child { node {DVI} } }
    child { node {dvisvgm.wasm} child { node {TikZ} } };
  \draw[decorate,decoration={brace,amplitude=4pt,mirror},thick] (-3.2,-2.6) -- (3.2,-2.6) node[midway,below=4pt,draw=none,fill=none] {all in the browser};
\end{tikzpicture}
\end{document}

Every library

All of PGF/TikZ's libraries are bundled, and so is spath3, whose knots and calligraphy libraries pgf does not ship. A few more of them, each a complete document; the same sources are in the gallery, where they can be edited.

Arrow tips — The arrows.meta tips, filled and open, stacked, round, harpoons and Bars, and the bending library's tips that follow a curved path. 177 ms TeX, 47 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=6pt]{standalone}
\usetikzlibrary{arrows.meta,bending}
\begin{document}
\begin{tikzpicture}[font=\footnotesize\ttfamily, line width=0.8pt]
  \foreach \tip [count=\i] in {Stealth, Latex, Triangle, Kite, Straight Barb, Hooks, Circle, Square} {
    \draw[arrows/.expanded={-{\tip[scale=1.3]}}] (0,-0.5*\i) -- ++(1.8,0) node[right=4pt] {\tip};
    \draw[arrows/.expanded={-{\tip[open,scale=1.3]}}] (4.3,-0.5*\i) -- ++(1.8,0) node[right=4pt] {open};
  }
  \draw[{Bar[width=6pt]}-{Bar[width=6pt]}, blue!70!black] (0,-4.7) -- node[below,font=\footnotesize] {$d = 1.8$ cm} ++(1.8,0);
  \draw[-{Stealth[length=3mm]Stealth[length=3mm]}, red!70!black] (2.6,-4.7) -- ++(1.4,0);
  \draw[{Latex[round]}-{Latex[round,reversed]}, thick] (4.3,-4.7) -- ++(1.8,0);
  \draw[-{Stealth[harpoon,swap]}, thick] (6.9,-4.5) -- ++(1.2,0);
  \draw[-{Stealth[harpoon,swap]}, thick] (8.1,-4.9) -- ++(-1.2,0);
  \draw[-{Latex[bend,length=4mm]}, very thick, orange!80!black] (7.4,-0.4) arc[start angle=160, end angle=-100, radius=1.1];
  \node[font=\footnotesize\rmfamily, align=center] at (7.95,-3.1) {tips that\\ bend with\\ the path};
\end{tikzpicture}
\end{document}
Decorations — Path morphing, shapes and markings along a path, footprints, and text along path. 829 ms TeX, 52 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=6pt]{standalone}
\usetikzlibrary{decorations.pathmorphing,decorations.pathreplacing,decorations.markings,decorations.text,decorations.shapes,decorations.footprints,shapes.geometric,arrows.meta}
\begin{document}
\begin{tikzpicture}[every node/.style={font=\footnotesize}]
  \draw[decorate,decoration={brace,amplitude=6pt,raise=2pt}] (0,0.2) -- (3,0.2) node[midway,above=9pt] {brace};
  \draw[decorate,decoration={snake,amplitude=2pt,segment length=8pt}] (0,0) -- (3,0) node[right] {snake};
  \draw[decorate,decoration={coil,aspect=0.5,amplitude=4pt,segment length=6pt},blue!70!black] (0,-0.7) -- (3,-0.7) node[right,black] {coil};
  \draw[decorate,decoration={zigzag,amplitude=3pt,segment length=6pt},red!70!black] (0,-1.4) -- (3,-1.4) node[right,black] {zigzag};
  \draw[decorate,decoration={bumps,amplitude=3pt,segment length=10pt},green!50!black] (0,-2.1) -- (3,-2.1) node[right,black] {bumps};
  \path[decorate,decoration={shape backgrounds,shape=star,shape size=6pt,shape sep=10pt},fill=yellow!70!orange,draw=orange!60!black] (0,-2.8) -- (3,-2.8) node[right,black] {shapes};
  \draw[postaction={decorate},decoration={markings,mark=between positions 0.1 and 0.9 step 0.2 with {\arrow{Stealth}}},thick,violet] (0,-3.6) .. controls (1,-3.1) and (2,-4.1) .. (3,-3.6) node[right,black] {markings};
  \draw[gray!50, fill=gray!5] (6.6,-1.4) circle (1.3);
  \path[decorate,decoration={text along path,text={text that follows a path},text align=center,raise=3pt}] (6.6,-1.4) ++(195:1.3) arc[start angle=195, end angle=-15, radius=1.3];
  \fill[brown!70!black,decorate,decoration={footprints,foot length=6pt,stride length=12pt,foot of=human}] (5.1,-3.7) .. controls (6,-3.1) and (7.2,-4.1) .. (8.1,-3.5);
  \node[align=center] at (6.6,-1.4) {decorations.\\text};
  \node[below=1pt] at (6.6,-3.8) {footprints};
  \path (8.4,0.3); % decorated text is not counted in the bounding box
\end{tikzpicture}
\end{document}
Knots (spath3) — spath3's knots library, bundled although pgf does not ship it: every crossing of a self-intersecting strand is found and the upper strand redrawn. 2957 ms TeX, 41 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=6pt]{standalone}
\usetikzlibrary{knots}
% one motif, turned three times: a single strand that crosses itself
\newcommand{\motif}[1]{
  to ++(180+#1:0.50) arc (270+#1:150+#1:0.15)
  to ++( 60+#1:0.50) arc (-30+#1:150+#1:0.15)
  to ++(240+#1:0.25) arc (150+#1:330+#1:0.25)
  to ++( 60+#1:0.55) arc (150+#1: 30+#1:0.20)
}
\begin{document}
\begin{tikzpicture}
  \begin{knot}[line width=2pt, line join=round, clip width=2, scale=5,
      consider self intersections, ignore endpoint intersections=false, background color=white,
      only when rendering/.style={draw=blue!55!black, double=white, double distance=6pt, line cap=round}]
    \strand (0,0) \motif{0}\motif{120}\motif{240};
    \flipcrossings{1,3,6,8,10}
  \end{knot}
  \begin{scope}[xshift=5.4cm, yshift=-1.6cm]
    \begin{knot}[clip width=5, flip crossing=2]
      \strand[red!70!black, ultra thick] (0,0) .. controls +(1,0) and +(-1,0) .. (2,1) .. controls +(1,0) and +(-1,0) .. (4,0);
      \strand[blue!70!black, ultra thick] (0,1) .. controls +(1,0) and +(-1,0) .. (2,0) .. controls +(1,0) and +(-1,0) .. (4,1);
    \end{knot}
  \end{scope}
\end{tikzpicture}
\end{document}
Calligraphy (spath3) — A broad nib, a copperplate nib, and the calligraphic brace and parentheses built on them. 337 ms TeX, 30 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=6pt]{standalone}
\usetikzlibrary{decorations.pathreplacing,calligraphy}
\begin{document}
\begin{tikzpicture}
  % a broad nib held at 45 degrees: the stroke is thick or thin with its direction
  \pen (-135:.2) -- (45:.2);
  \calligraphy[pen colour=blue!55!black] (0,0) .. controls +(45:1) and +(-135:1) .. +(3,0) ++(1.5,0) .. controls +(-135:2) and +(45:2) .. +(0,-3) (0,-3) .. controls +(45:1) and +(-135:1) .. +(3,0);
  % a copperplate nib: each segment swells and tapers
  \pen (0,0);
  \calligraphy[heavy, heavy line width=3pt, pen colour=red!60!black] (4.6,0) .. controls +(45:1) and +(-135:1) .. +(3,0) ++(1.5,0) .. controls +(-135:2) and +(45:2) .. +(0,-3) (4.6,-3) .. controls +(45:1) and +(-135:1) .. +(3,0);
  % the decorations built on it
  \draw[decorate, decoration={calligraphic brace, amplitude=3mm}, ultra thick] (9.4,-3) -- (9.4,0.4);
  \draw[decorate, decoration={calligraphic curved parenthesis, amplitude=3mm}, ultra thick] (10.3,-3) -- (10.3,0.4);
  \draw[decorate, decoration={calligraphic straight parenthesis, amplitude=3mm}, ultra thick] (11.2,-3) -- (11.2,0.4);
  \path (11.8,0); % the decorations are not counted in the bounding box
\end{tikzpicture}
\end{document}
Matrices — A matrix of math nodes with delimiters and a highlighted diagonal on the background layer, and arrows between the cells of a second matrix. 169 ms TeX, 54 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=6pt]{standalone}
\usetikzlibrary{matrix,fit,backgrounds,positioning,arrows.meta}
\begin{document}
\begin{tikzpicture}[>={Stealth}]
  \matrix (A) [matrix of math nodes, left delimiter={(}, right delimiter={)}, row sep=4pt, column sep=8pt, nodes={minimum width=1.4em}] {
    2 & -1 &  0 & 0 \\
   -1 &  2 & -1 & 0 \\
    0 & -1 &  2 & -1 \\
    0 &  0 & -1 & 2 \\
  };
  \begin{scope}[on background layer]
    \foreach \i in {1,...,4} \fill[blue!12, rounded corners=3pt] (A-\i-\i.north west) rectangle (A-\i-\i.south east);
  \end{scope}
  \node[left=6pt of A, anchor=east] (L) {$K_4 =$};
  \matrix (cd) [matrix of math nodes, row sep=1.4cm, column sep=1.8cm, right=1.6cm of A] {
    A & B \\
    C & D \\
  };
  \draw[->] (cd-1-1) -- node[above,font=\small] {$f$} (cd-1-2);
  \draw[->] (cd-1-1) -- node[left,font=\small] {$g$} (cd-2-1);
  \draw[->] (cd-1-2) -- node[right,font=\small] {$h$} (cd-2-2);
  \draw[->] (cd-2-1) -- node[below,font=\small] {$k$} (cd-2-2);
  \draw[->, dashed] (cd-1-1) -- node[above right=-2pt,font=\small] {$\varphi$} (cd-2-2);
  \node[draw=red!60!black, rounded corners, inner sep=6pt, fit=(cd-1-1)(cd-2-2), label={[font=\footnotesize]below:$h\circ f = k\circ g$}] {};
\end{tikzpicture}
\end{document}
A mind map — The mindmap library, whose shaded connection bars become SVG gradients. 246 ms TeX, 45 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[tikz,border=6pt]{standalone}
\usetikzlibrary{mindmap}
\begin{document}
\begin{tikzpicture}[mindmap, grow cyclic, every node/.style={concept, font=\sffamily\small}, concept color=blue!50!black, text=white,
  level 1/.append style={level distance=4cm, sibling angle=120, font=\sffamily\footnotesize},
  level 2/.append style={level distance=2.3cm, sibling angle=45, font=\sffamily\scriptsize}]
  \node {mp-tikz-wasm}
    child[concept color=red!60!black] { node {MetaPost} child { node {SVG} } child { node {EPS} } child { node {JSON} } }
    child[concept color=green!45!black] { node {pdf\TeX} child { node {LaTeX} } child { node {TikZ} } child { node {pgfplots} } }
    child[concept color=orange!80!black] { node {Lua\TeX} child { node {graph drawing} } child { node {fontspec} } child { node {Lua} } };
\end{tikzpicture}
\end{document}
Plain TeX too — engine: 'plain' runs plain TeX with e-TeX (TeX Live's etex), which is what PGF needs. 89 ms TeX, 54 ms dvisvgm
source (plain TeX)
\input tikz
\nopagenumbers
\tikzpicture
  \draw[thick,fill=blue!10] (0,0) circle (1);
  \foreach \a in {0,45,...,315} \draw (0,0) -- (\a:1) node[circle,fill=red,inner sep=1.5pt] {};
  \node at (0,-1.4) {plain \TeX\ with TikZ};
\endtikzpicture
\bye

Whole documents, not just pictures

The TikZ road is a real LaTeX road: the engine typesets the entire document, so paragraphs, fonts, hyphenation, amsmath and TeX's low-level primitives all come along. These two examples use \parshape, the primitive that gives every line of a paragraph its own indent and width, with inline TikZ pictures and displayed mathematics flowing through the shape.

A paragraph shaped by \parshape — This is a whole LaTeX document, not just a picture: prose, inline and displayed amsmath, and three inline TikZ pictures, all set inside a circle. TeX's \parshape primitive takes one (indent, width) pair per line; a \loop computes twenty of them with pgfmath as chords of the circle drawn behind the text. Line breaking, hyphenation and the display equation all follow the shape. 170 ms TeX, 98 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[border=8pt]{standalone}
\usepackage{lmodern}
\usepackage[T1]{fontenc}
\usepackage{amsmath,amssymb}
\usepackage{tikz}
\begin{document}
% A paragraph set inside a circle. \parshape takes one (indent, width) pair
% per line; pgfmath computes each width as a chord of the circle.
\def\R{130.9}               % radius in pt (4.6cm)
\def\B{12}                  % \baselineskip in pt
\newcount\n \n=20           % lines: the text block is n\B = 240pt tall
\newcount\i
\def\shape{}
\i=1
\loop
  \pgfmathsetmacro\yy{(\n*\B/2-(\i-0.5)*\B)/10}    % line centre, relative to circle centre, in 10pt units
  \pgfmathsetmacro\hw{10*sqrt(\R/10*\R/10-\yy*\yy)-6} % half chord, 6pt inside the rim (pgfmath overflows above 16383, hence the /10)
  \pgfmathsetmacro\ind{\R-\hw}
  \pgfmathsetmacro\lw{2*\hw}
  \edef\shape{\shape\ind pt \lw pt }
  \advance\i 1
\ifnum\i<\numexpr\n+1\relax\repeat
\hbox{%
  \rlap{\tikz[baseline=0pt]{\fill[blue!6] (\R pt,-111.6pt) circle (\R pt); \draw[blue!50!black,line width=.5pt] (\R pt,-111.6pt) circle (\R pt);}}%
  \vtop{\hsize=2\dimexpr\R pt\relax \baselineskip=\B pt \parindent=0pt \tolerance=3000 \emergencystretch=1.5em \hyphenpenalty=10
    \parshape \n \shape
    The ratio of a circle's circumference to its diameter,
    $\pi\approx3.14159\ldots$, is the same for every circle
    \tikz[baseline=-0.6ex]{\draw (0,0) circle (0.8ex); \draw (-0.8ex,0)--(0.8ex,0);}.
    Archimedes squeezed it between inscribed and circumscribed polygons
    \tikz[baseline=-0.6ex]{\draw (0,0) circle (0.8ex); \draw (0:0.8ex) \foreach \a in {60,120,...,300} {-- (\a:0.8ex)} -- cycle;},
    proving $3\tfrac{10}{71}<\pi<3\tfrac17$; two thousand years later Leibniz found
    \[ \frac{\pi}{4}=\sum_{k=0}^{\infty}\frac{(-1)^k}{2k+1}=1-\frac13+\frac15-\frac17+\cdots, \]
    and Euler tied $\pi$ to the primes through $\sum_{n\ge1}n^{-2}=\pi^2/6$
    and to $e$ through $e^{i\pi}+1=0$; Wallis wrote $\frac{\pi}{2}=\prod_{n\ge1}\frac{4n^2}{4n^2-1}$.
    The Gaussian integral
    $\int_{-\infty}^{\infty}e^{-x^2}\,dx=\sqrt{\pi}$
    \tikz[baseline=-0.3ex]{\draw[thick] plot[domain=-2.2:2.2,samples=30] ({\x*0.42em},{exp(-\x*\x)*1.4ex});}
    carries it into probability and statistics, and it hides in Stirling's
    formula $n!\sim\sqrt{2\pi n}\,(n/e)^n$. This paragraph is set by
    \TeX's \texttt{\char`\\parshape} primitive: twenty line widths
    computed by pgfmath as chords of the circle, with the
    display and the inline TikZ pictures flowing
    through the same shape as the prose.\par}}
\end{document}
Text flowing around a figure — The same primitive with nine narrow lines and then the full measure, the figure hung from the first baseline in a zero-width box: what wrapfig does, by hand. The figure is a TikZ plot of Fourier partial sums sampled by pgfmath; the paragraph carries integrals, limits and a display. 2258 ms TeX, 120 ms dvisvgm (snapshot)
source (LaTeX)
\documentclass[border=8pt]{standalone}
\usepackage{lmodern}
\usepackage[T1]{fontenc}
\usepackage{amsmath,amssymb}
\usepackage{tikz}
\begin{document}
% Text flowing around a figure, by hand: \parshape narrows the first k
% lines, and the figure hangs from the first baseline in a zero-width box.
\newdimen\W \W=11.5cm      % the measure
\newdimen\F \F=4.4cm       % width reserved for the figure
\newcount\k \k=9           % lines beside it
\def\shape{}\newcount\i \i=1
\loop \edef\shape{\shape 0pt \the\dimexpr\W-\F\relax\space}\advance\i 1 \ifnum\i<\numexpr\k+1\relax\repeat
\edef\shape{\shape 0pt \the\W}
\vtop{\hsize=\W \parindent=0pt \tolerance=2000 \emergencystretch=1em
  \parshape \numexpr\k+1\relax \shape
  % \vtop{\kern0pt ...} puts the whole picture below the baseline; \smash hides
  % that depth from the line spacing; \raise lines its top up with the first line.
  \rlap{\hskip\dimexpr\W-\F+3mm\relax\smash{\raise\ht\strutbox\vtop{\kern0pt\hbox{%
    \begin{tikzpicture}[x=0.55cm,y=0.95cm,font=\scriptsize]
      \draw[->] (-0.3,0) -- (6.9,0) node[right] {$x$};
      \draw[->] (0,-1.45) -- (0,1.55) node[left] {$S_N(x)$};
      \draw[gray!70,line width=1.2pt] (0,1) -- (3.1416,1) -- (3.1416,-1) -- (6.2832,-1);
      \draw[blue!70!black,domain=0:6.2832,samples=90] plot (\x,{4/pi*sin(\x r)});
      \draw[red!80!black,domain=0:6.2832,samples=200] plot (\x,{4/pi*(sin(\x r)+sin(3*\x r)/3+sin(5*\x r)/5)});
      \draw[violet!80!black,thick,domain=0:6.2832,samples=500] plot (\x,{4/pi*(sin(\x r)+sin(3*\x r)/3+sin(5*\x r)/5+sin(7*\x r)/7+sin(9*\x r)/9+sin(11*\x r)/11+sin(13*\x r)/13+sin(15*\x r)/15+sin(17*\x r)/17+sin(19*\x r)/19+sin(21*\x r)/21)});
      \foreach \t/\l in {3.1416/$\pi$,6.2832/$2\pi$} \draw (\t,2pt) -- (\t,-2pt) node[below=1pt,fill=white,inner sep=1pt] {\l};
      \node[anchor=north west,align=left,inner sep=1pt] at (3.4,1.55) {\textcolor{blue!70!black}{$N=1$}\\ \textcolor{red!80!black}{$N=5$}\\ \textcolor{violet!80!black}{$N=21$}};
    \end{tikzpicture}}}}}%
  Any reasonable periodic function is a sum of sines and cosines. For the square wave
  \tikz[baseline=-0.5ex]\draw (0,-0.7ex)--(0,0.7ex)--(0.9em,0.7ex)--(0.9em,-0.7ex)--(1.8em,-0.7ex);
  $f(x)=\operatorname{sgn}(\sin x)$ the cosine coefficients
  $a_n=\frac1\pi\int_{-\pi}^{\pi}f(x)\cos nx\,dx$ all vanish, while
  $b_n=\frac{2}{\pi n}\,(1-\cos n\pi)$, so only the odd harmonics survive:
  \[ f(x)=\frac{4}{\pi}\sum_{k=0}^{\infty}\frac{\sin\bigl((2k+1)x\bigr)}{2k+1}. \]
  The partial sums $S_N$ on the right overshoot each jump by about nine per cent
  however large $N$ becomes: the Gibbs phenomenon,
  $\lim_{N\to\infty}S_N\!\left(\tfrac{\pi}{2N}\right)=\tfrac{2}{\pi}\operatorname{Si}(\pi)\approx1.179$.
  Parseval's identity $\frac1\pi\int_{-\pi}^{\pi}f^2=\sum_n b_n^2$ then yields
  $\sum_{k\ge0}(2k+1)^{-2}=\pi^2/8$, from which Euler's $\zeta(2)=\pi^2/6$ follows in one line.
  The gap the text flows around is a \texttt{\char`\\parshape} too: nine narrow lines beside the
  figure, then the full measure, with the figure itself hung from the first baseline in a
  zero-width \texttt{\char`\\rlap} box.\par}
\end{document}

Nothing about \parshape needs LaTeX. The same primitive in plain TeX, with a loop computing the 21 line widths:

A diamond, in plain TeX — No packages: a plain-TeX loop builds a 21-line \parshape and the line breaker pours the paragraph into it. 22 ms TeX, 126 ms dvisvgm
source (plain TeX)
% plain TeX (pdfTeX): a paragraph set in a diamond with \parshape.
% Line i of 2m+1 is indented by |i-m|*d and is 2|i-m|*d narrower than \hsize.
\hsize=9cm \parindent=0pt \nopagenumbers \lefthyphenmin=2 \righthyphenmin=3
\baselineskip=12.5pt \tolerance=9999 \emergencystretch=1em \hyphenpenalty=10
\newcount\m \m=10 \newcount\i \newcount\k \newdimen\d \d=\hsize \divide\d by 22 \newdimen\ind \newdimen\wid
\def\shape{}
\i=0
\loop
  \k=\i \advance\k by -\m \ifnum\k<0 \k=-\k \fi   % k = |i - m|
  \ind=\k\d \wid=\hsize \advance\wid by -2\ind
  \edef\shape{\shape\space\the\ind\space\the\wid}
  \advance\i by 1
\ifnum\i<\numexpr 2*\m+1\relax \repeat
\parshape \numexpr 2*\m+1\relax \shape
\TeX\ breaks a paragraph into lines by looking at all of it at once. Every
feasible breakpoint gets a demerit that grows with the cube of how far its
line has to stretch or shrink, and the breaks chosen are the sequence with
the least total: a shortest path through a graph of possibilities, found by
dynamic programming. The {\tt\string\parshape} primitive gives each line its own
indentation and width, so the same algorithm pours text into any outline.
Here the lines widen by $2/22$ of the measure down to the middle one
and then narrow again, and the paragraph becomes a diamond without a single
manual line break, in plain \TeX, with no packages.\par
\bye

LuaTeX

A second engine, luatex.wasm, runs the same DVI pipeline with LuaTeX 1.21 in DVI mode (TeX Live's dvilualatex and dviluatex). It exists for the parts of TikZ that are written in Lua, above all the graphdrawing library with its layered, force-directed, tree, circular and phylogenetic layouts, plus \directlua, luacode and pgfplots' Lua backends. engine: 'auto' and the drop-in tags choose it whenever a document uses any of these; nothing else changes, and the 4.4 MB module and its 6.5 MB format are only fetched then. By default text is set in the same Type 1 fonts as the pdfTeX route; with the opt-in OpenType bundle it can use fontspec as well (next section). engine: 'lualatex' and engine: 'luatex' ask for LuaTeX explicitly, for LaTeX or plain TeX.

Graph drawing (LuaTeX) — The graphdrawing library computes layouts in Lua, so it needs LuaTeX. engine: 'auto' (and the tags) switch to luatex.wasm when a document uses it; layered, spring, tree, circular and phylogenetic layouts all run. This is the library tikzjax could never offer. 544 ms LuaTeX, 49 ms dvisvgm
source (LuaLaTeX)
\documentclass[tikz,border=3pt]{standalone}
\usetikzlibrary{graphs,graphs.standard,graphdrawing,quotes}
\usegdlibrary{trees,layered,force,circular}
\begin{document}
\begin{tikzpicture}[>=stealth, nodes={draw,circle,fill=blue!10,font=\small}]
  \graph[layered layout, sibling distance=8mm, level distance=8mm] {
    a -> {b -> {d, e}, c -> {f -> g, h}}; e -> g;
  };
  \begin{scope}[xshift=4.2cm, nodes={fill=red!10}]
    \graph[spring layout, node distance=9mm] { 1 -- {2,3,4}; 2 -- 3 -- 4 -- 5 -- 2; 5 -- 6 -- 7 -- 5 };
  \end{scope}
  \begin{scope}[xshift=8.4cm, nodes={fill=orange!20}]
    \graph[simple necklace layout, node distance=9mm] { subgraph C_n [n=7] };
  \end{scope}
\end{tikzpicture}
\end{document}
More graph drawing — A binary tree layout and a spring-electrical layout, both computed in Lua. 552 ms LuaTeX, 52 ms dvisvgm
source (LuaLaTeX)
\documentclass[tikz,border=6pt]{standalone}
\usetikzlibrary{graphs,graphdrawing,quotes}
\usegdlibrary{trees,force}
\begin{document}
\begin{tikzpicture}[>=stealth, every node/.style={draw, circle, inner sep=1.5pt, minimum size=5mm, font=\footnotesize, fill=green!12}]
  \graph[binary tree layout, level distance=9mm, sibling distance=7mm] {
    8 -> { 4 -> { 2 -> { 1, 3 }, 6 -> { 5, 7 } }, 12 -> { 10 -> { 9, 11 }, 14 -> { 13, 15 } } }
  };
  \begin{scope}[xshift=7.4cm, yshift=-1.4cm, every node/.style={draw, rectangle, rounded corners, font=\footnotesize, fill=blue!10}]
    \graph[spring electrical layout, node distance=13mm, electric charge=2, random seed=7] {
      TeX -- {LaTeX, plain, ConTeXt}; LaTeX -- {TikZ, amsmath, fontspec}; TikZ -- {pgfplots, graphs}; plain -- MetaPost -- ConTeXt; fontspec -- LuaTeX -- ConTeXt; graphs -- LuaTeX
    };
  \end{scope}
\end{tikzpicture}
\end{document}
Plain LuaTeX — engine: 'luatex': \directlua sieves the primes and hands the counts to TeX with token.set_macro; the bars are \vrules and the table an \halign. 68 ms LuaTeX, 48 ms dvisvgm
source (plain LuaTeX)
% plain LuaTeX: Lua does the arithmetic, TeX's primitives do the typesetting
\hsize=11.5cm \parindent=0pt \nopagenumbers
\font\big=cmbx12
\directlua{
  local N, sieve, count, c = 1000, {}, {}, 0
  for n = 2, N do sieve[n] = true end
  for n = 2, N do
    if sieve[n] then c = c + 1; for m = n*n, N, n do sieve[m] = false end end
    count[n] = c
  end
  % hand the results to TeX as macros \pi50, \pi100 ... and \li50 ...
  for x = 50, 1000, 50 do
    token.set_macro("pi" .. x, tostring(count[x]))
    token.set_macro("li" .. x, tostring(math.floor(10 * x / math.log(x) + 0.5) / 10))
  end
}
\def\p#1{\csname pi#1\endcsname}\def\l#1{\csname li#1\endcsname}
{\big Counting primes in {\tt\string\directlua}}\medskip
The number of primes up to $x$ grows like $x/\ln x$:
$$\pi(x)=\sum_{p\le x}1\;\sim\;{x\over\ln x}\qquad(x\to\infty).$$
Each bar is a {\tt\string\vrule} that is $0.6\,\pi(x)$ points tall, for $x=50,100,\ldots,1000$;
the red tick marks $x/\ln x$.\medskip
\newcount\x
\hbox{\vrule width .4pt height 105pt depth 0pt\kern4pt
  \x=50
  \loop
    \dimen0=\p{\the\x}pt \dimen0=.6\dimen0 \dimen2=\l{\the\x}pt \dimen2=.6\dimen2
    \special{color push rgb 0.2 0.35 0.7}\vrule width 7pt height \dimen0 depth 0pt\special{color pop}%
    \special{color push rgb 0.8 0.1 0.1}\llap{\raise\dimen2\hbox{\vrule width 9pt height .8pt depth 0pt\kern-1pt}}\special{color pop}%
    \kern3pt
    \advance\x by 50
  \ifnum\x<1001 \repeat}
\hrule width 8.2cm
\bigskip
\halign{\strut\hfil$#$\quad\vrule\quad&\hfil#\quad&\hfil#\cr
x&$\pi(x)$&$x/\ln x$\cr\noalign{\hrule}
100&\p{100}&\l{100}\cr
500&\p{500}&\l{500}\cr
1000&\p{1000}&\l{1000}\cr}
\bye
LuaLaTeX — engine: 'lualatex': Lua writes one \rule per odd entry of Pascal's triangle into the paragraph, and amsmath sets the theorem. 216 ms LuaTeX, 76 ms dvisvgm
source (LuaLaTeX)
\documentclass[border=8pt,varwidth=12cm]{standalone}
\usepackage{amsmath,xcolor}
\begin{document}
\section*{Pascal's triangle, modulo 2}
\directlua{
  % each row of Pascal's triangle mod 2, drawn as one line of \rule boxes;
  % \string\\ passes a backslash through TeX's expansion to Lua
  local rows, row = 32, {1}
  for n = 1, rows do
    tex.sprint("\string\\noindent\string\\hspace*{" .. (rows - n) * 2 .. "pt}")
    for _, v in ipairs(row) do
      if v == 1 then tex.sprint("\string\\textcolor{blue!60!black}{\string\\rule{3.6pt}{3.6pt}}\string\\hspace{0.4pt}")
      else tex.sprint("\string\\hspace{4pt}") end
    end
    tex.sprint("\string\\par\string\\nointerlineskip\string\\vspace{0.4pt}")
    local next = {1}   % row n has n entries (no # here: TeX would double it)
    for i = 2, n do next[i] = (row[i-1] + row[i]) \csstring\% 2 end
    next[n + 1] = 1
    row = next
  end
}
\medskip
Lua computed each entry; \LaTeX\ drew it with \verb|\rule|. The odd entries
of $\binom{n}{k}$ form Sierpi\'nski's triangle, because by Lucas' theorem
\[
  \binom{n}{k} \equiv \prod_i \binom{n_i}{k_i} \pmod 2,
  \qquad\text{so}\qquad
  \binom{n}{k}\ \text{is odd} \iff k \mathbin{\&} n = k .
\]
Row $31$ is all ones: $\sum_{k}\binom{31}{k} = 2^{31} = \directlua{tex.sprint(string.format("\csstring\%d", 2^31))}$.
\end{document}

OpenType fonts

\usepackage{fontspec} with real OpenType faces works under lualatex, and \input luaotfload.sty under plain luatex, through luaotfload shaping in Lua (mode=node). It is opt-in, because LaTeX under LuaTeX probes for luaotfload at start-up and, once it can find it, initialises it on every run — about 200 ms more for a document that never asks for it. Load the opentype bundle (and otf-fonts for unicode-math's maths font):

import { MetaPost, DEFAULT_BUNDLES } from './dist/index.js';
const mp = await MetaPost.create({ bundles: [...DEFAULT_BUNDLES, 'opentype', 'otf-fonts'] });
await mp.latex(doc, { engine: 'lualatex' });                    // glyph outlines, as always
await mp.latex(doc, { engine: 'lualatex', fonts: 'woff2' });    // or embedded web fonts and real <text>
await mp.addFiles({ 'Charter.ttf': bytes });                     // a face the host supplies: \setmainfont{Charter.ttf}[Path=./]

From the tags, data-bundles="+opentype" on the loader and data-engine="lualatex" on the diagram; from the command line, --opentype. The bundle carries luaotfload, fontspec, unicode-math and the whole Latin Modern OpenType family, every optical size the class options and size commands can select, each face fetched when a document first uses it. The bundle also carries luaotfload's font-name database, prebuilt against the bundled tree, so a fresh engine opens only the faces a document uses (a 12pt article: four faces, 0.44 MB) instead of scanning all 72 first. A face looked up by a name the database does not know — one supplied with addFiles(), say — makes luaotfload rescan, once per engine.

fontspec under LuaLaTeX — With the opentype bundle: ligatures, kerning, Unicode input, letter-spacing, a small-caps face and optical sizes, all from Latin Modern's OpenType faces. 1259 ms LuaTeX, 60 ms dvisvgm
source (LuaLaTeX, bundles +opentype)
\documentclass[border=8pt,varwidth=11cm]{standalone}
\usepackage{fontspec,xcolor}
\setmainfont{Latin Modern Roman}[SmallCapsFont=lmromancaps10-regular.otf]
\setsansfont{Latin Modern Sans}
\setmonofont{Latin Modern Mono}
\begin{document}
\raggedright
{\Large\bfseries OpenType through fontspec}\par\medskip
Real OpenType faces, shaped by luaotfload: ligatures (fi ffi ffl),
kerning (AVATAR, Tea, To), and Unicode input typed straight in —
Dvořák, Łódź, Ærøskøbing, façade, naïve, Straße, IJsselmeer, Việt Nam.\par\medskip
{\addfontfeatures{Ligatures=NoCommon}Ligatures switched off: fi ffi ffl.}\par
{\addfontfeatures{LetterSpace=12}\scshape Letter-spaced small capitals}\par
{\addfontfeatures{FakeSlant=0.25}A slant applied by the font loader.}\par\medskip
\textsf{Latin Modern Sans}, \texttt{Latin Modern Mono}, \textit{italic},
\textbf{bold} and \textsl{slanted}. Each size has its own optical design
— {\footnotesize footnote,} {\large large,} {\LARGE LARGE} — and each face
is fetched only when a document asks for it.
\end{document}
luaotfload in plain LuaTeX — \input luaotfload.sty and a \font with a feature list; no LaTeX involved. 365 ms LuaTeX, 54 ms dvisvgm
source (plain LuaTeX, bundles +opentype)
% plain LuaTeX with OpenType: no fontspec, just luaotfload and \font
\input luaotfload.sty
\font\body="[lmroman10-regular.otf]:mode=node;+liga;+kern" at 11pt
\font\nolig="[lmroman10-regular.otf]:mode=node;-liga;+kern" at 11pt
\font\head="[lmroman17-regular.otf]:mode=node;+liga;+kern" at 20pt
\font\sans="[lmsans10-bold.otf]:mode=node;+liga;+kern" at 11pt
\font\caps="[lmromancaps10-regular.otf]:mode=node;+kern" at 11pt
\hsize=10cm \parindent=0pt \nopagenumbers
{\head Plain \TeX, OpenType fonts}\medskip
\body A face is loaded by file name, with its OpenType features spelled out
after the colon: here {\tt +liga} gives ffi, ffl and fi, and
{\tt -liga} turns them off again: {\nolig ffi, ffl, fi}.
Accented letters come from the Unicode input: Dvořák, Łódź, Ærøskøbing.
\medskip
{\sans A sans-serif bold face,} {\caps and true small capitals.}
\medskip
\hrule
\smallskip
{\tt\string\input\ luaotfload.sty} works in plain {\tt luatex} to DVI here
thanks to a small patch that gives plain \TeX\ the shipout hook luaotfload needs.
\bye
Textas text ,notoutlines With fonts:'woff2' theSVGembedsasubsetofeachfaceasawebfontandwritesreal <text> :itcanbeselected,searchedandcopied,andthebrowser’sownrasteriserdrawsit—fiffl,Dvořák,Łódź.
Web fonts, not outlines — With fonts: 'woff2' the text in this figure is real <text> in an embedded subset of the face: select it. 806 ms LuaTeX, 72 ms dvisvgm
source (LuaLaTeX, bundles +opentype, fonts: 'woff2')
\documentclass[border=8pt]{standalone}
\usepackage{fontspec,xcolor}
\setmainfont{Latin Modern Roman}
\begin{document}
\begin{tabular}{@{}l@{}}
{\Large Text as \emph{text}, not outlines}\\[4pt]
With \texttt{fonts: 'woff2'} the SVG embeds a subset of\\
each face as a web font and writes real \texttt{<text>}:\\
it can be selected, searched and copied, and the browser's\\
own rasteriser draws it — \textcolor{blue!60!black}{fi ffl}, Dvořák, Łódź.
\end{tabular}
\end{document}
unicode-math — Unicode maths input in Latin Modern Math; needs the otf-fonts bundle as well. 1333 ms LuaTeX, 44 ms dvisvgm
source (LuaLaTeX, bundles +opentype +otf-fonts)
\documentclass[border=8pt,varwidth=11cm]{standalone}
\usepackage{unicode-math}
\setmainfont{Latin Modern Roman}
\setmathfont{Latin Modern Math}
\begin{document}
Maths typed in Unicode, set in the OpenType maths font:
\[ ∀ε>0\;∃δ>0:\quad |x-a|<δ ⟹ |f(x)-f(a)|<ε \]
\[ ∫_{-∞}^{∞} e^{-x²}\,dx = \sqrt{π}, \qquad ∑_{n=1}^{∞} \frac{1}{n²} = \frac{π²}{6}, \qquad
   𝔼[X] = ∫_Ω X\,d\mathbb{P}, \qquad 𝐀𝐱 = 𝐛 \]
\end{document}

Where the files come from

How the files reach the engine: at start-up the loader fetches one small manifest per bundle (a list of paths, sizes and hashes, fourteen in all, four of them opt-in), builds a virtual filesystem inside the Web Worker with a lazy placeholder for every one of the 4,200 files, and writes kpathsea's ls-R index from the manifests, so TeX's file lookups resolve without touching the network. When TeX actually opens a file, its placeholder fetches that one file with a synchronous request, which is why the worker is the default: a page without a worker has to prefetch everything up front. Formats and even the LuaTeX engine arrive the same way, on first use, and the browser's HTTP cache keeps every fetched file across runs and visits. Adding a package that is not bundled means rebuilding the bundles from a local TeX Live (two scripts, a couple of minutes); there is no installation from CTAN at run time. A single document can also be given extra files, such as a .sty from its own project, through the API's files option without rebuilding anything.

The bundles contain LaTeX 2025 with expl3, PGF/TikZ with every library, pgfplots, tikz-cd, amsmath, amssymb, amscls, mathtools, tools, graphics, xcolor, geometry, booktabs, standalone, hyperref, url, listings, fp, imakeidx, todonotes, the Latin Modern fonts in T1/TS1 encodings, Computer Modern, the AMS fonts, Euler, and the 35 standard PostScript fonts (Times, Helvetica, Courier, Palatino and the rest, as URW Type 1). spath3 (the knots and calligraphy libraries) rides along with PGF, as do chemfig, circuitikz (its current release; the rollback copies of old versions are left out) and tikz-3dplot, and this project's own svg.attributes library (above); the opt-in classico bundle adds URW Classico, an Optima (\usepackage{classico}; its fonts are under the Aladdin Free Public License, non-commercial distribution only), and the opt-in opentype and otf-fonts bundles add luaotfload, fontspec, unicode-math, the Latin Modern OpenType family and Latin Modern Math. The opt-in ghostscript bundle adds dvips's PostScript headers and the PSTricks family (every pst-* package but three data-heavy ones), and turns on the Ghostscript module for documents whose PostScript needs it. Adding a package is one line in scripts/build-texmf.sh; the loader fetches files per file, on demand, so bundle size costs nothing until a document uses a file.

How it works

The engines

ModuleSizeWhat it is
mplib.wasm1.2 MBMetaPost 2.11: interpreter, PostScript and SVG backends, Type 1 font machinery, TFM reader, scaled/double/decimal arithmetic, mpto and dvitomp
tex.wasm1.1 MBpdfTeX 1.40.27 in DVI mode, or PDF mode on request (= tex, etex, latex) with kpathsea, zlib, libpng
dvisvgm.wasm2.6 MBdvisvgm 3.4.3 with FreeType, potrace, clipper, woff2/brotli; PostScript specials through the optional Ghostscript module
luatex.wasm4.2 MBLuaTeX 1.21 with Lua 5.3, pplib, zziplib, the fontforge-derived font loader, kpathsea and our patched mplib; loaded on demand for graphdrawing and Lua documents
ghostscript/gs.wasm11.9 MBGhostscript 10.08.0 (AGPL): the PostScript and PDF interpreters with their fonts, a separate module (and release archive) that dvisvgm reaches through a bridge; loaded on demand, only for documents whose PostScript needs it (the opt-in ghostscript bundle)

Labels without a subprocess

Native MetaPost shells out to TeX from the middle of its scanner, which WebAssembly cannot do. The TeX step is lifted out of the run instead:

  1. Scan. The source and every input-ed file are scanned for btex/verbatimtex blocks with a TypeScript port of mpto's lexer, tested byte-for-byte against the C original.
  2. Typeset once. Every block not already cached goes into one batched pdfTeX run, one page per block.
  3. Convert. MetaPost's own dvitomp turns the DVI into .mpx picture expressions, split per block and cached by a hash of engine, format, preceding verbatimtex chain and body.
  4. Run. MetaPost runs with extensions=1; its make_text callback answers each btex synchronously from the cache.
  5. Fixpoint. A label the scan could not see (scantokens) misses, comes back as nullpicture, and the run repeats once it has been typeset.

Forty labels cost one TeX run; editing one label costs one TeX run with one page; an unchanged document costs none.

Formats, bundles and the snapshot

plain.fmt, etex.fmt and latex.fmt are built by the wasm engine itself at bundle-build time, so they match it byte for byte. tikz.fmt is the optional pre-warmed snapshot: latex.fmt with PGF, its definitional libraries, pgfplots and tikz-cd already loaded, the equivalent of tikzjax's memory image. It saves 50–160 ms per TikZ document and produces byte-identical pages (the test suite checks), but as a 5.8 MB file that does not compress it costs more to download than the 2.5 MB it replaces, so it is the default in Node and opt-in in browsers.

What a page downloadsrawgzipped
the three engines and the JavaScript, once5.2 MB2.2 MB
Ghostscript, once, for the first document with PostScript (opt-in)11.9 MB8.2 MB
a MetaPost figure, geometry only0.07 MB0.01 MB
a MetaPost figure with label() text0.16 MB0.05 MB
a MetaPost figure with plain-TeX btex0.37 MB0.25 MB
a MetaPost figure with LaTeX btex2.4 MB2.2 MB
a TikZ figure (latex.fmt)3.8 MB2.5 MB
a TikZ figure with the snapshot5.9 MB5.5 MB
the first LuaTeX figure (luatex.wasm + dvilualatex.fmt)+ 10.4 MB+ 4.9 MB

Everything is fetched once and cached by the browser; rendered figures are cached in IndexedDB as well. The whole bundle tree is 47 MB on the server, and no page downloads all of it.

Fidelity and tests

Compared with tikzjax

mp-tikz-wasmtikzjax
EnginepdfTeX in DVI mode with a real latex.fmt built by the engine; LuaTeX for graph drawing and LuaKnuth TeX with a plain-TeX memory snapshot; \documentclass and \usepackage shimmed or ignored
Documentsany LaTeX document: standalone, article, multi-page, any bundled packagea TikZ picture body and curated preloaded packages
DVI to SVGdvisvgm itself with PGF's dvisvgm driver: gradients, patterns, opacity, clipping, decorations, markers, pgfplotsa JavaScript re-implementation of the special language; no shadings or patterns
Textglyph outlines in the SVG (self-contained), or woff2 on request<text> runs plus a CSS font sheet
FontsComputer Modern, AMS, Euler, Latin Modern in T1/TS1, the 35 standard PostScript fonts; OpenType through fontspec under LuaTeXComputer Modern
MetaPostyes — the same script also renders <script type="text/metapost">no
Loadingper-file lazy fetch through real kpathsea; optional pre-warmed formatone snapshot plus a file archive
Verificationbyte-identical to TeX Live on the corporanone published
Errorsstructured diagnostics with line numbers, full logsthe TeX log
IsolationWeb Worker with a watchdog; deterministic outputpage thread

Limits

Building

The build is reproducible from the pinned TeX Live 2025 source tarball (vendor/SOURCES.lock records its SHA-256). It never modifies the vendored tree: patches are applied to a copy, ctangle is built from the vendored CWEB, and every generated file lands under build/ or dist/.

StepProduces
make tangle / make native / make contractthe tangled C, build/native/libmplib.a, and the native contract harness
make wasmdist/mplib.mjs + mplib.wasm (-O3 -flto); make wasm-debug for an ASSERTIONS/SAFE_HEAP variant
scripts/native-texlive.shthe web2c-generated C for pdfTeX and the native config headers (needs autotools' output, once)
scripts/build-tex-wasm.shdist/tex.mjs + tex.wasm from that C plus kpathsea, with Emscripten's zlib and libpng ports
scripts/native-luatex.sh, scripts/build-luatex-wasm.shdist/luatex.mjs + luatex.wasm: the native LuaTeX build's compile commands are recorded and replayed with emcc (Lua 5.3, pplib, zziplib compiled from the vendored sources; the C FFI stubbed)
scripts/native-dvisvgm.sh, scripts/build-dvisvgm-wasm.shdist/dvisvgm.mjs + dvisvgm.wasm (FreeType and zlib ports, -Oz)
scripts/build-texmf.shbuild/texmf: the flattened texmf tree copied from the local TeX Live, with the patches in patches/texmf/ applied, plus tikz.ini and the files in bundles/tex/
node scripts/make-formats.mjsplain.fmt, etex.fmt, latex.fmt, tikz.fmt, built by tex.wasm; dviluatex.fmt, dvilualatex.fmt by luatex.wasm
npm run build:tsdist/*.js: the library, the worker, the CLI, auto.js (before the bundles: the next step runs through it)
npm run build:fontdbluaotfload's font-name database, built by luatex.wasm against build/texmf, for the opentype bundle
node scripts/build-bundles.mjsdist/bundles/*: manifests and files per bundle
npm run build:hotdist/bundles/hot.json: the files a first run of each kind touches, prefetched in parallel (after any bundle rebuild, which deletes it)
npm run packagerelease/mp-tikz-wasm-0.5.0.tar.gz and .zip: the prebuilt distribution for a GitHub release
npm run build:guide, build:standalonethis page, and the single-file demo with everything inlined

The design documents in docs/ record every decision; docs/14-implementation-notes.md records what was learned building it, including the upstream defects.

Licences

The licence texts are in the repository (LICENSE, NOTICE.md, licenses/). MetaPost itself is public domain. mplib.wasm also contains avl.c (LGPL-3.0-or-later) and decNumber (ICU), so it is distributed under the LGPL-3.0-or-later with the sources, the patches and a reproducible build in the repository. pdfTeX and kpathsea are GPL, so tex.wasm is GPL; LuaTeX is GPL-2.0-or-later (Lua, pplib and zziplib carry MIT-style licences), so luatex.wasm is GPL; dvisvgm is GPL-3.0-or-later, so dvisvgm.wasm is too (it embeds FreeType, potrace, clipper, woff2, brotli and the URW base-14 fonts). The fonts and macro packages in the bundles keep their own free licences. This project's own code is LGPL-3.0-or-later. See LICENSE.md.