Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4a6d813
Add missing characters from dummy-index's adaptation, and adapt the z…
pelson May 26, 2026
01b130a
Fine for the surd, not fine for the line (left cap problem).
pelson May 2, 2026
74dc38a
Add mathjax poc
pelson May 2, 2026
5595082
Proof-of-concept for sqrt symbol
pelson May 6, 2026
14554b6
Working sqrt prototype
pelson May 6, 2026
7c8fc33
Inline stuff for mathjax
pelson May 7, 2026
87288fd
Add correct and programatic vinculum handling
pelson May 7, 2026
804fa26
Pretty much perfect for the cases that I've tested (very slight horiz…
pelson May 7, 2026
634f352
Include a mathjax integration, and bring it into the normal pipeline …
pelson May 26, 2026
128dfe1
Add generated rendered math equations to the samples
pelson May 27, 2026
f46fe0f
Avoid a manually generated path living in the mathjax font, and inste…
pelson May 29, 2026
6f55bbe
Review of the mathjax script
pelson May 29, 2026
9691e8e
Simplify the derivatives approach for the mathjax font. The next step…
pelson May 30, 2026
a347044
Use stylistic alternatives for larger versions of ∑/∏/∫. No need for …
pelson May 30, 2026
888639b
Tighten ss01 large-operator wiring and rework MathJax sample renders
pelson May 30, 2026
1b5134d
Fix linlined math text rendering, which was previously using mathtex'…
pelson May 30, 2026
50c73d0
Handle extending curly braces for piecewise equations
pelson May 31, 2026
aab366a
Handle brackets and parentheses, supporting stretched variants of them
pelson Jun 1, 2026
a3225f7
Fix the diacritics in the mathjax rendering
pelson Jun 1, 2026
3fdd9c5
Support tables and vertical line drawing
pelson Jun 2, 2026
39131ba
Add stretchy arrows, and use them for most non \\vec cases
pelson Jun 3, 2026
1643c6f
Add the ring and asterisk operators, and a zoo of symbols in a case t…
pelson Jun 4, 2026
bc0070a
Tidy up
pelson Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
500 changes: 500 additions & 0 deletions line-extend-demo.html
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

Large diffs are not rendered by default.

233 changes: 233 additions & 0 deletions mathjax-demo.html
Comment thread
pelson marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>xkcd-script + MathJax</title>

<style>
body {
font-family: 'xkcd-script', sans-serif;
font-size: 1.25em;
max-width: 920px;
margin: 2em auto;
padding: 0 1.5em;
line-height: 1.7;
color: #111;
background: #fff;
}

h1 { font-size: 2.2em; margin-bottom: 0.2em; }
h2 { font-size: 1.5em; margin-top: 2em; border-bottom: 1px solid #ccc; padding-bottom: 0.2em; }
p { margin: 0.8em 0; }

/* ── Editor ── */
.editor-wrap {
display: flex;
gap: 1em;
align-items: flex-start;
margin-top: 0.5em;
}
#latex-input {
flex: 1;
min-height: 220px;
font-family: monospace;
font-size: 0.78em;
padding: 0.6em;
border: 1px solid #999;
resize: vertical;
background: #fffff0;
box-sizing: border-box;
}
#latex-preview {
flex: 1;
min-height: 220px;
padding: 0.6em 1em;
border: 1px solid #ccc;
background: #fff;
box-sizing: border-box;
overflow-wrap: break-word;
}
</style>

<script>
// MathJax TeX-input config (delimiters). Must be set BEFORE xkcd-mathjax3.js
// (which then merges in its startup hook) and BEFORE MathJax itself loads.
MathJax = {
loader: { load: ['[tex]/amscd'] },
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
packages: {'[+]': ['amscd']},
},
};
</script>
<script src="xkcd-script/xkcd-mathjax3.js"></script>
<script id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>

</head>
<body>

<h1>xkcd-script + MathJax</h1>
<p>MathJax CHTML with xkcd-script substituted for letter and Greek glyphs.</p>

<div class="editor-wrap">
<textarea id="latex-input" spellcheck="false">Inline vs display large operators:

$\sum_{k=1}^{n} k$ and $\int_0^1 x^2\,dx$

$$\sum_{k=1}^{n} k = \frac{n(n+1)}{2}$$

$$\int_0^\infty e^{-x}\,dx = 1$$

$$\prod_{p\;\text{prime}} \frac{1}{1-p^{-s}} = \sum_{n=1}^{\infty} \frac{1}{n^s}$$</textarea>
<div id="latex-preview"></div>
</div>

