Enhance math curriculum content and improve rendering: Update contributing guidelines for math widget integration and clarify KaTeX usage. Add interactive demo technology choices to SKILL.md for better guidance on chapter interactions. Introduce new visual elements in coordinate geometry chapters, including an empty Cartesian plane SVG and detailed proofs for distance, midpoint, and slope formulas. Revise Pythagorean theorem content to include geometric proofs and applications, enhancing clarity and educational value.

This commit is contained in:
Hananoshika Yomaru 2026-05-21 22:18:34 -07:00
parent f74867fe4c
commit 2ca490ff81
17 changed files with 1571 additions and 201 deletions

View file

@ -135,6 +135,18 @@ Live chapters live under `content/{subject}/{slug}/`:
**Math checkpoints / dots:** follow [math-chapter-patterns.md](math-chapter-patterns.md); shared dot classes in `content/math/subject.scss`.
### Interactive demo technology choices
Interactive demos can use different technologies based on the learning goal and interaction type:
- **Vanilla JavaScript / Solid widget**: default for lightweight chapter interactions and custom canvas/SVG behavior.
- **JSXGraph**: geometric constructions (points, lines, labeled angles, right-angle markers, draggable Euclidean figures).
- **Plotly.js**: data-heavy charts, coordinate plots, and quick interactive graphing.
- **D3**: custom data visualizations where fine-grained control over rendering/animation is needed.
- **three.js**: 3D scenes, spatial intuition demos, and rotation/perspective interactions.
Choose the simplest tool that satisfies the pedagogy, performance, and accessibility needs of the chapter.
Reference: `content/math/quadratic-equations/`.
## Verify before PR

View file

@ -122,7 +122,7 @@ strand = "Number & Algebra"
- Put styles reused across several chapters in `content/{subject}/subject.scss`, not in each `chapter.scss`.
- **Do not** mention DSE, IB, or other curriculum names in chapter HTML.
- Mount widgets with `<div class="math-widget-mount" data-widget="widget-name" data-pagefind-ignore></div>` (filename without `.tsx` = `data-widget` value).
- **Math (KaTeX):** write `\( … \)` and `\[ … \]` in HTML; prose is auto-rendered on chapter load. In widgets use `import { renderLatex } from "@ofb/katex"` — do not add a per-widget `renderLatex` helper.
- **Math (KaTeX):** write `\( … \)` and `\[ … \]` in HTML; prose and widget DOM are auto-rendered globally on chapter load and dynamic updates. For computed strings in widgets, use `import { renderLatex } from "@ofb/katex"` — do not add a per-widget `renderLatex` helper.
- Reference synced assets with root-relative paths: `/chapters/math/your-chapter-slug/image.png` (chapter HTML is not processed as Tera)
### 4. Build and check

View file

