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:
pdf2svgis built on Poppler, which rasterizes or quietly drops the PostScript-backed features that make a diagram worth drawing — opacity, gradient fills, manypatternsanddecorations. The translucent field sheets above came out flat and opaque, or vanished entirely.pdflatexhas fixed memory. A data-heavypgfplotsfigure could hitTeX capacity exceededand take the whole build down with it.
The fix was two substitutions in the renderer:
lualatexinstead ofpdflatex— dynamic memory, so large diagrams stop overflowing.dvisvgm(withmutoolas its PDF backend) instead ofpdf2svg— it preserves transparency and vector detail, and--no-fontsconverts 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 svgFileTwo 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 ghostscript—mupdfprovidesmutool, whichdvisvgmuses as its PDF backend (the bundled Ghostscript 10.x turned out to be too new fordvisvgm’s direct PDF reader, somutoolis the reliable path).- TeX via
tlmgr install pgfplots mhchem standaloneon top of BasicTeX, which already shipslualatex.
CI (apt):
- added
dvisvgm,mupdf-tools, andtexlive-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.