Enhance math content presentation and widget styles: Update SKILL.md to specify the use of shared modal patterns for chapter widgets. Revise algebraic fractions examples in _index.md, core.html, and supplement.html to utilize aligned multi-line KaTeX for improved readability. Introduce responsive styles in chapter.scss for better mobile layout in addition groups and big numbers scale controls.

This commit is contained in:
Hananoshika Yomaru 2026-05-21 23:25:20 -07:00
parent 3f666cf1b5
commit 669fbd59fa
8 changed files with 436 additions and 122 deletions

View file

@ -137,6 +137,8 @@ Live chapters live under `content/{subject}/{slug}/`:
**Math equation layout (all subjects):** for chained calculations, **always prefer multi-line KaTeX** and **always align at the equal sign** using `aligned` (e.g. `\begin{aligned} ... &= ... \\ ... \end{aligned}`). Do not write long one-line chains; aligned multi-line steps are the default for readability and mobile layout.
**Dialog/modals in widgets:** when adding a `<dialog>` in chapter widgets, use the shared modal pattern and classes from `themes/openfreebooks/sass/_contributors.scss` (`contributor-dialog`, `contributor-dialog-panel`, `contributor-dialog-close`) as the base style, keep the dialog centered in modal mode, and only layer minimal widget-specific overrides on top.
### Interactive demo technology choices
Interactive demos can use different technologies based on the learning goal and interaction type:

View file

