mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
Add catalog subject banner feature: Introduce optional header images for subjects in the catalog, update CONTRIBUTING.md with guidelines for adding banners, and modify catalog data structure to support banner URLs. Enhance frontend components and styles to display banners effectively. Update TypeScript types and curriculum structure to accommodate new banner functionality.
This commit is contained in:
parent
8a7f6449a7
commit
7636bb4a86
35 changed files with 4140 additions and 10 deletions
|
|
@ -8,6 +8,7 @@ site_title = "Open Free Books"
|
|||
github_url = "https://github.com/hananoshikayomaru/openfreebooks"
|
||||
browse_url = "/browse/"
|
||||
about_url = "/about/"
|
||||
faq_url = "/faq/"
|
||||
credits_url = "/credits/"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ Thank you for helping grow free, open textbooks.
|
|||
| Publish a **live** chapter (readable page) | [§ Publish a live chapter](#publish-a-live-chapter) |
|
||||
| Add a **branch** in the catalog map | [§ Chapter graph](#chapter-graph-branches) |
|
||||
| Add a new **subject** | [§ New subject](#add-a-new-subject) |
|
||||
| Add a **catalog subject banner** | [§ Catalog subject banner](#catalog-subject-banner) |
|
||||
| Add a **curriculum** filter (e.g. GCSE) | [§ New curriculum label](#add-a-curriculum-label) |
|
||||
|
||||
**Browse the catalog locally:** `bun run build:js && zola serve` → [http://127.0.0.1:1111/catalog/](http://127.0.0.1:1111/catalog/)
|
||||
|
|
@ -152,6 +153,36 @@ Add edges in `data/{subject}-curriculum.json`:
|
|||
|
||||
---
|
||||
|
||||
## Catalog subject banner
|
||||
|
||||
Optional header image when browsing a subject in the catalog (List and Map). Subjects without a `banner` field keep the plain header.
|
||||
|
||||
1. Prepare a **2:1** image (e.g. 2000×1000). Keep the large source file out of the repo if possible.
|
||||
2. Optimize to WebP under `static/catalog/banners/`:
|
||||
|
||||
```bash
|
||||
mkdir -p static/catalog/banners
|
||||
|
||||
# ffmpeg (requires libwebp in your build):
|
||||
ffmpeg -y -i your-banner.jpg -vf "scale=1400:-2" -c:v libwebp -quality 82 \
|
||||
static/catalog/banners/math.webp
|
||||
|
||||
# or cwebp (Homebrew: brew install webp):
|
||||
cwebp -resize 1400 0 -q 82 your-banner.jpg -o static/catalog/banners/math.webp
|
||||
```
|
||||
|
||||
3. In `data/catalog.json`, add a root-relative path on the subject:
|
||||
|
||||
```json
|
||||
{ "id": "math", "name": "Mathematics", "banner": "/catalog/banners/math.webp" }
|
||||
```
|
||||
|
||||
4. Check locally: `bun run build:js && zola serve` → `/catalog/?subject=math`
|
||||
|
||||
Omit `banner` for subjects that should stay unchanged (e.g. roadmap-only placeholders).
|
||||
|
||||
---
|
||||
|
||||
## Add a curriculum label
|
||||
|
||||
1. Add the label to `curriculums` in `data/catalog.json`.
|
||||
|
|
|
|||
4
content/faq.md
Normal file
4
content/faq.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
+++
|
||||
title = "FAQ"
|
||||
template = "faq.html"
|
||||
+++
|
||||
|
|
@ -11,8 +11,9 @@ strand = "Number & Algebra"
|
|||
|
||||
<div class="book-prose">
|
||||
<p>
|
||||
You already know how to count and work with whole numbers from <strong>0</strong> to
|
||||
<strong>10</strong>. On a number line, those values lie on or to the <strong>right</strong> of zero.
|
||||
You can already add, subtract, multiply, and divide whole numbers using <strong>written
|
||||
calculation</strong> (columns, carry, and borrow). On a number line, those values lie on or to the
|
||||
<strong>right</strong> of zero.
|
||||
Numbers can also lie to the <strong>left</strong> of zero — we call them <strong>negative</strong>.
|
||||
Temperatures below freezing, bank overdrafts, and positions below sea level are everyday examples
|
||||
of <strong>directed numbers</strong>.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<div class="book-prose">
|
||||
<p>
|
||||
You already know how to count and work with whole numbers from <strong>0</strong> to
|
||||
<strong>10</strong>. On a number line, those values lie on or to the <strong>right</strong> of zero.
|
||||
You can already add, subtract, multiply, and divide whole numbers using <strong>written
|
||||
calculation</strong> (columns, carry, and borrow). On a number line, those values lie on or to the
|
||||
<strong>right</strong> of zero.
|
||||
Numbers can also lie to the <strong>left</strong> of zero — we call them <strong>negative</strong>.
|
||||
Temperatures below freezing, bank overdrafts, and positions below sea level are everyday examples
|
||||
of <strong>directed numbers</strong>.
|
||||
|
|
|
|||
271
content/math/properties-of-operations/_index.md
Normal file
271
content/math/properties-of-operations/_index.md
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
+++
|
||||
title = "Properties of operations"
|
||||
description = "See why order, grouping, and distribution can change how you write a sum or product without changing the result — with interactive area and dot models."
|
||||
template = "chapter.html"
|
||||
weight = 2
|
||||
[extra]
|
||||
subject = "math"
|
||||
chapter_id = "properties-of-operations"
|
||||
strand = "Number & Algebra"
|
||||
+++
|
||||
|
||||
<div class="book-prose">
|
||||
<p>
|
||||
You can already read number sentences from <strong>basic math notation</strong> and combine
|
||||
numbers from <strong>counting and numbers</strong>. This chapter names three patterns that keep
|
||||
showing up: rules about <strong>order</strong>, <strong>grouping</strong>, and
|
||||
<strong>splitting a sum before you multiply</strong>. They do not replace careful arithmetic —
|
||||
they explain why different written forms can still mean the same amount.
|
||||
</p>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Three properties</p>
|
||||
<ul>
|
||||
<li><strong>Commutative</strong> — swap the order (for + and ×).</li>
|
||||
<li><strong>Associative</strong> — regroup with brackets (for + and ×).</li>
|
||||
<li><strong>Distributive</strong> — multiply each part of a sum (× over +).</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="compare-three" class="book-prose__heading">Same story, three rules</h2>
|
||||
<p>
|
||||
Imagine three friends. Friend A gets <strong>\$2</strong> each day for <strong>3</strong> days and
|
||||
<strong>\$2</strong> each day for <strong>5</strong> more days. You can count the money in
|
||||
different ways:
|
||||
</p>
|
||||
<div class="prop-example">
|
||||
<span class="prop-example__label">Distribute</span>
|
||||
\(2(3 + 5) = (2 \times 3) + (2 \times 5) = 6 + 10 = 16\)
|
||||
</div>
|
||||
<div class="prop-example">
|
||||
<span class="prop-example__label">Commute</span>
|
||||
\(3 + 5 = 5 + 3 = 8\) days in either order before you multiply by \$2 per day in a lump sum.
|
||||
</div>
|
||||
<div class="prop-example">
|
||||
<span class="prop-example__label">Associate</span>
|
||||
\((2 + 3) + 5 = 2 + (3 + 5) = 10\) when you group which daily amounts you add first.
|
||||
</div>
|
||||
<p>
|
||||
<strong>Distribute</strong> is about multiplying through a bracket.
|
||||
<strong>Commute</strong> is about order.
|
||||
<strong>Associate</strong> is about which pair you combine first when there are three or more
|
||||
numbers.
|
||||
</p>
|
||||
|
||||
<h2 id="commutative" class="book-prose__heading">Commutative property</h2>
|
||||
<p>
|
||||
<strong>Commute</strong> means to move or swap. For <strong>addition</strong> and
|
||||
<strong>multiplication</strong> of whole numbers, changing the order does not change the result.
|
||||
</p>
|
||||
<div class="book-formula">
|
||||
\[ a + b = b + a \qquad ab = ba \]
|
||||
</div>
|
||||
<p>
|
||||
Example: \(3 + 5 = 5 + 3 = 8\). Three dots then five dots is the same total as five dots then
|
||||
three dots.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Subtraction and division are not commutative.</strong> \(5 - 3 \neq 3 - 5\) and
|
||||
\(6 \div 2 \neq 2 \div 6\). Order matters there.
|
||||
</p>
|
||||
<div class="math-widget-mount" data-widget="commutative-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h2 id="associative" class="book-prose__heading">Associative property</h2>
|
||||
<p>
|
||||
When you add or multiply <strong>three or more</strong> numbers, brackets tell you which pair to
|
||||
do first. The <strong>associative</strong> property says different groupings give the same answer
|
||||
(for + and × only).
|
||||
</p>
|
||||
<div class="book-formula">
|
||||
\[ (a + b) + c = a + (b + c) \qquad (ab)c = a(bc) \]
|
||||
</div>
|
||||
<p>
|
||||
Example: \((2 + 3) + 5 = 5 + 5 = 10\) and \(2 + (3 + 5) = 2 + 8 = 10\). You may combine
|
||||
\(2 + 3\) first or \(3 + 5\) first.
|
||||
</p>
|
||||
<p>
|
||||
Again, <strong>subtraction and division are not associative.</strong> \((10 - 4) - 2 \neq
|
||||
10 - (4 - 2)\).
|
||||
</p>
|
||||
<div class="math-widget-mount" data-widget="associative-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h2 id="distributive" class="book-prose__heading">Distributive property</h2>
|
||||
<p>
|
||||
The <strong>distributive</strong> property links multiplication to addition. Multiplying a sum
|
||||
is the same as multiplying each addend and then adding — “distribute” the multiply across the
|
||||
bracket.
|
||||
</p>
|
||||
<div class="book-formula">
|
||||
\[ a(b + c) = ab + ac \]
|
||||
</div>
|
||||
<p>
|
||||
Example: \(2(3 + 5) = 2 \times 8 = 16\). Split the width into \(3\) and \(5\): \(2 \times 3 = 6\)
|
||||
and \(2 \times 5 = 10\), then \(6 + 10 = 16\). The area model below shows one tall rectangle
|
||||
split into two parts.
|
||||
</p>
|
||||
<p>
|
||||
Later, in <strong>written calculation</strong>, partial products in multiplication use this idea:
|
||||
you multiply by each digit of the bottom number separately, then add the rows.
|
||||
</p>
|
||||
<div class="math-widget-mount" data-widget="distributive-area-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h2 id="which-property" class="book-prose__heading">Which property is it?</h2>
|
||||
<div class="book-table-wrap">
|
||||
<table class="book-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">You change…</th>
|
||||
<th scope="col">Property</th>
|
||||
<th scope="col">Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Order of two numbers</td>
|
||||
<td>Commutative</td>
|
||||
<td>\(4 + 7 = 7 + 4\)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Brackets with three numbers</td>
|
||||
<td>Associative</td>
|
||||
<td>\((1 + 2) + 3 = 1 + (2 + 3)\)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multiply through a sum</td>
|
||||
<td>Distributive</td>
|
||||
<td>\(3(2 + 4) = 3 \times 2 + 3 \times 4\)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 id="next" class="book-prose__heading">What comes next</h2>
|
||||
<p>
|
||||
In <strong>written calculation</strong> you will stack digits and use carry and borrow. Knowing
|
||||
these properties helps you check work (e.g. swap order to verify an addition) and understand why
|
||||
multiplication splits into several rows before you add them.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="book-prose">
|
||||
<h2 id="history" class="book-prose__heading">History</h2>
|
||||
<p>
|
||||
Long before algebra used letters, merchants and accountants rearranged piles and rows of goods.
|
||||
Indian and Islamic mathematicians in the Middle Ages wrote rules for how sums and products
|
||||
behave. The words <strong>commutative</strong>, <strong>associative</strong>, and
|
||||
<strong>distributive</strong> became standard in European algebra books in the 1800s, when
|
||||
school math settled on one notation for all.
|
||||
</p>
|
||||
<p>
|
||||
The distributive rule is ancient geometry in disguise: a large rectangle split into two smaller
|
||||
ones has total area equal to the sum of the parts. That picture is still one of the clearest
|
||||
explanations today.
|
||||
</p>
|
||||
|
||||
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
|
||||
<ol>
|
||||
<li>
|
||||
Which property lets you write \(7 + 4\) as \(4 + 7\) without changing the total?
|
||||
</li>
|
||||
<li>
|
||||
Is \((5 \times 2) \times 3\) the same as \(5 \times (2 \times 3)\)? Which property says so?
|
||||
</li>
|
||||
<li>
|
||||
Rewrite \(4(6 + 1)\) as a sum of two products.
|
||||
</li>
|
||||
<li>
|
||||
Why is \(8 - 3\) not commutative? Give two different answers for \(3 - 8\) and \(8 - 3\).
|
||||
</li>
|
||||
<li>
|
||||
A rectangle is 5 units tall and \((2 + 3)\) units wide. How does the distributive property
|
||||
describe its area?
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="applications" class="book-prose__heading">Applications</h2>
|
||||
<h3 id="mental-math" class="book-prose__heading">Mental math</h3>
|
||||
<p>
|
||||
Distribute to make friendly numbers: \(7 \times 98 = 7 \times (100 - 2) = 700 - 14 = 686\).
|
||||
Commute to add easier first: \(6 + 47 + 4 = (6 + 4) + 47 = 57\).
|
||||
</p>
|
||||
<h3 id="shopping" class="book-prose__heading">Shopping and sharing</h3>
|
||||
<p>
|
||||
Buying 3 packs of \((2 + 5)\) items is the same as \(3 \times 2\) items plus \(3 \times 5\)
|
||||
items. Swapping the order you scan barcodes does not change how many items you have (commute on
|
||||
addition when you merge carts).
|
||||
</p>
|
||||
<h3 id="algebra-later" class="book-prose__heading">Later algebra</h3>
|
||||
<p>
|
||||
Expanding \(a(b + c)\) in polynomials and factorising common factors both rely on the
|
||||
distributive property. You will meet them again in <strong>polynomials: expansion &
|
||||
factorisation</strong>.
|
||||
</p>
|
||||
|
||||
<h2 id="question-bank" class="book-prose__heading">Question bank</h2>
|
||||
|
||||
<h3 id="questions-easy" class="book-prose__heading">Easy</h3>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
Fill in the blank using a property: \(9 + 6 = 6 + \underline{\hspace{1.5em}}\).
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(9\) — commutative property of addition.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Compute: \(2(4 + 3)\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
<strong>Answer:</strong> \(14\). Either \(2 \times 7 = 14\), or \(2 \times 4 + 2 \times 3 = 8 +
|
||||
6 = 14\) (distributive).
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h3 id="questions-intermediate" class="book-prose__heading">Intermediate</h3>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
Evaluate both sides and compare: \((3 + 4) + 5\) and \(3 + (4 + 5)\).
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> Both equal \(12\) — associative property of addition.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Use distribution to find \(5 \times 102\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(510\). Steps: \(5 \times (100 + 2) = 500 + 10 = 510\).</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h3 id="questions-difficult" class="book-prose__heading">Difficult</h3>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
Explain why \(12 \div (3 \div 2)\) is not the same as \((12 \div 3) \div 2\).
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
<strong>Answer:</strong> Left: \(12 \div 1.5 = 8\). Right: \(4 \div 2 = 2\). Division is not
|
||||
associative; grouping changes the meaning.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
A garden bed is 4 m wide and \((2 + 3)\) m long. Write two ways to find its area and show they
|
||||
match.
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
<strong>Answer:</strong> \(20\) m². One rectangle: \(4 \times 5 = 20\). Split length:
|
||||
\(4 \times 2 + 4 \times 3 = 8 + 12 = 20\) (distributive).
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
469
content/math/properties-of-operations/chapter.scss
Normal file
469
content/math/properties-of-operations/chapter.scss
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
// Wide interactive demos (match written-calculation breakout)
|
||||
.book-prose .math-widget-mount:has(.prop-dist),
|
||||
.book-prose .math-widget-mount:has(.prop-comm),
|
||||
.book-prose .math-widget-mount:has(.prop-assoc) {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.prop-dist,
|
||||
.prop-comm,
|
||||
.prop-assoc {
|
||||
width: min(1100px, 92vw);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
// --- Distributive area model ---
|
||||
.prop-dist__header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px 20px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prop-dist__title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.prop-dist__toggle {
|
||||
font: inherit;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--paper);
|
||||
color: var(--soft-ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.prop-dist__toggle:hover,
|
||||
.prop-dist__toggle:focus-visible {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.prop-dist__formula {
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.prop-dist__formula .katex {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.prop-dist__var-a {
|
||||
color: #db2777;
|
||||
}
|
||||
|
||||
.prop-dist__var-b {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.prop-dist__var-c {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.prop-dist__figure {
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.prop-dist__model {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 8px 10px;
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.prop-dist__height {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.prop-dist__dim {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.prop-dist__dim--a {
|
||||
width: 2rem;
|
||||
padding-right: 8px;
|
||||
color: #db2777;
|
||||
border-right: 3px solid #db2777;
|
||||
}
|
||||
|
||||
.prop-dist__rects {
|
||||
display: flex;
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
min-height: 100px;
|
||||
border-left: 3px solid #db2777;
|
||||
border-right: 3px solid #db2777;
|
||||
}
|
||||
|
||||
.prop-dist__rect {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 3rem;
|
||||
padding: 12px 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.prop-dist__rect--b {
|
||||
border-top: 3px solid #2563eb;
|
||||
border-bottom: 3px solid #2563eb;
|
||||
background: color-mix(in srgb, #2563eb 10%, var(--card-bg));
|
||||
}
|
||||
|
||||
.prop-dist__rect--c {
|
||||
border-top: 3px solid #16a34a;
|
||||
border-bottom: 3px solid #16a34a;
|
||||
background: color-mix(in srgb, #16a34a 10%, var(--card-bg));
|
||||
}
|
||||
|
||||
.prop-dist__area .katex {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.prop-dist__widths {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.prop-dist__dim--b,
|
||||
.prop-dist__dim--c {
|
||||
text-align: center;
|
||||
padding-top: 6px;
|
||||
border-top: 2px solid currentColor;
|
||||
}
|
||||
|
||||
.prop-dist__dim--b {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.prop-dist__dim--c {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.prop-dist__label-a {
|
||||
color: #db2777;
|
||||
}
|
||||
|
||||
.prop-dist__label-b {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.prop-dist__label-c {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.prop-dist__slider-a {
|
||||
accent-color: #db2777;
|
||||
}
|
||||
|
||||
.prop-dist__slider-b {
|
||||
accent-color: #2563eb;
|
||||
}
|
||||
|
||||
.prop-dist__slider-c {
|
||||
accent-color: #16a34a;
|
||||
}
|
||||
|
||||
.prop-dist__controls {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prop-dist__controls {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Commutative ---
|
||||
.prop-comm__mode,
|
||||
.prop-assoc__mode {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prop-comm__mode-btn,
|
||||
.prop-assoc__mode-btn {
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--paper);
|
||||
color: var(--soft-ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.prop-comm__mode-btn--active,
|
||||
.prop-assoc__mode-btn--active {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--paper));
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.prop-comm__compare {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.95rem;
|
||||
color: var(--soft-ink);
|
||||
}
|
||||
|
||||
.prop-comm__compare .katex {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.prop-comm__stage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 1rem 0;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.prop-comm__add-piles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px 16px;
|
||||
}
|
||||
|
||||
.prop-comm__pile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.prop-comm__pile-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.prop-comm__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.prop-comm__dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #0f766e;
|
||||
}
|
||||
|
||||
.prop-comm__pile-dots--a .prop-comm__dot {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.prop-comm__pile-dots--b .prop-comm__dot {
|
||||
background: #c2410c;
|
||||
}
|
||||
|
||||
.prop-comm__op {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.prop-comm__merged {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.prop-comm__grid {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: color-mix(in srgb, var(--accent) 6%, var(--card-bg));
|
||||
}
|
||||
|
||||
.prop-comm__grid-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.prop-comm__swap,
|
||||
.prop-assoc__toggle {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
padding: 10px 18px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.prop-comm__swap:hover,
|
||||
.prop-comm__swap:focus-visible,
|
||||
.prop-assoc__toggle:hover,
|
||||
.prop-assoc__toggle:focus-visible {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
// --- Associative ---
|
||||
.prop-assoc__formulas {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prop-assoc__formulas .math-widget__formula {
|
||||
margin: 0 0 0.35rem;
|
||||
}
|
||||
|
||||
.prop-assoc__alt {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.prop-assoc__stage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 1.25rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.prop-assoc__group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px dashed var(--line);
|
||||
background: color-mix(in srgb, var(--paper) 50%, var(--card-bg));
|
||||
}
|
||||
|
||||
.prop-assoc__bracket {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.prop-assoc__op,
|
||||
.prop-assoc__eq {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.prop-assoc__block {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.prop-assoc__block--x {
|
||||
background: color-mix(in srgb, #2563eb 14%, transparent);
|
||||
border: 1px solid color-mix(in srgb, #2563eb 35%, transparent);
|
||||
}
|
||||
|
||||
.prop-assoc__block--y {
|
||||
background: color-mix(in srgb, #c2410c 14%, transparent);
|
||||
border: 1px solid color-mix(in srgb, #c2410c 35%, transparent);
|
||||
}
|
||||
|
||||
.prop-assoc__block--z {
|
||||
background: color-mix(in srgb, #16a34a 14%, transparent);
|
||||
border: 1px solid color-mix(in srgb, #16a34a 35%, transparent);
|
||||
}
|
||||
|
||||
.prop-assoc__dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.prop-assoc__block--x .prop-assoc__dot {
|
||||
color: #2563eb;
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.prop-assoc__block--y .prop-assoc__dot {
|
||||
background: #c2410c;
|
||||
}
|
||||
|
||||
.prop-assoc__block--z .prop-assoc__dot {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
.prop-assoc__result {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
max-width: 140px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.prop-assoc__dot--result {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.prop-assoc__many {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.prop-example {
|
||||
margin: 1rem 0;
|
||||
padding: 14px 18px;
|
||||
border-left: 3px solid var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 8%, var(--card-bg));
|
||||
border-radius: 0 10px 10px 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.prop-example__label {
|
||||
display: block;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
137
content/math/properties-of-operations/core.html
Normal file
137
content/math/properties-of-operations/core.html
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<div class="book-prose">
|
||||
<p>
|
||||
You can already read number sentences from <strong>basic math notation</strong> and combine
|
||||
numbers from <strong>counting and numbers</strong>. This chapter names three patterns that keep
|
||||
showing up: rules about <strong>order</strong>, <strong>grouping</strong>, and
|
||||
<strong>splitting a sum before you multiply</strong>. They do not replace careful arithmetic —
|
||||
they explain why different written forms can still mean the same amount.
|
||||
</p>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Three properties</p>
|
||||
<ul>
|
||||
<li><strong>Commutative</strong> — swap the order (for + and ×).</li>
|
||||
<li><strong>Associative</strong> — regroup with brackets (for + and ×).</li>
|
||||
<li><strong>Distributive</strong> — multiply each part of a sum (× over +).</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="compare-three" class="book-prose__heading">Same story, three rules</h2>
|
||||
<p>
|
||||
Imagine three friends. Friend A gets <strong>\$2</strong> each day for <strong>3</strong> days and
|
||||
<strong>\$2</strong> each day for <strong>5</strong> more days. You can count the money in
|
||||
different ways:
|
||||
</p>
|
||||
<div class="prop-example">
|
||||
<span class="prop-example__label">Distribute</span>
|
||||
\(2(3 + 5) = (2 \times 3) + (2 \times 5) = 6 + 10 = 16\)
|
||||
</div>
|
||||
<div class="prop-example">
|
||||
<span class="prop-example__label">Commute</span>
|
||||
\(3 + 5 = 5 + 3 = 8\) days in either order before you multiply by \$2 per day in a lump sum.
|
||||
</div>
|
||||
<div class="prop-example">
|
||||
<span class="prop-example__label">Associate</span>
|
||||
\((2 + 3) + 5 = 2 + (3 + 5) = 10\) when you group which daily amounts you add first.
|
||||
</div>
|
||||
<p>
|
||||
<strong>Distribute</strong> is about multiplying through a bracket.
|
||||
<strong>Commute</strong> is about order.
|
||||
<strong>Associate</strong> is about which pair you combine first when there are three or more
|
||||
numbers.
|
||||
</p>
|
||||
|
||||
<h2 id="commutative" class="book-prose__heading">Commutative property</h2>
|
||||
<p>
|
||||
<strong>Commute</strong> means to move or swap. For <strong>addition</strong> and
|
||||
<strong>multiplication</strong> of whole numbers, changing the order does not change the result.
|
||||
</p>
|
||||
<div class="book-formula">
|
||||
\[ a + b = b + a \qquad ab = ba \]
|
||||
</div>
|
||||
<p>
|
||||
Example: \(3 + 5 = 5 + 3 = 8\). Three dots then five dots is the same total as five dots then
|
||||
three dots.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Subtraction and division are not commutative.</strong> \(5 - 3 \neq 3 - 5\) and
|
||||
\(6 \div 2 \neq 2 \div 6\). Order matters there.
|
||||
</p>
|
||||
<div class="math-widget-mount" data-widget="commutative-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h2 id="associative" class="book-prose__heading">Associative property</h2>
|
||||
<p>
|
||||
When you add or multiply <strong>three or more</strong> numbers, brackets tell you which pair to
|
||||
do first. The <strong>associative</strong> property says different groupings give the same answer
|
||||
(for + and × only).
|
||||
</p>
|
||||
<div class="book-formula">
|
||||
\[ (a + b) + c = a + (b + c) \qquad (ab)c = a(bc) \]
|
||||
</div>
|
||||
<p>
|
||||
Example: \((2 + 3) + 5 = 5 + 5 = 10\) and \(2 + (3 + 5) = 2 + 8 = 10\). You may combine
|
||||
\(2 + 3\) first or \(3 + 5\) first.
|
||||
</p>
|
||||
<p>
|
||||
Again, <strong>subtraction and division are not associative.</strong> \((10 - 4) - 2 \neq
|
||||
10 - (4 - 2)\).
|
||||
</p>
|
||||
<div class="math-widget-mount" data-widget="associative-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h2 id="distributive" class="book-prose__heading">Distributive property</h2>
|
||||
<p>
|
||||
The <strong>distributive</strong> property links multiplication to addition. Multiplying a sum
|
||||
is the same as multiplying each addend and then adding — “distribute” the multiply across the
|
||||
bracket.
|
||||
</p>
|
||||
<div class="book-formula">
|
||||
\[ a(b + c) = ab + ac \]
|
||||
</div>
|
||||
<p>
|
||||
Example: \(2(3 + 5) = 2 \times 8 = 16\). Split the width into \(3\) and \(5\): \(2 \times 3 = 6\)
|
||||
and \(2 \times 5 = 10\), then \(6 + 10 = 16\). The area model below shows one tall rectangle
|
||||
split into two parts.
|
||||
</p>
|
||||
<p>
|
||||
Later, in <strong>written calculation</strong>, partial products in multiplication use this idea:
|
||||
you multiply by each digit of the bottom number separately, then add the rows.
|
||||
</p>
|
||||
<div class="math-widget-mount" data-widget="distributive-area-demo" data-pagefind-ignore></div>
|
||||
|
||||
<h2 id="which-property" class="book-prose__heading">Which property is it?</h2>
|
||||
<div class="book-table-wrap">
|
||||
<table class="book-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">You change…</th>
|
||||
<th scope="col">Property</th>
|
||||
<th scope="col">Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Order of two numbers</td>
|
||||
<td>Commutative</td>
|
||||
<td>\(4 + 7 = 7 + 4\)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Brackets with three numbers</td>
|
||||
<td>Associative</td>
|
||||
<td>\((1 + 2) + 3 = 1 + (2 + 3)\)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multiply through a sum</td>
|
||||
<td>Distributive</td>
|
||||
<td>\(3(2 + 4) = 3 \times 2 + 3 \times 4\)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 id="next" class="book-prose__heading">What comes next</h2>
|
||||
<p>
|
||||
In <strong>written calculation</strong> you will stack digits and use carry and borrow. Knowing
|
||||
these properties helps you check work (e.g. swap order to verify an addition) and understand why
|
||||
multiplication splits into several rows before you add them.
|
||||
</p>
|
||||
</div>
|
||||
122
content/math/properties-of-operations/supplement.html
Normal file
122
content/math/properties-of-operations/supplement.html
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<div class="book-prose">
|
||||
<h2 id="history" class="book-prose__heading">History</h2>
|
||||
<p>
|
||||
Long before algebra used letters, merchants and accountants rearranged piles and rows of goods.
|
||||
Indian and Islamic mathematicians in the Middle Ages wrote rules for how sums and products
|
||||
behave. The words <strong>commutative</strong>, <strong>associative</strong>, and
|
||||
<strong>distributive</strong> became standard in European algebra books in the 1800s, when
|
||||
school math settled on one notation for all.
|
||||
</p>
|
||||
<p>
|
||||
The distributive rule is ancient geometry in disguise: a large rectangle split into two smaller
|
||||
ones has total area equal to the sum of the parts. That picture is still one of the clearest
|
||||
explanations today.
|
||||
</p>
|
||||
|
||||
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
|
||||
<ol>
|
||||
<li>
|
||||
Which property lets you write \(7 + 4\) as \(4 + 7\) without changing the total?
|
||||
</li>
|
||||
<li>
|
||||
Is \((5 \times 2) \times 3\) the same as \(5 \times (2 \times 3)\)? Which property says so?
|
||||
</li>
|
||||
<li>
|
||||
Rewrite \(4(6 + 1)\) as a sum of two products.
|
||||
</li>
|
||||
<li>
|
||||
Why is \(8 - 3\) not commutative? Give two different answers for \(3 - 8\) and \(8 - 3\).
|
||||
</li>
|
||||
<li>
|
||||
A rectangle is 5 units tall and \((2 + 3)\) units wide. How does the distributive property
|
||||
describe its area?
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="applications" class="book-prose__heading">Applications</h2>
|
||||
<h3 id="mental-math" class="book-prose__heading">Mental math</h3>
|
||||
<p>
|
||||
Distribute to make friendly numbers: \(7 \times 98 = 7 \times (100 - 2) = 700 - 14 = 686\).
|
||||
Commute to add easier first: \(6 + 47 + 4 = (6 + 4) + 47 = 57\).
|
||||
</p>
|
||||
<h3 id="shopping" class="book-prose__heading">Shopping and sharing</h3>
|
||||
<p>
|
||||
Buying 3 packs of \((2 + 5)\) items is the same as \(3 \times 2\) items plus \(3 \times 5\)
|
||||
items. Swapping the order you scan barcodes does not change how many items you have (commute on
|
||||
addition when you merge carts).
|
||||
</p>
|
||||
<h3 id="algebra-later" class="book-prose__heading">Later algebra</h3>
|
||||
<p>
|
||||
Expanding \(a(b + c)\) in polynomials and factorising common factors both rely on the
|
||||
distributive property. You will meet them again in <strong>polynomials: expansion &
|
||||
factorisation</strong>.
|
||||
</p>
|
||||
|
||||
<h2 id="question-bank" class="book-prose__heading">Question bank</h2>
|
||||
|
||||
<h3 id="questions-easy" class="book-prose__heading">Easy</h3>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
Fill in the blank using a property: \(9 + 6 = 6 + \underline{\hspace{1.5em}}\).
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(9\) — commutative property of addition.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Compute: \(2(4 + 3)\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
<strong>Answer:</strong> \(14\). Either \(2 \times 7 = 14\), or \(2 \times 4 + 2 \times 3 = 8 +
|
||||
6 = 14\) (distributive).
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h3 id="questions-intermediate" class="book-prose__heading">Intermediate</h3>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
Evaluate both sides and compare: \((3 + 4) + 5\) and \(3 + (4 + 5)\).
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> Both equal \(12\) — associative property of addition.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Use distribution to find \(5 \times 102\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(510\). Steps: \(5 \times (100 + 2) = 500 + 10 = 510\).</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h3 id="questions-difficult" class="book-prose__heading">Difficult</h3>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
Explain why \(12 \div (3 \div 2)\) is not the same as \((12 \div 3) \div 2\).
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
<strong>Answer:</strong> Left: \(12 \div 1.5 = 8\). Right: \(4 \div 2 = 2\). Division is not
|
||||
associative; grouping changes the meaning.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
A garden bed is 4 m wide and \((2 + 3)\) m long. Write two ways to find its area and show they
|
||||
match.
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
<strong>Answer:</strong> \(20\) m². One rectangle: \(4 \times 5 = 20\). Split length:
|
||||
\(4 \times 2 + 4 \times 3 = 8 + 12 = 20\) (distributive).
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
import katex from "katex";
|
||||
import { createMemo, createSignal, For } from "solid-js";
|
||||
|
||||
function renderLatex(latex: string) {
|
||||
return katex.renderToString(latex, { throwOnError: false, strict: "ignore" });
|
||||
}
|
||||
|
||||
function DotBlock(props: { count: number; tone: "x" | "y" | "z" }) {
|
||||
return (
|
||||
<div class={`prop-assoc__block prop-assoc__block--${props.tone}`}>
|
||||
<For each={Array.from({ length: props.count })}>
|
||||
{() => <span class="prop-assoc__dot" />}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AssociativeDemo() {
|
||||
const [mode, setMode] = createSignal<"add" | "mul">("add");
|
||||
const [x, setX] = createSignal(2);
|
||||
const [y, setY] = createSignal(3);
|
||||
const [z, setZ] = createSignal(5);
|
||||
const [groupFirst, setGroupFirst] = createSignal(true);
|
||||
|
||||
const xy = createMemo(() => (mode() === "add" ? x() + y() : x() * y()));
|
||||
const total = createMemo(() => (mode() === "add" ? xy() + z() : xy() * z()));
|
||||
|
||||
const leftGroupHtml = createMemo(() => {
|
||||
if (mode() === "add") {
|
||||
return groupFirst()
|
||||
? renderLatex(`(${x()}+${y()})+${z()} = ${xy()}+${z()} = ${total()}`)
|
||||
: renderLatex(`${x()}+(${y()}+${z()}) = ${x()}+${y() + z()} = ${total()}`);
|
||||
}
|
||||
return groupFirst()
|
||||
? renderLatex(`(${x()}\\times${y()})\\times${z()} = ${xy()}\\times${z()} = ${total()}`)
|
||||
: renderLatex(
|
||||
`${x()}\\times(${y()}\\times${z()}) = ${x()}\\times${y() * z()} = ${total()}`
|
||||
);
|
||||
});
|
||||
|
||||
const otherGroupHtml = createMemo(() => {
|
||||
if (mode() === "add") {
|
||||
return groupFirst()
|
||||
? renderLatex(`${x()}+(${y()}+${z()}) = ${total()}`)
|
||||
: renderLatex(`(${x()}+${y()})+${z()} = ${total()}`);
|
||||
}
|
||||
return groupFirst()
|
||||
? renderLatex(`${x()}\\times(${y()}\\times${z()}) = ${total()}`)
|
||||
: renderLatex(`(${x()}\\times${y()})\\times${z()} = ${total()}`);
|
||||
});
|
||||
|
||||
return (
|
||||
<section class="math-widget prop-assoc" aria-labelledby="prop-assoc-title">
|
||||
<h2 id="prop-assoc-title" class="math-widget__title">
|
||||
Associative property
|
||||
</h2>
|
||||
<div class="math-widget__body">
|
||||
<div class="prop-assoc__mode" role="group" aria-label="Operation">
|
||||
<button
|
||||
type="button"
|
||||
class="prop-assoc__mode-btn"
|
||||
classList={{ "prop-assoc__mode-btn--active": mode() === "add" }}
|
||||
onClick={() => setMode("add")}
|
||||
>
|
||||
Addition
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="prop-assoc__mode-btn"
|
||||
classList={{ "prop-assoc__mode-btn--active": mode() === "mul" }}
|
||||
onClick={() => setMode("mul")}
|
||||
>
|
||||
Multiplication
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="math-widget__controls">
|
||||
<div class="scenario-demo__control">
|
||||
<label class="scenario-demo__label" for="prop-assoc-x">
|
||||
<span>First</span>
|
||||
<span class="scenario-demo__value">{x()}</span>
|
||||
</label>
|
||||
<input
|
||||
id="prop-assoc-x"
|
||||
class="scenario-demo__slider"
|
||||
type="range"
|
||||
min={1}
|
||||
max={6}
|
||||
step={1}
|
||||
value={x()}
|
||||
onInput={(e) => setX(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
<div class="scenario-demo__control">
|
||||
<label class="scenario-demo__label" for="prop-assoc-y">
|
||||
<span>Middle</span>
|
||||
<span class="scenario-demo__value">{y()}</span>
|
||||
</label>
|
||||
<input
|
||||
id="prop-assoc-y"
|
||||
class="scenario-demo__slider"
|
||||
type="range"
|
||||
min={1}
|
||||
max={6}
|
||||
step={1}
|
||||
value={y()}
|
||||
onInput={(e) => setY(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
<div class="scenario-demo__control">
|
||||
<label class="scenario-demo__label" for="prop-assoc-z">
|
||||
<span>Last</span>
|
||||
<span class="scenario-demo__value">{z()}</span>
|
||||
</label>
|
||||
<input
|
||||
id="prop-assoc-z"
|
||||
class="scenario-demo__slider"
|
||||
type="range"
|
||||
min={1}
|
||||
max={6}
|
||||
step={1}
|
||||
value={z()}
|
||||
onInput={(e) => setZ(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prop-assoc__formulas">
|
||||
<p class="math-widget__formula" innerHTML={leftGroupHtml()} aria-live="polite" />
|
||||
<p class="prop-assoc__alt" innerHTML={otherGroupHtml()} aria-live="polite" />
|
||||
</div>
|
||||
|
||||
<div class="prop-assoc__stage">
|
||||
<div
|
||||
class="prop-assoc__group"
|
||||
classList={{ "prop-assoc__group--right": !groupFirst() }}
|
||||
>
|
||||
{groupFirst() ? (
|
||||
<>
|
||||
<div class="prop-assoc__bracket prop-assoc__bracket--open" aria-hidden="true">
|
||||
(
|
||||
</div>
|
||||
<DotBlock count={x()} tone="x" />
|
||||
<span class="prop-assoc__op" aria-hidden="true">
|
||||
{mode() === "add" ? "+" : "×"}
|
||||
</span>
|
||||
<DotBlock count={y()} tone="y" />
|
||||
<div class="prop-assoc__bracket prop-assoc__bracket--close" aria-hidden="true">
|
||||
)
|
||||
</div>
|
||||
<span class="prop-assoc__op" aria-hidden="true">
|
||||
{mode() === "add" ? "+" : "×"}
|
||||
</span>
|
||||
<DotBlock count={z()} tone="z" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DotBlock count={x()} tone="x" />
|
||||
<span class="prop-assoc__op" aria-hidden="true">
|
||||
{mode() === "add" ? "+" : "×"}
|
||||
</span>
|
||||
<div class="prop-assoc__bracket prop-assoc__bracket--open" aria-hidden="true">
|
||||
(
|
||||
</div>
|
||||
<DotBlock count={y()} tone="y" />
|
||||
<span class="prop-assoc__op" aria-hidden="true">
|
||||
{mode() === "add" ? "+" : "×"}
|
||||
</span>
|
||||
<DotBlock count={z()} tone="z" />
|
||||
<div class="prop-assoc__bracket prop-assoc__bracket--close" aria-hidden="true">
|
||||
)
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<span class="prop-assoc__eq" aria-hidden="true">
|
||||
=
|
||||
</span>
|
||||
<div class="prop-assoc__result">
|
||||
<For each={Array.from({ length: Math.min(total(), 48) })}>
|
||||
{() => <span class="prop-assoc__dot prop-assoc__dot--result" />}
|
||||
</For>
|
||||
{total() > 48 ? (
|
||||
<span class="prop-assoc__many">+{total() - 48} more</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="prop-assoc__toggle"
|
||||
onClick={() => setGroupFirst((v) => !v)}
|
||||
>
|
||||
{groupFirst() ? "Group the last two instead" : "Group the first two instead"}
|
||||
</button>
|
||||
|
||||
<p class="math-widget__caption">
|
||||
Brackets show which pair to combine first. Different groupings give the same final answer.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default AssociativeDemo;
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
import katex from "katex";
|
||||
import { createMemo, createSignal, For } from "solid-js";
|
||||
|
||||
function renderLatex(latex: string) {
|
||||
return katex.renderToString(latex, { throwOnError: false, strict: "ignore" });
|
||||
}
|
||||
|
||||
function DotRow(props: { count: number; class?: string }) {
|
||||
return (
|
||||
<div class={`prop-comm__row ${props.class ?? ""}`}>
|
||||
<For each={Array.from({ length: props.count })}>
|
||||
{() => <span class="prop-comm__dot" />}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommutativeDemo() {
|
||||
const [mode, setMode] = createSignal<"add" | "mul">("add");
|
||||
const [left, setLeft] = createSignal(3);
|
||||
const [right, setRight] = createSignal(5);
|
||||
const [swapped, setSwapped] = createSignal(false);
|
||||
|
||||
const first = createMemo(() => (swapped() ? right() : left()));
|
||||
const second = createMemo(() => (swapped() ? left() : right()));
|
||||
const product = createMemo(() => left() * right());
|
||||
|
||||
const equationHtml = createMemo(() => {
|
||||
if (mode() === "add") {
|
||||
return renderLatex(`${first()} + ${second()} = ${first() + second()}`);
|
||||
}
|
||||
return renderLatex(`${first()} \\times ${second()} = ${product()}`);
|
||||
});
|
||||
|
||||
const compareHtml = createMemo(() => {
|
||||
if (mode() === "add") {
|
||||
return renderLatex(`${left()} + ${right()} = ${right()} + ${left()} = ${left() + right()}`);
|
||||
}
|
||||
return renderLatex(`${left()} \\times ${right()} = ${right()} \\times ${left()} = ${product()}`);
|
||||
});
|
||||
|
||||
return (
|
||||
<section class="math-widget prop-comm" aria-labelledby="prop-comm-title">
|
||||
<h2 id="prop-comm-title" class="math-widget__title">
|
||||
Commutative property
|
||||
</h2>
|
||||
<div class="math-widget__body">
|
||||
<div class="prop-comm__mode" role="group" aria-label="Operation">
|
||||
<button
|
||||
type="button"
|
||||
class="prop-comm__mode-btn"
|
||||
classList={{ "prop-comm__mode-btn--active": mode() === "add" }}
|
||||
onClick={() => {
|
||||
setMode("add");
|
||||
setSwapped(false);
|
||||
}}
|
||||
>
|
||||
Addition
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="prop-comm__mode-btn"
|
||||
classList={{ "prop-comm__mode-btn--active": mode() === "mul" }}
|
||||
onClick={() => {
|
||||
setMode("mul");
|
||||
setSwapped(false);
|
||||
}}
|
||||
>
|
||||
Multiplication
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="math-widget__controls">
|
||||
<div class="scenario-demo__control">
|
||||
<label class="scenario-demo__label" for="prop-comm-left">
|
||||
<span>First number</span>
|
||||
<span class="scenario-demo__value">{left()}</span>
|
||||
</label>
|
||||
<input
|
||||
id="prop-comm-left"
|
||||
class="scenario-demo__slider"
|
||||
type="range"
|
||||
min={1}
|
||||
max={8}
|
||||
step={1}
|
||||
value={left()}
|
||||
onInput={(e) => setLeft(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
<div class="scenario-demo__control">
|
||||
<label class="scenario-demo__label" for="prop-comm-right">
|
||||
<span>Second number</span>
|
||||
<span class="scenario-demo__value">{right()}</span>
|
||||
</label>
|
||||
<input
|
||||
id="prop-comm-right"
|
||||
class="scenario-demo__slider"
|
||||
type="range"
|
||||
min={1}
|
||||
max={8}
|
||||
step={1}
|
||||
value={right()}
|
||||
onInput={(e) => setRight(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="math-widget__formula" innerHTML={equationHtml()} aria-live="polite" />
|
||||
<p class="prop-comm__compare" innerHTML={compareHtml()} aria-live="polite" />
|
||||
|
||||
<div class="prop-comm__stage">
|
||||
{mode() === "add" ? (
|
||||
<div class="prop-comm__add-piles">
|
||||
<div class="prop-comm__pile">
|
||||
<span class="prop-comm__pile-label">{first()}</span>
|
||||
<DotRow count={first()} class="prop-comm__pile-dots--a" />
|
||||
</div>
|
||||
<span class="prop-comm__op" aria-hidden="true">
|
||||
+
|
||||
</span>
|
||||
<div class="prop-comm__pile">
|
||||
<span class="prop-comm__pile-label">{second()}</span>
|
||||
<DotRow count={second()} class="prop-comm__pile-dots--b" />
|
||||
</div>
|
||||
<span class="prop-comm__op" aria-hidden="true">
|
||||
=
|
||||
</span>
|
||||
<div class="prop-comm__pile prop-comm__pile--total">
|
||||
<span class="prop-comm__pile-label">{first() + second()}</span>
|
||||
<div class="prop-comm__merged">
|
||||
<DotRow count={first()} class="prop-comm__pile-dots--a" />
|
||||
<DotRow count={second()} class="prop-comm__pile-dots--b" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
class="prop-comm__grid"
|
||||
role="img"
|
||||
aria-label={`${first()} rows of ${second()} dots, ${product()} total`}
|
||||
style={{
|
||||
"grid-template-rows": `repeat(${first()}, 1fr)`,
|
||||
"grid-template-columns": `repeat(${second()}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
<For each={Array.from({ length: product() })}>
|
||||
{() => <span class="prop-comm__grid-dot" />}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="prop-comm__swap"
|
||||
onClick={() => setSwapped((v) => !v)}
|
||||
>
|
||||
{swapped() ? "Original order" : "Swap order"}
|
||||
</button>
|
||||
|
||||
<p class="math-widget__caption">
|
||||
Swapping the order does not change the total. Commutative means you can
|
||||
<strong> commute</strong> (move) the numbers.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommutativeDemo;
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import katex from "katex";
|
||||
import { createMemo, createSignal } from "solid-js";
|
||||
|
||||
function renderLatex(latex: string) {
|
||||
return katex.renderToString(latex, { throwOnError: false, strict: "ignore" });
|
||||
}
|
||||
|
||||
function DistributiveAreaDemo() {
|
||||
const [a, setA] = createSignal(2);
|
||||
const [b, setB] = createSignal(3);
|
||||
const [c, setC] = createSignal(5);
|
||||
const [showNumbers, setShowNumbers] = createSignal(true);
|
||||
|
||||
const leftArea = createMemo(() => a() * b());
|
||||
const rightArea = createMemo(() => a() * c());
|
||||
const total = createMemo(() => leftArea() + rightArea());
|
||||
|
||||
const formulaHtml = createMemo(() => {
|
||||
if (showNumbers()) {
|
||||
return renderLatex(
|
||||
`${a()}\\times(${b()}+${c()}) = ${a()}\\times${b()} + ${a()}\\times${c()} = ${leftArea()} + ${rightArea()} = ${total()}`
|
||||
);
|
||||
}
|
||||
return renderLatex("a(b+c) = a \\times b + a \\times c");
|
||||
});
|
||||
|
||||
const leftLabel = createMemo(() =>
|
||||
showNumbers() ? `${a()} \\times ${b()} = ${leftArea()}` : "a \\times b"
|
||||
);
|
||||
const rightLabel = createMemo(() =>
|
||||
showNumbers() ? `${a()} \\times ${c()} = ${rightArea()}` : "a \\times c"
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
class="math-widget prop-dist"
|
||||
aria-labelledby="prop-dist-title"
|
||||
>
|
||||
<div class="prop-dist__header">
|
||||
<h2 id="prop-dist-title" class="math-widget__title prop-dist__title">
|
||||
Distributive property
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="prop-dist__toggle"
|
||||
aria-pressed={showNumbers()}
|
||||
onClick={() => setShowNumbers((v) => !v)}
|
||||
>
|
||||
{showNumbers() ? "Show letters" : "Show numbers"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="math-widget__body">
|
||||
<div class="prop-dist__formula" innerHTML={formulaHtml()} aria-live="polite" />
|
||||
|
||||
<figure class="math-widget__figure prop-dist__figure">
|
||||
<div
|
||||
class="prop-dist__model"
|
||||
role="img"
|
||||
aria-label={`Rectangle height ${a()}, width ${b()} plus ${c()}, total area ${total()}`}
|
||||
>
|
||||
<div class="prop-dist__height" aria-hidden="true">
|
||||
<span class="prop-dist__dim prop-dist__dim--a" innerHTML={renderLatex("a")} />
|
||||
</div>
|
||||
<div
|
||||
class="prop-dist__rects"
|
||||
style={{ "min-height": `${a() * 28}px` }}
|
||||
>
|
||||
<div
|
||||
class="prop-dist__rect prop-dist__rect--b"
|
||||
style={{ flex: `${b()} 1 0` }}
|
||||
>
|
||||
<span class="prop-dist__area" innerHTML={renderLatex(leftLabel())} />
|
||||
</div>
|
||||
<div
|
||||
class="prop-dist__rect prop-dist__rect--c"
|
||||
style={{ flex: `${c()} 1 0` }}
|
||||
>
|
||||
<span class="prop-dist__area" innerHTML={renderLatex(rightLabel())} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-dist__widths" aria-hidden="true">
|
||||
<span
|
||||
class="prop-dist__dim prop-dist__dim--b"
|
||||
style={{ flex: `${b()} 1 0` }}
|
||||
innerHTML={renderLatex("b")}
|
||||
/>
|
||||
<span
|
||||
class="prop-dist__dim prop-dist__dim--c"
|
||||
style={{ flex: `${c()} 1 0` }}
|
||||
innerHTML={renderLatex("c")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption class="math-widget__caption">
|
||||
One rectangle of height <span class="prop-dist__var-a">a</span> and width
|
||||
<span class="prop-dist__var-b">b</span> + <span class="prop-dist__var-c">c</span> splits
|
||||
into two areas <span class="prop-dist__var-a">a</span>×<span class="prop-dist__var-b">b</span>{" "}
|
||||
and <span class="prop-dist__var-a">a</span>×<span class="prop-dist__var-c">c</span>.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<div class="math-widget__controls prop-dist__controls">
|
||||
<div class="scenario-demo__control">
|
||||
<label class="scenario-demo__label prop-dist__label-a" for="prop-dist-a">
|
||||
<span>a (height)</span>
|
||||
<span class="scenario-demo__value">{a()}</span>
|
||||
</label>
|
||||
<input
|
||||
id="prop-dist-a"
|
||||
class="scenario-demo__slider prop-dist__slider-a"
|
||||
type="range"
|
||||
min={1}
|
||||
max={6}
|
||||
step={1}
|
||||
value={a()}
|
||||
onInput={(e) => setA(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
<div class="scenario-demo__control">
|
||||
<label class="scenario-demo__label prop-dist__label-b" for="prop-dist-b">
|
||||
<span>b (width)</span>
|
||||
<span class="scenario-demo__value">{b()}</span>
|
||||
</label>
|
||||
<input
|
||||
id="prop-dist-b"
|
||||
class="scenario-demo__slider prop-dist__slider-b"
|
||||
type="range"
|
||||
min={1}
|
||||
max={8}
|
||||
step={1}
|
||||
value={b()}
|
||||
onInput={(e) => setB(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
<div class="scenario-demo__control">
|
||||
<label class="scenario-demo__label prop-dist__label-c" for="prop-dist-c">
|
||||
<span>c (width)</span>
|
||||
<span class="scenario-demo__value">{c()}</span>
|
||||
</label>
|
||||
<input
|
||||
id="prop-dist-c"
|
||||
class="scenario-demo__slider prop-dist__slider-c"
|
||||
type="range"
|
||||
min={1}
|
||||
max={8}
|
||||
step={1}
|
||||
value={c()}
|
||||
onInput={(e) => setC(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default DistributiveAreaDemo;
|
||||
293
content/math/written-calculation/_index.md
Normal file
293
content/math/written-calculation/_index.md
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
+++
|
||||
title = "Written calculation"
|
||||
description = "Add, subtract, multiply, and divide whole numbers using vertical written form: align columns, regroup, and read the result."
|
||||
template = "chapter.html"
|
||||
weight = 2
|
||||
[extra]
|
||||
subject = "math"
|
||||
chapter_id = "written-calculation"
|
||||
strand = "Number & Algebra"
|
||||
+++
|
||||
|
||||
<div class="book-prose">
|
||||
<p>
|
||||
You can read number sentences from <strong>basic math notation</strong> and use the
|
||||
<strong>properties of operations</strong> to see why reordering or splitting sums can still give
|
||||
the same answer. This chapter teaches
|
||||
<strong>written calculation</strong> — how to add, subtract, multiply, and divide on paper by
|
||||
stacking digits in <strong>vertical form</strong>: one number above another, digit under digit.
|
||||
The ideas work for any size number; the visualizer below works best with about eight digits or
|
||||
fewer per line.
|
||||
</p>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Vertical form</p>
|
||||
<p>
|
||||
Line up the same places in columns, work from right to left, and write the answer under a horizontal
|
||||
line. Carry and borrow keep each column within a single digit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 id="why-columns" class="book-prose__heading">Why columns?</h2>
|
||||
<p>
|
||||
In <strong>counting and numbers</strong> you learned <strong>place value</strong>: the ones column,
|
||||
tens column, hundreds column. In vertical form, digits that share a place must sit in the same
|
||||
column. If you shift a digit left or right, you are changing its value.
|
||||
</p>
|
||||
<p>
|
||||
Always line up the <strong>ones</strong> under the ones, then tens under tens, and so on. Draw a
|
||||
horizontal line under the stack before you write the answer.
|
||||
</p>
|
||||
|
||||
<h2 id="carry-borrow" class="book-prose__heading">Carry and borrow</h2>
|
||||
<p>
|
||||
In the four operations, a column sometimes gives a digit from <strong>0 to 9</strong> and sometimes
|
||||
gives <strong>10 or more</strong> (or too small to subtract). Then you adjust the next column to the
|
||||
<strong>left</strong>.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Carry</strong> — in addition, when a column sums to 10 or more, you write the ones
|
||||
digit of that sum in the column and pass the extra ten to the next column on the left.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Borrow</strong> — in subtraction, when the top digit is smaller than the bottom
|
||||
digit, you take one ten from the column on the left, add 10 to the current column, then subtract.
|
||||
</li>
|
||||
</ul>
|
||||
<p>Work from the <strong>right</strong> (ones) toward the <strong>left</strong>, one column at a time.</p>
|
||||
|
||||
<h3 id="carry-example" class="book-prose__heading">Carry: 27 + 59 = 86</h3>
|
||||
<pre class="va-static" aria-label="27 plus 59 equals 86 with carry"><span class="va-static__carry">¹</span>
|
||||
27
|
||||
+ 59
|
||||
────
|
||||
86</pre>
|
||||
<p>
|
||||
In the ones column: \(7 + 9 = 16\). Write <strong>6</strong> in the ones place. Carry
|
||||
<strong>1</strong> to the tens column (written small above the tens). In the tens column:
|
||||
\(2 + 5 + 1 = 8\).
|
||||
</p>
|
||||
|
||||
<h3 id="borrow-example" class="book-prose__heading">Borrow: 47 − 19 = 28</h3>
|
||||
<pre class="va-static" aria-label="47 minus 19 equals 28 with borrow"><span class="va-static__borrow">−1</span>
|
||||
47
|
||||
− 19
|
||||
────
|
||||
28</pre>
|
||||
<p>
|
||||
In the ones column: \(7 - 9\) does not work. Borrow 10 from the tens: the <strong>4</strong> becomes
|
||||
<strong>3</strong>, and the ones digit becomes <strong>17</strong>. Then \(17 - 9 = 8\). In the tens
|
||||
column: \(3 - 1 = 2\).
|
||||
</p>
|
||||
|
||||
<h2 id="addition" class="book-prose__heading">Addition</h2>
|
||||
<p>
|
||||
Write the numbers one above the other, plus sign to the left of the bottom number, line underneath,
|
||||
sum below. Add each column from right to left; carry when needed.
|
||||
</p>
|
||||
|
||||
<h2 id="subtraction" class="book-prose__heading">Subtraction</h2>
|
||||
<p>
|
||||
Put the <strong>larger</strong> number on top. Subtract column by column. When the top digit is too
|
||||
small, borrow from the left (decrease that digit by 1, add 10 to the current column). Example with a
|
||||
zero in the middle: \(503 - 278 = 225\) needs borrowing across the tens column.
|
||||
</p>
|
||||
|
||||
<h2 id="multiplication" class="book-prose__heading">Multiplication</h2>
|
||||
<p>
|
||||
Write the larger number on top (often). Multiply the top number by each digit of the bottom number,
|
||||
one <strong>partial product</strong> per digit, shifted so ones line up with ones. Add the partial
|
||||
products for the final answer.
|
||||
</p>
|
||||
<pre class="va-static" aria-label="23 times 14 equals 322">
|
||||
23
|
||||
× 14
|
||||
────
|
||||
92
|
||||
230
|
||||
────
|
||||
322</pre>
|
||||
<p>Here \(23 \times 4 = 92\) and \(23 \times 10 = 230\); \(92 + 230 = 322\).</p>
|
||||
|
||||
<h2 id="division" class="book-prose__heading">Division</h2>
|
||||
<p>
|
||||
<strong>Long division</strong> writes the divisor outside a bracket, the dividend inside, and the
|
||||
quotient on top. At each step: how many times does the divisor fit into the current part of the
|
||||
dividend? Write that digit in the quotient, multiply, subtract, then bring down the next digit. If
|
||||
the division is not exact, the amount left is the <strong>remainder</strong>.
|
||||
</p>
|
||||
|
||||
<h2 id="terminology" class="book-prose__heading">Names of the parts</h2>
|
||||
<div class="book-table-wrap">
|
||||
<table class="book-table written-terms-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Operation</th>
|
||||
<th scope="col">Parts</th>
|
||||
<th scope="col">Answer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Addition</td>
|
||||
<td>addend + addend</td>
|
||||
<td>sum</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subtraction</td>
|
||||
<td>minuend − subtrahend</td>
|
||||
<td>difference</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multiplication</td>
|
||||
<td>factor × factor</td>
|
||||
<td>product</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Division</td>
|
||||
<td>dividend ÷ divisor</td>
|
||||
<td>quotient (and remainder if any)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 id="try-it-yourself" class="book-prose__heading">Try it yourself</h2>
|
||||
<p>
|
||||
Use the visualizer below: enter two or more whole numbers (one per line), choose an operation, and
|
||||
click <strong>Generate visualization</strong>. Step through with <strong>Play</strong> or the arrow
|
||||
buttons to see each carry, borrow, and column update.
|
||||
</p>
|
||||
<div class="math-widget-mount" data-widget="vertical-arithmetic-workbench" data-pagefind-ignore></div>
|
||||
</div>
|
||||
|
||||
<div class="book-prose">
|
||||
<h2 id="history" class="book-prose__heading">History</h2>
|
||||
<p>
|
||||
People calculated with tokens, rods, and abacuses long before everyone used pencil and paper. In many
|
||||
places, including China, India, and the Arab world, scribes lined up digits in columns so addition
|
||||
and subtraction could be done digit by digit.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Mechanical calculators</strong> (1600s–1900s) had to solve the same puzzle: when one wheel
|
||||
moved from 9 to 0, it had to add 1 to the wheel for the next place. Early machines often did addition
|
||||
first, then ran a separate <strong>carry cycle</strong> to update higher digits. Later designs passed
|
||||
the carry instantly along the whole machine. That idea appears again inside modern chips: a
|
||||
<strong>carry flag</strong> records whether the last step overflowed a column.
|
||||
</p>
|
||||
|
||||
<h2 id="derivation" class="book-prose__heading">Derivation</h2>
|
||||
<h3 id="place-value-add" class="book-prose__heading">Why column addition works</h3>
|
||||
<p>
|
||||
Split each number by place value. For example,
|
||||
\(27 + 59 = (20 + 7) + (50 + 9) = (20 + 50) + (7 + 9) = 70 + 16 = 86\). The ones column adds
|
||||
\(7 + 9\); the tens column adds \(2 + 5\) plus any carry. Each column only mixes the same place —
|
||||
that is why alignment matters.
|
||||
</p>
|
||||
<h3 id="inverse" class="book-prose__heading">Subtraction and addition</h3>
|
||||
<p>
|
||||
Subtraction is the inverse of addition: \(c = a - b\) means the same as \(c + b = a\). Borrowing
|
||||
moves one ten from the left so the subtraction in the current column matches that story.
|
||||
</p>
|
||||
|
||||
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>When you add 7 and 9 in the ones column, what do you write in the ones place? What do you carry?</p>
|
||||
</div>
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>In \(47 - 19\), why does the 4 in the tens column become 3 before you subtract the ones?</p>
|
||||
</div>
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>In \(23 \times 14\), why are there two rows under the line before the final sum?</p>
|
||||
</div>
|
||||
|
||||
<h2 id="applications" class="book-prose__heading">Real life</h2>
|
||||
<ul>
|
||||
<li><strong>Shopping</strong> — adding prices on a receipt column by column.</li>
|
||||
<li><strong>Scores</strong> — finding the difference between two team totals.</li>
|
||||
<li><strong>Sharing</strong> — dividing a total equally (e.g. \(157\) stickers among \(12\) children: quotient 13, remainder 1).</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="questions-easy" class="book-prose__heading">Question bank — Easy</h2>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(34 + 25\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<pre class="va-static"> 34
|
||||
+ 25
|
||||
────
|
||||
59</pre>
|
||||
<p><strong>Answer:</strong> 59</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(61 - 28\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> 33 (borrow from tens in the ones column).</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2 id="questions-intermediate" class="book-prose__heading">Intermediate</h2>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(27 + 59\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<pre class="va-static"><span class="va-static__carry">¹</span>
|
||||
27
|
||||
+ 59
|
||||
────
|
||||
86</pre>
|
||||
<p><strong>Answer:</strong> 86</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(503 - 278\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> 225</p>
|
||||
<p><strong>Steps:</strong> Borrow across the tens (0) from hundreds; ones become 13−8=5; tens 9−7=2; hundreds 4−2=2.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(23 \times 14\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> 322</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2 id="questions-difficult" class="book-prose__heading">Difficult</h2>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(156 \div 12\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> 13</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(157 \div 12\) and state the remainder.</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> Quotient 13, remainder 1.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2 id="questions-hardcore" class="book-prose__heading">Hardcore</h2>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">A book has 864 pages. You read 379 pages. How many pages are left?</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(864 - 379 = 485\) pages.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">A classroom orders 24 packs of 15 pencils. How many pencils is that?</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(24 \times 15 = 360\) pencils.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2 id="references" class="book-prose__heading">References</h2>
|
||||
<ul>
|
||||
<li>Wikipedia — <em>Carry (arithmetic)</em> and <em>Subtraction</em>.</li>
|
||||
<li>Cajori, Florian — <em>A History of Mathematical Notations</em>.</li>
|
||||
<li>Other school methods (Austrian/additions, partial differences) exist; this chapter uses column borrow as in most HK primary texts.</li>
|
||||
</ul>
|
||||
</div>
|
||||
371
content/math/written-calculation/chapter.scss
Normal file
371
content/math/written-calculation/chapter.scss
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
// Break out of prose max-width (68ch) to the same width as .container
|
||||
.book-prose .math-widget-mount:has(.va-workbench) {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.va-workbench {
|
||||
--va-text: var(--ink);
|
||||
--va-operator: var(--muted);
|
||||
--va-carry: #dc2626;
|
||||
--va-borrow: var(--accent);
|
||||
--va-result: var(--success);
|
||||
--va-line: var(--line);
|
||||
--va-highlight: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
--va-highlight-border: color-mix(in srgb, var(--accent) 38%, transparent);
|
||||
--va-error: #dc2626;
|
||||
--va-canvas-dot: color-mix(in srgb, var(--line) 90%, transparent);
|
||||
--va-panel-bg: var(--card-bg);
|
||||
--va-panel-border: var(--card-border);
|
||||
--va-stage-bg: var(--paper);
|
||||
|
||||
width: min(1100px, 92vw);
|
||||
max-width: none;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .va-workbench,
|
||||
:root:not([data-theme="light"]) .va-workbench {
|
||||
--va-carry: #f87171;
|
||||
--va-error: #f87171;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .va-workbench {
|
||||
--va-carry: #f87171;
|
||||
--va-error: #f87171;
|
||||
}
|
||||
}
|
||||
|
||||
.va-workbench__shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 28rem;
|
||||
border: 1px solid var(--va-panel-border);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: var(--va-panel-bg);
|
||||
box-shadow: 0 10px 32px var(--shadow-soft);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.va-workbench__shell {
|
||||
flex-direction: row;
|
||||
min-height: 32rem;
|
||||
}
|
||||
}
|
||||
|
||||
.va-workbench__sidebar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--va-panel-bg);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.va-workbench__sidebar {
|
||||
width: 18rem;
|
||||
border-bottom: none;
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
}
|
||||
|
||||
.va-workbench__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.va-workbench__brand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.75rem;
|
||||
background: var(--accent);
|
||||
color: var(--paper);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 4px 12px var(--shadow-soft);
|
||||
}
|
||||
|
||||
.va-workbench__brand-text {
|
||||
font-size: 1rem;
|
||||
line-height: 1.3;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.va-workbench__brand-text strong {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.va-workbench__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.va-workbench__label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--soft-ink);
|
||||
}
|
||||
|
||||
.va-workbench__op-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.va-workbench__op-btn {
|
||||
font: inherit;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.5rem;
|
||||
background: color-mix(in srgb, var(--paper) 60%, var(--card-bg));
|
||||
color: var(--soft-ink);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.12s ease,
|
||||
border-color 0.12s ease,
|
||||
color 0.12s ease;
|
||||
}
|
||||
|
||||
.va-workbench__op-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.va-workbench__op-btn--active {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--card-bg));
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.va-workbench__textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.4;
|
||||
color: var(--ink);
|
||||
background: color-mix(in srgb, var(--paper) 50%, var(--card-bg));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.5rem;
|
||||
resize: vertical;
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.va-workbench__textarea:focus {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.va-workbench__hint,
|
||||
.va-workbench__footer-note {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.va-workbench__generate {
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--accent);
|
||||
color: var(--paper);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px var(--shadow-soft);
|
||||
transition:
|
||||
filter 0.12s ease,
|
||||
transform 0.08s ease;
|
||||
}
|
||||
|
||||
.va-workbench__generate:hover {
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
|
||||
.va-workbench__generate:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.va-workbench__anim {
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--line);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.va-workbench__transport {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.5rem;
|
||||
background: color-mix(in srgb, var(--paper) 40%, var(--card-bg));
|
||||
}
|
||||
|
||||
.va-workbench__transport-btn {
|
||||
font: inherit;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border: none;
|
||||
border-radius: 0.35rem;
|
||||
background: transparent;
|
||||
color: var(--soft-ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.va-workbench__transport-btn:hover {
|
||||
color: var(--accent);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
.va-workbench__play {
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
padding: 0.45rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.35rem;
|
||||
background: var(--accent);
|
||||
color: var(--paper);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.va-workbench__play:hover {
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
|
||||
.va-workbench__speed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.va-workbench__speed-slider {
|
||||
flex: 1;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.va-workbench__footer-note {
|
||||
margin-top: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.va-workbench__stage {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 16rem;
|
||||
background: var(--va-stage-bg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.va-workbench__message-box {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
width: min(92%, 32rem);
|
||||
padding: 0.85rem 1.1rem;
|
||||
text-align: center;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.75rem;
|
||||
background: color-mix(in srgb, var(--card-bg) 92%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 8px 24px var(--shadow-soft);
|
||||
}
|
||||
|
||||
.va-workbench__message-box--error .va-workbench__message {
|
||||
color: var(--va-error);
|
||||
}
|
||||
|
||||
.va-workbench__message {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.va-workbench__progress-track {
|
||||
width: 100%;
|
||||
height: 0.35rem;
|
||||
border-radius: 999px;
|
||||
background: var(--line);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.va-workbench__progress-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
.va-workbench__step-counter {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
margin: 0.35rem 0 0;
|
||||
}
|
||||
|
||||
.va-workbench__canvas-wrap {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-height: 14rem;
|
||||
background-color: var(--va-stage-bg);
|
||||
background-image: radial-gradient(var(--va-canvas-dot) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.va-workbench__canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.va-static {
|
||||
font-family: var(--mono);
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.35;
|
||||
letter-spacing: 0.08em;
|
||||
margin: 1rem 0;
|
||||
padding: 1rem 1.25rem;
|
||||
color: var(--ink);
|
||||
background: color-mix(in srgb, var(--paper) 50%, var(--card-bg));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.va-static__carry,
|
||||
.va-static__borrow {
|
||||
font-size: 0.85em;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
.book-prose .written-terms-table td:first-child {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
152
content/math/written-calculation/core.html
Normal file
152
content/math/written-calculation/core.html
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<div class="book-prose">
|
||||
<p>
|
||||
You can read number sentences from <strong>basic math notation</strong> and use the
|
||||
<strong>properties of operations</strong> to see why reordering or splitting sums can still give
|
||||
the same answer. This chapter teaches
|
||||
<strong>written calculation</strong> — how to add, subtract, multiply, and divide on paper by
|
||||
stacking digits in <strong>vertical form</strong>: one number above another, digit under digit.
|
||||
The ideas work for any size number; the visualizer below works best with about eight digits or
|
||||
fewer per line.
|
||||
</p>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Vertical form</p>
|
||||
<p>
|
||||
Line up the same places in columns, work from right to left, and write the answer under a horizontal
|
||||
line. Carry and borrow keep each column within a single digit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 id="why-columns" class="book-prose__heading">Why columns?</h2>
|
||||
<p>
|
||||
In <strong>counting and numbers</strong> you learned <strong>place value</strong>: the ones column,
|
||||
tens column, hundreds column. In vertical form, digits that share a place must sit in the same
|
||||
column. If you shift a digit left or right, you are changing its value.
|
||||
</p>
|
||||
<p>
|
||||
Always line up the <strong>ones</strong> under the ones, then tens under tens, and so on. Draw a
|
||||
horizontal line under the stack before you write the answer.
|
||||
</p>
|
||||
|
||||
<h2 id="carry-borrow" class="book-prose__heading">Carry and borrow</h2>
|
||||
<p>
|
||||
In the four operations, a column sometimes gives a digit from <strong>0 to 9</strong> and sometimes
|
||||
gives <strong>10 or more</strong> (or too small to subtract). Then you adjust the next column to the
|
||||
<strong>left</strong>.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Carry</strong> — in addition, when a column sums to 10 or more, you write the ones
|
||||
digit of that sum in the column and pass the extra ten to the next column on the left.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Borrow</strong> — in subtraction, when the top digit is smaller than the bottom
|
||||
digit, you take one ten from the column on the left, add 10 to the current column, then subtract.
|
||||
</li>
|
||||
</ul>
|
||||
<p>Work from the <strong>right</strong> (ones) toward the <strong>left</strong>, one column at a time.</p>
|
||||
|
||||
<h3 id="carry-example" class="book-prose__heading">Carry: 27 + 59 = 86</h3>
|
||||
<pre class="va-static" aria-label="27 plus 59 equals 86 with carry"><span class="va-static__carry">¹</span>
|
||||
27
|
||||
+ 59
|
||||
────
|
||||
86</pre>
|
||||
<p>
|
||||
In the ones column: \(7 + 9 = 16\). Write <strong>6</strong> in the ones place. Carry
|
||||
<strong>1</strong> to the tens column (written small above the tens). In the tens column:
|
||||
\(2 + 5 + 1 = 8\).
|
||||
</p>
|
||||
|
||||
<h3 id="borrow-example" class="book-prose__heading">Borrow: 47 − 19 = 28</h3>
|
||||
<pre class="va-static" aria-label="47 minus 19 equals 28 with borrow"><span class="va-static__borrow">−1</span>
|
||||
47
|
||||
− 19
|
||||
────
|
||||
28</pre>
|
||||
<p>
|
||||
In the ones column: \(7 - 9\) does not work. Borrow 10 from the tens: the <strong>4</strong> becomes
|
||||
<strong>3</strong>, and the ones digit becomes <strong>17</strong>. Then \(17 - 9 = 8\). In the tens
|
||||
column: \(3 - 1 = 2\).
|
||||
</p>
|
||||
|
||||
<h2 id="addition" class="book-prose__heading">Addition</h2>
|
||||
<p>
|
||||
Write the numbers one above the other, plus sign to the left of the bottom number, line underneath,
|
||||
sum below. Add each column from right to left; carry when needed.
|
||||
</p>
|
||||
|
||||
<h2 id="subtraction" class="book-prose__heading">Subtraction</h2>
|
||||
<p>
|
||||
Put the <strong>larger</strong> number on top. Subtract column by column. When the top digit is too
|
||||
small, borrow from the left (decrease that digit by 1, add 10 to the current column). Example with a
|
||||
zero in the middle: \(503 - 278 = 225\) needs borrowing across the tens column.
|
||||
</p>
|
||||
|
||||
<h2 id="multiplication" class="book-prose__heading">Multiplication</h2>
|
||||
<p>
|
||||
Write the larger number on top (often). Multiply the top number by each digit of the bottom number,
|
||||
one <strong>partial product</strong> per digit, shifted so ones line up with ones. Add the partial
|
||||
products for the final answer.
|
||||
</p>
|
||||
<pre class="va-static" aria-label="23 times 14 equals 322">
|
||||
23
|
||||
× 14
|
||||
────
|
||||
92
|
||||
230
|
||||
────
|
||||
322</pre>
|
||||
<p>Here \(23 \times 4 = 92\) and \(23 \times 10 = 230\); \(92 + 230 = 322\).</p>
|
||||
|
||||
<h2 id="division" class="book-prose__heading">Division</h2>
|
||||
<p>
|
||||
<strong>Long division</strong> writes the divisor outside a bracket, the dividend inside, and the
|
||||
quotient on top. At each step: how many times does the divisor fit into the current part of the
|
||||
dividend? Write that digit in the quotient, multiply, subtract, then bring down the next digit. If
|
||||
the division is not exact, the amount left is the <strong>remainder</strong>.
|
||||
</p>
|
||||
|
||||
<h2 id="terminology" class="book-prose__heading">Names of the parts</h2>
|
||||
<div class="book-table-wrap">
|
||||
<table class="book-table written-terms-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Operation</th>
|
||||
<th scope="col">Parts</th>
|
||||
<th scope="col">Answer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Addition</td>
|
||||
<td>addend + addend</td>
|
||||
<td>sum</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subtraction</td>
|
||||
<td>minuend − subtrahend</td>
|
||||
<td>difference</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multiplication</td>
|
||||
<td>factor × factor</td>
|
||||
<td>product</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Division</td>
|
||||
<td>dividend ÷ divisor</td>
|
||||
<td>quotient (and remainder if any)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 id="try-it-yourself" class="book-prose__heading">Try it yourself</h2>
|
||||
<p>
|
||||
Use the visualizer below: enter two or more whole numbers (one per line), choose an operation, and
|
||||
click <strong>Generate visualization</strong>. Step through with <strong>Play</strong> or the arrow
|
||||
buttons to see each carry, borrow, and column update.
|
||||
</p>
|
||||
<div class="math-widget-mount" data-widget="vertical-arithmetic-workbench" data-pagefind-ignore></div>
|
||||
</div>
|
||||
129
content/math/written-calculation/supplement.html
Normal file
129
content/math/written-calculation/supplement.html
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<div class="book-prose">
|
||||
<h2 id="history" class="book-prose__heading">History</h2>
|
||||
<p>
|
||||
People calculated with tokens, rods, and abacuses long before everyone used pencil and paper. In many
|
||||
places, including China, India, and the Arab world, scribes lined up digits in columns so addition
|
||||
and subtraction could be done digit by digit.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Mechanical calculators</strong> (1600s–1900s) had to solve the same puzzle: when one wheel
|
||||
moved from 9 to 0, it had to add 1 to the wheel for the next place. Early machines often did addition
|
||||
first, then ran a separate <strong>carry cycle</strong> to update higher digits. Later designs passed
|
||||
the carry instantly along the whole machine. That idea appears again inside modern chips: a
|
||||
<strong>carry flag</strong> records whether the last step overflowed a column.
|
||||
</p>
|
||||
|
||||
<h2 id="derivation" class="book-prose__heading">Derivation</h2>
|
||||
<h3 id="place-value-add" class="book-prose__heading">Why column addition works</h3>
|
||||
<p>
|
||||
Split each number by place value. For example,
|
||||
\(27 + 59 = (20 + 7) + (50 + 9) = (20 + 50) + (7 + 9) = 70 + 16 = 86\). The ones column adds
|
||||
\(7 + 9\); the tens column adds \(2 + 5\) plus any carry. Each column only mixes the same place —
|
||||
that is why alignment matters.
|
||||
</p>
|
||||
<h3 id="inverse" class="book-prose__heading">Subtraction and addition</h3>
|
||||
<p>
|
||||
Subtraction is the inverse of addition: \(c = a - b\) means the same as \(c + b = a\). Borrowing
|
||||
moves one ten from the left so the subtraction in the current column matches that story.
|
||||
</p>
|
||||
|
||||
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>When you add 7 and 9 in the ones column, what do you write in the ones place? What do you carry?</p>
|
||||
</div>
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>In \(47 - 19\), why does the 4 in the tens column become 3 before you subtract the ones?</p>
|
||||
</div>
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Checkpoint</p>
|
||||
<p>In \(23 \times 14\), why are there two rows under the line before the final sum?</p>
|
||||
</div>
|
||||
|
||||
<h2 id="applications" class="book-prose__heading">Real life</h2>
|
||||
<ul>
|
||||
<li><strong>Shopping</strong> — adding prices on a receipt column by column.</li>
|
||||
<li><strong>Scores</strong> — finding the difference between two team totals.</li>
|
||||
<li><strong>Sharing</strong> — dividing a total equally (e.g. \(157\) stickers among \(12\) children: quotient 13, remainder 1).</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="questions-easy" class="book-prose__heading">Question bank — Easy</h2>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(34 + 25\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<pre class="va-static"> 34
|
||||
+ 25
|
||||
────
|
||||
59</pre>
|
||||
<p><strong>Answer:</strong> 59</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(61 - 28\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> 33 (borrow from tens in the ones column).</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2 id="questions-intermediate" class="book-prose__heading">Intermediate</h2>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(27 + 59\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<pre class="va-static"><span class="va-static__carry">¹</span>
|
||||
27
|
||||
+ 59
|
||||
────
|
||||
86</pre>
|
||||
<p><strong>Answer:</strong> 86</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(503 - 278\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> 225</p>
|
||||
<p><strong>Steps:</strong> Borrow across the tens (0) from hundreds; ones become 13−8=5; tens 9−7=2; hundreds 4−2=2.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(23 \times 14\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> 322</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2 id="questions-difficult" class="book-prose__heading">Difficult</h2>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(156 \div 12\).</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> 13</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Work out \(157 \div 12\) and state the remainder.</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> Quotient 13, remainder 1.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2 id="questions-hardcore" class="book-prose__heading">Hardcore</h2>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">A book has 864 pages. You read 379 pages. How many pages are left?</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(864 - 379 = 485\) pages.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">A classroom orders 24 packs of 15 pencils. How many pencils is that?</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(24 \times 15 = 360\) pencils.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2 id="references" class="book-prose__heading">References</h2>
|
||||
<ul>
|
||||
<li>Wikipedia — <em>Carry (arithmetic)</em> and <em>Subtraction</em>.</li>
|
||||
<li>Cajori, Florian — <em>A History of Mathematical Notations</em>.</li>
|
||||
<li>Other school methods (Austrian/additions, partial differences) exist; this chapter uses column borrow as in most HK primary texts.</li>
|
||||
</ul>
|
||||
</div>
|
||||
841
content/math/written-calculation/widgets/lib/math-visualizer.ts
Normal file
841
content/math/written-calculation/widgets/lib/math-visualizer.ts
Normal file
|
|
@ -0,0 +1,841 @@
|
|||
export type VisualizerColors = {
|
||||
text: string;
|
||||
operator: string;
|
||||
carry: string;
|
||||
borrow: string;
|
||||
result: string;
|
||||
line: string;
|
||||
highlight: string;
|
||||
highlightBorder: string;
|
||||
error: string;
|
||||
};
|
||||
|
||||
type CellData = {
|
||||
text?: string;
|
||||
color?: string;
|
||||
crossedOut?: boolean;
|
||||
smallText?: string;
|
||||
smallColor?: string;
|
||||
};
|
||||
|
||||
type Frame = {
|
||||
cells: Record<string, CellData>;
|
||||
lines: Array<{ y: number; startX: number; endX: number }>;
|
||||
highlights: Array<{ x: number; y: number; w: number; h: number }>;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export class MathVisualizer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private wrapper: HTMLElement;
|
||||
private themeRoot: HTMLElement;
|
||||
|
||||
frames: Frame[] = [];
|
||||
currentFrame = 0;
|
||||
isPlaying = false;
|
||||
private playInterval: ReturnType<typeof setInterval> | null = null;
|
||||
speed = 800;
|
||||
operation: "+" | "-" | "*" | "/" = "+";
|
||||
|
||||
private colors: VisualizerColors = {
|
||||
text: "#1e293b",
|
||||
operator: "#64748b",
|
||||
carry: "#ef4444",
|
||||
borrow: "#9a6b2e",
|
||||
result: "#16a34a",
|
||||
line: "#cbd5e1",
|
||||
highlight: "rgba(59, 130, 246, 0.15)",
|
||||
highlightBorder: "rgba(59, 130, 246, 0.4)",
|
||||
error: "#ef4444",
|
||||
};
|
||||
|
||||
onMessage?: (msg: string, isError?: boolean) => void;
|
||||
onStep?: (current: number, total: number) => void;
|
||||
onPlayState?: (playing: boolean) => void;
|
||||
|
||||
constructor(opts: {
|
||||
canvas: HTMLCanvasElement;
|
||||
wrapper: HTMLElement;
|
||||
themeRoot: HTMLElement;
|
||||
}) {
|
||||
this.canvas = opts.canvas;
|
||||
const ctx = opts.canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("Canvas 2D not available");
|
||||
this.ctx = ctx;
|
||||
this.wrapper = opts.wrapper;
|
||||
this.themeRoot = opts.themeRoot;
|
||||
this.refreshColors();
|
||||
}
|
||||
|
||||
refreshColors() {
|
||||
const s = getComputedStyle(this.themeRoot);
|
||||
const get = (name: string, fallback: string) =>
|
||||
s.getPropertyValue(name).trim() || fallback;
|
||||
this.colors = {
|
||||
text: get("--va-text", "#1a1a1a"),
|
||||
operator: get("--va-operator", "#767676"),
|
||||
carry: get("--va-carry", "#dc2626"),
|
||||
borrow: get("--va-borrow", "#9a6b2e"),
|
||||
result: get("--va-result", "#2a7d52"),
|
||||
line: get("--va-line", "#e8e8e8"),
|
||||
highlight: get("--va-highlight", "rgba(154, 107, 46, 0.15)"),
|
||||
highlightBorder: get("--va-highlight-border", "rgba(154, 107, 46, 0.4)"),
|
||||
error: get("--va-error", "#dc2626"),
|
||||
};
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
this.canvas.width = rect.width * dpr;
|
||||
this.canvas.height = rect.height * dpr;
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.scale(dpr, dpr);
|
||||
this.canvas.style.width = `${rect.width}px`;
|
||||
this.canvas.style.height = `${rect.height}px`;
|
||||
this.render();
|
||||
}
|
||||
|
||||
setSpeed(ms: number) {
|
||||
this.speed = ms;
|
||||
if (this.isPlaying) {
|
||||
this.pause();
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
setOperation(op: "+" | "-" | "*" | "/") {
|
||||
this.operation = op;
|
||||
}
|
||||
|
||||
generateFromInput(raw: string) {
|
||||
const lines = raw
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => /^\d+$/.test(s));
|
||||
|
||||
if (lines.length < 2) {
|
||||
this.showMessage("Please enter at least two numbers.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.frames = [];
|
||||
this.currentFrame = 0;
|
||||
this.pause();
|
||||
|
||||
try {
|
||||
if (this.operation === "+") {
|
||||
this.generateAddition(lines);
|
||||
} else {
|
||||
let current = lines[0];
|
||||
const finalStepFrames: Frame[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const next = lines[i];
|
||||
|
||||
if (i > 1) {
|
||||
const transFrame = this.createEmptyFrame();
|
||||
transFrame.message = `Intermediate result: ${current}. Preparing next operation...`;
|
||||
for (let k = 0; k < current.length; k++) {
|
||||
this.setCell(transFrame, k, 1, {
|
||||
text: current[k],
|
||||
color: this.colors.result,
|
||||
});
|
||||
}
|
||||
this.frames.push(transFrame);
|
||||
}
|
||||
|
||||
if (this.operation === "-") {
|
||||
this.generateSubtraction(current, next);
|
||||
const diff = BigInt(current) - BigInt(next);
|
||||
if (diff < 0n) {
|
||||
throw new Error(
|
||||
"Result dropped below zero. Negative visualization not supported."
|
||||
);
|
||||
}
|
||||
current = diff.toString();
|
||||
} else if (this.operation === "*") {
|
||||
this.generateMultiplication(current, next);
|
||||
current = (BigInt(current) * BigInt(next)).toString();
|
||||
} else if (this.operation === "/") {
|
||||
if (BigInt(next) === 0n) throw new Error("Cannot divide by zero.");
|
||||
this.generateDivision(current, next);
|
||||
current = (BigInt(current) / BigInt(next)).toString();
|
||||
}
|
||||
|
||||
finalStepFrames.push(this.cloneGrid(this.frames[this.frames.length - 1]));
|
||||
}
|
||||
|
||||
if (finalStepFrames.length > 1) {
|
||||
this.frames.push(this.mergeFramesToSummary(finalStepFrames));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.frames.length > 0) {
|
||||
this.render();
|
||||
this.play();
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
this.showMessage(`Error: ${msg}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
private showMessage(msg: string, isError = false) {
|
||||
this.onMessage?.(msg, isError);
|
||||
}
|
||||
|
||||
private cloneGrid(grid: Frame): Frame {
|
||||
return JSON.parse(JSON.stringify(grid)) as Frame;
|
||||
}
|
||||
|
||||
private createEmptyFrame(): Frame {
|
||||
return { cells: {}, lines: [], highlights: [], message: "" };
|
||||
}
|
||||
|
||||
private setCell(frame: Frame, x: number, y: number, data: CellData) {
|
||||
frame.cells[`${x},${y}`] = { ...frame.cells[`${x},${y}`], ...data };
|
||||
}
|
||||
|
||||
private getCell(frame: Frame, x: number, y: number): CellData | null {
|
||||
return frame.cells[`${x},${y}`] ?? null;
|
||||
}
|
||||
|
||||
private mergeFramesToSummary(finalFrames: Frame[]): Frame {
|
||||
const summaryFrame = this.createEmptyFrame();
|
||||
summaryFrame.message = "Final Summary of All Steps";
|
||||
let currentOffsetX = 0;
|
||||
|
||||
for (let i = 0; i < finalFrames.length; i++) {
|
||||
const f = finalFrames[i];
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let hasCells = false;
|
||||
Object.keys(f.cells).forEach((key) => {
|
||||
const [x] = key.split(",").map(Number);
|
||||
minX = Math.min(minX, x);
|
||||
maxX = Math.max(maxX, x);
|
||||
hasCells = true;
|
||||
});
|
||||
if (!hasCells) continue;
|
||||
|
||||
const shiftX = currentOffsetX - minX;
|
||||
|
||||
Object.entries(f.cells).forEach(([key, cell]) => {
|
||||
const [x, y] = key.split(",").map(Number);
|
||||
summaryFrame.cells[`${x + shiftX},${y}`] = { ...cell };
|
||||
});
|
||||
|
||||
f.lines.forEach((line) => {
|
||||
summaryFrame.lines.push({
|
||||
y: line.y,
|
||||
startX: line.startX + shiftX,
|
||||
endX: line.endX + shiftX,
|
||||
});
|
||||
});
|
||||
|
||||
if (i > 0) {
|
||||
summaryFrame.cells[`${currentOffsetX - 2},2`] = {
|
||||
text: "→",
|
||||
color: this.colors.operator,
|
||||
};
|
||||
}
|
||||
|
||||
currentOffsetX += maxX - minX + 1 + 3;
|
||||
}
|
||||
|
||||
return summaryFrame;
|
||||
}
|
||||
|
||||
private generateAddition(numbers: string[]) {
|
||||
const maxLen = Math.max(...numbers.map((s) => s.length));
|
||||
const padded = numbers.map((s) => s.padStart(maxLen, "0"));
|
||||
const frame = this.createEmptyFrame();
|
||||
frame.message = `Setting up addition for ${numbers.join(" + ")}`;
|
||||
const numCount = numbers.length;
|
||||
const width = maxLen + 2;
|
||||
|
||||
this.setCell(frame, 0, numCount, { text: "+", color: this.colors.operator });
|
||||
|
||||
for (let r = 0; r < numCount; r++) {
|
||||
const pStr = padded[r];
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
if (i >= maxLen - numbers[r].length) {
|
||||
this.setCell(frame, i + 2, r + 1, { text: pStr[i], color: this.colors.text });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lineY = numCount + 1;
|
||||
frame.lines.push({ y: lineY, startX: 0, endX: width });
|
||||
this.frames.push(this.cloneGrid(frame));
|
||||
|
||||
let carry = 0;
|
||||
|
||||
for (let i = maxLen - 1; i >= 0; i--) {
|
||||
let sum = carry;
|
||||
const columnDigits: number[] = [];
|
||||
for (let r = 0; r < numCount; r++) {
|
||||
const d = parseInt(padded[r][i], 10);
|
||||
sum += d;
|
||||
columnDigits.push(d);
|
||||
}
|
||||
|
||||
const nextCarry = Math.floor(sum / 10);
|
||||
const digit = sum % 10;
|
||||
const col = i + 2;
|
||||
|
||||
const stepFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
stepFrame.highlights.push({
|
||||
x: col,
|
||||
y: carry > 0 ? 0 : 1,
|
||||
w: 1,
|
||||
h: carry > 0 ? numCount + 1 : numCount,
|
||||
});
|
||||
|
||||
let msg = `Add column: ${columnDigits.join(" + ")}`;
|
||||
if (carry > 0) msg += ` + ${carry} (carry) = ${sum}.`;
|
||||
else msg += ` = ${sum}.`;
|
||||
if (nextCarry > 0) msg += ` Write ${digit}, carry ${nextCarry}.`;
|
||||
else msg += ` Write ${digit}.`;
|
||||
stepFrame.message = msg;
|
||||
this.frames.push(stepFrame);
|
||||
|
||||
const resultFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
resultFrame.highlights = [];
|
||||
this.setCell(resultFrame, col, lineY + 1, {
|
||||
text: digit.toString(),
|
||||
color: this.colors.result,
|
||||
});
|
||||
if (nextCarry > 0) {
|
||||
this.setCell(resultFrame, col - 1, 0, {
|
||||
text: nextCarry.toString(),
|
||||
color: this.colors.carry,
|
||||
});
|
||||
}
|
||||
resultFrame.message = msg;
|
||||
this.frames.push(resultFrame);
|
||||
|
||||
carry = nextCarry;
|
||||
}
|
||||
|
||||
if (carry > 0) {
|
||||
const finalFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
const carryStr = carry.toString();
|
||||
for (let c = 0; c < carryStr.length; c++) {
|
||||
this.setCell(finalFrame, 1 - (carryStr.length - 1 - c), lineY + 1, {
|
||||
text: carryStr[c],
|
||||
color: this.colors.result,
|
||||
});
|
||||
}
|
||||
finalFrame.message = "Write final carry down.";
|
||||
this.frames.push(finalFrame);
|
||||
}
|
||||
}
|
||||
|
||||
private generateSubtraction(s1: string, s2: string) {
|
||||
const n1 = BigInt(s1);
|
||||
const n2 = BigInt(s2);
|
||||
if (n1 < n2) {
|
||||
throw new Error(
|
||||
"First number must be greater than or equal to second number for visualizer."
|
||||
);
|
||||
}
|
||||
|
||||
const maxLen = Math.max(s1.length, s2.length);
|
||||
const p1 = s1.padStart(maxLen, "0").split("").map(Number);
|
||||
const p2 = s2.padStart(maxLen, "0").split("").map(Number);
|
||||
|
||||
const frame = this.createEmptyFrame();
|
||||
frame.message = `Setting up subtraction for ${s1} - ${s2}`;
|
||||
const width = maxLen + 2;
|
||||
|
||||
this.setCell(frame, 0, 2, { text: "−", color: this.colors.operator });
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
if (i >= maxLen - s1.length) {
|
||||
this.setCell(frame, i + 2, 1, { text: p1[i].toString(), color: this.colors.text });
|
||||
}
|
||||
if (i >= maxLen - s2.length) {
|
||||
this.setCell(frame, i + 2, 2, { text: p2[i].toString(), color: this.colors.text });
|
||||
}
|
||||
}
|
||||
frame.lines.push({ y: 3, startX: 0, endX: width });
|
||||
this.frames.push(this.cloneGrid(frame));
|
||||
|
||||
for (let i = maxLen - 1; i >= 0; i--) {
|
||||
let d1 = p1[i];
|
||||
const d2 = p2[i];
|
||||
const col = i + 2;
|
||||
|
||||
if (d1 < d2) {
|
||||
let borrowIdx = i - 1;
|
||||
while (borrowIdx >= 0 && p1[borrowIdx] === 0) borrowIdx--;
|
||||
|
||||
for (let j = borrowIdx; j < i; j++) {
|
||||
const bFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
const cellToCross = this.getCell(bFrame, j + 2, 1);
|
||||
this.setCell(bFrame, j + 2, 1, { crossedOut: true });
|
||||
const newVal = p1[j] === 0 ? 9 : p1[j] - 1;
|
||||
p1[j] = newVal;
|
||||
this.setCell(bFrame, j + 2, 0, {
|
||||
text: newVal.toString(),
|
||||
color: this.colors.borrow,
|
||||
});
|
||||
if (j < i - 1) {
|
||||
bFrame.message = "Borrowing across zero. Cross out 0, make it 9.";
|
||||
} else {
|
||||
bFrame.message = `Borrow 1 from ${cellToCross?.text ?? ""}, leaving ${newVal}.`;
|
||||
}
|
||||
bFrame.highlights = [{ x: j + 2, y: 0, w: 1, h: 2 }];
|
||||
this.frames.push(bFrame);
|
||||
}
|
||||
|
||||
p1[i] += 10;
|
||||
d1 = p1[i];
|
||||
const addFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
this.setCell(addFrame, col, 1, { crossedOut: true });
|
||||
this.setCell(addFrame, col, 0, { text: d1.toString(), color: this.colors.borrow });
|
||||
addFrame.message = `Add 10 to current column, making it ${d1}.`;
|
||||
addFrame.highlights = [{ x: col, y: 0, w: 1, h: 2 }];
|
||||
this.frames.push(addFrame);
|
||||
}
|
||||
|
||||
const subFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
const origDigit = parseInt(s1.padStart(maxLen, "0")[i], 10);
|
||||
subFrame.highlights = [
|
||||
{
|
||||
x: col,
|
||||
y: p1[i] > origDigit ? 0 : 1,
|
||||
w: 1,
|
||||
h: p1[i] > origDigit ? 3 : 2,
|
||||
},
|
||||
];
|
||||
subFrame.message = `Subtract: ${d1} - ${d2} = ${d1 - d2}.`;
|
||||
this.frames.push(subFrame);
|
||||
|
||||
const resultFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
resultFrame.highlights = [];
|
||||
const res = d1 - d2;
|
||||
const currentP1 = BigInt(p1.slice(0, i + 1).join(""));
|
||||
const currentP2 = BigInt(p2.slice(0, i + 1).join(""));
|
||||
if (res > 0 || i === maxLen - 1 || currentP1 > currentP2) {
|
||||
this.setCell(resultFrame, col, 4, {
|
||||
text: res.toString(),
|
||||
color: this.colors.result,
|
||||
});
|
||||
}
|
||||
resultFrame.message = `Write ${res}.`;
|
||||
this.frames.push(resultFrame);
|
||||
}
|
||||
}
|
||||
|
||||
private generateMultiplication(s1: string, s2: string) {
|
||||
const topOffset = 2;
|
||||
const stackWidth = Math.max(s1.length, s2.length);
|
||||
const rightOffset = stackWidth;
|
||||
const opCol = rightOffset - s2.length;
|
||||
const width = rightOffset + 1;
|
||||
|
||||
const frame = this.createEmptyFrame();
|
||||
frame.message = `Setting up multiplication: ${s1} × ${s2}`;
|
||||
|
||||
this.setCell(frame, opCol, topOffset + 1, {
|
||||
text: "×",
|
||||
color: this.colors.operator,
|
||||
});
|
||||
for (let i = 0; i < s1.length; i++) {
|
||||
this.setCell(frame, rightOffset - s1.length + 1 + i, topOffset, {
|
||||
text: s1[i],
|
||||
color: this.colors.text,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < s2.length; i++) {
|
||||
this.setCell(frame, rightOffset - s2.length + 1 + i, topOffset + 1, {
|
||||
text: s2[i],
|
||||
color: this.colors.text,
|
||||
});
|
||||
}
|
||||
frame.lines.push({ y: topOffset + 2, startX: opCol, endX: rightOffset });
|
||||
this.frames.push(this.cloneGrid(frame));
|
||||
|
||||
const partialProducts: string[] = [];
|
||||
let rowOffset = topOffset + 2;
|
||||
|
||||
for (let j = s2.length - 1; j >= 0; j--) {
|
||||
const d2 = parseInt(s2[j], 10);
|
||||
let carry = 0;
|
||||
let partial = "";
|
||||
const shift = s2.length - 1 - j;
|
||||
|
||||
const startFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
Object.keys(startFrame.cells).forEach((k) => {
|
||||
if (k.endsWith(",0") || k.endsWith(",1")) delete startFrame.cells[k];
|
||||
});
|
||||
|
||||
for (let i = s1.length - 1; i >= 0; i--) {
|
||||
const d1 = parseInt(s1[i], 10);
|
||||
const prod = d1 * d2 + carry;
|
||||
const nextCarry = Math.floor(prod / 10);
|
||||
const digit = prod % 10;
|
||||
const highlightCol = rightOffset - s1.length + 1 + i;
|
||||
const bottomCol = rightOffset - s2.length + 1 + j;
|
||||
|
||||
const stepFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
stepFrame.highlights = [
|
||||
{ x: highlightCol, y: topOffset, w: 1, h: 1 },
|
||||
{ x: bottomCol, y: topOffset + 1, w: 1, h: 1 },
|
||||
];
|
||||
let msg = `${d1} × ${d2}`;
|
||||
if (carry > 0) msg += ` + ${carry} (carry) = ${prod}.`;
|
||||
else msg += ` = ${prod}.`;
|
||||
if (nextCarry > 0) msg += ` Write ${digit}, carry ${nextCarry}.`;
|
||||
else msg += ` Write ${digit}.`;
|
||||
stepFrame.message = msg;
|
||||
if (carry > 0) {
|
||||
this.setCell(stepFrame, highlightCol, topOffset - 1, {
|
||||
text: carry.toString(),
|
||||
color: this.colors.carry,
|
||||
});
|
||||
}
|
||||
this.frames.push(stepFrame);
|
||||
|
||||
const writeFrame = this.cloneGrid(stepFrame);
|
||||
writeFrame.highlights = [];
|
||||
const writeCol = rightOffset - shift - (s1.length - 1 - i);
|
||||
this.setCell(writeFrame, writeCol, rowOffset, {
|
||||
text: digit.toString(),
|
||||
color: this.colors.result,
|
||||
});
|
||||
if (nextCarry > 0) {
|
||||
this.setCell(writeFrame, highlightCol - 1, topOffset - 1, {
|
||||
text: nextCarry.toString(),
|
||||
color: this.colors.carry,
|
||||
});
|
||||
}
|
||||
this.frames.push(writeFrame);
|
||||
|
||||
carry = nextCarry;
|
||||
partial = digit.toString() + partial;
|
||||
}
|
||||
|
||||
if (carry > 0) {
|
||||
const finalFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
const writeCol = rightOffset - shift - s1.length;
|
||||
this.setCell(finalFrame, writeCol, rowOffset, {
|
||||
text: carry.toString(),
|
||||
color: this.colors.result,
|
||||
});
|
||||
finalFrame.message = "Write final carry down.";
|
||||
this.frames.push(finalFrame);
|
||||
partial = carry.toString() + partial;
|
||||
}
|
||||
|
||||
if (shift > 0) {
|
||||
const padFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
for (let k = 0; k < shift; k++) {
|
||||
this.setCell(padFrame, rightOffset - k, rowOffset, {
|
||||
text: "0",
|
||||
color: this.colors.operator,
|
||||
});
|
||||
}
|
||||
padFrame.message = "Add placeholder zero(s).";
|
||||
this.frames.push(padFrame);
|
||||
partial = partial + "0".repeat(shift);
|
||||
}
|
||||
|
||||
partialProducts.push(partial);
|
||||
rowOffset++;
|
||||
}
|
||||
|
||||
if (partialProducts.length > 1) {
|
||||
const addSetupFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
addSetupFrame.lines.push({ y: rowOffset, startX: opCol, endX: rightOffset });
|
||||
this.setCell(addSetupFrame, opCol, rowOffset - 1, {
|
||||
text: "+",
|
||||
color: this.colors.operator,
|
||||
});
|
||||
addSetupFrame.message = "Add the partial products together.";
|
||||
Object.keys(addSetupFrame.cells).forEach((k) => {
|
||||
if (k.endsWith(`,${topOffset - 1}`)) delete addSetupFrame.cells[k];
|
||||
});
|
||||
this.frames.push(addSetupFrame);
|
||||
|
||||
let carry = 0;
|
||||
const maxP = Math.max(...partialProducts.map((p) => p.length));
|
||||
|
||||
for (let i = 0; i < maxP; i++) {
|
||||
let sum = carry;
|
||||
for (let j = 0; j < partialProducts.length; j++) {
|
||||
const pStr = partialProducts[j];
|
||||
if (i < pStr.length) sum += parseInt(pStr[pStr.length - 1 - i], 10);
|
||||
}
|
||||
const digit = sum % 10;
|
||||
const nextCarry = Math.floor(sum / 10);
|
||||
const col = rightOffset - i;
|
||||
const resFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
this.setCell(resFrame, col, rowOffset, {
|
||||
text: digit.toString(),
|
||||
color: this.colors.result,
|
||||
});
|
||||
let msg = `Sum column = ${sum}. Write ${digit}.`;
|
||||
if (nextCarry > 0) {
|
||||
this.setCell(resFrame, col - 1, topOffset + 1, {
|
||||
text: nextCarry.toString(),
|
||||
color: this.colors.carry,
|
||||
});
|
||||
msg += ` Carry ${nextCarry}.`;
|
||||
}
|
||||
resFrame.message = msg;
|
||||
this.frames.push(resFrame);
|
||||
carry = nextCarry;
|
||||
}
|
||||
|
||||
if (carry > 0) {
|
||||
const finalFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
this.setCell(finalFrame, rightOffset - maxP, rowOffset, {
|
||||
text: carry.toString(),
|
||||
color: this.colors.result,
|
||||
});
|
||||
finalFrame.message = "Write final addition carry.";
|
||||
this.frames.push(finalFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private generateDivision(s1: string, s2: string) {
|
||||
if (s2.length > 15) throw new Error("Divisor too large for visualizer (max 15 digits).");
|
||||
const div = parseInt(s2, 10);
|
||||
if (div === 0) throw new Error("Cannot divide by zero");
|
||||
if (BigInt(s1) === 0n) throw new Error("0 divided by anything is 0");
|
||||
|
||||
const frame = this.createEmptyFrame();
|
||||
const divLen = s2.length;
|
||||
const divOffset = 0;
|
||||
const divStart = divLen + 1;
|
||||
frame.message = `Setup long division: ${s1} ÷ ${s2}`;
|
||||
|
||||
for (let i = 0; i < divLen; i++) {
|
||||
this.setCell(frame, divOffset + i, 2, { text: s2[i], color: this.colors.operator });
|
||||
}
|
||||
this.setCell(frame, divLen, 2, { text: ")", color: this.colors.text });
|
||||
for (let i = 0; i < s1.length; i++) {
|
||||
this.setCell(frame, divStart + i, 2, { text: s1[i], color: this.colors.text });
|
||||
}
|
||||
frame.lines.push({ y: 1.5, startX: divStart - 0.2, endX: divStart + s1.length });
|
||||
this.frames.push(this.cloneGrid(frame));
|
||||
|
||||
const dividendArr = s1.split("");
|
||||
let currentVal = 0;
|
||||
let row = 3;
|
||||
let quotientStr = "";
|
||||
|
||||
for (let i = 0; i < dividendArr.length; i++) {
|
||||
const nextDigit = parseInt(dividendArr[i], 10);
|
||||
currentVal = currentVal * 10 + nextDigit;
|
||||
|
||||
const bringFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
if (row > 3) {
|
||||
this.setCell(bringFrame, divStart + i, row - 1, {
|
||||
text: nextDigit.toString(),
|
||||
color: this.colors.text,
|
||||
});
|
||||
bringFrame.message = `Bring down ${nextDigit}. Current value is ${currentVal}.`;
|
||||
bringFrame.highlights = [{ x: divStart + i, y: 2, w: 1, h: row - 2 }];
|
||||
} else {
|
||||
bringFrame.message = `Consider ${currentVal}.`;
|
||||
bringFrame.highlights = [{ x: divStart, y: 2, w: i + 1, h: 1 }];
|
||||
}
|
||||
this.frames.push(bringFrame);
|
||||
|
||||
if (currentVal >= div || i === dividendArr.length - 1 || quotientStr.length > 0) {
|
||||
const qDigit = Math.floor(currentVal / div);
|
||||
quotientStr += qDigit.toString();
|
||||
|
||||
const qFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
qFrame.highlights = [];
|
||||
this.setCell(qFrame, divStart + i, 0, {
|
||||
text: qDigit.toString(),
|
||||
color: this.colors.result,
|
||||
});
|
||||
qFrame.message = `${div} goes into ${currentVal} exactly ${qDigit} times.`;
|
||||
this.frames.push(qFrame);
|
||||
|
||||
if (qDigit > 0 || i === dividendArr.length - 1) {
|
||||
const subVal = qDigit * div;
|
||||
const subStr = subVal.toString();
|
||||
const subFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
this.setCell(subFrame, divStart + i - subStr.length, row, {
|
||||
text: "-",
|
||||
color: this.colors.operator,
|
||||
});
|
||||
for (let k = 0; k < subStr.length; k++) {
|
||||
this.setCell(subFrame, divStart + i - subStr.length + 1 + k, row, {
|
||||
text: subStr[k],
|
||||
color: this.colors.borrow,
|
||||
});
|
||||
}
|
||||
subFrame.lines.push({
|
||||
y: row + 1,
|
||||
startX: divStart + i - subStr.length,
|
||||
endX: divStart + i + 1,
|
||||
});
|
||||
subFrame.message = `Multiply ${qDigit} × ${div} = ${subVal}, then subtract.`;
|
||||
this.frames.push(subFrame);
|
||||
|
||||
currentVal = currentVal - subVal;
|
||||
row += 2;
|
||||
const resFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
|
||||
const currStr = currentVal.toString();
|
||||
for (let k = 0; k < currStr.length; k++) {
|
||||
this.setCell(resFrame, divStart + i - currStr.length + 1 + k, row - 1, {
|
||||
text: currStr[k],
|
||||
color: this.colors.text,
|
||||
});
|
||||
}
|
||||
resFrame.message = `Remainder is ${currentVal}.`;
|
||||
this.frames.push(resFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.refreshColors();
|
||||
if (this.frames.length === 0) {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
this.ctx.clearRect(0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = this.frames[this.currentFrame];
|
||||
const ctx = this.ctx;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
ctx.clearRect(0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
|
||||
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let minY = Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
Object.keys(frame.cells).forEach((key) => {
|
||||
const [x, y] = key.split(",").map(Number);
|
||||
minX = Math.min(minX, x);
|
||||
maxX = Math.max(maxX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxY = Math.max(maxY, y);
|
||||
});
|
||||
|
||||
if (!Number.isFinite(minX)) {
|
||||
this.updateUI();
|
||||
return;
|
||||
}
|
||||
|
||||
minX -= 1;
|
||||
maxX += 1;
|
||||
minY -= 1;
|
||||
maxY += 1;
|
||||
|
||||
const cols = maxX - minX + 1;
|
||||
const rows = maxY - minY + 1;
|
||||
const w = this.canvas.width / dpr;
|
||||
const h = this.canvas.height / dpr;
|
||||
const cellSize = Math.min(w / cols, h / rows, 60);
|
||||
const offsetX = (w - cols * cellSize) / 2 - minX * cellSize;
|
||||
const offsetY = (h - rows * cellSize) / 2 - minY * cellSize;
|
||||
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.textAlign = "center";
|
||||
|
||||
frame.highlights.forEach((hl) => {
|
||||
ctx.fillStyle = this.colors.highlight;
|
||||
ctx.fillRect(
|
||||
offsetX + hl.x * cellSize,
|
||||
offsetY + hl.y * cellSize,
|
||||
hl.w * cellSize,
|
||||
hl.h * cellSize
|
||||
);
|
||||
ctx.strokeStyle = this.colors.highlightBorder;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(
|
||||
offsetX + hl.x * cellSize,
|
||||
offsetY + hl.y * cellSize,
|
||||
hl.w * cellSize,
|
||||
hl.h * cellSize
|
||||
);
|
||||
});
|
||||
|
||||
ctx.strokeStyle = this.colors.line;
|
||||
ctx.lineWidth = 3;
|
||||
frame.lines.forEach((line) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(offsetX + line.startX * cellSize, offsetY + line.y * cellSize);
|
||||
ctx.lineTo(offsetX + line.endX * cellSize, offsetY + line.y * cellSize);
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
const mono = getComputedStyle(this.themeRoot).getPropertyValue("--mono").trim() || "monospace";
|
||||
ctx.font = `bold ${cellSize * 0.6}px ${mono}`;
|
||||
|
||||
Object.entries(frame.cells).forEach(([key, cell]) => {
|
||||
const [x, y] = key.split(",").map(Number);
|
||||
const cx = offsetX + x * cellSize + cellSize / 2;
|
||||
const cy = offsetY + y * cellSize + cellSize / 2;
|
||||
ctx.fillStyle = cell.color || this.colors.text;
|
||||
if (cell.text) ctx.fillText(cell.text, cx, cy);
|
||||
if (cell.crossedOut) {
|
||||
ctx.strokeStyle = this.colors.carry;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx - cellSize * 0.3, cy + cellSize * 0.3);
|
||||
ctx.lineTo(cx + cellSize * 0.3, cy - cellSize * 0.3);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
private updateUI() {
|
||||
if (this.frames.length > 0) {
|
||||
const msg = this.frames[this.currentFrame].message;
|
||||
this.onMessage?.(msg, false);
|
||||
this.onStep?.(this.currentFrame + 1, this.frames.length);
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.frames.length === 0) return;
|
||||
if (this.currentFrame >= this.frames.length - 1) this.currentFrame = 0;
|
||||
this.isPlaying = true;
|
||||
this.onPlayState?.(true);
|
||||
this.playInterval = setInterval(() => {
|
||||
if (this.currentFrame < this.frames.length - 1) this.step(1);
|
||||
else this.pause();
|
||||
}, this.speed);
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.isPlaying = false;
|
||||
this.onPlayState?.(false);
|
||||
if (this.playInterval) clearInterval(this.playInterval);
|
||||
this.playInterval = null;
|
||||
}
|
||||
|
||||
togglePlay() {
|
||||
if (this.isPlaying) this.pause();
|
||||
else this.play();
|
||||
}
|
||||
|
||||
step(dir: number) {
|
||||
const newFrame = this.currentFrame + dir;
|
||||
if (newFrame >= 0 && newFrame < this.frames.length) {
|
||||
this.currentFrame = newFrame;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
|
||||
import { MathVisualizer } from "./lib/math-visualizer";
|
||||
|
||||
type Op = "+" | "-" | "*" | "/";
|
||||
|
||||
const DEFAULT_NUMBERS = "27\n59";
|
||||
|
||||
function VerticalArithmeticWorkbench() {
|
||||
let rootRef!: HTMLElement;
|
||||
let canvasRef!: HTMLCanvasElement;
|
||||
let canvasWrapRef!: HTMLDivElement;
|
||||
let visualizer: MathVisualizer | undefined;
|
||||
|
||||
const [operation, setOperation] = createSignal<Op>("+");
|
||||
const [numbersInput, setNumbersInput] = createSignal(DEFAULT_NUMBERS);
|
||||
const [message, setMessage] = createSignal("Enter numbers and click Generate");
|
||||
const [isError, setIsError] = createSignal(false);
|
||||
const [stepCurrent, setStepCurrent] = createSignal(0);
|
||||
const [stepTotal, setStepTotal] = createSignal(0);
|
||||
const [playing, setPlaying] = createSignal(false);
|
||||
const [speed, setSpeed] = createSignal(800);
|
||||
|
||||
const progress = () =>
|
||||
stepTotal() > 0 ? (stepCurrent() / stepTotal()) * 100 : 0;
|
||||
|
||||
onMount(() => {
|
||||
visualizer = new MathVisualizer({
|
||||
canvas: canvasRef,
|
||||
wrapper: canvasWrapRef,
|
||||
themeRoot: rootRef,
|
||||
});
|
||||
visualizer.onMessage = (msg, err) => {
|
||||
setMessage(msg);
|
||||
setIsError(!!err);
|
||||
};
|
||||
visualizer.onStep = (current, total) => {
|
||||
setStepCurrent(current);
|
||||
setStepTotal(total);
|
||||
};
|
||||
visualizer.onPlayState = setPlaying;
|
||||
|
||||
const resize = () => visualizer?.resizeCanvas();
|
||||
resize();
|
||||
const ro = new ResizeObserver(resize);
|
||||
ro.observe(canvasWrapRef);
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
const themeObserver = new MutationObserver(() => {
|
||||
visualizer?.refreshColors();
|
||||
visualizer?.render();
|
||||
});
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme"],
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener("resize", resize);
|
||||
themeObserver.disconnect();
|
||||
visualizer?.destroy();
|
||||
});
|
||||
|
||||
setTimeout(() => generate(), 100);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
visualizer?.setOperation(operation());
|
||||
visualizer?.setSpeed(speed());
|
||||
});
|
||||
|
||||
const generate = () => {
|
||||
visualizer?.setOperation(operation());
|
||||
visualizer?.generateFromInput(numbersInput());
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={rootRef}
|
||||
class="math-widget va-workbench"
|
||||
aria-labelledby="va-workbench-title"
|
||||
>
|
||||
<h2 id="va-workbench-title" class="math-widget__title">
|
||||
Carry & borrow arithmetic visualizer
|
||||
</h2>
|
||||
|
||||
<div class="va-workbench__shell">
|
||||
<aside class="va-workbench__sidebar">
|
||||
<div class="va-workbench__brand">
|
||||
<span class="va-workbench__brand-icon" aria-hidden="true">
|
||||
±
|
||||
</span>
|
||||
<span class="va-workbench__brand-text">
|
||||
Arithmetic <strong>Visualizer</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="va-workbench__field">
|
||||
<span class="va-workbench__label">Operation</span>
|
||||
<div class="va-workbench__op-grid" role="group" aria-label="Operation">
|
||||
{(
|
||||
[
|
||||
["+", "Add"],
|
||||
["-", "Subtract"],
|
||||
["*", "Multiply"],
|
||||
["/", "Divide"],
|
||||
] as const
|
||||
).map(([op, label]) => (
|
||||
<button
|
||||
type="button"
|
||||
class="va-workbench__op-btn"
|
||||
classList={{ "va-workbench__op-btn--active": operation() === op }}
|
||||
aria-pressed={operation() === op}
|
||||
aria-label={label}
|
||||
onClick={() => setOperation(op)}
|
||||
>
|
||||
{op === "*" ? "×" : op === "/" ? "÷" : op}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="va-workbench__field">
|
||||
<label class="va-workbench__label" for="va-numbers">
|
||||
Numbers (one per line)
|
||||
</label>
|
||||
<textarea
|
||||
id="va-numbers"
|
||||
class="va-workbench__textarea"
|
||||
rows={4}
|
||||
value={numbersInput()}
|
||||
placeholder={"99\n99\n273"}
|
||||
onInput={(e) => setNumbersInput(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
generate();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<p class="va-workbench__hint">
|
||||
Multi-step operations run in order (e.g. subtract or divide each line from the
|
||||
result so far).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="va-workbench__generate" onClick={generate}>
|
||||
Generate visualization
|
||||
</button>
|
||||
|
||||
<div class="va-workbench__anim">
|
||||
<span class="va-workbench__label">Animation controls</span>
|
||||
<div class="va-workbench__transport">
|
||||
<button
|
||||
type="button"
|
||||
class="va-workbench__transport-btn"
|
||||
title="Previous step"
|
||||
aria-label="Previous step"
|
||||
onClick={() => visualizer?.step(-1)}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="va-workbench__play"
|
||||
title={playing() ? "Pause" : "Play"}
|
||||
onClick={() => visualizer?.togglePlay()}
|
||||
>
|
||||
{playing() ? "Pause" : "Play"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="va-workbench__transport-btn"
|
||||
title="Next step"
|
||||
aria-label="Next step"
|
||||
onClick={() => visualizer?.step(1)}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
<div class="va-workbench__speed">
|
||||
<span>Slow</span>
|
||||
<input
|
||||
type="range"
|
||||
class="va-workbench__speed-slider"
|
||||
min={200}
|
||||
max={2000}
|
||||
step={100}
|
||||
value={speed()}
|
||||
dir="rtl"
|
||||
aria-label="Animation speed"
|
||||
onInput={(e) => setSpeed(Number(e.currentTarget.value))}
|
||||
/>
|
||||
<span>Fast</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="va-workbench__footer-note">
|
||||
Max 8 digits recommended for best visualization.
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
<div class="va-workbench__stage">
|
||||
<div
|
||||
class="va-workbench__message-box"
|
||||
classList={{ "va-workbench__message-box--error": isError() }}
|
||||
>
|
||||
<p class="va-workbench__message" aria-live="polite">
|
||||
{message()}
|
||||
</p>
|
||||
<div class="va-workbench__progress-track">
|
||||
<div
|
||||
class="va-workbench__progress-fill"
|
||||
style={{ width: `${progress()}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p class="va-workbench__step-counter">
|
||||
Step {stepCurrent()} / {stepTotal()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div ref={canvasWrapRef} class="va-workbench__canvas-wrap">
|
||||
<canvas ref={canvasRef} class="va-workbench__canvas" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default VerticalArithmeticWorkbench;
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
"subtitle": "Browse chapters by subject and curriculum",
|
||||
"curriculums": ["Foundation", "DSE", "IB", "A-Level", "AP", "IGCSE"],
|
||||
"subjects": [
|
||||
{ "id": "math", "name": "Mathematics" },
|
||||
{ "id": "math", "name": "Mathematics", "banner": "/catalog/banners/math.webp" },
|
||||
{ "id": "science", "name": "Science" },
|
||||
{ "id": "humanities", "name": "Humanities" },
|
||||
{ "id": "computing", "name": "Computing" }
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export type CatalogGraph = {
|
|||
export type CatalogSubject = {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Root-relative URL, e.g. /catalog/banners/math.webp */
|
||||
banner?: string;
|
||||
strands: CatalogStrand[];
|
||||
graph?: CatalogGraph;
|
||||
};
|
||||
|
|
|
|||
142
data/faq.json
Normal file
142
data/faq.json
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
{
|
||||
"intro": "Quick answers about Open Free Books — free textbooks for learners worldwide, how we work with different syllabi, and how to use the catalog.",
|
||||
"items": [
|
||||
{
|
||||
"id": "what-is-ofb",
|
||||
"type": "standard",
|
||||
"question": "What is Open Free Books?",
|
||||
"answer": [
|
||||
"Open Free Books aims to be more than a learning resource: free textbooks for everyone worldwide, from elementary school through university — on the web, at no cost.",
|
||||
"We aim to cater to different syllabi and curricula across the globe. It is a community effort by educators and contributors around the world, built in the open on GitHub.",
|
||||
"Free means the whole book for everyone: read online without paywalls or premium unlocks. Open means anyone can fix a typo, add a chapter, or fork a book for a classroom."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "how-different",
|
||||
"type": "comparison",
|
||||
"question": "How is Open Free Books different from GeoGebra, Math is Fun, and Wikipedia?",
|
||||
"intro": "We respect these projects — they solve different problems. Open Free Books is a free, open-source textbook library on the web.",
|
||||
"peers": [
|
||||
{
|
||||
"name": "GeoGebra",
|
||||
"url": "https://www.geogebra.org/",
|
||||
"role": "Interactive geometry and algebra tools, activities, and classroom apps.",
|
||||
"contrast": "Open Free Books publishes structured textbook chapters and learning paths, not a general-purpose construction sandbox."
|
||||
},
|
||||
{
|
||||
"name": "Math is Fun",
|
||||
"url": "https://www.mathsisfun.com/",
|
||||
"role": "Friendly reference pages, games, and explanations across many topics.",
|
||||
"contrast": "Open Free Books is curriculum-shaped: ordered chapters, prerequisites in the catalog map, and authored books — not a grab-bag of articles."
|
||||
},
|
||||
{
|
||||
"name": "Wikipedia",
|
||||
"url": "https://www.wikipedia.org/",
|
||||
"role": "Encyclopedic, community-edited reference on almost everything.",
|
||||
"contrast": "Not a wiki-style fact pile. We publish opinionated paths from prerequisite to mastery — structure on purpose, not everything about X."
|
||||
}
|
||||
],
|
||||
"ofb": "Free forever, no ads, no commercial upsell, and open on GitHub for anyone to fix or extend."
|
||||
},
|
||||
{
|
||||
"id": "free-forever",
|
||||
"type": "standard",
|
||||
"question": "Is it really free? Will there be ads or paid tiers?",
|
||||
"answer": [
|
||||
"Yes — free is the product, not a trial. Every book and chapter is readable without a subscription or premium unlock.",
|
||||
"Open Free Books offers no commercial services and no ads, now or in the future. Sustainability comes from community contribution and shared open infrastructure, not from turning learners into the product."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "open-github",
|
||||
"type": "standard",
|
||||
"question": "What does \"open\" mean? How do I contribute?",
|
||||
"answer": [
|
||||
"Open means the textbooks are built in public on GitHub. You can read the source, open a pull request, fork for your syllabus, and see every change in version control.",
|
||||
"To add or improve material, start with the contributing guide on this site or open a discussion or pull request on GitHub. You do not need permission from a publisher."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "vs-commercial",
|
||||
"type": "standard",
|
||||
"question": "How is this different from commercial textbook platforms?",
|
||||
"answer": [
|
||||
"Many commercial platforms sell access, host ads, or bundle paid services around \"digital\" textbooks. Content may be locked behind logins, and terms can change when a vendor updates its product.",
|
||||
"Open Free Books is non-commercial: no paywalls, no ad network, no investor-driven upsell. Books are static HTML in an open repository — readable in a browser today, forkable for your classroom tomorrow."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "not-wiki",
|
||||
"type": "standard",
|
||||
"question": "Is this a wiki?",
|
||||
"answer": [
|
||||
"No. Wikis excel at scattered facts across many topics; they do not usually carry a learner from prerequisite to mastery in one opinionated path.",
|
||||
"We publish ordered chapters, assessed skills, and a catalog map of what to study next — curated structure, not an everything-about-X article farm."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "not-replace-teachers",
|
||||
"type": "standard",
|
||||
"question": "Does Open Free Books replace teachers?",
|
||||
"answer": [
|
||||
"No. A great teacher still sets pace, asks hard questions, and notices when a student is stuck. That relationship is not what we are trying to automate.",
|
||||
"Textbooks are reference and structure: explanations, exercises, diagrams. Those can be free, forkable, and kept current on the web so teachers spend less time hunting permissions and more time teaching."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "why-html",
|
||||
"type": "standard",
|
||||
"question": "Why web pages instead of PDFs?",
|
||||
"answer": [
|
||||
"Paper and most \"digital\" textbooks are fixed layouts — hard to search, hard to update, and poor homes for live diagrams or interactives.",
|
||||
"HTML on the open web can link to sources, resize for any screen, embed simulations, and improve over time. The book becomes something you use, not a file you download once."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ai-content",
|
||||
"type": "standard",
|
||||
"question": "Is the content AI-generated?",
|
||||
"answer": [
|
||||
"No. Chapters are authored HTML with care: figures, interactives, and maps where they help understanding — not bulk-generated slop or print PDFs pasted online.",
|
||||
"Core curriculum changes slowly; one carefully reviewed book, shared openly, beats regenerating the same chapter thousands of times in a data center."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "subjects",
|
||||
"type": "standard",
|
||||
"question": "What subjects are available?",
|
||||
"answer": [
|
||||
"Mathematics is live today — browse strands and chapters in the catalog. Science, humanities, and computing are on the roadmap.",
|
||||
"New subjects and chapters appear as contributors publish them on GitHub. Planned chapters show in the catalog before a full page exists."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "catalog-curriculum",
|
||||
"type": "standard",
|
||||
"question": "What are curriculum labels on the catalog?",
|
||||
"answer": [
|
||||
"Each subject in the catalog has a set of curricula you can filter by — exam boards, school programs, and similar labels — so you can see which chapters fit your syllabus.",
|
||||
"We try to keep chapter structure and prerequisite maps compatible with as many curricula as possible. We do our best, but coverage is not perfect; the catalog will grow and improve as more people contribute.",
|
||||
"Those labels appear only in the catalog (badges and filters), not inside chapter pages. You choose what applies to you on the catalog; inside a chapter, the focus stays on the topic itself."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "find-chapters",
|
||||
"type": "standard",
|
||||
"question": "How do I find a chapter?",
|
||||
"answer": [
|
||||
"Open the catalog for a subject — list view by default, or map view to follow prerequisites. Use curriculum filters if you need to narrow the list.",
|
||||
"Press ⌘K or Ctrl+K (or open Search in the header) to search live chapter pages. The dedicated search page indexes chapter content, not every site page."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "about-manifesto",
|
||||
"type": "standard",
|
||||
"question": "Where are the project principles?",
|
||||
"answer": [
|
||||
"The About page (Manifesto) lists the values we will not trade away as the catalog grows: free, open, curriculum-shaped, independent, durable, crafted, inspectable, and web-native.",
|
||||
"Read the manifesto for the full principles behind every book on this site."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
"graph": {
|
||||
"edges": [
|
||||
{ "from": "counting-and-numbers", "to": "basic-math-notation" },
|
||||
{ "from": "basic-math-notation", "to": "directed-numbers-rates" },
|
||||
{ "from": "basic-math-notation", "to": "properties-of-operations" },
|
||||
{ "from": "properties-of-operations", "to": "written-calculation" },
|
||||
{ "from": "written-calculation", "to": "directed-numbers-rates" },
|
||||
{ "from": "directed-numbers-rates", "to": "polynomials-expansion-factorisation" },
|
||||
{ "from": "polynomials-expansion-factorisation", "to": "algebraic-fractions-formulas" },
|
||||
{ "from": "polynomials-expansion-factorisation", "to": "linear-equations-two-unknowns" },
|
||||
|
|
@ -65,6 +67,20 @@
|
|||
"status": "live",
|
||||
"curriculums": ["Foundation"]
|
||||
},
|
||||
{
|
||||
"slug": "properties-of-operations",
|
||||
"title": "Properties of operations",
|
||||
"description": "See why order, grouping, and distribution can change how you write a sum or product without changing the result — with interactive area and dot models.",
|
||||
"status": "live",
|
||||
"curriculums": ["Foundation"]
|
||||
},
|
||||
{
|
||||
"slug": "written-calculation",
|
||||
"title": "Written calculation",
|
||||
"description": "Add, subtract, multiply, and divide whole numbers using vertical written form: align columns, regroup, and read the result.",
|
||||
"status": "live",
|
||||
"curriculums": ["Foundation"]
|
||||
},
|
||||
{
|
||||
"slug": "directed-numbers-rates",
|
||||
"title": "Directed numbers, rates & ratios",
|
||||
|
|
|
|||
|
|
@ -153,7 +153,21 @@ export function CatalogApp() {
|
|||
</aside>
|
||||
|
||||
<div class="catalog-main">
|
||||
<header class="catalog-header">
|
||||
<header
|
||||
class="catalog-header"
|
||||
classList={{ "catalog-header--has-banner": Boolean(currentSubject().banner) }}
|
||||
>
|
||||
<Show when={currentSubject().banner}>
|
||||
<div class="catalog-header__banner" aria-hidden="true">
|
||||
<img
|
||||
src={currentSubject().banner!}
|
||||
alt=""
|
||||
width={1400}
|
||||
height={700}
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="catalog-header__head">
|
||||
<div class="catalog-header__intro">
|
||||
<h1 class="catalog-header__title">{currentSubject().name}</h1>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function readConfig() {
|
|||
homeUrl: "/",
|
||||
browseUrl: "/catalog/",
|
||||
aboutUrl: "/about/",
|
||||
faqUrl: "/faq/",
|
||||
searchUrl: "/search/",
|
||||
contributingUrl: "/contributing/",
|
||||
githubUrl: "https://github.com/openfreebooks/openfreebooks",
|
||||
|
|
@ -23,6 +24,7 @@ function readConfig() {
|
|||
homeUrl: string;
|
||||
browseUrl: string;
|
||||
aboutUrl: string;
|
||||
faqUrl: string;
|
||||
searchUrl: string;
|
||||
contributingUrl: string;
|
||||
githubUrl: string;
|
||||
|
|
@ -86,6 +88,7 @@ const navLinks = [
|
|||
{ label: "Browse", href: config.browseUrl },
|
||||
{ label: "Search", href: config.searchUrl },
|
||||
{ label: "About", href: config.aboutUrl },
|
||||
{ label: "FAQ", href: config.faqUrl },
|
||||
{ label: "GitHub", href: config.githubUrl },
|
||||
];
|
||||
|
||||
|
|
|
|||
BIN
math-banner.jpg
Normal file
BIN
math-banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 360 KiB |
BIN
static/catalog/banners/math.webp
Normal file
BIN
static/catalog/banners/math.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
|
|
@ -162,6 +162,73 @@
|
|||
z-index: 10;
|
||||
}
|
||||
|
||||
.catalog-header--has-banner {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.catalog-header--has-banner .catalog-header__head,
|
||||
.catalog-header--has-banner .catalog-header__filters {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.catalog-header--has-banner .catalog-header__head {
|
||||
margin-top: -3rem;
|
||||
}
|
||||
|
||||
.catalog-header--has-banner .catalog-header__filters {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.catalog-header__banner {
|
||||
position: relative;
|
||||
width: calc(100% + 64px);
|
||||
max-height: 220px;
|
||||
margin: 0 -32px;
|
||||
aspect-ratio: 2 / 1;
|
||||
overflow: hidden;
|
||||
background: var(--card-bg);
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
#000 0%,
|
||||
#000 38%,
|
||||
rgb(0 0 0 / 0.55) 72%,
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
#000 0%,
|
||||
#000 38%,
|
||||
rgb(0 0 0 / 0.55) 72%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
transparent 58%,
|
||||
color-mix(in srgb, var(--card-bg) 45%, transparent) 80%,
|
||||
var(--card-bg) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-header__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -907,6 +974,22 @@ a.catalog-chapter-card {
|
|||
gap: 18px;
|
||||
}
|
||||
|
||||
.catalog-header--has-banner {
|
||||
padding: 0 0 16px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.catalog-header--has-banner .catalog-header__head,
|
||||
.catalog-header--has-banner .catalog-header__filters {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.catalog-header__banner {
|
||||
width: calc(100% + 40px);
|
||||
margin: 0 -20px;
|
||||
}
|
||||
|
||||
.catalog-header__head {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
164
themes/openfreebooks/sass/_faq.scss
Normal file
164
themes/openfreebooks/sass/_faq.scss
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
.faq-page {
|
||||
padding: 54px 0 72px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.faq-page__title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.faq-page__intro {
|
||||
margin: 0 0 32px;
|
||||
max-width: var(--prose-width);
|
||||
color: var(--soft-ink);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.72;
|
||||
}
|
||||
|
||||
.faq-jump {
|
||||
margin: 0 0 40px;
|
||||
padding: 20px 24px;
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 12px;
|
||||
background: var(--card-bg);
|
||||
box-shadow: 0 10px 28px var(--shadow-soft);
|
||||
}
|
||||
|
||||
.faq-jump__label {
|
||||
margin: 0 0 12px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.faq-jump__list {
|
||||
margin: 0;
|
||||
padding: 0 0 0 1.25rem;
|
||||
max-width: var(--prose-width);
|
||||
color: var(--soft-ink);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.faq-jump__link {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.faq-jump__link:hover,
|
||||
.faq-jump__link:focus-visible {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.faq-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
max-width: var(--prose-width);
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
scroll-margin-top: 5rem;
|
||||
}
|
||||
|
||||
.faq-item__question {
|
||||
margin: 0 0 14px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: clamp(1.2rem, 2vw, 1.45rem);
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.faq-item__answer {
|
||||
color: var(--soft-ink);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.72;
|
||||
}
|
||||
|
||||
.faq-item__answer p {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.faq-item__answer p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.faq-item__links {
|
||||
margin-top: 1rem !important;
|
||||
}
|
||||
|
||||
.faq-item__links a {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.faq-item__links a:hover,
|
||||
.faq-item__links a:focus-visible {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.faq-compare__peers {
|
||||
margin: 20px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.faq-compare__card {
|
||||
padding: 18px 20px;
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 12px;
|
||||
background: var(--card-bg);
|
||||
box-shadow: 0 10px 28px var(--shadow-soft);
|
||||
}
|
||||
|
||||
.faq-compare__name {
|
||||
margin: 0 0 8px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.faq-compare__name a {
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.faq-compare__name a:hover,
|
||||
.faq-compare__name a:focus-visible {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.faq-compare__role {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.faq-compare__contrast {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.faq-compare__ofb {
|
||||
margin: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.faq-page__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
@import "credits";
|
||||
@import "contributing";
|
||||
@import "about-manifest";
|
||||
@import "faq";
|
||||
@import "book";
|
||||
@import "catalog";
|
||||
@import "search";
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"homeUrl": {{ get_url(path="/") | json_encode() | safe }},
|
||||
"browseUrl": {{ config.extra.browse_url | json_encode() | safe }},
|
||||
"aboutUrl": {{ config.extra.about_url | json_encode() | safe }},
|
||||
"faqUrl": {{ config.extra.faq_url | json_encode() | safe }},
|
||||
"searchUrl": {{ config.extra.search_url | json_encode() | safe }},
|
||||
"githubUrl": {{ config.extra.github_url | json_encode() | safe }}
|
||||
}
|
||||
|
|
@ -56,6 +57,7 @@
|
|||
<span class="footer-copy">© <span id="footer-year"></span> {{ config.extra.site_title }} — Free, open textbooks for every learner</span>
|
||||
<nav class="footer-links" aria-label="Footer links">
|
||||
<a class="footer-link" href="{{ config.extra.about_url | safe }}">About</a>
|
||||
<a class="footer-link" href="{{ config.extra.faq_url | safe }}">FAQ</a>
|
||||
<a class="footer-link" href="{{ config.extra.contributing_url | safe }}">Contribute</a>
|
||||
<a class="footer-link" href="{{ config.extra.credits_url | safe }}">Credits</a>
|
||||
<a class="footer-link" href="{{ config.extra.github_url | safe }}">GitHub</a>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
{% for subject in catalog.subjects %}
|
||||
{
|
||||
"id": {{ subject.id | json_encode() | safe }},
|
||||
"name": {{ subject.name | json_encode() | safe }},
|
||||
"name": {{ subject.name | json_encode() | safe }}{% if subject.banner %},
|
||||
"banner": {{ subject.banner | json_encode() | safe }}{% endif %},
|
||||
"strands": {% if subject.id == "math" %}{{ math_catalog.strands | json_encode() | safe }}{% else %}[]{% endif %}{% if subject.id == "math" and math_catalog.graph %},
|
||||
"graph": {{ math_catalog.graph | json_encode() | safe }}{% endif %}
|
||||
}{% if not loop.last %},{% endif %}
|
||||
|
|
|
|||
87
themes/openfreebooks/templates/faq.html
Normal file
87
themes/openfreebooks/templates/faq.html
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ page.title }} | {{ config.extra.site_title }}{% endblock title %}
|
||||
{% block meta_description %}
|
||||
<meta name="description" content="Frequently asked questions about {{ config.extra.site_title }} — free, open textbooks, how we differ from other sites, and how to use the catalog." />
|
||||
{% endblock meta_description %}
|
||||
|
||||
{% block content %}
|
||||
{% set faq = load_data(path="data/faq.json", format="json") %}
|
||||
<section class="faq-page">
|
||||
<div class="container">
|
||||
<p class="section-kicker">Help</p>
|
||||
<h1 class="section-title faq-page__title">{{ page.title }}</h1>
|
||||
<p class="faq-page__intro">{{ faq.intro }}</p>
|
||||
|
||||
<nav class="faq-jump" aria-label="On this page">
|
||||
<p class="faq-jump__label">On this page</p>
|
||||
<ol class="faq-jump__list">
|
||||
{% for item in faq.items %}
|
||||
<li>
|
||||
<a class="faq-jump__link" href="#faq-{{ item.id }}">{{ item.question }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="faq-list">
|
||||
{% for item in faq.items %}
|
||||
<article class="faq-item" id="faq-{{ item.id }}">
|
||||
<h2 class="faq-item__question">{{ item.question }}</h2>
|
||||
|
||||
{% if item.type == "comparison" %}
|
||||
<div class="faq-item__answer faq-compare">
|
||||
<p>{{ item.intro }}</p>
|
||||
<ul class="faq-compare__peers">
|
||||
{% for peer in item.peers %}
|
||||
<li class="faq-compare__card">
|
||||
<h3 class="faq-compare__name">
|
||||
<a href="{{ peer.url | safe }}" rel="noopener noreferrer">{{ peer.name }}</a>
|
||||
</h3>
|
||||
<p class="faq-compare__role">{{ peer.role }}</p>
|
||||
<p class="faq-compare__contrast">{{ peer.contrast }}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="faq-compare__ofb">
|
||||
<strong>{{ config.extra.site_title }}</strong> — {{ item.ofb }}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="faq-item__answer">
|
||||
{% for paragraph in item.answer %}
|
||||
<p>{{ paragraph }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% if item.id == "open-github" %}
|
||||
<p class="faq-item__links">
|
||||
<a href="{{ config.extra.contributing_url | safe }}">Contributing guide</a>
|
||||
·
|
||||
<a href="{{ config.extra.github_url | safe }}">GitHub repository</a>
|
||||
</p>
|
||||
{% elif item.id == "about-manifesto" %}
|
||||
<p class="faq-item__links">
|
||||
<a href="{{ config.extra.about_url | safe }}">Read the manifesto</a>
|
||||
</p>
|
||||
{% elif item.id == "subjects" or item.id == "find-chapters" or item.id == "catalog-curriculum" %}
|
||||
<p class="faq-item__links">
|
||||
<a href="{{ config.extra.browse_url | safe }}">Browse the catalog</a>
|
||||
{% if item.id == "find-chapters" %}
|
||||
·
|
||||
<a href="{{ config.extra.search_url | safe }}">Search chapters</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="faq-page__actions">
|
||||
<a class="button button-primary" href="{{ config.extra.browse_url | safe }}">Browse books</a>
|
||||
<a class="button button-secondary" href="{{ config.extra.contributing_url | safe }}">Contribute</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
|
@ -23,6 +23,10 @@
|
|||
<span class="contributing-quick-nav__label">Chapter graph</span>
|
||||
<span class="contributing-quick-nav__text">Branches & prerequisites on the map</span>
|
||||
</a>
|
||||
<a class="contributing-quick-nav__card" href="#catalog-banner">
|
||||
<span class="contributing-quick-nav__label">Catalog banner</span>
|
||||
<span class="contributing-quick-nav__text">Optional subject header image</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<section id="overview" class="contributing-section">
|
||||
|
|
@ -133,6 +137,29 @@ bun run build # production build</code></pre>
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section id="catalog-banner" class="contributing-section">
|
||||
<h2 class="contributing-section__title">Catalog subject banner</h2>
|
||||
<div class="contributing-prose">
|
||||
<p>
|
||||
Optional image at the top of the catalog header for a subject (List and Map). Subjects without
|
||||
<code>banner</code> in <code>data/catalog.json</code> stay plain.
|
||||
</p>
|
||||
<ol>
|
||||
<li>Prepare a <strong>2:1</strong> image (e.g. 2000×1000).</li>
|
||||
<li>
|
||||
Optimize to WebP in <code>static/catalog/banners/{subject-id}.webp</code> — use
|
||||
<code>ffmpeg</code> with <code>libwebp</code>, or <code>cwebp -resize 1400 0 -q 82 …</code>
|
||||
if ffmpeg lacks WebP support.
|
||||
</li>
|
||||
<li>
|
||||
Add <code>"banner": "/catalog/banners/{subject-id}.webp"</code> on the subject in
|
||||
<code>data/catalog.json</code>.
|
||||
</li>
|
||||
<li>Verify at <code>/catalog/?subject=…</code> after <code>bun run build:js && zola serve</code>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="contributing-section contributing-section--cta">
|
||||
<h2 class="contributing-section__title">Ready to open a pull request?</h2>
|
||||
<p class="contributing-page__lead">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@
|
|||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["frontend/src"],
|
||||
"exclude": ["frontend/src/**/*.test.ts"]
|
||||
"include": ["frontend/src", "content/**/widgets/**/*.ts", "content/**/widgets/**/*.tsx"],
|
||||
"exclude": ["frontend/src/**/*.test.ts", "content/**/widgets/**/*.test.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ site_title = "Open Free Books"
|
|||
github_url = "https://github.com/hananoshikayomaru/openfreebooks"
|
||||
browse_url = "/catalog/"
|
||||
about_url = "/about/"
|
||||
faq_url = "/faq/"
|
||||
credits_url = "/credits/"
|
||||
contributing_url = "/contributing/"
|
||||
search_url = "/search/"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue