← All posts
tikz, rendering, haskell, hakyll, electromagnetism, physics

A light wave from Maxwell's equations, rendered in pure TikZ

This post is two things at once. It is a small derivation — how a propagating light wave falls out of Maxwell’s equations — and it is the proof that this blog’s diagram pipeline can now draw that wave honestly, transparency and all. Every figure here is compiled from TikZ source at build time: the diagram in the Markdown is the diagram on the page, with no image file checked into the repository.

The wave hiding in Maxwell’s equations

In a source-free region of vacuum, Maxwell’s four equations reduce to a strikingly symmetric set1,2:

\[\nabla \cdot \vec{E} = 0, \qquad \nabla \cdot \vec{B} = 0,\]

\[\nabla \times \vec{E} = -\frac{\partial \vec{B}}{\partial t}, \qquad \nabla \times \vec{B} = \mu_0 \varepsilon_0 \frac{\partial \vec{E}}{\partial t}.\]

The two curl equations couple the fields: a changing magnetic field sources a circulating electric field, and a changing electric field sources a circulating magnetic one. Take the curl of Faraday’s law and substitute Ampère’s law, using the identity \(\nabla \times (\nabla \times \vec{E}) = \nabla(\nabla \cdot \vec{E}) - \nabla^2 \vec{E}\) together with \(\nabla \cdot \vec{E} = 0\):

\[\nabla^2 \vec{E} = \mu_0 \varepsilon_0 \frac{\partial^2 \vec{E}}{\partial t^2}.\]

That is the wave equation. Reading off the coefficient, the disturbance propagates at \(c = 1/\sqrt{\mu_0 \varepsilon_0}\) — a speed assembled entirely from electric and magnetic constants, which is how Maxwell recognized that light is an electromagnetic phenomenon3. A plane-wave solution travelling along \(\hat{v}\) is

\[\vec{E} = E_0 \,\hat{z}\,\sin(kx - \omega t), \qquad \vec{B} = B_0 \,\hat{y}\,\sin(kx - \omega t),\]

with three properties worth seeing rather than just stating: \(\vec{E}\) and \(\vec{B}\) are mutually perpendicular, both are transverse to the direction of travel, and they oscillate in phase. A static picture of those three facts is exactly the kind of figure that is tedious to fake and easy to draw if your toolchain cooperates.

Drawing it: build-time TikZ

The figure below is the test case. It is a 3D electromagnetic plane wave drawn in a hand-rolled basis: the red sheet is the electric field \(\vec{E}\), the blue sheet the magnetic field \(\vec{B}\), perpendicular and in phase, propagating along \(\vec{v}\). The fills are translucent — where the two field envelopes cross you can see through both, and that single visual cue is what carried the whole toolchain rebuild, because the old pipeline threw the transparency away.

TikZ is the right tool for this because it is a programmatic drawing language — the two field sheets are \foreach loops over \(\sin(kx)\), not hand-placed vertices, so the geometry is the physics4. The cost is that TikZ runs inside TeX, which means the blog has to run TeX during its build.

The pipeline, and why it had to change

The blog is a Hakyll site: a Haskell program compiles Markdown into the static HTML you are reading5. A custom Pandoc filter walks the document, finds every code block tagged tikzpicture, shells out to render it, and splices the result back into the page. The original filter did this:

pdflatex → PDF → pdf2svg → SVG, embedded as a data-URI <img>.

It worked for line art and pgfplots charts and failed, silently, on anything richer. Two independent problems:

  • pdf2svg is built on Poppler, which rasterizes or quietly drops the PostScript-backed features that make a diagram worth drawing — opacity, gradient fills, many patterns and decorations. The translucent field sheets above came out flat and opaque, or vanished entirely.
  • pdflatex has fixed memory. A data-heavy pgfplots figure could hit TeX capacity exceeded and take the whole build down with it.

The fix was two substitutions in the renderer:

  • lualatex instead of pdflatex — dynamic memory, so large diagrams stop overflowing.
  • dvisvgm (with mutool as its PDF backend) instead of pdf2svg — it preserves transparency and vector detail, and --no-fonts converts text to paths so the SVG is self-contained6.