@ -59,13 +59,26 @@ strand = "Number & Algebra"
<p>
Factorise: \(x^2 - 1 = (x-1)(x+1)\) and \(x^2 + x = x(x+1)\). Cancel the common factor
\((x+1)\):
\[ \frac{(x-1)(x+1)}{x(x+1)} = \frac{x-1}{x}, \quad x \neq 0,\; x \neq -1. \]
\[
\begin{aligned}
\frac{(x-1)(x+1)}{x(x+1)}
&= \frac{x-1}{x}, \\
x &\neq 0,\; x \neq -1
\end{aligned}
\]
The restrictions matter: at \(x = -1\) the original denominator is zero.
</p>
<p><strong>Example.</strong> Simplify \(\dfrac{2x^2 - 8}{x^2 - 4}\).</p>
<p>
\[ \frac{2(x^2-4)}{x^2-4} = \frac{2(x-2)(x+2)}{(x-2)(x+2)} = 2, \quad x \neq 2,\; x \neq -2. \]
\[
\begin{aligned}
\frac{2(x^2-4)}{x^2-4}
&= \frac{2(x-2)(x+2)}{(x-2)(x+2)} \\
&= 2, \\
x &\neq 2,\; x \neq -2
\end{aligned}
\]
</p>
<h2 id="add-subtract" class="book-prose__heading">Adding and subtracting</h2>
@ -77,13 +90,26 @@ strand = "Number & Algebra"
<p><strong>Example.</strong> \(\dfrac{2}{x} + \dfrac{3}{x+1}\).</p>
<p>
LCD is \(x(x+1)\):
\[ \frac{2}{x} + \frac{3}{x+1} = \frac{2(x+1) + 3x}{x(x+1)} = \frac{5x+2}{x(x+1)}, \quad x \neq 0,\; x \neq -1. \]
\[
\begin{aligned}
\frac{2}{x} + \frac{3}{x+1}
&= \frac{2(x+1) + 3x}{x(x+1)} \\
&= \frac{5x+2}{x(x+1)}, \\
x &\neq 0,\; x \neq -1
\end{aligned}
\]
</p>
<p><strong>Example.</strong> \(\dfrac{1}{x-2} - \dfrac{3}{x^2 - 4}\).</p>
<p>
Note \(x^2 - 4 = (x-2)(x+2)\). With LCD \((x-2)(x+2)\):
\[ \frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)} = \frac{x-1}{(x-2)(x+2)}, \quad x \neq 2,\; x \neq -2. \]
\[
\begin{aligned}
\frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)}
&= \frac{x-1}{(x-2)(x+2)}, \\
x &\neq 2,\; x \neq -2
\end{aligned}
\]
</p>
<h2 id="multiply-divide" class="book-prose__heading">Multiplying and dividing</h2>
@ -101,12 +127,25 @@ strand = "Number & Algebra"
<p><strong>Example.</strong> \(\dfrac{x^2 - 9}{2x} \times \dfrac{4}{x+3}\).</p>
<p>
\[ \frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3} = \frac{4(x-3)}{2x} = \frac{2(x-3)}{x}, \quad x \neq 0,\; x \neq -3. \]
\[
\begin{aligned}
\frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3}
&= \frac{4(x-3)}{2x} \\
&= \frac{2(x-3)}{x}, \\
x &\neq 0,\; x \neq -3
\end{aligned}
\]
</p>
<p><strong>Example.</strong> \(\dfrac{x^2 - 1}{x+1} \div \dfrac{x-1}{2}\).</p>
<p>
\[ \frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1} = 2, \quad x \neq -1,\; x \neq 1. \]
\[
\begin{aligned}
\frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1}
&= 2, \\
x &\neq -1,\; x \neq 1
\end{aligned}
\]
</p>
<h2 id="harder-cases" class="book-prose__heading">Harder examples</h2>
@ -273,8 +312,16 @@ strand = "Number & Algebra"
<h3 id="uniform-motion" class="book-prose__heading">Uniform motion</h3>
<p>
For constant speed, distance \(d\), speed \(v\), and time \(t\) are related by \(v = d/t\). If a
train travels \(240\) km in \(3\) h, then \(v = 240/3 = 80\) km/h. If you know \(v\) and \(t\)
but need distance, rearrange to \(d = vt\).
train travels \(240\) km in \(3\) h, then:
</p>
<p class="book-formula">\[
\begin{aligned}
v &= \frac{240}{3} \\
&= 80\ \text{km/h}
\end{aligned}
\]</p>
<p>
If you know \(v\) and \(t\) but need distance, rearrange to \(d = vt\).
</p>
<div class="math-widget-mount" data-widget="formula-rearrange-demo" data-pagefind-ignore></div>
@ -282,8 +329,16 @@ strand = "Number & Algebra"
<h3 id="density" class="book-prose__heading">Density</h3>
<p>
Density \(\rho\) (rho) is mass per unit volume: \(\rho = m/V\). A block of mass \(2.7\) kg with
volume \(0.001\) m³ has \(\rho = 2.7/0.001 = 2700\) kg/m³. To find mass from \(\rho\) and \(V\),
use \(m = \rho V\).
volume \(0.001\) m³ has:
</p>
<p class="book-formula">\[
\begin{aligned}
\rho &= \frac{2.7}{0.001} \\
&= 2700\ \text{kg/m}^3
\end{aligned}
\]</p>
<p>
To find mass from \(\rho\) and \(V\), use \(m = \rho V\).
</p>
<h3 id="pressure-circuits" class="book-prose__heading">Pressure and electric circuits</h3>
@ -408,7 +463,14 @@ strand = "Number & Algebra"
<details class="book-question">
<summary class="book-question__prompt">Make \(t\) the subject of \(v = \dfrac{d}{t}\) and then find \(t\) when \(d = 150\) km and \(v = 75\) km/h.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(t = d/v = 2\) h.</p>
<p><strong>Answer:</strong></p>
<p class="book-formula">\[
\begin{aligned}
t &= \frac{d}{v} \\
&= \frac{150}{75} \\
&= 2\ \text{h}
\end{aligned}
\]</p>
<p><strong>Steps:</strong> \(vt = d \Rightarrow t = d/v\); substitute values.</p>
</div>
</details>
@ -424,7 +486,20 @@ strand = "Number & Algebra"
<details class="book-question">
<summary class="book-question__prompt">A formula is \(P = \dfrac{F}{A}\). The force is \(120\) N and the area is \(0.05\) m². Find \(P\), then make \(F\) the subject and verify your force value.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(P = 120/0.05 = 2400\) Pa; \(F = PA = 2400 \times 0.05 = 120\) N.</p>
<p><strong>Answer:</strong></p>
<p class="book-formula">\[
\begin{aligned}
P &= \frac{120}{0.05} \\
&= 2400\ \text{Pa}
\end{aligned}
\]</p>
<p class="book-formula">\[
\begin{aligned}
F &= PA \\
&= 2400 \times 0.05 \\
&= 120\ \text{N}
\end{aligned}
\]</p>
<p><strong>Explanation:</strong> Rearranging checks that the formula is consistent.</p>
</div>
</details>