@ -28,12 +28,53 @@ strand = "Measures, Shape & Space"
<li>Q3: \((-,-)\)</li>
<li>Q4: \((+,-)\)</li>
</ul>
<figure class="coord-empty-plane">
<svg
viewBox="0 0 360 360"
width="360"
height="360"
role="img"
aria-label="Empty Cartesian plane with x-axis, y-axis, and quadrant labels"
>
<defs>
<pattern id="coord-grid-small" width="18" height="18" patternUnits="userSpaceOnUse">
<path d="M 18 0 L 0 0 0 18" fill="none" class="coord-empty-plane__grid-small" />
</pattern>
<pattern id="coord-grid-major" width="90" height="90" patternUnits="userSpaceOnUse">
<rect width="90" height="90" fill="url(#coord-grid-small)" />
<path d="M 90 0 L 0 0 0 90" fill="none" class="coord-empty-plane__grid-major" />
</pattern>
<marker id="coord-arrow-end" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" class="coord-empty-plane__axis-arrow" />
</marker>
</defs>
<rect x="10" y="10" width="340" height="340" class="coord-empty-plane__bg" />
<rect x="10" y="10" width="340" height="340" fill="url(#coord-grid-major)" />
<line x1="180" y1="16" x2="180" y2="344" class="coord-empty-plane__axis" marker-start="url(#coord-arrow-end)" marker-end="url(#coord-arrow-end)" />
<line x1="16" y1="180" x2="344" y2="180" class="coord-empty-plane__axis" marker-start="url(#coord-arrow-end)" marker-end="url(#coord-arrow-end)" />
<text x="312" y="172" class="coord-empty-plane__axis-label">x-axis</text>
<text x="188" y="30" class="coord-empty-plane__axis-label">y-axis</text>
<text x="188" y="198" class="coord-empty-plane__origin-label">O</text>
<text x="258" y="62" class="coord-empty-plane__quad-title">Quadrant I</text>
<text x="268" y="86" class="coord-empty-plane__quad-sign">(+,+)</text>
<text x="46" y="62" class="coord-empty-plane__quad-title">Quadrant II</text>
<text x="58" y="86" class="coord-empty-plane__quad-sign">(-,+)</text>
<text x="42" y="324" class="coord-empty-plane__quad-title">Quadrant III</text>
<text x="58" y="300" class="coord-empty-plane__quad-sign">(-,-)</text>
<text x="250" y="324" class="coord-empty-plane__quad-title">Quadrant IV</text>
<text x="268" y="300" class="coord-empty-plane__quad-sign">(+,-)</text>
</svg>
<figcaption class="math-widget__caption">
Empty Cartesian plane with axis directions and quadrant signs.
</figcaption>
</figure>
<h2 id="plot-points" class="book-prose__heading">Plotting and reading points</h2>
<p>
Ordered pair \((a,b)\) means move \(a\) units along \(x\), then \(b\) units along \(y\).
</p>
<p><strong>Example:</strong> \((-3,2)\) lies in Quadrant II.</p>
<div class="math-widget-mount" data-widget="plane-relations-demo" data-pagefind-ignore></div>
<h2 id="distance-formula" class="book-prose__heading">Distance between two points</h2>
<p>
@ -89,6 +130,41 @@ strand = "Measures, Shape & Space"
<li>Perpendicular lines satisfy \(m_1m_2=-1\) (when both slopes are defined).</li>
</ul>
<h2 id="why-these-formulas-work" class="book-prose__heading">Why these formulas work (derivation)</h2>
<h3 id="distance-proof" class="book-prose__heading">Distance formula from Pythagoras</h3>
<p>
Between \(A(x_1,y_1)\) and \(B(x_2,y_2)\), horizontal change is
\(\Delta x=x_2-x_1\) and vertical change is \(\Delta y=y_2-y_1\). These are the
legs of a right triangle, and segment \(AB\) is its hypotenuse.
</p>
<p class="book-formula">\[
AB^2=(\Delta x)^2+(\Delta y)^2
\]</p>
<p class="book-formula">\[
AB=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}
\]</p>
<h3 id="midpoint-proof" class="book-prose__heading">Midpoint as averaging coordinates</h3>
<p>
A midpoint lies exactly halfway in both horizontal and vertical directions, so each coordinate is
an average of the endpoints:
</p>
<p class="book-formula">\[
M\left(\frac{x_1+x_2}{2},\frac{y_1+y_2}{2}\right)
\]</p>
<h3 id="slope-proof" class="book-prose__heading">Slope formula from rise over run</h3>
<p>
Slope measures steepness: rise divided by run. For two points, rise is \(y_2-y_1\) and run is
\(x_2-x_1\), giving:
</p>
<p class="book-formula">\[
m=\frac{y_2-y_1}{x_2-x_1}
\]</p>
<p>
This is why vertical lines (\(x_2=x_1\)) have undefined slope: the denominator becomes zero.
</p>
<h2 id="exam-strategy" class="book-prose__heading">Exam strategy</h2>
<ul>
<li>Write coordinates clearly before substituting formulas.</li>
@ -106,10 +182,43 @@ strand = "Measures, Shape & Space"
</p>
<h2 id="derivation" class="book-prose__heading">Derivation and reasoning</h2>
<h3 id="distance-derivation" class="book-prose__heading">Why the distance formula works</h3>
<h3 id="distance-derivation" class="book-prose__heading">Distance formula (step by step)</h3>
<p>Let \(A(x_1,y_1)\) and \(B(x_2,y_2)\).</p>
<ol>
<li>Construct a right triangle by drawing horizontal and vertical guide lines.</li>
<li>Its horizontal leg is \(|x_2-x_1|\), and vertical leg is \(|y_2-y_1|\).</li>
<li>By Pythagoras, \(AB^2=(x_2-x_1)^2+(y_2-y_1)^2\).</li>
<li>Take the positive square root because distance is nonnegative.</li>
</ol>
<p class="book-formula">\[
AB=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}
\]</p>
<h3 id="midpoint-derivation" class="book-prose__heading">Midpoint formula (step by step)</h3>
<p>
Horizontal change is \(\Delta x=x_2-x_1\), vertical change is \(\Delta y=y_2-y_1\). These form
a right triangle, so distance is \(\sqrt{(\Delta x)^2+(\Delta y)^2}\).
A midpoint splits a segment into two equal parts in both directions. So the \(x\)-coordinate is
halfway between \(x_1\) and \(x_2\), and the \(y\)-coordinate is halfway between \(y_1\) and \(y_2\).
</p>
<p class="book-formula">\[
M\left(\frac{x_1+x_2}{2},\frac{y_1+y_2}{2}\right)
\]</p>
<h3 id="slope-derivation" class="book-prose__heading">Slope formula and line forms</h3>
<p>
Slope is rise over run. For points \(A(x_1,y_1)\) and \(B(x_2,y_2)\):
</p>
<p class="book-formula">\[
m=\frac{y_2-y_1}{x_2-x_1}
\]</p>
<p>
Rearranging the same idea for a generic point \((x,y)\) on the line through \((x_1,y_1)\)
gives point-slope form:
</p>
<p class="book-formula">\[
y-y_1=m(x-x_1)
\]</p>
<p>
Expanding gives slope-intercept form \(y=mx+c\), where \(c\) is the \(y\)-intercept.
</p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
@ -143,6 +252,24 @@ strand = "Measures, Shape & Space"
<p><strong>Answer:</strong> \(y-4=3(x-1)\Rightarrow y=3x+1\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Explain in one sentence why the distance formula has a square root.</summary>
<div class="book-question__solution">
<p>
<strong>Answer:</strong> Pythagoras gives \(AB^2\), so we take the positive square root to get
the actual distance \(AB\).
</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Why is slope undefined for a vertical line?</summary>
<div class="book-question__solution">
<p>
<strong>Answer:</strong> For a vertical line, \(\Delta x=0\), so
\(m=\frac{\Delta y}{\Delta x}\) would require division by zero.
</p>
</div>
</details>
<h2 id="applications" class="book-prose__heading">Applications</h2>
<ul>

View file

@ -0,0 +1,173 @@
.coord-plane-demo__canvas {
display: block;
width: 100%;
max-width: 780px;
margin: 0 auto;
border: 1px solid var(--line);
border-radius: 10px;
background: color-mix(in srgb, var(--paper) 84%, #f8fafc 16%);
touch-action: none;
}
.coord-plane-demo__readout,
.coord-plane-demo__summary {
margin: 0.7rem 0 0;
text-align: center;
color: var(--soft-ink);
font-size: 0.92rem;
}
.coord-plane-demo__readout .katex,
.coord-plane-demo__summary .katex {
font-size: 1em;
}
.coord-empty-plane {
margin: 1rem 0 1.25rem;
}
.coord-empty-plane svg {
display: block;
width: min(100%, 520px);
height: auto;
margin: 0 auto;
border: 1px solid var(--line);
border-radius: 10px;
}
.coord-empty-plane__bg {
fill: #f8fafc;
}
.coord-empty-plane__grid-small {
stroke: rgba(71, 85, 105, 0.16);
stroke-width: 1;
}
.coord-empty-plane__grid-major {
stroke: rgba(71, 85, 105, 0.28);
stroke-width: 1.2;
}
.coord-empty-plane__axis {
stroke: #0f172a;
stroke-width: 2.2;
}
.coord-empty-plane__axis-arrow {
fill: #0f172a;
}
.coord-empty-plane__axis-label {
font-family: var(--mono);
font-size: 0.78rem;
font-weight: 700;
fill: #334155;
}
.coord-empty-plane__origin-label {
font-family: var(--mono);
font-size: 0.78rem;
font-weight: 700;
fill: #0f172a;
}
.coord-empty-plane__quad-title {
font-family: var(--font-sans);
font-size: 0.84rem;
font-weight: 700;
fill: #b91c1c;
}
.coord-empty-plane__quad-sign {
font-family: var(--mono);
font-size: 0.9rem;
font-weight: 700;
fill: #0f172a;
}
[data-theme="dark"] .coord-plane-demo__canvas {
border-color: rgba(148, 163, 184, 0.5);
background: #0f172a;
}
[data-theme="dark"] .coord-plane-demo__readout,
[data-theme="dark"] .coord-plane-demo__summary {
color: #e2e8f0;
}
[data-theme="dark"] .coord-empty-plane svg {
border-color: rgba(148, 163, 184, 0.5);
}
[data-theme="dark"] .coord-empty-plane__bg {
fill: #0f172a;
}
[data-theme="dark"] .coord-empty-plane__grid-small {
stroke: rgba(148, 163, 184, 0.2);
}
[data-theme="dark"] .coord-empty-plane__grid-major {
stroke: rgba(148, 163, 184, 0.34);
}
[data-theme="dark"] .coord-empty-plane__axis,
[data-theme="dark"] .coord-empty-plane__axis-arrow,
[data-theme="dark"] .coord-empty-plane__quad-sign,
[data-theme="dark"] .coord-empty-plane__origin-label {
stroke: #f8fafc;
fill: #f8fafc;
}
[data-theme="dark"] .coord-empty-plane__axis-label {
fill: #cbd5e1;
}
[data-theme="dark"] .coord-empty-plane__quad-title {
fill: #fda4af;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .coord-plane-demo__canvas {
border-color: rgba(148, 163, 184, 0.5);
background: #0f172a;
}
:root:not([data-theme="light"]) .coord-plane-demo__readout,
:root:not([data-theme="light"]) .coord-plane-demo__summary {
color: #e2e8f0;
}
:root:not([data-theme="light"]) .coord-empty-plane svg {
border-color: rgba(148, 163, 184, 0.5);
}
:root:not([data-theme="light"]) .coord-empty-plane__bg {
fill: #0f172a;
}
:root:not([data-theme="light"]) .coord-empty-plane__grid-small {
stroke: rgba(148, 163, 184, 0.2);
}
:root:not([data-theme="light"]) .coord-empty-plane__grid-major {
stroke: rgba(148, 163, 184, 0.34);
}
:root:not([data-theme="light"]) .coord-empty-plane__axis,
:root:not([data-theme="light"]) .coord-empty-plane__axis-arrow,
:root:not([data-theme="light"]) .coord-empty-plane__quad-sign,
:root:not([data-theme="light"]) .coord-empty-plane__origin-label {
stroke: #f8fafc;
fill: #f8fafc;
}
:root:not([data-theme="light"]) .coord-empty-plane__axis-label {
fill: #cbd5e1;
}
:root:not([data-theme="light"]) .coord-empty-plane__quad-title {
fill: #fda4af;
}
}

View file

@ -17,12 +17,53 @@
<li>Q3: \((-,-)\)</li>
<li>Q4: \((+,-)\)</li>
</ul>
<figure class="coord-empty-plane">
<svg
viewBox="0 0 360 360"
width="360"
height="360"
role="img"
aria-label="Empty Cartesian plane with x-axis, y-axis, and quadrant labels"
>
<defs>
<pattern id="coord-grid-small" width="18" height="18" patternUnits="userSpaceOnUse">
<path d="M 18 0 L 0 0 0 18" fill="none" class="coord-empty-plane__grid-small" />
</pattern>
<pattern id="coord-grid-major" width="90" height="90" patternUnits="userSpaceOnUse">
<rect width="90" height="90" fill="url(#coord-grid-small)" />
<path d="M 90 0 L 0 0 0 90" fill="none" class="coord-empty-plane__grid-major" />
</pattern>
<marker id="coord-arrow-end" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" class="coord-empty-plane__axis-arrow" />
</marker>
</defs>
<rect x="10" y="10" width="340" height="340" class="coord-empty-plane__bg" />
<rect x="10" y="10" width="340" height="340" fill="url(#coord-grid-major)" />
<line x1="180" y1="16" x2="180" y2="344" class="coord-empty-plane__axis" marker-start="url(#coord-arrow-end)" marker-end="url(#coord-arrow-end)" />
<line x1="16" y1="180" x2="344" y2="180" class="coord-empty-plane__axis" marker-start="url(#coord-arrow-end)" marker-end="url(#coord-arrow-end)" />
<text x="312" y="172" class="coord-empty-plane__axis-label">x-axis</text>
<text x="188" y="30" class="coord-empty-plane__axis-label">y-axis</text>
<text x="188" y="198" class="coord-empty-plane__origin-label">O</text>
<text x="258" y="62" class="coord-empty-plane__quad-title">Quadrant I</text>
<text x="268" y="86" class="coord-empty-plane__quad-sign">(+,+)</text>
<text x="46" y="62" class="coord-empty-plane__quad-title">Quadrant II</text>
<text x="58" y="86" class="coord-empty-plane__quad-sign">(-,+)</text>
<text x="42" y="324" class="coord-empty-plane__quad-title">Quadrant III</text>
<text x="58" y="300" class="coord-empty-plane__quad-sign">(-,-)</text>
<text x="250" y="324" class="coord-empty-plane__quad-title">Quadrant IV</text>
<text x="268" y="300" class="coord-empty-plane__quad-sign">(+,-)</text>
</svg>
<figcaption class="math-widget__caption">
Empty Cartesian plane with axis directions and quadrant signs.
</figcaption>
</figure>
<h2 id="plot-points" class="book-prose__heading">Plotting and reading points</h2>
<p>
Ordered pair \((a,b)\) means move \(a\) units along \(x\), then \(b\) units along \(y\).
</p>
<p><strong>Example:</strong> \((-3,2)\) lies in Quadrant II.</p>
<div class="math-widget-mount" data-widget="plane-relations-demo" data-pagefind-ignore></div>
<h2 id="distance-formula" class="book-prose__heading">Distance between two points</h2>
<p>
@ -78,6 +119,41 @@
<li>Perpendicular lines satisfy \(m_1m_2=-1\) (when both slopes are defined).</li>
</ul>
<h2 id="why-these-formulas-work" class="book-prose__heading">Why these formulas work (derivation)</h2>
<h3 id="distance-proof" class="book-prose__heading">Distance formula from Pythagoras</h3>
<p>
Between \(A(x_1,y_1)\) and \(B(x_2,y_2)\), horizontal change is
\(\Delta x=x_2-x_1\) and vertical change is \(\Delta y=y_2-y_1\). These are the
legs of a right triangle, and segment \(AB\) is its hypotenuse.
</p>
<p class="book-formula">\[
AB^2=(\Delta x)^2+(\Delta y)^2
\]</p>
<p class="book-formula">\[
AB=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}
\]</p>
<h3 id="midpoint-proof" class="book-prose__heading">Midpoint as averaging coordinates</h3>
<p>
A midpoint lies exactly halfway in both horizontal and vertical directions, so each coordinate is
an average of the endpoints:
</p>
<p class="book-formula">\[
M\left(\frac{x_1+x_2}{2},\frac{y_1+y_2}{2}\right)
\]</p>
<h3 id="slope-proof" class="book-prose__heading">Slope formula from rise over run</h3>
<p>
Slope measures steepness: rise divided by run. For two points, rise is \(y_2-y_1\) and run is
\(x_2-x_1\), giving:
</p>
<p class="book-formula">\[
m=\frac{y_2-y_1}{x_2-x_1}
\]</p>
<p>
This is why vertical lines (\(x_2=x_1\)) have undefined slope: the denominator becomes zero.
</p>
<h2 id="exam-strategy" class="book-prose__heading">Exam strategy</h2>
<ul>
<li>Write coordinates clearly before substituting formulas.</li>