The SVG is now embedded inline rather than as a data-URI image, so it scales crisply and inherits page styling. And the filter degrades gracefully: a diagram that won’t compile is logged to the build output and replaced with a small error box, instead of aborting the entire site.

The Haskell, concretely

The renderer lives in a Blog.TikZ module. Its core is one IO action that writes a standalone LaTeX document into a temp directory, runs the two external tools, and returns Either String String — a diagnostic on failure, the SVG on success:

renderTikz :: String -> IO (Either String String)
renderTikz tikzCode = withSystemTempDirectory "blog-tikz" $ \dir -> do
  -- ... write preamble + body to tikz.tex ...
  (texCode, texOut, texErr) <- readProcessWithExitCode "lualatex"
    ["-halt-on-error", "-interaction=nonstopmode", "-output-directory=" ++ dir, texFile] ""
  case texCode of
    ExitFailure _ -> bail "lualatex" (texOut ++ texErr)
    ExitSuccess -> do
      (svgCode, svgOut, svgErr) <- readProcessWithExitCode "dvisvgm"
        ["--pdf", "--no-fonts", "--output=" ++ svgFile, pdfFile] ""
      case svgCode of
        ExitFailure _ -> bail "dvisvgm" (svgOut ++ svgErr)
        ExitSuccess   -> Right . T.unpack <$> TIO.readFile svgFile

Two details earn their keep. First, the filter is smart about wrapping: a block that opens its own \begin{tikzpicture} — as the wave above does, to pass the custom 3D basis in [x={...}, y={...}, z={...}] — is used verbatim, while a bare body (like a lone pgfplots axis) is auto-wrapped. Second, inlineSvg is a pure function that strips dvisvgm’s XML prolog and DOCTYPE so the markup is safe to drop straight into HTML — pure, therefore unit-tested in the base-only test suite, no TeX required.

Dependencies

Reproducing the pipeline means two layers of tooling — local (macOS, for previewing) and CI (Ubuntu, for the real deploy build):

Local (Homebrew + TeX Live):

  • brew install dvisvgm mupdf ghostscriptmupdf provides mutool, which dvisvgm uses as its PDF backend (the bundled Ghostscript 10.x turned out to be too new for dvisvgm’s direct PDF reader, so mutool is the reliable path).
  • TeX via tlmgr install pgfplots mhchem standalone on top of BasicTeX, which already ships lualatex.

CI (apt):

  • added dvisvgm, mupdf-tools, and texlive-luatex;
  • dropped pdf2svg.

A machine with no TeX at all still builds the site fine — the filter only shells out when a post actually contains a tikzpicture block, so prose-only posts never touch the toolchain.

What the diagram proves

Look again at the figure: where the red and blue sheets overlap, you can see straight through both. That transparency is the single feature pdf2svg used to discard, and getting it back — without rasterizing, without a checked-in PNG, and without making TeX-free builds impossible — is the whole point of the rebuild. The physics earns the picture; the toolchain finally lets the picture be drawn.

Credit: the electromagnetic-wave figure is adapted from the diagram gallery at diagrams.janosh.dev (originally a CeTZ/Typst drawing), ported here to native TikZ.

References

1.
Maxwell, J. C. A Treatise on Electricity and Magnetism; A treatise on electricity and magnetism; Dover Publications, 1954.
2.
Griffiths, D. J.; Schroeter, D. F. Introduction to Electrodynamics, 4th ed.; Cambridge University Press: Cambridge, 2018. https://doi.org/10.1017/9781108333511.
3.
Jackson, J. D. Classical Electrodynamics, 3rd ed.; Wiley: New York, 1999.
4.
5.
Jeugt, J. V. der. Hakyll: A Static Website Compiler Library in Haskell, 2024. https://jaspervdj.be/hakyll/.
6.
Gieseking, M. Dvisvgm: A Fast DVI to SVG Converter, 2023. https://dvisvgm.de/.
← All posts