View file

@ -48,13 +48,26 @@
<p>
Factorise: \(x^2 - 1 = (x-1)(x+1)\) and \(x^2 + x = x(x+1)\). Cancel the common factor
\((x+1)\):
\[ \frac{(x-1)(x+1)}{x(x+1)} = \frac{x-1}{x}, \quad x \neq 0,\; x \neq -1. \]
\[
\begin{aligned}
\frac{(x-1)(x+1)}{x(x+1)}
&= \frac{x-1}{x}, \\
x &\neq 0,\; x \neq -1
\end{aligned}
\]
The restrictions matter: at \(x = -1\) the original denominator is zero.
</p>
<p><strong>Example.</strong> Simplify \(\dfrac{2x^2 - 8}{x^2 - 4}\).</p>
<p>
\[ \frac{2(x^2-4)}{x^2-4} = \frac{2(x-2)(x+2)}{(x-2)(x+2)} = 2, \quad x \neq 2,\; x \neq -2. \]
\[
\begin{aligned}
\frac{2(x^2-4)}{x^2-4}
&= \frac{2(x-2)(x+2)}{(x-2)(x+2)} \\
&= 2, \\
x &\neq 2,\; x \neq -2
\end{aligned}
\]
</p>
<h2 id="add-subtract" class="book-prose__heading">Adding and subtracting</h2>
@ -66,13 +79,26 @@
<p><strong>Example.</strong> \(\dfrac{2}{x} + \dfrac{3}{x+1}\).</p>
<p>
LCD is \(x(x+1)\):
\[ \frac{2}{x} + \frac{3}{x+1} = \frac{2(x+1) + 3x}{x(x+1)} = \frac{5x+2}{x(x+1)}, \quad x \neq 0,\; x \neq -1. \]
\[
\begin{aligned}
\frac{2}{x} + \frac{3}{x+1}
&= \frac{2(x+1) + 3x}{x(x+1)} \\
&= \frac{5x+2}{x(x+1)}, \\
x &\neq 0,\; x \neq -1
\end{aligned}
\]
</p>
<p><strong>Example.</strong> \(\dfrac{1}{x-2} - \dfrac{3}{x^2 - 4}\).</p>
<p>
Note \(x^2 - 4 = (x-2)(x+2)\). With LCD \((x-2)(x+2)\):
\[ \frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)} = \frac{x-1}{(x-2)(x+2)}, \quad x \neq 2,\; x \neq -2. \]
\[
\begin{aligned}
\frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)}
&= \frac{x-1}{(x-2)(x+2)}, \\
x &\neq 2,\; x \neq -2
\end{aligned}
\]
</p>
<h2 id="multiply-divide" class="book-prose__heading">Multiplying and dividing</h2>
@ -90,12 +116,25 @@
<p><strong>Example.</strong> \(\dfrac{x^2 - 9}{2x} \times \dfrac{4}{x+3}\).</p>
<p>
\[ \frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3} = \frac{4(x-3)}{2x} = \frac{2(x-3)}{x}, \quad x \neq 0,\; x \neq -3. \]
\[
\begin{aligned}
\frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3}
&= \frac{4(x-3)}{2x} \\
&= \frac{2(x-3)}{x}, \\
x &\neq 0,\; x \neq -3
\end{aligned}
\]
</p>
<p><strong>Example.</strong> \(\dfrac{x^2 - 1}{x+1} \div \dfrac{x-1}{2}\).</p>
<p>
\[ \frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1} = 2, \quad x \neq -1,\; x \neq 1. \]
\[
\begin{aligned}
\frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1}
&= 2, \\
x &\neq -1,\; x \neq 1
\end{aligned}
\]
</p>
<h2 id="harder-cases" class="book-prose__heading">Harder examples</h2>

View file