View file

@ -6,10 +6,43 @@
</p>
<h2 id="derivation" class="book-prose__heading">Derivation and reasoning</h2>
<h3 id="distance-derivation" class="book-prose__heading">Why the distance formula works</h3>
<h3 id="distance-derivation" class="book-prose__heading">Distance formula (step by step)</h3>
<p>Let \(A(x_1,y_1)\) and \(B(x_2,y_2)\).</p>
<ol>
<li>Construct a right triangle by drawing horizontal and vertical guide lines.</li>
<li>Its horizontal leg is \(|x_2-x_1|\), and vertical leg is \(|y_2-y_1|\).</li>
<li>By Pythagoras, \(AB^2=(x_2-x_1)^2+(y_2-y_1)^2\).</li>
<li>Take the positive square root because distance is nonnegative.</li>
</ol>
<p class="book-formula">\[
AB=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}
\]</p>
<h3 id="midpoint-derivation" class="book-prose__heading">Midpoint formula (step by step)</h3>
<p>
Horizontal change is \(\Delta x=x_2-x_1\), vertical change is \(\Delta y=y_2-y_1\). These form
a right triangle, so distance is \(\sqrt{(\Delta x)^2+(\Delta y)^2}\).
A midpoint splits a segment into two equal parts in both directions. So the \(x\)-coordinate is
halfway between \(x_1\) and \(x_2\), and the \(y\)-coordinate is halfway between \(y_1\) and \(y_2\).
</p>
<p class="book-formula">\[
M\left(\frac{x_1+x_2}{2},\frac{y_1+y_2}{2}\right)
\]</p>
<h3 id="slope-derivation" class="book-prose__heading">Slope formula and line forms</h3>
<p>
Slope is rise over run. For points \(A(x_1,y_1)\) and \(B(x_2,y_2)\):
</p>
<p class="book-formula">\[
m=\frac{y_2-y_1}{x_2-x_1}
\]</p>
<p>
Rearranging the same idea for a generic point \((x,y)\) on the line through \((x_1,y_1)\)
gives point-slope form:
</p>
<p class="book-formula">\[
y-y_1=m(x-x_1)
\]</p>
<p>
Expanding gives slope-intercept form \(y=mx+c\), where \(c\) is the \(y\)-intercept.
</p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
@ -43,6 +76,24 @@
<p><strong>Answer:</strong> \(y-4=3(x-1)\Rightarrow y=3x+1\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Explain in one sentence why the distance formula has a square root.</summary>
<div class="book-question__solution">
<p>
<strong>Answer:</strong> Pythagoras gives \(AB^2\), so we take the positive square root to get
the actual distance \(AB\).
</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Why is slope undefined for a vertical line?</summary>
<div class="book-question__solution">
<p>
<strong>Answer:</strong> For a vertical line, \(\Delta x=0\), so
\(m=\frac{\Delta y}{\Delta x}\) would require division by zero.
</p>
</div>
</details>
<h2 id="applications" class="book-prose__heading">Applications</h2>
<ul>

View file

@ -0,0 +1,346 @@
import { renderLatex } from "@ofb/katex";
import { createMemo, createSignal, onCleanup, onMount } from "solid-js";
type Point = { x: number; y: number };
const MIN = -10;
const MAX = 10;
const STEP = 0.5;
function snap(value: number) {
const clamped = Math.max(MIN, Math.min(MAX, value));
return Math.round(clamped / STEP) * STEP;
}
function pointDistance(a: Point, b: Point) {
return Math.hypot(b.x - a.x, b.y - a.y);
}
function PlaneRelationsDemo() {
let canvasRef: HTMLCanvasElement | undefined;
const [pointA, setPointA] = createSignal<Point>({ x: -6, y: -2 });
const [pointB, setPointB] = createSignal<Point>({ x: 5, y: 4 });
const [width, setWidth] = createSignal(760);
const [themeTick, setThemeTick] = createSignal(0);
const dx = createMemo(() => pointB().x - pointA().x);
const dy = createMemo(() => pointB().y - pointA().y);
const midpoint = createMemo(() => ({
x: (pointA().x + pointB().x) / 2,
y: (pointA().y + pointB().y) / 2,
}));
const distance = createMemo(() => pointDistance(pointA(), pointB()));
const slopeText = createMemo(() =>
dx() === 0 ? "undefined (vertical line)" : (dy() / dx()).toFixed(2)
);
const formulaLatex = createMemo(() => {
if (dx() === 0) {
return `\\Delta x=0,\\;\\Delta y=${dy().toFixed(1)},\\;AB=${distance().toFixed(2)},\\;m\\text{ is undefined}`;
}
return `\\Delta x=${dx().toFixed(1)},\\;\\Delta y=${dy().toFixed(1)},\\;AB=${distance().toFixed(
2
)},\\;m=\\frac{\\Delta y}{\\Delta x}=${slopeText()}`;
});
onMount(() => {
const canvas = canvasRef;
if (!canvas) return;
const ctxCandidate = canvas.getContext("2d");
if (!(ctxCandidate instanceof CanvasRenderingContext2D)) return;
const ctx = ctxCandidate;
const canvasEl: HTMLCanvasElement = canvas;
let dpr = window.devicePixelRatio || 1;
const logicalHeight = 430;
let dragging: "a" | "b" | null = null;
const margin = { left: 60, right: 34, top: 26, bottom: 64 };
const graphWidth = () => width() - margin.left - margin.right;
const graphHeight = () => logicalHeight - margin.top - margin.bottom;
const toX = (x: number) => margin.left + ((x - MIN) / (MAX - MIN)) * graphWidth();
const toY = (y: number) => margin.top + ((MAX - y) / (MAX - MIN)) * graphHeight();
const fromX = (x: number) => MIN + ((x - margin.left) / graphWidth()) * (MAX - MIN);
const fromY = (y: number) => MAX - ((y - margin.top) / graphHeight()) * (MAX - MIN);
function drawArrow(x: number, y: number, direction: "x" | "y") {
ctx.beginPath();
if (direction === "x") {
ctx.moveTo(x, y);
ctx.lineTo(x - 9, y - 5);
ctx.lineTo(x - 9, y + 5);
} else {
ctx.moveTo(x, y);
ctx.lineTo(x - 5, y + 9);
ctx.lineTo(x + 5, y + 9);
}
ctx.closePath();
ctx.fill();
}
function drawPoint(point: Point, color: string, label: string) {
const x = toX(point.x);
const y = toY(point.y);
ctx.beginPath();
ctx.arc(x, y, 7.5, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = "#ffffff";
ctx.stroke();
ctx.font = "700 12px var(--mono, ui-monospace, monospace)";
ctx.fillStyle = color;
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
ctx.fillText(`${label}(${point.x.toFixed(1)}, ${point.y.toFixed(1)})`, x + 8, y - 7);
}
function draw() {
themeTick();
const explicitTheme = document.documentElement.getAttribute("data-theme");
const isDark =
explicitTheme === "dark" ||
(explicitTheme !== "light" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
const palette = isDark
? {
bg: "#0f172a",
grid: "rgba(148, 163, 184, 0.28)",
axis: "#f8fafc",
axisText: "#e2e8f0",
segment: "#38bdf8",
helper: "#f59e0b",
midpoint: "#a78bfa",
pointA: "#34d399",
pointB: "#fda4af",
}
: {
bg: "rgba(148, 163, 184, 0.08)",
grid: "rgba(100, 116, 139, 0.24)",
axis: "#0f172a",
axisText: "#334155",
segment: "#0369a1",
helper: "#b45309",
midpoint: "#7c3aed",
pointA: "#0f766e",
pointB: "#be123c",
};
ctx.clearRect(0, 0, width(), logicalHeight);
ctx.fillStyle = palette.bg;
ctx.fillRect(0, 0, width(), logicalHeight);
ctx.lineWidth = 1;
ctx.strokeStyle = palette.grid;
for (let n = MIN; n <= MAX; n += 1) {
const x = toX(n);
const y = toY(n);
ctx.beginPath();
ctx.moveTo(x, margin.top);
ctx.lineTo(x, margin.top + graphHeight());
ctx.stroke();
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left + graphWidth(), y);
ctx.stroke();
}
const axisX = toY(0);
const axisY = toX(0);
ctx.strokeStyle = palette.axis;
ctx.fillStyle = palette.axis;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(margin.left, axisX);
ctx.lineTo(margin.left + graphWidth() + 12, axisX);
ctx.stroke();
drawArrow(margin.left + graphWidth() + 12, axisX, "x");
ctx.beginPath();
ctx.moveTo(axisY, margin.top + graphHeight());
ctx.lineTo(axisY, margin.top - 12);
ctx.stroke();
drawArrow(axisY, margin.top - 12, "y");
ctx.font = "12px var(--mono, ui-monospace, monospace)";
ctx.fillStyle = palette.axisText;
ctx.textAlign = "center";
ctx.textBaseline = "top";
for (let n = MIN; n <= MAX; n += 2) {
const x = toX(n);
ctx.fillText(String(n), x, axisX + 8);
}
ctx.textAlign = "right";
ctx.textBaseline = "middle";
for (let n = MIN; n <= MAX; n += 2) {
if (n === 0) continue;
const y = toY(n);
ctx.fillText(String(n), axisY - 8, y);
}
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText("x", margin.left + graphWidth() + 18, axisX);
ctx.fillText("y", axisY + 10, margin.top - 12);
const a = pointA();
const b = pointB();
const m = midpoint();
ctx.strokeStyle = palette.segment;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(toX(a.x), toY(a.y));
ctx.lineTo(toX(b.x), toY(b.y));
ctx.stroke();
ctx.strokeStyle = palette.helper;
ctx.setLineDash([6, 5]);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(toX(a.x), toY(a.y));
ctx.lineTo(toX(b.x), toY(a.y));
ctx.lineTo(toX(b.x), toY(b.y));
ctx.stroke();
ctx.setLineDash([]);
ctx.font = "600 12px var(--mono, ui-monospace, monospace)";
ctx.fillStyle = palette.helper;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText(`Δx = ${dx().toFixed(1)}`, (toX(a.x) + toX(b.x)) / 2, toY(a.y) - 4);
ctx.save();
ctx.translate(toX(b.x) + 8, (toY(a.y) + toY(b.y)) / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText(`Δy = ${dy().toFixed(1)}`, 0, 0);
ctx.restore();
ctx.beginPath();
ctx.arc(toX(m.x), toY(m.y), 6, 0, Math.PI * 2);
ctx.fillStyle = palette.midpoint;
ctx.fill();
ctx.fillStyle = palette.midpoint;
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
ctx.fillText(`M(${m.x.toFixed(1)}, ${m.y.toFixed(1)})`, toX(m.x) + 8, toY(m.y) - 8);
drawPoint(a, palette.pointA, "A");
drawPoint(b, palette.pointB, "B");
}
function resize() {
dpr = window.devicePixelRatio || 1;
const rect = canvasEl.getBoundingClientRect();
const nextWidth = Math.max(320, rect.width);
setWidth(nextWidth);
canvasEl.width = Math.round(nextWidth * dpr);
canvasEl.height = Math.round(logicalHeight * dpr);
canvasEl.style.height = `${logicalHeight}px`;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
draw();
}
function hitTest(clientX: number, clientY: number) {
const rect = canvasEl.getBoundingClientRect();
const x = clientX - rect.left;
const y = clientY - rect.top;
const distA = Math.hypot(x - toX(pointA().x), y - toY(pointA().y));
const distB = Math.hypot(x - toX(pointB().x), y - toY(pointB().y));
if (distA <= 14 && distA <= distB) return "a";
if (distB <= 14) return "b";
return null;
}
function onPointerDown(event: PointerEvent) {
dragging = hitTest(event.clientX, event.clientY);
if (!dragging) return;
canvasEl.setPointerCapture(event.pointerId);
canvasEl.style.cursor = "grabbing";
event.preventDefault();
}
function onPointerMove(event: PointerEvent) {
if (!dragging) {
const hit = hitTest(event.clientX, event.clientY);
canvasEl.style.cursor = hit ? "grab" : "default";
return;
}
const rect = canvasEl.getBoundingClientRect();
const x = snap(fromX(event.clientX - rect.left));
const y = snap(fromY(event.clientY - rect.top));
if (dragging === "a") {
setPointA({ x, y });
} else {
setPointB({ x, y });
}
draw();
event.preventDefault();
}
function onPointerEnd(event: PointerEvent) {
if (dragging) canvasEl.releasePointerCapture(event.pointerId);
dragging = null;
canvasEl.style.cursor = "default";
}
const onResize = () => resize();
const themeObserver = new MutationObserver(() => {
setThemeTick((v) => v + 1);
draw();
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
canvasEl.addEventListener("pointerdown", onPointerDown);
canvasEl.addEventListener("pointermove", onPointerMove);
canvasEl.addEventListener("pointerup", onPointerEnd);
canvasEl.addEventListener("pointercancel", onPointerEnd);
window.addEventListener("resize", onResize);
resize();
onCleanup(() => {
themeObserver.disconnect();
canvasEl.removeEventListener("pointerdown", onPointerDown);
canvasEl.removeEventListener("pointermove", onPointerMove);
canvasEl.removeEventListener("pointerup", onPointerEnd);
canvasEl.removeEventListener("pointercancel", onPointerEnd);
window.removeEventListener("resize", onResize);
});
});
return (
<section class="math-widget coord-plane-demo" aria-labelledby="coord-plane-demo-title">
<h3 id="coord-plane-demo-title" class="math-widget__title">
Coordinate relations demo
</h3>
<div class="math-widget__body">
<p class="math-widget__caption">
Drag A and B. The graph shows \( \Delta x \), \( \Delta y \), segment length, midpoint, and
slope from the same picture.
</p>
<canvas
ref={canvasRef}
class="coord-plane-demo__canvas"
aria-label="Interactive coordinate plane with draggable points"
/>
<p class="coord-plane-demo__readout" innerHTML={renderLatex(formulaLatex())} aria-live="polite" />
<p class="coord-plane-demo__summary">
Midpoint \(M\): ({midpoint().x.toFixed(1)}, {midpoint().y.toFixed(1)}) · Slope \(m\):{" "}
{slopeText()}
</p>
</div>
</section>
);
}
export default PlaneRelationsDemo;

View file

@ -10,135 +10,217 @@ strand = "Measures, Shape & Space"
+++
<div class="book-prose">
<p>Discover, geometrically prove, and apply the Pythagorean Theorem.</p>
<p>
Pythagoras' theorem links the side lengths of a right-angled triangle. It is one of the most
useful tools in geometry and coordinate problems.
Imagine you are helping an architect build a scale model of a sculpture. One triangular frame is
a right triangle with legs 9 ft and 12 ft. How long is the third side?
</p>
<h2 id="theorem" class="book-prose__heading">Pythagoras' theorem</h2>
<p>
In a right triangle with legs \(a,b\) and hypotenuse \(c\):
</p>
<h2 id="theorem" class="book-prose__heading">Pythagorean Theorem</h2>
<p>In any right triangle with legs \(a\) and \(b\), and hypotenuse \(c\):</p>
<p class="book-formula">\[
a^2+b^2=c^2.
a^2 + b^2 = c^2
\]</p>
<p>
The hypotenuse is always the side opposite the right angle and is the longest side.
The hypotenuse is opposite the right angle, so it is always the longest side.
</p>
<h2 id="finding-missing-side" class="book-prose__heading">Finding a missing side</h2>
<p><strong>Example 1.</strong> If \(a=6\), \(b=8\), then:</p>
<h2 id="right-triangle-labels" class="book-prose__heading">Right triangle labels</h2>
<p>We usually name vertices with uppercase letters and opposite sides with matching lowercase letters.</p>
<p>
<svg
viewBox="0 0 320 220"
width="320"
height="220"
role="img"
aria-label="Right triangle with vertices A, B, C and side labels a, b, c"
>
<title>Right triangle with labels</title>
<line x1="40" y1="180" x2="260" y2="180" stroke="currentColor" stroke-width="3"></line>
<line x1="40" y1="180" x2="40" y2="40" stroke="currentColor" stroke-width="3"></line>
<line x1="40" y1="40" x2="260" y2="180" stroke="currentColor" stroke-width="3"></line>
<rect x="40" y="160" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"></rect>
<text x="28" y="195" font-size="16" fill="currentColor">A</text>
<text x="265" y="195" font-size="16" fill="currentColor">B</text>
<text x="28" y="35" font-size="16" fill="currentColor">C</text>
<text x="145" y="202" font-size="16" fill="currentColor">a</text>
<text x="16" y="118" font-size="16" fill="currentColor">b</text>
<text x="145" y="100" font-size="16" fill="currentColor">c</text>
<text x="64" y="158" font-size="14" fill="currentColor">90°</text>
</svg>
</p>
<h2 id="finding-side-lengths" class="book-prose__heading">Finding side lengths</h2>
<h3 id="find-hypotenuse" class="book-prose__heading">Find the hypotenuse</h3>
<p><strong>Example.</strong> If \(a=9\) and \(b=12\), find \(c\).</p>
<p class="book-formula">\[
c=\sqrt{6^2+8^2}=\sqrt{36+64}=\sqrt{100}=10.
c = \sqrt{9^2 + 12^2}
= \sqrt{81 + 144}
= \sqrt{225}
= 15
\]</p>
<p><strong>Example 2.</strong> If \(c=13\), \(a=5\), then:</p>
<p>The third side is 15 ft.</p>
<h3 id="find-leg" class="book-prose__heading">Find a missing leg</h3>
<p><strong>Example.</strong> If \(c=13\) and \(a=5\), find \(b\).</p>
<p class="book-formula">\[
b=\sqrt{13^2-5^2}=\sqrt{169-25}=\sqrt{144}=12.
b = \sqrt{c^2 - a^2}
= \sqrt{13^2 - 5^2}
= \sqrt{169 - 25}
= \sqrt{144}
= 12
\]</p>
<h2 id="converse" class="book-prose__heading">Converse of Pythagoras</h2>
<h3 id="radical-form" class="book-prose__heading">Leave answers in simplest radical form</h3>
<p><strong>Example.</strong> If \(c=10\) and \(a=2\), find \(b\).</p>
<p class="book-formula">\[
b = \sqrt{10^2 - 2^2}
= \sqrt{100 - 4}
= \sqrt{96}
= \sqrt{16 \cdot 6}
= 4\sqrt{6}
\]</p>
<h2 id="common-pitfall" class="book-prose__heading">Common pitfall</h2>
<p>
If side lengths satisfy \(a^2+b^2=c^2\), the triangle is right-angled.
</p>
<p>
Example: \(7,24,25\) gives \(7^2+24^2=49+576=625=25^2\), so right-angled.
When solving \(c^2=289\), the equation has two numerical roots (\(\pm 17\)), but side lengths in
geometry are nonnegative. So the triangle side is \(17\), not \(-17\).
</p>
<h2 id="classify-triangle" class="book-prose__heading">Classifying triangle type</h2>
<h2 id="converse" class="book-prose__heading">Converse of the theorem</h2>
<p>
For largest side \(c\):
If three side lengths satisfy \(a^2+b^2=c^2\), then the triangle is right-angled.
</p>
<p><strong>Example:</strong> \(7,24,25\) because \(7^2+24^2=49+576=625=25^2\).</p>
<h2 id="coordinate-geometry" class="book-prose__heading">Coordinate geometry use</h2>
<p>Distance between points \(A(x_1,y_1)\) and \(B(x_2,y_2)\):</p>
<p class="book-formula">\[
AB=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}
\]</p>
<p>This is Pythagoras applied to horizontal and vertical change.</p>
<h2 id="applications" class="book-prose__heading">Applications</h2>
<ul>
<li>\(a^2+b^2=c^2\): right triangle.</li>
<li>\(a^2+b^2&gt;c^2\): acute triangle.</li>
<li>\(a^2+b^2&lt;c^2\): obtuse triangle.</li>
</ul>
<h2 id="coordinate-use" class="book-prose__heading">Using in coordinate geometry</h2>
<p>
Distance between \(A(x_1,y_1)\) and \(B(x_2,y_2)\):
</p>
<p class="book-formula">\[
AB=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}.
\]</p>
<p>
This is Pythagoras applied to horizontal and vertical changes.
</p>
<h2 id="word-problems" class="book-prose__heading">Typical applications</h2>
<ul>
<li>Diagonal of rectangles/squares.</li>
<li>Ladders against walls.</li>
<li>Shortest straight-line distances on maps/grids.</li>
<li>Checking whether corners are right angles in construction.</li>
</ul>
<h2 id="exam-strategy" class="book-prose__heading">Exam strategy</h2>
<ul>
<li>Identify the right angle first.</li>
<li>Label hypotenuse clearly before substitution.</li>
<li>Keep exact surd form unless decimal is requested.</li>
<li>Check answer size: hypotenuse must be longest.</li>
<li>Diagonal of squares and rectangles.</li>
<li>Ladders, ramps, and roof braces in construction.</li>
<li>Map/grid displacement and shortest straight-line paths.</li>
<li>Checking if a corner is exactly \(90^\circ\).</li>
</ul>
</div>
<div class="book-prose">
<h2 id="history" class="book-prose__heading">History</h2>
<p>
The theorem is named after Pythagoras, but knowledge of right-triangle relationships existed in
earlier Babylonian and Indian mathematics. It became central in Euclidean geometry and later in
coordinate methods.
The theorem is associated with Pythagoras, but right-triangle relationships appeared in earlier
Babylonian, Egyptian, and Indian mathematics. In Euclidean geometry, the theorem became a central
tool for proof, measurement, and construction.
</p>
<h2 id="derivation" class="book-prose__heading">Derivation and reasoning</h2>
<h3 id="area-idea" class="book-prose__heading">Area-based proof idea</h3>
<h2 id="geometric-proof" class="book-prose__heading">Geometric proof by area</h2>
<p>
A classic proof rearranges four identical right triangles inside a square in two different ways.
Equating uncovered areas leads to \(a^2+b^2=c^2\).
Build a large square of side length \(a+b\). Place four identical right triangles (legs \(a,b\),
hypotenuse \(c\)) inside it so the uncovered center is a square of side \(c\).
</p>
<div class="math-widget-mount" data-widget="area-proof-demo" data-pagefind-ignore></div>
<p>
Area of big square: \((a+b)^2\). Area of the same figure as pieces:
\(4\left(\frac{1}{2}ab\right)+c^2=2ab+c^2\). Set them equal:
</p>
<p class="book-formula">\[
(a+b)^2 = 2ab + c^2
\]</p>
<p class="book-formula">\[
a^2 + 2ab + b^2 = 2ab + c^2
\]</p>
<p class="book-formula">\[
a^2 + b^2 = c^2
\]</p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
<details class="book-question">
<summary class="book-question__prompt">Find hypotenuse if legs are 9 and 12.</summary>
<summary class="book-question__prompt">If the legs are 3 and 4, what is the hypotenuse?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(c=\sqrt{9^2+12^2}=\sqrt{225}=15\).</p>
<p><strong>Answer:</strong> \(c=\sqrt{3^2+4^2}=\sqrt{25}=5\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">A right triangle has hypotenuse 17 and one leg 8. Find the other leg.</summary>
<summary class="book-question__prompt">If the legs are 6 and 8, what is the hypotenuse?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{17^2-8^2}=\sqrt{225}=15\).</p>
<p><strong>Answer:</strong> \(c=\sqrt{6^2+8^2}=\sqrt{100}=10\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Are sides 10, 24, 26 right-angled?</summary>
<summary class="book-question__prompt">If the legs are 5 and 12, what is the hypotenuse?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> Yes, \(10^2+24^2=100+576=676=26^2\).</p>
<p><strong>Answer:</strong> \(c=\sqrt{5^2+12^2}=\sqrt{169}=13\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Distance between \((-2,1)\) and \((4,9)\).</summary>
<summary class="book-question__prompt">A square has side length 6. Find its diagonal.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{6^2+8^2}=10\).</p>
<p><strong>Answer:</strong> \(d=\sqrt{6^2+6^2}=\sqrt{72}=6\sqrt{2}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Rectangle has side lengths 7 and 24. Find diagonal.</summary>
<summary class="book-question__prompt">A square has side length 9. Find its diagonal.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{7^2+24^2}=25\).</p>
<p><strong>Answer:</strong> \(d=\sqrt{9^2+9^2}=\sqrt{162}=9\sqrt{2}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">If one leg is 4 and the hypotenuse is 8, find the other leg.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{8^2-4^2}=\sqrt{64-16}=\sqrt{48}=4\sqrt{3}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">If one leg is 10 and the hypotenuse is 15, find the other leg.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{15^2-10^2}=\sqrt{225-100}=\sqrt{125}=5\sqrt{5}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">For legs \(x\) and \(y\), write the hypotenuse.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(c=\sqrt{x^2+y^2}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Using the proof figure, explain why \((a+b)^2\) and \(2ab+c^2\) must be equal.</summary>
<div class="book-question__solution">
<p>
<strong>Answer:</strong> They are two area expressions for the same large square: one as a full
square of side \(a+b\), and one as four right triangles plus the center square of side \(c\).
</p>
</div>
</details>
<h2 id="applications" class="book-prose__heading">Applications</h2>
<ul>
<li><strong>Architecture:</strong> checking right angles and structural diagonals.</li>
<li><strong>Navigation:</strong> shortest displacement calculations on grid maps.</li>
<li><strong>Computer graphics:</strong> pixel distance and vector magnitude.</li>
</ul>
<h2 id="references" class="book-prose__heading">References</h2>
<ul>
<li>Standard high-school geometry curricula on Pythagoras and its converse.</li>
<li>Classical Euclidean geometry references.</li>
</ul>
<h2 id="vocabulary" class="book-prose__heading">Vocabulary</h2>
<table>
<thead>
<tr>
<th>Term</th>
<th>Definition</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Right triangle</strong></td>
<td>A triangle with one angle equal to \(90^\circ\).</td>
</tr>
<tr>
<td><strong>Legs</strong></td>
<td>The two sides that form the right angle.</td>
</tr>
<tr>
<td><strong>Hypotenuse</strong></td>
<td>The side opposite the right angle; the longest side in a right triangle.</td>
</tr>
<tr>
<td><strong>Pythagorean Theorem</strong></td>
<td>The relationship \(a^2+b^2=c^2\) for right triangles.</td>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,166 @@
.pyth-proof-demo__figure {
margin: 0 0 1rem;
}
.pyth-proof-demo svg {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
border: 1px solid var(--line);
border-radius: 10px;
background: color-mix(in srgb, var(--paper) 75%, #f8fafc 25%);
}
.pyth-proof-demo__outer {
fill: none;
stroke: var(--ink);
stroke-width: 2.4;
}
.pyth-proof-demo__tri {
stroke: var(--soft-ink);
stroke-width: 1.3;
}
.pyth-proof-demo__tri--1,
.pyth-proof-demo__tri--3 {
fill: color-mix(in srgb, #2b6cb0 16%, transparent);
}
.pyth-proof-demo__tri--2,
.pyth-proof-demo__tri--4 {
fill: color-mix(in srgb, #059669 16%, transparent);
}
.pyth-proof-demo__center {
fill: color-mix(in srgb, #c2410c 18%, transparent);
stroke: #c2410c;
stroke-width: 1.6;
}
.pyth-proof-demo__label {
font-family: var(--mono);
font-size: 0.78rem;
text-anchor: middle;
fill: var(--soft-ink);
}
.pyth-proof-demo__label--vertical {
writing-mode: vertical-rl;
text-orientation: mixed;
}
.pyth-proof-demo__center-label {
font-family: var(--mono);
font-size: 0.86rem;
font-weight: 700;
text-anchor: middle;
fill: var(--ink);
}
.pyth-proof-demo__controls {
display: grid;
gap: 1rem;
margin-top: 0.9rem;
}
@media (min-width: 640px) {
.pyth-proof-demo__controls {
grid-template-columns: repeat(2, 1fr);
}
}
.pyth-proof-demo__label-a {
color: #2563eb;
}
.pyth-proof-demo__label-b {
color: #059669;
}
.pyth-proof-demo__slider-a {
accent-color: #2563eb;
}
.pyth-proof-demo__slider-b {
accent-color: #059669;
}
.pyth-proof-demo__equations {
margin-top: 1rem;
}
.pyth-proof-demo__equation-header {
display: flex;
justify-content: flex-end;
margin-bottom: 0.45rem;
}
.pyth-proof-demo__toggle {
font: inherit;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 7px 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper);
color: var(--soft-ink);
cursor: pointer;
}
.pyth-proof-demo__toggle:hover,
.pyth-proof-demo__toggle:focus-visible {
border-color: var(--accent);
color: var(--accent);
}
.pyth-proof-demo__areas {
margin: 0.45rem 0 0;
font-size: 0.93rem;
color: var(--soft-ink);
}
[data-theme="dark"] .pyth-proof-demo svg {
background: #0f172a;
border-color: rgba(148, 163, 184, 0.5);
}
[data-theme="dark"] .pyth-proof-demo__outer {
stroke: #e2e8f0;
}
[data-theme="dark"] .pyth-proof-demo__tri {
stroke: #cbd5e1;
}
[data-theme="dark"] .pyth-proof-demo__center-label,
[data-theme="dark"] .pyth-proof-demo__label,
[data-theme="dark"] .pyth-proof-demo__areas {
fill: #e2e8f0;
color: #e2e8f0;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .pyth-proof-demo svg {
background: #0f172a;
border-color: rgba(148, 163, 184, 0.5);
}
:root:not([data-theme="light"]) .pyth-proof-demo__outer {
stroke: #e2e8f0;
}
:root:not([data-theme="light"]) .pyth-proof-demo__tri {
stroke: #cbd5e1;
}
:root:not([data-theme="light"]) .pyth-proof-demo__center-label,
:root:not([data-theme="light"]) .pyth-proof-demo__label,
:root:not([data-theme="light"]) .pyth-proof-demo__areas {
fill: #e2e8f0;
color: #e2e8f0;
}
}

View file

@ -1,72 +1,99 @@
<div class="book-prose">
<p>Discover, geometrically prove, and apply the Pythagorean Theorem.</p>
<p>
Pythagoras' theorem links the side lengths of a right-angled triangle. It is one of the most
useful tools in geometry and coordinate problems.
Imagine you are helping an architect build a scale model of a sculpture. One triangular frame is
a right triangle with legs 9 ft and 12 ft. How long is the third side?
</p>
<h2 id="theorem" class="book-prose__heading">Pythagoras' theorem</h2>
<p>
In a right triangle with legs \(a,b\) and hypotenuse \(c\):
</p>
<h2 id="theorem" class="book-prose__heading">Pythagorean Theorem</h2>
<p>In any right triangle with legs \(a\) and \(b\), and hypotenuse \(c\):</p>
<p class="book-formula">\[
a^2+b^2=c^2.
a^2 + b^2 = c^2
\]</p>
<p>
The hypotenuse is always the side opposite the right angle and is the longest side.
The hypotenuse is opposite the right angle, so it is always the longest side.
</p>
<h2 id="finding-missing-side" class="book-prose__heading">Finding a missing side</h2>
<p><strong>Example 1.</strong> If \(a=6\), \(b=8\), then:</p>
<h2 id="right-triangle-labels" class="book-prose__heading">Right triangle labels</h2>
<p>We usually name vertices with uppercase letters and opposite sides with matching lowercase letters.</p>
<p>
<svg
viewBox="0 0 320 220"
width="320"
height="220"
role="img"
aria-label="Right triangle with vertices A, B, C and side labels a, b, c"
>
<title>Right triangle with labels</title>
<line x1="40" y1="180" x2="260" y2="180" stroke="currentColor" stroke-width="3"></line>
<line x1="40" y1="180" x2="40" y2="40" stroke="currentColor" stroke-width="3"></line>
<line x1="40" y1="40" x2="260" y2="180" stroke="currentColor" stroke-width="3"></line>
<rect x="40" y="160" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"></rect>
<text x="28" y="195" font-size="16" fill="currentColor">A</text>
<text x="265" y="195" font-size="16" fill="currentColor">B</text>
<text x="28" y="35" font-size="16" fill="currentColor">C</text>
<text x="145" y="202" font-size="16" fill="currentColor">a</text>
<text x="16" y="118" font-size="16" fill="currentColor">b</text>
<text x="145" y="100" font-size="16" fill="currentColor">c</text>
<text x="64" y="158" font-size="14" fill="currentColor">90°</text>
</svg>
</p>
<h2 id="finding-side-lengths" class="book-prose__heading">Finding side lengths</h2>
<h3 id="find-hypotenuse" class="book-prose__heading">Find the hypotenuse</h3>
<p><strong>Example.</strong> If \(a=9\) and \(b=12\), find \(c\).</p>
<p class="book-formula">\[
c=\sqrt{6^2+8^2}=\sqrt{36+64}=\sqrt{100}=10.
c = \sqrt{9^2 + 12^2}
= \sqrt{81 + 144}
= \sqrt{225}
= 15
\]</p>
<p><strong>Example 2.</strong> If \(c=13\), \(a=5\), then:</p>
<p>The third side is 15 ft.</p>
<h3 id="find-leg" class="book-prose__heading">Find a missing leg</h3>
<p><strong>Example.</strong> If \(c=13\) and \(a=5\), find \(b\).</p>
<p class="book-formula">\[
b=\sqrt{13^2-5^2}=\sqrt{169-25}=\sqrt{144}=12.
b = \sqrt{c^2 - a^2}
= \sqrt{13^2 - 5^2}
= \sqrt{169 - 25}
= \sqrt{144}
= 12
\]</p>
<h2 id="converse" class="book-prose__heading">Converse of Pythagoras</h2>
<h3 id="radical-form" class="book-prose__heading">Leave answers in simplest radical form</h3>
<p><strong>Example.</strong> If \(c=10\) and \(a=2\), find \(b\).</p>
<p class="book-formula">\[
b = \sqrt{10^2 - 2^2}
= \sqrt{100 - 4}
= \sqrt{96}
= \sqrt{16 \cdot 6}
= 4\sqrt{6}
\]</p>
<h2 id="common-pitfall" class="book-prose__heading">Common pitfall</h2>
<p>
If side lengths satisfy \(a^2+b^2=c^2\), the triangle is right-angled.
</p>
<p>
Example: \(7,24,25\) gives \(7^2+24^2=49+576=625=25^2\), so right-angled.
When solving \(c^2=289\), the equation has two numerical roots (\(\pm 17\)), but side lengths in
geometry are nonnegative. So the triangle side is \(17\), not \(-17\).
</p>
<h2 id="classify-triangle" class="book-prose__heading">Classifying triangle type</h2>
<h2 id="converse" class="book-prose__heading">Converse of the theorem</h2>
<p>
For largest side \(c\):
If three side lengths satisfy \(a^2+b^2=c^2\), then the triangle is right-angled.
</p>
<p><strong>Example:</strong> \(7,24,25\) because \(7^2+24^2=49+576=625=25^2\).</p>
<h2 id="coordinate-geometry" class="book-prose__heading">Coordinate geometry use</h2>
<p>Distance between points \(A(x_1,y_1)\) and \(B(x_2,y_2)\):</p>
<p class="book-formula">\[
AB=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}
\]</p>
<p>This is Pythagoras applied to horizontal and vertical change.</p>
<h2 id="applications" class="book-prose__heading">Applications</h2>
<ul>
<li>\(a^2+b^2=c^2\): right triangle.</li>
<li>\(a^2+b^2&gt;c^2\): acute triangle.</li>
<li>\(a^2+b^2&lt;c^2\): obtuse triangle.</li>
</ul>
<h2 id="coordinate-use" class="book-prose__heading">Using in coordinate geometry</h2>
<p>
Distance between \(A(x_1,y_1)\) and \(B(x_2,y_2)\):
</p>
<p class="book-formula">\[
AB=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}.
\]</p>
<p>
This is Pythagoras applied to horizontal and vertical changes.
</p>
<h2 id="word-problems" class="book-prose__heading">Typical applications</h2>
<ul>
<li>Diagonal of rectangles/squares.</li>
<li>Ladders against walls.</li>
<li>Shortest straight-line distances on maps/grids.</li>
<li>Checking whether corners are right angles in construction.</li>
</ul>
<h2 id="exam-strategy" class="book-prose__heading">Exam strategy</h2>
<ul>
<li>Identify the right angle first.</li>
<li>Label hypotenuse clearly before substitution.</li>
<li>Keep exact surd form unless decimal is requested.</li>
<li>Check answer size: hypotenuse must be longest.</li>
<li>Diagonal of squares and rectangles.</li>
<li>Ladders, ramps, and roof braces in construction.</li>
<li>Map/grid displacement and shortest straight-line paths.</li>
<li>Checking if a corner is exactly \(90^\circ\).</li>
</ul>
</div>

View file

@ -1,60 +1,115 @@
<div class="book-prose">
<h2 id="history" class="book-prose__heading">History</h2>
<p>
The theorem is named after Pythagoras, but knowledge of right-triangle relationships existed in
earlier Babylonian and Indian mathematics. It became central in Euclidean geometry and later in
coordinate methods.
The theorem is associated with Pythagoras, but right-triangle relationships appeared in earlier
Babylonian, Egyptian, and Indian mathematics. In Euclidean geometry, the theorem became a central
tool for proof, measurement, and construction.
</p>
<h2 id="derivation" class="book-prose__heading">Derivation and reasoning</h2>
<h3 id="area-idea" class="book-prose__heading">Area-based proof idea</h3>
<h2 id="geometric-proof" class="book-prose__heading">Geometric proof by area</h2>
<p>
A classic proof rearranges four identical right triangles inside a square in two different ways.
Equating uncovered areas leads to \(a^2+b^2=c^2\).
Build a large square of side length \(a+b\). Place four identical right triangles (legs \(a,b\),
hypotenuse \(c\)) inside it so the uncovered center is a square of side \(c\).
</p>
<div class="math-widget-mount" data-widget="area-proof-demo" data-pagefind-ignore></div>
<p>
Area of big square: \((a+b)^2\). Area of the same figure as pieces:
\(4\left(\frac{1}{2}ab\right)+c^2=2ab+c^2\). Set them equal:
</p>
<p class="book-formula">\[
(a+b)^2 = 2ab + c^2
\]</p>
<p class="book-formula">\[
a^2 + 2ab + b^2 = 2ab + c^2
\]</p>
<p class="book-formula">\[
a^2 + b^2 = c^2
\]</p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
<details class="book-question">
<summary class="book-question__prompt">Find hypotenuse if legs are 9 and 12.</summary>
<summary class="book-question__prompt">If the legs are 3 and 4, what is the hypotenuse?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(c=\sqrt{9^2+12^2}=\sqrt{225}=15\).</p>
<p><strong>Answer:</strong> \(c=\sqrt{3^2+4^2}=\sqrt{25}=5\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">A right triangle has hypotenuse 17 and one leg 8. Find the other leg.</summary>
<summary class="book-question__prompt">If the legs are 6 and 8, what is the hypotenuse?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{17^2-8^2}=\sqrt{225}=15\).</p>
<p><strong>Answer:</strong> \(c=\sqrt{6^2+8^2}=\sqrt{100}=10\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Are sides 10, 24, 26 right-angled?</summary>
<summary class="book-question__prompt">If the legs are 5 and 12, what is the hypotenuse?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> Yes, \(10^2+24^2=100+576=676=26^2\).</p>
<p><strong>Answer:</strong> \(c=\sqrt{5^2+12^2}=\sqrt{169}=13\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Distance between \((-2,1)\) and \((4,9)\).</summary>
<summary class="book-question__prompt">A square has side length 6. Find its diagonal.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{6^2+8^2}=10\).</p>
<p><strong>Answer:</strong> \(d=\sqrt{6^2+6^2}=\sqrt{72}=6\sqrt{2}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Rectangle has side lengths 7 and 24. Find diagonal.</summary>
<summary class="book-question__prompt">A square has side length 9. Find its diagonal.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{7^2+24^2}=25\).</p>
<p><strong>Answer:</strong> \(d=\sqrt{9^2+9^2}=\sqrt{162}=9\sqrt{2}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">If one leg is 4 and the hypotenuse is 8, find the other leg.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{8^2-4^2}=\sqrt{64-16}=\sqrt{48}=4\sqrt{3}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">If one leg is 10 and the hypotenuse is 15, find the other leg.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(\sqrt{15^2-10^2}=\sqrt{225-100}=\sqrt{125}=5\sqrt{5}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">For legs \(x\) and \(y\), write the hypotenuse.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(c=\sqrt{x^2+y^2}\).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Using the proof figure, explain why \((a+b)^2\) and \(2ab+c^2\) must be equal.</summary>
<div class="book-question__solution">
<p>
<strong>Answer:</strong> They are two area expressions for the same large square: one as a full
square of side \(a+b\), and one as four right triangles plus the center square of side \(c\).
</p>
</div>
</details>
<h2 id="applications" class="book-prose__heading">Applications</h2>
<ul>
<li><strong>Architecture:</strong> checking right angles and structural diagonals.</li>
<li><strong>Navigation:</strong> shortest displacement calculations on grid maps.</li>
<li><strong>Computer graphics:</strong> pixel distance and vector magnitude.</li>
</ul>
<h2 id="references" class="book-prose__heading">References</h2>
<ul>
<li>Standard high-school geometry curricula on Pythagoras and its converse.</li>
<li>Classical Euclidean geometry references.</li>
</ul>
<h2 id="vocabulary" class="book-prose__heading">Vocabulary</h2>
<table>
<thead>
<tr>
<th>Term</th>
<th>Definition</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Right triangle</strong></td>
<td>A triangle with one angle equal to \(90^\circ\).</td>
</tr>
<tr>
<td><strong>Legs</strong></td>
<td>The two sides that form the right angle.</td>
</tr>
<tr>
<td><strong>Hypotenuse</strong></td>
<td>The side opposite the right angle; the longest side in a right triangle.</td>
</tr>
<tr>
<td><strong>Pythagorean Theorem</strong></td>
<td>The relationship \(a^2+b^2=c^2\) for right triangles.</td>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,164 @@
import { renderLatex } from "@ofb/katex";
import { createMemo, createSignal } from "solid-js";
function AreaProofDemo() {
const [a, setA] = createSignal(4);
const [b, setB] = createSignal(3);
const [showNumbers, setShowNumbers] = createSignal(false);
const sum = createMemo(() => a() + b());
const c2 = createMemo(() => a() * a() + b() * b());
const bigArea = createMemo(() => sum() * sum());
const fourTriangles = createMemo(() => 2 * a() * b());
const centerArea = createMemo(() => c2());
const sideScale = createMemo(() => 260 / sum());
const sidePx = createMemo(() => sum() * sideScale());
const offset = 30;
const ax = createMemo(() => a() * sideScale());
const bx = createMemo(() => b() * sideScale());
const top = createMemo(() => `${offset + ax()},${offset}`);
const right = createMemo(() => `${offset + sidePx()},${offset + ax()}`);
const bottom = createMemo(() => `${offset + bx()},${offset + sidePx()}`);
const left = createMemo(() => `${offset},${offset + bx()}`);
const centerSquarePoints = createMemo(
() => `${top()} ${right()} ${bottom()} ${left()}`
);
const triangleTL = createMemo(
() =>
`${offset},${offset} ${offset + ax()},${offset} ${offset},${offset + bx()}`
);
const triangleTR = createMemo(
() =>
`${offset + sidePx()},${offset} ${offset + sidePx()},${offset + ax()} ${offset + ax()},${offset}`
);
const triangleBR = createMemo(
() =>
`${offset + sidePx()},${offset + sidePx()} ${offset + bx()},${offset + sidePx()} ${offset + sidePx()},${offset + ax()}`
);
const triangleBL = createMemo(
() =>
`${offset},${offset + sidePx()} ${offset},${offset + bx()} ${offset + bx()},${offset + sidePx()}`
);
const modeFormulaLatex = createMemo(() => {
if (showNumbers()) {
return `(${a()}+${b()})^2 = 4\\left(\\frac{1}{2}\\cdot ${a()}\\cdot ${b()}\\right) + ${centerArea()} \\Rightarrow ${a()}^2 + ${b()}^2 = ${c2()}`;
}
return "(a+b)^2 = 4\\left(\\frac{1}{2}ab\\right) + c^2 \\Rightarrow a^2 + b^2 = c^2";
});
return (
<section class="math-widget pyth-proof-demo" aria-labelledby="pyth-proof-demo-title">
<h3 id="pyth-proof-demo-title" class="math-widget__title">
Interactive <span innerHTML={renderLatex("(a+b)^2")} /> area proof
</h3>
<div class="math-widget__body">
<p class="math-widget__caption">
Move <strong>a</strong> and <strong>b</strong> to resize the same diagram. The large square
area and the piece-sum area stay equal.
</p>
<figure class="math-widget__figure pyth-proof-demo__figure">
<svg
viewBox="0 0 320 320"
width="320"
height="320"
role="img"
aria-label={`Area proof diagram with a=${a()} and b=${b()}`}
>
<rect
x={offset}
y={offset}
width={sidePx()}
height={sidePx()}
class="pyth-proof-demo__outer"
/>
<polygon points={triangleTL()} class="pyth-proof-demo__tri pyth-proof-demo__tri--1" />
<polygon points={triangleTR()} class="pyth-proof-demo__tri pyth-proof-demo__tri--2" />
<polygon points={triangleBR()} class="pyth-proof-demo__tri pyth-proof-demo__tri--3" />
<polygon points={triangleBL()} class="pyth-proof-demo__tri pyth-proof-demo__tri--4" />
<polygon points={centerSquarePoints()} class="pyth-proof-demo__center" />
<text x={offset + sidePx() / 2} y={offset - 10} class="pyth-proof-demo__label">
a + b
</text>
<text x={offset - 12} y={offset + sidePx() / 2} class="pyth-proof-demo__label pyth-proof-demo__label--vertical">
a + b
</text>
<text x={offset + sidePx() / 2 + 6} y={offset + sidePx() / 2 + 4} class="pyth-proof-demo__center-label">
c²
</text>
</svg>
<figcaption class="math-widget__caption">
Four congruent right triangles each have area{" "}
<span innerHTML={renderLatex("\\tfrac{1}{2}ab")} />. The middle square has area{" "}
<span innerHTML={renderLatex("c^2")} />.
</figcaption>
</figure>
<div class="math-widget__controls pyth-proof-demo__controls">
<div class="scenario-demo__control">
<label class="scenario-demo__label pyth-proof-demo__label-a" for="pyth-proof-a">
<span>a (leg)</span>
<span class="scenario-demo__value">{a()}</span>
</label>
<input
id="pyth-proof-a"
class="scenario-demo__slider pyth-proof-demo__slider-a"
type="range"
min={1}
max={10}
step={1}
value={a()}
onInput={(e) => setA(Number(e.currentTarget.value))}
/>
</div>
<div class="scenario-demo__control">
<label class="scenario-demo__label pyth-proof-demo__label-b" for="pyth-proof-b">
<span>b (leg)</span>
<span class="scenario-demo__value">{b()}</span>
</label>
<input
id="pyth-proof-b"
class="scenario-demo__slider pyth-proof-demo__slider-b"
type="range"
min={1}
max={10}
step={1}
value={b()}
onInput={(e) => setB(Number(e.currentTarget.value))}
/>
</div>
</div>
<div class="pyth-proof-demo__equations" aria-live="polite">
<div class="pyth-proof-demo__equation-header">
<button
type="button"
class="pyth-proof-demo__toggle"
aria-pressed={showNumbers()}
onClick={() => setShowNumbers((v) => !v)}
>
{showNumbers() ? "Show variables" : "Show numbers"}
</button>
</div>
<p class="math-widget__formula" innerHTML={renderLatex(modeFormulaLatex())} />
<p class="pyth-proof-demo__areas">
Big square area: <strong>{bigArea()}</strong>, four triangles total:{" "}
<strong>{fourTriangles()}</strong>, center square area: <strong>{centerArea()}</strong>.
</p>
</div>
</div>
</section>
);
}
export default AreaProofDemo;

View file

@ -6,6 +6,14 @@ export type RenderLatexOptions = {
displayMode?: boolean;
};
export type RenderMathInContainerOptions = {
force?: boolean;
};
export type RenderBookMathOptions = {
force?: boolean;
};
const DELIMITERS = [
{ left: "$$", right: "$$", display: true },
{ left: "\\(", right: "\\)", display: false },
@ -20,7 +28,7 @@ const AUTO_RENDER_OPTIONS = {
/** Prose and question blocks that may contain \\( … \\) or \\[ … \\] delimiters. */
const AUTO_RENDER_TARGETS =
".book-prose, .book-callout, .book-formula, .book-question__prompt, .book-question__solution, .math-widget__caption";
".book-prose, .book-callout, .book-formula, .book-question__prompt, .book-question__solution, .math-widget";
/**
* Render a LaTeX string to HTML for use with Solid `innerHTML` (widgets, dynamic labels).
@ -36,16 +44,26 @@ export function renderLatex(latex: string, options: RenderLatexOptions = {}) {
/**
* Auto-render delimiter-based math inside a single element (idempotent per element).
*/
export function renderMathInContainer(el: HTMLElement) {
if (el.dataset.katexRendered === "true") return;
export function renderMathInContainer(
el: HTMLElement,
options: RenderMathInContainerOptions = {}
) {
if (options.force) {
delete el.dataset.katexRendered;
} else if (el.dataset.katexRendered === "true") {
return;
}
renderMathInElement(el, AUTO_RENDER_OPTIONS);
el.dataset.katexRendered = "true";
}
/**
* Scan chapter / book content for math delimiters. Skips `.math-widget` trees (widgets use renderLatex).
* Scan chapter / book content for math delimiters in prose and widget trees.
*/
export function renderBookMath(root: ParentNode = document) {
export function renderBookMath(
root: ParentNode = document,
options: RenderBookMathOptions = {}
) {
const article =
root instanceof Document
? root.querySelector<HTMLElement>("[data-pagefind-body]")
@ -56,7 +74,64 @@ export function renderBookMath(root: ParentNode = document) {
const scope: ParentNode = article ?? root;
scope.querySelectorAll<HTMLElement>(AUTO_RENDER_TARGETS).forEach((el) => {
if (el.closest(".math-widget")) return;
renderMathInContainer(el);
renderMathInContainer(el, { force: options.force });
});
}
function nodeInsideKatex(node: Node) {
const element =
node instanceof Element ? node : node.parentElement;
return Boolean(element?.closest(".katex"));
}
/**
* Observe dynamic DOM updates (including Solid widget updates) and auto-render math delimiters.
* Returns a disposer function.
*/
export function observeMathAutoRender(root: ParentNode = document) {
const observedNode =
root instanceof Document
? root.documentElement
: root instanceof HTMLElement
? root
: null;
if (!observedNode) {
return () => {};
}
let scheduled = false;
let rendering = false;
const scheduleRender = () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
rendering = true;
renderBookMath(root, { force: true });
rendering = false;
});
};
const observer = new MutationObserver((mutations) => {
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 (hasExternalChange) scheduleRender();
});
observer.observe(observedNode, {
subtree: true,
childList: true,
characterData: true,
});
return () => observer.disconnect();
}

View file

@ -1,3 +1,12 @@
/** @deprecated Import from `./katex` instead. Kept for existing paths. */
export { renderBookMath, renderLatex, renderMathInContainer } from "./katex";
export type { RenderLatexOptions } from "./katex";
export {
observeMathAutoRender,
renderBookMath,
renderLatex,
renderMathInContainer,
} from "./katex";
export type {
RenderBookMathOptions,
RenderLatexOptions,
RenderMathInContainerOptions,
} from "./katex";

View file

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

View file

@ -4,7 +4,28 @@
<title>Open Free Books</title>
<link>https://openfreebooks.org/</link>
<description>Recent updates to Open Free Books pages and live chapters.</description>
<lastBuildDate>Thu, 21 May 2026 18:40:42 GMT</lastBuildDate>
<lastBuildDate>Thu, 21 May 2026 23:42:19 GMT</lastBuildDate>
<item>
<title>Basic math notation</title>
<link>https://openfreebooks.org/math/basic-math-notation/</link>
<guid isPermaLink="true">https://openfreebooks.org/math/basic-math-notation/</guid>
<pubDate>Thu, 21 May 2026 23:42:19 GMT</pubDate>
<description>Learn the names and meanings of plus, minus, times, divide, equals, brackets, and other signs used in number sentences.</description>
</item>
<item>
<title>Integer foundations</title>
<link>https://openfreebooks.org/math/integer-foundations/</link>
<guid isPermaLink="true">https://openfreebooks.org/math/integer-foundations/</guid>
<pubDate>Thu, 21 May 2026 23:42:19 GMT</pubDate>
<description>Model directed magnitude on number lines; use the zero principle and tiles to add and subtract integers, and see subtraction as adding an opposite.</description>
</item>
<item>
<title>Percentage applications</title>
<link>https://openfreebooks.org/math/percentage-applications/</link>
<guid isPermaLink="true">https://openfreebooks.org/math/percentage-applications/</guid>
<pubDate>Thu, 21 May 2026 23:42:19 GMT</pubDate>
<description>Relate fractions and decimals to base-100 ratios; calculate percent increase and decrease in financial and real-world contexts.</description>
</item>
<item>
<title>Contribute</title>
<link>https://openfreebooks.org/contributing/</link>
@ -19,20 +40,6 @@
<pubDate>Thu, 21 May 2026 17:54:08 GMT</pubDate>
<description>Multiply and divide integers from models to symbolic rules; apply order of operations with signed numbers in multi-step expressions.</description>
</item>
<item>
<title>Integer foundations</title>
<link>https://openfreebooks.org/math/integer-foundations/</link>
<guid isPermaLink="true">https://openfreebooks.org/math/integer-foundations/</guid>
<pubDate>Thu, 21 May 2026 17:54:08 GMT</pubDate>
<description>Model directed magnitude on number lines; use the zero principle and tiles to add and subtract integers, and see subtraction as adding an opposite.</description>
</item>
<item>
<title>Percentage applications</title>
<link>https://openfreebooks.org/math/percentage-applications/</link>
<guid isPermaLink="true">https://openfreebooks.org/math/percentage-applications/</guid>
<pubDate>Thu, 21 May 2026 17:54:08 GMT</pubDate>
<description>Relate fractions and decimals to base-100 ratios; calculate percent increase and decrease in financial and real-world contexts.</description>
</item>
<item>
<title>Fractions and decimals</title>
<link>https://openfreebooks.org/math/rational-number-operations/</link>
@ -264,13 +271,6 @@
<pubDate>Thu, 21 May 2026 02:10:55 GMT</pubDate>
<description>Use free-body diagrams, F = ma, and motion graphs to predict how objects move.</description>
</item>
<item>
<title>Basic math notation</title>
<link>https://openfreebooks.org/math/basic-math-notation/</link>
<guid isPermaLink="true">https://openfreebooks.org/math/basic-math-notation/</guid>
<pubDate>Thu, 21 May 2026 01:07:39 GMT</pubDate>
<description>Learn the names and meanings of plus, minus, times, divide, equals, brackets, and other signs used in number sentences.</description>
</item>
<item>
<title>Zero and one</title>
<link>https://openfreebooks.org/math/zero-and-one/</link>

View file

@ -38,7 +38,7 @@
</url>
<url>
<loc>https://openfreebooks.org/math/basic-math-notation/</loc>
<lastmod>2026-05-21T01:07:39.000Z</lastmod>
<lastmod>2026-05-21T23:42:19.000Z</lastmod>
</url>
<url>
<loc>https://openfreebooks.org/math/basic-probability/</loc>
@ -90,7 +90,7 @@
</url>
<url>
<loc>https://openfreebooks.org/math/integer-foundations/</loc>
<lastmod>2026-05-21T17:54:08.000Z</lastmod>
<lastmod>2026-05-21T23:42:19.000Z</lastmod>
</url>
<url>
<loc>https://openfreebooks.org/math/introduction-trigonometry/</loc>
@ -130,7 +130,7 @@
</url>
<url>
<loc>https://openfreebooks.org/math/percentage-applications/</loc>
<lastmod>2026-05-21T17:54:08.000Z</lastmod>
<lastmod>2026-05-21T23:42:19.000Z</lastmod>
</url>
<url>
<loc>https://openfreebooks.org/math/permutations-combinations-probability/</loc>