<script>
const input = document.getElementById('latex-input');
const preview = document.getElementById('latex-preview');
let renderTimer;

function renderPreview() {
MathJax.typesetClear([preview]);
preview.textContent = input.value;
MathJax.typesetPromise([preview]).then(() => XkcdMathJax.refresh(preview));
}

input.addEventListener('input', () => {
clearTimeout(renderTimer);
renderTimer = setTimeout(renderPreview, 350);
});

// Initial preview render fires once MathJax + xkcd-mathjax finish setup.
XkcdMathJax.ready.then(renderPreview);

// On resize the library already re-places overlays on the static formulae;
// we additionally re-render the live preview so its layout stays in sync.
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(renderPreview, 150);
});
</script>

<h2>Large operators: inline vs display</h2>

<p>Inline: $\sum_{k=1}^{n} k$ and $\int_0^1 x^2\,dx$ and $\prod_{k=1}^{n} k$</p>

<p>Display:
$$\sum_{k=1}^{n} k = \frac{n(n+1)}{2}$$
$$\int_0^\infty e^{-x}\,dx = 1 \qquad \int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}$$
$$\prod_{p\;\text{prime}} \frac{1}{1-p^{-s}} = \sum_{n=1}^{\infty} \frac{1}{n^s}$$
</p>

<h2>Algebra</h2>

<p>Quadratic formula:
$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$</p>

<p>Pythagorean theorem: $a^2 + b^2 = c^2$</p>

<p>Binomial theorem:
$$\sum_{k=0}^{n} \binom{n}{k} x^k y^{n-k} = (x + y)^n$$</p>

<h2>Calculus</h2>

<p>Definition of derivative:
$$f'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}$$</p>

<p>Gaussian integral:
$$\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}$$</p>

<p>Product rule: $\dfrac{d}{dx}\bigl[f(x)\,g(x)\bigr] = f'(x)\,g(x) + f(x)\,g'(x)$</p>

<h2>Greek letters</h2>

<p>Euler's identity: $e^{i\pi} + 1 = 0$</p>

<p>$\alpha,\ \beta,\ \gamma,\ \delta,\ \varepsilon,\ \zeta,\ \eta,\
\theta,\ \iota,\ \kappa,\ \lambda,\ \mu,\ \nu,\ \xi,\ \pi,\
\rho,\ \sigma,\ \tau,\ \upsilon,\ \phi,\ \chi,\ \psi,\ \omega$</p>

<p>Fourier transform:
$$\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x)\,e^{-2\pi i x \xi}\,dx$$</p>

<h2>Physics</h2>

<p>Einstein: $E = mc^2$</p>

<p>Schrödinger equation:
$$i\hbar\frac{\partial\psi}{\partial t} = \hat{H}\psi$$</p>

<p>Maxwell in free space:
$$\nabla \cdot \mathbf{E} = 0 \qquad \nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t}$$</p>