@ -90,8 +90,16 @@
<h3 id="uniform-motion" class="book-prose__heading">Uniform motion</h3>
<p>
For constant speed, distance \(d\), speed \(v\), and time \(t\) are related by \(v = d/t\). If a
train travels \(240\) km in \(3\) h, then \(v = 240/3 = 80\) km/h. If you know \(v\) and \(t\)
but need distance, rearrange to \(d = vt\).
train travels \(240\) km in \(3\) h, then:
</p>
<p class="book-formula">\[
\begin{aligned}
v &= \frac{240}{3} \\
&= 80\ \text{km/h}
\end{aligned}
\]</p>
<p>
If you know \(v\) and \(t\) but need distance, rearrange to \(d = vt\).
</p>
<div class="math-widget-mount" data-widget="formula-rearrange-demo" data-pagefind-ignore></div>
@ -99,8 +107,16 @@
<h3 id="density" class="book-prose__heading">Density</h3>
<p>
Density \(\rho\) (rho) is mass per unit volume: \(\rho = m/V\). A block of mass \(2.7\) kg with
volume \(0.001\) m³ has \(\rho = 2.7/0.001 = 2700\) kg/m³. To find mass from \(\rho\) and \(V\),
use \(m = \rho V\).
volume \(0.001\) m³ has:
</p>
<p class="book-formula">\[
\begin{aligned}
\rho &= \frac{2.7}{0.001} \\
&= 2700\ \text{kg/m}^3
\end{aligned}
\]</p>
<p>
To find mass from \(\rho\) and \(V\), use \(m = \rho V\).
</p>
<h3 id="pressure-circuits" class="book-prose__heading">Pressure and electric circuits</h3>
@ -225,7 +241,14 @@
<details class="book-question">
<summary class="book-question__prompt">Make \(t\) the subject of \(v = \dfrac{d}{t}\) and then find \(t\) when \(d = 150\) km and \(v = 75\) km/h.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(t = d/v = 2\) h.</p>
<p><strong>Answer:</strong></p>
<p class="book-formula">\[
\begin{aligned}
t &= \frac{d}{v} \\
&= \frac{150}{75} \\
&= 2\ \text{h}
\end{aligned}
\]</p>
<p><strong>Steps:</strong> \(vt = d \Rightarrow t = d/v\); substitute values.</p>
</div>
</details>
@ -241,7 +264,20 @@
<details class="book-question">
<summary class="book-question__prompt">A formula is \(P = \dfrac{F}{A}\). The force is \(120\) N and the area is \(0.05\) m². Find \(P\), then make \(F\) the subject and verify your force value.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(P = 120/0.05 = 2400\) Pa; \(F = PA = 2400 \times 0.05 = 120\) N.</p>
<p><strong>Answer:</strong></p>
<p class="book-formula">\[
\begin{aligned}
P &= \frac{120}{0.05} \\
&= 2400\ \text{Pa}
\end{aligned}
\]</p>
<p class="book-formula">\[
\begin{aligned}
F &= PA \\
&= 2400 \times 0.05 \\
&= 120\ \text{N}
\end{aligned}
\]</p>
<p><strong>Explanation:</strong> Rearranging checks that the formula is consistent.</p>
</div>
</details>

View file

@ -235,6 +235,33 @@
margin-top: 12px;
}
@media (max-width: 640px) {
.addition-groups__piles {
gap: 12px;
}
.addition-groups__pile {
gap: 6px;
}
.addition-groups__dots {
width: 96px;
min-height: 78px;
padding: 8px;
gap: 4px;
}
.addition-groups__dot {
width: 10px;
height: 10px;
}
.addition-groups__plus,
.addition-groups__equals {
font-size: 1.2rem;
}
}
.multiplication-array__grid {
display: grid;
gap: 6px;
@ -587,11 +614,6 @@
width: 100%;
height: 100%;
display: block;
cursor: grab;
}
.big-numbers-scale__canvas:active {
cursor: grabbing;
}
.big-numbers-scale__status {
@ -619,6 +641,36 @@
gap: 10px;
}
.big-numbers-scale__controls-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.1rem;
padding: 7px 9px;
}
.big-numbers-scale__controls-dialog {
margin: auto;
padding: 0;
border: 0;
background: transparent;
color: inherit;
}
.big-numbers-scale__controls-dialog__sheet {
width: 100%;
}
.big-numbers-scale__controls-dialog__header {
display: none;
}
.big-numbers-scale__controls-dialog__title {
margin: 0;
font-size: 0.9rem;
color: var(--soft-ink);
}
.big-numbers-scale__section {
border: 1px solid var(--line);
border-radius: 10px;
@ -710,4 +762,63 @@
.big-numbers-scale__hud {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.big-numbers-scale__controls-toggle {
display: none;
}
.big-numbers-scale__controls-dialog {
display: block;
position: static;
width: auto;
max-width: none;
min-width: 0;
margin: 0;
background: transparent;
border: 0;
}
.big-numbers-scale__controls-dialog__sheet {
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
}
}
@media (max-width: 959px) {
.big-numbers-scale__layout {
grid-template-columns: 1fr;
}
.big-numbers-scale__controls-dialog {
width: min(92vw, 420px);
max-width: none;
margin: auto;
border: 0;
}
.big-numbers-scale__controls-dialog__sheet {
padding: 18px 18px 14px;
opacity: 0;
transform: translateY(12px);
transition: opacity 200ms ease, transform 200ms ease;
}
.big-numbers-scale__controls-dialog[open] .big-numbers-scale__controls-dialog__sheet {
opacity: 1;
transform: translateY(0);
}
.big-numbers-scale__controls-dialog__header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--line);
}
.big-numbers-scale__controls {
max-height: 70vh;
overflow: auto;
padding-right: 2px;
}
}