<h2>Piecewise definitions (stretchy <code>{</code>)</h2>

<p>One row (no stretch — MathJax uses an inline <code>{</code> and we leave it alone):
$$y = \begin{cases} x & \text{if } x > 0 \end{cases}$$</p>

<p>Two rows (short brace):
$$|x| = \begin{cases} x, & \text{if } x \geq 0 \\ -x, & \text{if } x < 0 \end{cases}$$</p>

<p>Four rows (tall brace):
$$f(x) = \begin{cases}
\dfrac{\sin x}{x}, & \text{if } x > 0 \\[6pt]
\displaystyle\sum_{k=0}^{\infty} \frac{(-1)^k x^{2k}}{(2k+1)!}, & \text{if } x = 0 \\[6pt]
-\sqrt{|x|}, & \text{if } -1 \leq x < 0 \\[4pt]
\infty, & \text{otherwise}
\end{cases}$$</p>

<h2>Stretchy delimiters (parens and brackets, side-by-side)</h2>

<p>Two-high (<code>\binom</code>):
$$\binom{n}{k}$$</p>

<p>Around a fraction (<code>\left(\dfrac{...}{...}\right)^n</code> / <code>\left[...\right]^n</code>):
$$\left( \dfrac{\frac{a}{b}}{\frac{c}{d}} \right)^{n} \qquad
\left[ \dfrac{\frac{a}{b}}{\frac{c}{d}} \right]^{n}$$</p>

<p>3×3 matrix (<code>pmatrix</code> / <code>bmatrix</code>):
$$\begin{pmatrix}
a_{11} & a_{12} & a_{13} \\
a_{21} & a_{22} & a_{23} \\
a_{31} & a_{32} & a_{33}
\end{pmatrix} \qquad
\begin{bmatrix}
a_{11} & a_{12} & a_{13} \\
a_{21} & a_{22} & a_{23} \\
a_{31} & a_{32} & a_{33}
\end{bmatrix}$$</p>

<p>Five-row column vector (extreme stretch):
$$\begin{pmatrix} v_1 \\ v_2 \\ v_3 \\ v_4 \\ v_5 \end{pmatrix} \qquad
\begin{bmatrix} v_1 \\ v_2 \\ v_3 \\ v_4 \\ v_5 \end{bmatrix}$$</p>

<h2>Stretchy arrows</h2>

<p>Vector accents (<code>\vec</code>, <code>\overrightarrow</code>, <code>\overleftarrow</code>):
$\vec{v},\ \vec{F},\ \vec{p},\ \vec{\mathbf{0}},\ \vec{\mathbf{B}},\ \vec{ABC},\ \overrightarrow{ABC},\ \overleftarrow{ABC}$</p>

<p>Stretchy operators with labels (<code>\xrightarrow</code>, <code>\xleftarrow</code>):
$A \xrightarrow{f} B \xrightarrow{n+\mu-1} C \qquad X \xleftarrow{g} Y$</p>

<p>Inline arrows (should NOT be replaced — xkcd-script's own glyph):
$\lim_{h \to 0} f(h) \qquad x \to \infty \qquad a \leftarrow b$</p>

<p>AMScd commutative diagram:
$$\begin{CD} A @>f>> B \\ @VgVV @VVhV \\ C @>>k> D \end{CD}$$</p>

<p>AMScd four-direction:
$$\begin{CD} A @>f>> B \\ @AAA @VVV \\ C @<<< D \end{CD}$$</p>

<p>Deeply nested sqrt fractions:
$$\sqrt{b^2 - 4ac}
\sqrt{\frac{b^2}{x}}{2a}
\sqrt{\frac{b^2}{\frac{b^2}{x}}}{2a}
\sqrt{\frac{b^2}{\frac{b^2}{\frac{b^2}{x}}}}{2a}
\sqrt{\frac{b^2}{\frac{b^2}{\frac{b^2}{\frac{b^2}{4ac}}}}}{2a}$$
</p>
</body>
</html>
1 change: 1 addition & 0 deletions run_mathjax.sh
Comment thread
pelson marked this conversation as resolved.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this file.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker run --rm -v /media/important/github/jupyter/xkcd-font:/work fontbuilder-test python3 /work/xkcd-script/generator/gen_mathjax_font.py
132 changes: 132 additions & 0 deletions sqrt-builder.html
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't want to ship this.

Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>sqrt line builder</title>
<style>
body { font-family: monospace; padding: 20px; background: #f0f0f0; }
#output svg { display: block; background: white; margin-top: 12px; border: 1px solid #ccc; }
label { display: block; margin: 10px 0 4px; }
input[type=range] { width: 600px; vertical-align: middle; }
#info { margin-top: 8px; color: #555; font-size: 0.9em; }
</style>
</head>
<body>
<h2>sqrt line builder</h2>

<label><input type="checkbox" id="surdCheck" checked> Include surd (&radic;)</label>
<label>
Target width:
<input type="range" id="widthSlider" min="128" max="2000" value="300">
<span id="widthLabel">300</span> pt
</label>
<div id="info"></div>
<div id="output"></div>

<script>
// All coordinates are in the source SVG's pt space (viewBox "0 0 170 61").
// Cut points derived from pixel-profile analysis of sqrt.png (400x143px → 0.425pt/px).
const SRC_W = 170, SRC_H = 61;
const CUT_A = 57.5; // end of surd cap / start of tile (flat zone)
const CUT_B = 59.0; // start of right cap (still in flat zone, fixes the step)
const TILE_W = CUT_B - CUT_A; // 42.5 pt — the stretchable flat-bar tile
const RIGHT_CAP_W = SRC_W - CUT_B; // 85 pt

const PATH_D = `m 410.6448,596.25455 c -52.01622,-1.52077 -90.0699,-7.17037 -96.13414,-14.11479 -5.06429,-6.95363 -20.44144,-47.81367 -32.84636,-91.70123 -12.40492,-43.88756 -36.13187,-122.67228 -52.61034,-174.52264 -15.47851,-51.85958 -36.11344,-120.67237 -45.40792,-152.58808 -9.28526,-30.91575 -20.52437,-56.81329 -23.52424,-56.78565 -3.99983,0.0369 -17.7873,23.16488 -30.52875,51.28348 C 95.267178,231.14504 70.535454,260.37417 54.416347,247.52215 36.278897,232.68864 140.33939,21.72084 165.33833,21.490482 c 12.99945,-0.119786 25.22008,23.768624 49.91935,99.544258 48.37094,148.5606 70.01505,218.36413 102.04573,330.07373 l 28.93867,101.73767 400.02909,1.31406 c 221.01827,0.96351 522.03313,1.18988 670.02683,-0.17384 147.9937,-1.36371 271.0069,-0.49716 273.0345,2.48429 3.0275,2.97223 3.1104,11.97184 -0.8065,21.00832 -4.8707,14.04548 -64.8498,16.59825 -597.8548,18.5096 -326.00457,1.00395 -632.01001,1.82361 -680.0264,0.26598 z`;

// Caps extend this far into the tile region so their clipped edges
// land on flat-bar content rather than at a path outline boundary.
// The tile is drawn behind; caps (opaque black) paint over any tile
// bleed in their regions — black on black, no visible change.
const OVERLAP = 2;

const slider = document.getElementById('widthSlider');
const surdCheck = document.getElementById('surdCheck');
const widthLabel = document.getElementById('widthLabel');
const info = document.getElementById('info');
const output = document.getElementById('output');

surdCheck.addEventListener('change', () => {
const minW = surdCheck.checked
? Math.ceil(CUT_A + RIGHT_CAP_W)
: Math.ceil(2 * RIGHT_CAP_W);
slider.min = minW;
if (+slider.value < minW) slider.value = minW;
redraw();
});
slider.addEventListener('input', redraw);

function f(n) { return n.toFixed(3); }

function redraw() {
const W = +slider.value;
widthLabel.textContent = W;
const surd = surdCheck.checked;
const leftCapW = surd ? CUT_A : RIGHT_CAP_W;

const M = Math.max(0, W - leftCapW - RIGHT_CAP_W);
const sx = M > 0 ? M / TILE_W : 1;
const tileTx = leftCapW - CUT_A * sx;
const rightTx = W - RIGHT_CAP_W - CUT_B;

// Below this threshold the scaled tile is too thin to rasterize cleanly —
// skip it and let the extended left cap bridge the gap instead.
const TILE_THRESHOLD = 4;
const useTile = M >= TILE_THRESHOLD;

// When tiling: tight sub-clip on the tile, caps extend OVERLAP into it.
// When not tiling: left cap extends all the way across the gap (+ OVERLAP
// into the right cap), so no tile clip is needed.
const leftClipW = useTile ? leftCapW + OVERLAP : leftCapW + M + OVERLAP;

const clipMid = useTile ? `<clipPath id="cm">
<rect x="${f(leftCapW)}" y="0" width="${f(M)}" height="${SRC_H}"/>
</clipPath>` : '';

const clipLeft = `<clipPath id="cl">
<rect x="0" y="0" width="${f(leftClipW)}" height="${SRC_H}"/>
</clipPath>`;

const clipRight = `<clipPath id="cr">
<rect x="${f(W - RIGHT_CAP_W - OVERLAP)}" y="0" width="${f(RIGHT_CAP_W + OVERLAP)}" height="${SRC_H}"/>
</clipPath>`;

const mid = useTile ? `<g clip-path="url(#cm)">
<g transform="translate(${f(tileTx)},0) scale(${f(sx)},1)"><use href="#src"/></g>
</g>` : '';

// Left cap drawn on top of tile — covers the tile's left clip seam.
const leftCap = surd
? `<g clip-path="url(#cl)"><use href="#src"/></g>`
: `<g clip-path="url(#cl)">
<g transform="translate(${f(SRC_W)},0) scale(-1,1)"><use href="#src"/></g>
</g>`;

// Right cap drawn on top of tile — covers the tile's right clip seam.
const right = `<g clip-path="url(#cr)">
<g transform="translate(${f(rightTx)},0)"><use href="#src"/></g>
</g>`;

// Render order: tile (back) → left cap → right cap (front).
output.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg"
width="${W}pt" height="${SRC_H}pt" viewBox="0 0 ${W} ${SRC_H}">
<defs>
<g id="src" transform="translate(0,61) scale(0.1,-0.1)">
<path d="${PATH_D}" fill="black"/>
</g>
${clipMid}
${clipLeft}
${clipRight}
</defs>
${mid}
${leftCap}
${right}
</svg>`;

info.textContent = `left: ${f(leftCapW)}pt | tile: ${f(M)}pt (${f(sx)}×) | right: ${f(RIGHT_CAP_W)}pt | total: ${W}pt`;
}

redraw();
</script>
</body>
</html>
Loading
Loading