View file

@ -27,6 +27,8 @@ function BigNumbersScale() {
const soundToggle = byId<HTMLButtonElement>("soundToggle");
const soundIcon = byId<HTMLSpanElement>("soundIcon");
const soundText = byId<HTMLSpanElement>("soundText");
const controlsToggleBtn = byId<HTMLButtonElement>("controlsToggleBtn");
const controlsDialog = byId<HTMLDialogElement>("controlsDialog");
const presetStepsContainer = byId<HTMLDivElement>("presetStepsContainer");
if (
@ -49,6 +51,8 @@ function BigNumbersScale() {
!soundToggle ||
!soundIcon ||
!soundText ||
!controlsToggleBtn ||
!controlsDialog ||
!presetStepsContainer
) {
return;
@ -121,11 +125,6 @@ function BigNumbersScale() {
currentTierIdx: 0,
visualMode: "NORMAL" as VisualMode,
transitionProgress: 0,
panX: 0,
panY: 0,
isDragging: false,
startX: 0,
startY: 0,
particles: [] as Particle[],
ambientStars: [] as CosmicStar[],
shockwaves: [] as Shockwave[],
@ -555,8 +554,8 @@ function BigNumbersScale() {
ctx.fillStyle = canvasBg;
ctx.fillRect(0, 0, width, height);
const cx = width / 2 + state.panX;
const cy = height / 2 + state.panY;
const cx = width / 2;
const cy = height / 2;
state.ambientStars.forEach((s) => s.draw(cx, cy, starColor));
@ -566,8 +565,8 @@ function BigNumbersScale() {
const gridSpacing = 40;
const gridCols = Math.ceil(width / gridSpacing);
const gridRows = Math.ceil(height / gridSpacing);
const startX = state.panX % gridSpacing;
const startY = state.panY % gridSpacing;
const startX = 0;
const startY = 0;
for (let i = 0; i < gridCols; i += 1) {
ctx.beginPath();
ctx.moveTo(startX + i * gridSpacing, 0);
@ -711,47 +710,33 @@ function BigNumbersScale() {
}
});
const toLocal = (evtX: number, evtY: number) => {
const rect = canvas.getBoundingClientRect();
return { x: evtX - rect.left, y: evtY - rect.top };
const isMobileControlsMode = () => window.matchMedia("(max-width: 959px)").matches;
const onControlsToggleClick = () => {
if (!isMobileControlsMode()) return;
if (!controlsDialog.open) {
controlsDialog.showModal();
}
};
canvas.addEventListener("mousedown", (e) => {
state.isDragging = true;
const p = toLocal(e.clientX, e.clientY);
state.startX = p.x - state.panX;
state.startY = p.y - state.panY;
});
const onControlsDialogClick = (event: MouseEvent) => {
if (event.target === controlsDialog) {
controlsDialog.close();
return;
}
if (!isMobileControlsMode()) return;
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const clickedButton = target.closest("button");
if (!clickedButton) return;
if (clickedButton.id === "controlsToggleBtn") return;
requestAnimationFrame(() => {
if (controlsDialog.open) controlsDialog.close();
});
};
canvas.addEventListener("mousemove", (e) => {
if (!state.isDragging) return;
const p = toLocal(e.clientX, e.clientY);
state.panX = p.x - state.startX;
state.panY = p.y - state.startY;
});
window.addEventListener("mouseup", () => {
state.isDragging = false;
});
canvas.addEventListener("touchstart", (e) => {
if (e.touches.length !== 1) return;
state.isDragging = true;
const p = toLocal(e.touches[0]!.clientX, e.touches[0]!.clientY);
state.startX = p.x - state.panX;
state.startY = p.y - state.panY;
});
canvas.addEventListener("touchmove", (e) => {
if (!state.isDragging || e.touches.length !== 1) return;
const p = toLocal(e.touches[0]!.clientX, e.touches[0]!.clientY);
state.panX = p.x - state.startX;
state.panY = p.y - state.startY;
});
canvas.addEventListener("touchend", () => {
state.isDragging = false;
});
controlsToggleBtn.addEventListener("click", onControlsToggleClick);
controlsDialog.addEventListener("click", onControlsDialogClick);
let rafId = 0;
let lastTime = performance.now();
@ -786,6 +771,8 @@ function BigNumbersScale() {
onCleanup(() => {
cancelAnimationFrame(rafId);
window.removeEventListener("resize", resizeCanvas);
controlsToggleBtn.removeEventListener("click", onControlsToggleClick);
controlsDialog.removeEventListener("click", onControlsDialogClick);
});
});
@ -801,6 +788,16 @@ function BigNumbersScale() {
<span id="soundIcon">🔇</span>
<span id="soundText">Muted</span>
</button>
<button
id="controlsToggleBtn"
type="button"
class="big-numbers-scale__tool-btn big-numbers-scale__controls-toggle"
aria-label="Open controls panel"
aria-haspopup="dialog"
aria-controls="controlsDialog"
>
⚙
</button>
<span id="speedIndicatorText" class="big-numbers-scale__badge">
Speed: Normal
</span>
@ -840,49 +837,64 @@ function BigNumbersScale() {
</div>
</div>
<div class="big-numbers-scale__controls">
<div class="big-numbers-scale__section">
<h3 class="big-numbers-scale__section-title">Mathematical milestones</h3>
<div id="presetStepsContainer" class="big-numbers-scale__preset-grid" />
</div>
<div class="big-numbers-scale__section">
<h3 class="big-numbers-scale__section-title">Motion control</h3>
<div class="big-numbers-scale__speed-grid">
<button type="button" class="speed-btn" data-speed="slow">
Slow
</button>
<button type="button" class="speed-btn speed-btn--active" data-speed="normal">
Normal
</button>
<button type="button" class="speed-btn" data-speed="fast">
Fast
<dialog id="controlsDialog" class="big-numbers-scale__controls-dialog contributor-dialog">
<div class="big-numbers-scale__controls-dialog__sheet contributor-dialog-panel">
<div class="big-numbers-scale__controls-dialog__header">
<h3 class="big-numbers-scale__controls-dialog__title">Controls</h3>
<button
type="button"
class="contributor-dialog-close"
aria-label="Close controls panel"
>
×
</button>
</div>
<div class="big-numbers-scale__nav-row">
<button id="btnPrevStep" type="button" class="big-numbers-scale__tool-btn">
Down step
</button>
<button id="btnPlayPause" type="button" class="big-numbers-scale__autoplay-btn">
Auto Increment
</button>
<button id="btnNextStep" type="button" class="big-numbers-scale__tool-btn">
Up step
</button>
<div class="big-numbers-scale__controls">
<div class="big-numbers-scale__section">
<h3 class="big-numbers-scale__section-title">Mathematical milestones</h3>
<div id="presetStepsContainer" class="big-numbers-scale__preset-grid" />
</div>
<div class="big-numbers-scale__section">
<h3 class="big-numbers-scale__section-title">Motion control</h3>
<div class="big-numbers-scale__speed-grid">
<button type="button" class="speed-btn" data-speed="slow">
Slow
</button>
<button type="button" class="speed-btn speed-btn--active" data-speed="normal">
Normal
</button>
<button type="button" class="speed-btn" data-speed="fast">
Fast
</button>
</div>
<div class="big-numbers-scale__nav-row">
<button id="btnPrevStep" type="button" class="big-numbers-scale__tool-btn">
Down step
</button>
<button id="btnPlayPause" type="button" class="big-numbers-scale__autoplay-btn">
Auto Increment
</button>
<button id="btnNextStep" type="button" class="big-numbers-scale__tool-btn">
Up step
</button>
</div>
</div>
<div class="big-numbers-scale__section big-numbers-scale__stats">
<h3 class="big-numbers-scale__section-title">Time to count individually</h3>
<p class="big-numbers-scale__stat-text">
Counting to <span id="analogyValueText">1</span> at one number per second takes:
</p>
<div id="timeStatValue" class="big-numbers-scale__stat-value">
1 Second
</div>
<p id="analogyText" class="big-numbers-scale__analogy">
"A single heartbeat. The primary unit of all structures."
</p>
</div>
</div>
</div>
<div class="big-numbers-scale__section big-numbers-scale__stats">
<h3 class="big-numbers-scale__section-title">Time to count individually</h3>
<p class="big-numbers-scale__stat-text">
Counting to <span id="analogyValueText">1</span> at one number per second takes:
</p>
<div id="timeStatValue" class="big-numbers-scale__stat-value">
1 Second
</div>
<p id="analogyText" class="big-numbers-scale__analogy">
"A single heartbeat. The primary unit of all structures."
</p>
</div>
</div>
</dialog>
</div>
</section>
);

View file

@ -102,14 +102,45 @@ export function observeMathAutoRender(root: ParentNode = document) {
let scheduled = false;
let rendering = false;
const pendingTargets = new Set<HTMLElement>();
const queueTargetFromNode = (node: Node) => {
if (nodeInsideKatex(node)) return;
if (node instanceof HTMLElement) {
if (node.matches(AUTO_RENDER_TARGETS)) {
pendingTargets.add(node);
}
node.querySelectorAll<HTMLElement>(AUTO_RENDER_TARGETS).forEach((el) => {
pendingTargets.add(el);
});
return;
}
if (node instanceof DocumentFragment) {
node.querySelectorAll<HTMLElement>(AUTO_RENDER_TARGETS).forEach((el) => {
pendingTargets.add(el);
});
return;
}
const parent = node.parentElement;
if (!parent || nodeInsideKatex(parent)) return;
const host = parent.closest<HTMLElement>(AUTO_RENDER_TARGETS);
if (host) pendingTargets.add(host);
};
const scheduleRender = () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
if (pendingTargets.size === 0) return;
rendering = true;
renderBookMath(root, { force: true });
const targets = Array.from(pendingTargets);
pendingTargets.clear();
targets.forEach((el) => renderMathInContainer(el));
rendering = false;
});
};
@ -118,11 +149,19 @@ export function observeMathAutoRender(root: ParentNode = document) {
if (rendering) return;
const hasExternalChange = mutations.some((mutation) => {
if (nodeInsideKatex(mutation.target)) return false;
if (mutation.type === "characterData") return true;
if (mutation.addedNodes.length === 0 && mutation.removedNodes.length === 0) return false;
const addedInsideKatex = Array.from(mutation.addedNodes).every(nodeInsideKatex);
const removedInsideKatex = Array.from(mutation.removedNodes).every(nodeInsideKatex);
return !(addedInsideKatex && removedInsideKatex);
if (mutation.type === "characterData") {
const parent = mutation.target.parentElement;
if (!parent || nodeInsideKatex(parent)) return false;
const host = parent.closest<HTMLElement>(AUTO_RENDER_TARGETS);
if (!host) return false;
delete host.dataset.katexRendered;
pendingTargets.add(host);
return true;
}
mutation.addedNodes.forEach((node) => queueTargetFromNode(node));
return mutation.addedNodes.length > 0;
});
if (hasExternalChange) scheduleRender();
});

View file

@ -66,7 +66,7 @@ let stopMathObserver: (() => void) | null = null;
function scheduleBookMath() {
void import("./lib/katex").then(({ observeMathAutoRender, renderBookMath }) => {
requestAnimationFrame(() => {
renderBookMath(document, { force: true });
renderBookMath(document);
if (!stopMathObserver) {
stopMathObserver = observeMathAutoRender(document);
}