diff --git a/.agents/skills/zola/reference.md b/.agents/skills/zola/reference.md index 08d71fd..217385c 100644 --- a/.agents/skills/zola/reference.md +++ b/.agents/skills/zola/reference.md @@ -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/" ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6e1b57..e7ca932 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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`. diff --git a/content/faq.md b/content/faq.md new file mode 100644 index 0000000..cc04d88 --- /dev/null +++ b/content/faq.md @@ -0,0 +1,4 @@ ++++ +title = "FAQ" +template = "faq.html" ++++ diff --git a/content/math/directed-numbers-rates/_index.md b/content/math/directed-numbers-rates/_index.md index 8088db2..a472e31 100644 --- a/content/math/directed-numbers-rates/_index.md +++ b/content/math/directed-numbers-rates/_index.md @@ -11,8 +11,9 @@ strand = "Number & Algebra"

- You already know how to count and work with whole numbers from 0 to - 10. On a number line, those values lie on or to the right of zero. + You can already add, subtract, multiply, and divide whole numbers using written + calculation (columns, carry, and borrow). On a number line, those values lie on or to the + right of zero. Numbers can also lie to the left of zero — we call them negative. Temperatures below freezing, bank overdrafts, and positions below sea level are everyday examples of directed numbers. diff --git a/content/math/directed-numbers-rates/core.html b/content/math/directed-numbers-rates/core.html index 94da8b4..e7184a7 100644 --- a/content/math/directed-numbers-rates/core.html +++ b/content/math/directed-numbers-rates/core.html @@ -1,7 +1,8 @@

- You already know how to count and work with whole numbers from 0 to - 10. On a number line, those values lie on or to the right of zero. + You can already add, subtract, multiply, and divide whole numbers using written + calculation (columns, carry, and borrow). On a number line, those values lie on or to the + right of zero. Numbers can also lie to the left of zero — we call them negative. Temperatures below freezing, bank overdrafts, and positions below sea level are everyday examples of directed numbers. diff --git a/content/math/properties-of-operations/_index.md b/content/math/properties-of-operations/_index.md new file mode 100644 index 0000000..45455d3 --- /dev/null +++ b/content/math/properties-of-operations/_index.md @@ -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" ++++ + +

+

+ You can already read number sentences from basic math notation and combine + numbers from counting and numbers. This chapter names three patterns that keep + showing up: rules about order, grouping, and + splitting a sum before you multiply. They do not replace careful arithmetic — + they explain why different written forms can still mean the same amount. +

+ +
+

Three properties

+
    +
  • Commutative — swap the order (for + and ×).
  • +
  • Associative — regroup with brackets (for + and ×).
  • +
  • Distributive — multiply each part of a sum (× over +).
  • +
+
+ +

Same story, three rules

+

+ Imagine three friends. Friend A gets \$2 each day for 3 days and + \$2 each day for 5 more days. You can count the money in + different ways: +

+
+ Distribute + \(2(3 + 5) = (2 \times 3) + (2 \times 5) = 6 + 10 = 16\) +
+
+ Commute + \(3 + 5 = 5 + 3 = 8\) days in either order before you multiply by \$2 per day in a lump sum. +
+
+ Associate + \((2 + 3) + 5 = 2 + (3 + 5) = 10\) when you group which daily amounts you add first. +
+

+ Distribute is about multiplying through a bracket. + Commute is about order. + Associate is about which pair you combine first when there are three or more + numbers. +

+ +

Commutative property

+

+ Commute means to move or swap. For addition and + multiplication of whole numbers, changing the order does not change the result. +

+
+ \[ a + b = b + a \qquad ab = ba \] +
+

+ Example: \(3 + 5 = 5 + 3 = 8\). Three dots then five dots is the same total as five dots then + three dots. +

+

+ Subtraction and division are not commutative. \(5 - 3 \neq 3 - 5\) and + \(6 \div 2 \neq 2 \div 6\). Order matters there. +

+
+ +

Associative property

+

+ When you add or multiply three or more numbers, brackets tell you which pair to + do first. The associative property says different groupings give the same answer + (for + and × only). +

+
+ \[ (a + b) + c = a + (b + c) \qquad (ab)c = a(bc) \] +
+

+ Example: \((2 + 3) + 5 = 5 + 5 = 10\) and \(2 + (3 + 5) = 2 + 8 = 10\). You may combine + \(2 + 3\) first or \(3 + 5\) first. +

+

+ Again, subtraction and division are not associative. \((10 - 4) - 2 \neq + 10 - (4 - 2)\). +

+
+ +

Distributive property

+

+ The distributive property links multiplication to addition. Multiplying a sum + is the same as multiplying each addend and then adding — “distribute” the multiply across the + bracket. +

+
+ \[ a(b + c) = ab + ac \] +
+

+ 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. +

+

+ Later, in written calculation, partial products in multiplication use this idea: + you multiply by each digit of the bottom number separately, then add the rows. +

+
+ +

Which property is it?

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
You change…PropertyExample
Order of two numbersCommutative\(4 + 7 = 7 + 4\)
Brackets with three numbersAssociative\((1 + 2) + 3 = 1 + (2 + 3)\)
Multiply through a sumDistributive\(3(2 + 4) = 3 \times 2 + 3 \times 4\)
+
+ +

What comes next

+

+ In written calculation 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. +

+
+ +
+

History

+

+ 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 commutative, associative, and + distributive became standard in European algebra books in the 1800s, when + school math settled on one notation for all. +

+

+ 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. +

+ +

Checkpoints

+
    +
  1. + Which property lets you write \(7 + 4\) as \(4 + 7\) without changing the total? +
  2. +
  3. + Is \((5 \times 2) \times 3\) the same as \(5 \times (2 \times 3)\)? Which property says so? +
  4. +
  5. + Rewrite \(4(6 + 1)\) as a sum of two products. +
  6. +
  7. + Why is \(8 - 3\) not commutative? Give two different answers for \(3 - 8\) and \(8 - 3\). +
  8. +
  9. + A rectangle is 5 units tall and \((2 + 3)\) units wide. How does the distributive property + describe its area? +
  10. +
+ +

Applications

+

Mental math

+

+ 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\). +

+

Shopping and sharing

+

+ 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). +

+

Later algebra

+

+ Expanding \(a(b + c)\) in polynomials and factorising common factors both rely on the + distributive property. You will meet them again in polynomials: expansion & + factorisation. +

+ +

Question bank

+ +

Easy

+ +
+ + Fill in the blank using a property: \(9 + 6 = 6 + \underline{\hspace{1.5em}}\). + +
+

Answer: \(9\) — commutative property of addition.

+
+
+ +
+ Compute: \(2(4 + 3)\). +
+

+ Answer: \(14\). Either \(2 \times 7 = 14\), or \(2 \times 4 + 2 \times 3 = 8 + + 6 = 14\) (distributive). +

+
+
+ +

Intermediate

+ +
+ + Evaluate both sides and compare: \((3 + 4) + 5\) and \(3 + (4 + 5)\). + +
+

Answer: Both equal \(12\) — associative property of addition.

+
+
+ +
+ Use distribution to find \(5 \times 102\). +
+

Answer: \(510\). Steps: \(5 \times (100 + 2) = 500 + 10 = 510\).

+
+
+ +

Difficult

+ +
+ + Explain why \(12 \div (3 \div 2)\) is not the same as \((12 \div 3) \div 2\). + +
+

+ Answer: Left: \(12 \div 1.5 = 8\). Right: \(4 \div 2 = 2\). Division is not + associative; grouping changes the meaning. +

+
+
+ +
+ + A garden bed is 4 m wide and \((2 + 3)\) m long. Write two ways to find its area and show they + match. + +
+

+ Answer: \(20\) m². One rectangle: \(4 \times 5 = 20\). Split length: + \(4 \times 2 + 4 \times 3 = 8 + 12 = 20\) (distributive). +

+
+
+
diff --git a/content/math/properties-of-operations/chapter.scss b/content/math/properties-of-operations/chapter.scss new file mode 100644 index 0000000..2ea54a1 --- /dev/null +++ b/content/math/properties-of-operations/chapter.scss @@ -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; +} diff --git a/content/math/properties-of-operations/core.html b/content/math/properties-of-operations/core.html new file mode 100644 index 0000000..c28a325 --- /dev/null +++ b/content/math/properties-of-operations/core.html @@ -0,0 +1,137 @@ +
+

+ You can already read number sentences from basic math notation and combine + numbers from counting and numbers. This chapter names three patterns that keep + showing up: rules about order, grouping, and + splitting a sum before you multiply. They do not replace careful arithmetic — + they explain why different written forms can still mean the same amount. +

+ +
+

Three properties

+
    +
  • Commutative — swap the order (for + and ×).
  • +
  • Associative — regroup with brackets (for + and ×).
  • +
  • Distributive — multiply each part of a sum (× over +).
  • +
+
+ +

Same story, three rules

+

+ Imagine three friends. Friend A gets \$2 each day for 3 days and + \$2 each day for 5 more days. You can count the money in + different ways: +

+
+ Distribute + \(2(3 + 5) = (2 \times 3) + (2 \times 5) = 6 + 10 = 16\) +
+
+ Commute + \(3 + 5 = 5 + 3 = 8\) days in either order before you multiply by \$2 per day in a lump sum. +
+
+ Associate + \((2 + 3) + 5 = 2 + (3 + 5) = 10\) when you group which daily amounts you add first. +
+

+ Distribute is about multiplying through a bracket. + Commute is about order. + Associate is about which pair you combine first when there are three or more + numbers. +

+ +

Commutative property

+

+ Commute means to move or swap. For addition and + multiplication of whole numbers, changing the order does not change the result. +

+
+ \[ a + b = b + a \qquad ab = ba \] +
+

+ Example: \(3 + 5 = 5 + 3 = 8\). Three dots then five dots is the same total as five dots then + three dots. +

+

+ Subtraction and division are not commutative. \(5 - 3 \neq 3 - 5\) and + \(6 \div 2 \neq 2 \div 6\). Order matters there. +

+
+ +

Associative property

+

+ When you add or multiply three or more numbers, brackets tell you which pair to + do first. The associative property says different groupings give the same answer + (for + and × only). +

+
+ \[ (a + b) + c = a + (b + c) \qquad (ab)c = a(bc) \] +
+

+ Example: \((2 + 3) + 5 = 5 + 5 = 10\) and \(2 + (3 + 5) = 2 + 8 = 10\). You may combine + \(2 + 3\) first or \(3 + 5\) first. +

+

+ Again, subtraction and division are not associative. \((10 - 4) - 2 \neq + 10 - (4 - 2)\). +

+
+ +

Distributive property

+

+ The distributive property links multiplication to addition. Multiplying a sum + is the same as multiplying each addend and then adding — “distribute” the multiply across the + bracket. +

+
+ \[ a(b + c) = ab + ac \] +
+

+ 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. +

+

+ Later, in written calculation, partial products in multiplication use this idea: + you multiply by each digit of the bottom number separately, then add the rows. +

+
+ +

Which property is it?

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
You change…PropertyExample
Order of two numbersCommutative\(4 + 7 = 7 + 4\)
Brackets with three numbersAssociative\((1 + 2) + 3 = 1 + (2 + 3)\)
Multiply through a sumDistributive\(3(2 + 4) = 3 \times 2 + 3 \times 4\)
+
+ +

What comes next

+

+ In written calculation 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. +

+
diff --git a/content/math/properties-of-operations/supplement.html b/content/math/properties-of-operations/supplement.html new file mode 100644 index 0000000..04a49d1 --- /dev/null +++ b/content/math/properties-of-operations/supplement.html @@ -0,0 +1,122 @@ +
+

History

+

+ 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 commutative, associative, and + distributive became standard in European algebra books in the 1800s, when + school math settled on one notation for all. +

+

+ 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. +

+ +

Checkpoints

+
    +
  1. + Which property lets you write \(7 + 4\) as \(4 + 7\) without changing the total? +
  2. +
  3. + Is \((5 \times 2) \times 3\) the same as \(5 \times (2 \times 3)\)? Which property says so? +
  4. +
  5. + Rewrite \(4(6 + 1)\) as a sum of two products. +
  6. +
  7. + Why is \(8 - 3\) not commutative? Give two different answers for \(3 - 8\) and \(8 - 3\). +
  8. +
  9. + A rectangle is 5 units tall and \((2 + 3)\) units wide. How does the distributive property + describe its area? +
  10. +
+ +

Applications

+

Mental math

+

+ 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\). +

+

Shopping and sharing

+

+ 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). +

+

Later algebra

+

+ Expanding \(a(b + c)\) in polynomials and factorising common factors both rely on the + distributive property. You will meet them again in polynomials: expansion & + factorisation. +

+ +

Question bank

+ +

Easy

+ +
+ + Fill in the blank using a property: \(9 + 6 = 6 + \underline{\hspace{1.5em}}\). + +
+

Answer: \(9\) — commutative property of addition.

+
+
+ +
+ Compute: \(2(4 + 3)\). +
+

+ Answer: \(14\). Either \(2 \times 7 = 14\), or \(2 \times 4 + 2 \times 3 = 8 + + 6 = 14\) (distributive). +

+
+
+ +

Intermediate

+ +
+ + Evaluate both sides and compare: \((3 + 4) + 5\) and \(3 + (4 + 5)\). + +
+

Answer: Both equal \(12\) — associative property of addition.

+
+
+ +
+ Use distribution to find \(5 \times 102\). +
+

Answer: \(510\). Steps: \(5 \times (100 + 2) = 500 + 10 = 510\).

+
+
+ +

Difficult

+ +
+ + Explain why \(12 \div (3 \div 2)\) is not the same as \((12 \div 3) \div 2\). + +
+

+ Answer: Left: \(12 \div 1.5 = 8\). Right: \(4 \div 2 = 2\). Division is not + associative; grouping changes the meaning. +

+
+
+ +
+ + A garden bed is 4 m wide and \((2 + 3)\) m long. Write two ways to find its area and show they + match. + +
+

+ Answer: \(20\) m². One rectangle: \(4 \times 5 = 20\). Split length: + \(4 \times 2 + 4 \times 3 = 8 + 12 = 20\) (distributive). +

+
+
+
diff --git a/content/math/properties-of-operations/widgets/associative-demo.tsx b/content/math/properties-of-operations/widgets/associative-demo.tsx new file mode 100644 index 0000000..b63454d --- /dev/null +++ b/content/math/properties-of-operations/widgets/associative-demo.tsx @@ -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 ( +
+ + {() => } + +
+ ); +} + +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 ( +
+

+ Associative property +

+
+
+ + +
+ +
+
+ + setX(Number(e.currentTarget.value))} + /> +
+
+ + setY(Number(e.currentTarget.value))} + /> +
+
+ + setZ(Number(e.currentTarget.value))} + /> +
+
+ +
+

+

+

+ +
+
+ {groupFirst() ? ( + <> + + + + + + + + + ) : ( + <> + + + + + + + + + )} + +
+ + {() => } + + {total() > 48 ? ( + +{total() - 48} more + ) : null} +
+
+
+ + + +

+ Brackets show which pair to combine first. Different groupings give the same final answer. +

+
+
+ ); +} + +export default AssociativeDemo; diff --git a/content/math/properties-of-operations/widgets/commutative-demo.tsx b/content/math/properties-of-operations/widgets/commutative-demo.tsx new file mode 100644 index 0000000..711b1e9 --- /dev/null +++ b/content/math/properties-of-operations/widgets/commutative-demo.tsx @@ -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 ( +
+ + {() => } + +
+ ); +} + +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 ( +
+

+ Commutative property +

+
+
+ + +
+ +
+
+ + setLeft(Number(e.currentTarget.value))} + /> +
+
+ + setRight(Number(e.currentTarget.value))} + /> +
+
+ +
+

+ +

+ {mode() === "add" ? ( +
+
+ {first()} + +
+ +
+ {second()} + +
+ +
+ {first() + second()} +
+ + +
+
+
+ ) : ( + + )} +
+ + + +

+ Swapping the order does not change the total. Commutative means you can + commute (move) the numbers. +

+
+
+ ); +} + +export default CommutativeDemo; diff --git a/content/math/properties-of-operations/widgets/distributive-area-demo.tsx b/content/math/properties-of-operations/widgets/distributive-area-demo.tsx new file mode 100644 index 0000000..1b0faed --- /dev/null +++ b/content/math/properties-of-operations/widgets/distributive-area-demo.tsx @@ -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 ( +
+
+

+ Distributive property +

+ +
+ +
+
+ +
+ +
+ One rectangle of height a and width + b + c splits + into two areas a×b{" "} + and a×c. +
+
+ +
+
+ + setA(Number(e.currentTarget.value))} + /> +
+
+ + setB(Number(e.currentTarget.value))} + /> +
+
+ + setC(Number(e.currentTarget.value))} + /> +
+
+
+
+ ); +} + +export default DistributiveAreaDemo; diff --git a/content/math/written-calculation/_index.md b/content/math/written-calculation/_index.md new file mode 100644 index 0000000..010bb60 --- /dev/null +++ b/content/math/written-calculation/_index.md @@ -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" ++++ + +
+

+ You can read number sentences from basic math notation and use the + properties of operations to see why reordering or splitting sums can still give + the same answer. This chapter teaches + written calculation — how to add, subtract, multiply, and divide on paper by + stacking digits in vertical form: 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. +

+ +
+

Vertical form

+

+ 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. +

+
+ +

Why columns?

+

+ In counting and numbers you learned place value: 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. +

+

+ Always line up the ones under the ones, then tens under tens, and so on. Draw a + horizontal line under the stack before you write the answer. +

+ +

Carry and borrow

+

+ In the four operations, a column sometimes gives a digit from 0 to 9 and sometimes + gives 10 or more (or too small to subtract). Then you adjust the next column to the + left. +

+
    +
  • + Carry — 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. +
  • +
  • + Borrow — 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. +
  • +
+

Work from the right (ones) toward the left, one column at a time.

+ +

Carry: 27 + 59 = 86

+
¹
+  27
++ 59
+────
+  86
+

+ In the ones column: \(7 + 9 = 16\). Write 6 in the ones place. Carry + 1 to the tens column (written small above the tens). In the tens column: + \(2 + 5 + 1 = 8\). +

+ +

Borrow: 47 − 19 = 28

+
−1
+  47
+− 19
+────
+  28
+

+ In the ones column: \(7 - 9\) does not work. Borrow 10 from the tens: the 4 becomes + 3, and the ones digit becomes 17. Then \(17 - 9 = 8\). In the tens + column: \(3 - 1 = 2\). +

+ +

Addition

+

+ 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. +

+ +

Subtraction

+

+ Put the larger 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. +

+ +

Multiplication

+

+ Write the larger number on top (often). Multiply the top number by each digit of the bottom number, + one partial product per digit, shifted so ones line up with ones. Add the partial + products for the final answer. +

+
+   23
+ × 14
+ ────
+   92
+  230
+ ────
+  322
+

Here \(23 \times 4 = 92\) and \(23 \times 10 = 230\); \(92 + 230 = 322\).

+ +

Division

+

+ Long division 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 remainder. +

+ +

Names of the parts

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OperationPartsAnswer
Additionaddend + addendsum
Subtractionminuend − subtrahenddifference
Multiplicationfactor × factorproduct
Divisiondividend ÷ divisorquotient (and remainder if any)
+
+ +

Try it yourself

+

+ Use the visualizer below: enter two or more whole numbers (one per line), choose an operation, and + click Generate visualization. Step through with Play or the arrow + buttons to see each carry, borrow, and column update. +

+
+
+ +
+

History

+

+ 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. +

+

+ Mechanical calculators (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 carry cycle to update higher digits. Later designs passed + the carry instantly along the whole machine. That idea appears again inside modern chips: a + carry flag records whether the last step overflowed a column. +

+ +

Derivation

+

Why column addition works

+

+ 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. +

+

Subtraction and addition

+

+ 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. +

+ +

Checkpoints

+
+

Checkpoint

+

When you add 7 and 9 in the ones column, what do you write in the ones place? What do you carry?

+
+
+

Checkpoint

+

In \(47 - 19\), why does the 4 in the tens column become 3 before you subtract the ones?

+
+
+

Checkpoint

+

In \(23 \times 14\), why are there two rows under the line before the final sum?

+
+ +

Real life

+
    +
  • Shopping — adding prices on a receipt column by column.
  • +
  • Scores — finding the difference between two team totals.
  • +
  • Sharing — dividing a total equally (e.g. \(157\) stickers among \(12\) children: quotient 13, remainder 1).
  • +
+ +

Question bank — Easy

+
+ Work out \(34 + 25\). +
+
  34
++ 25
+────
+  59
+

Answer: 59

+
+
+
+ Work out \(61 - 28\). +
+

Answer: 33 (borrow from tens in the ones column).

+
+
+ +

Intermediate

+
+ Work out \(27 + 59\). +
+
¹
+  27
++ 59
+────
+  86
+

Answer: 86

+
+
+
+ Work out \(503 - 278\). +
+

Answer: 225

+

Steps: Borrow across the tens (0) from hundreds; ones become 13−8=5; tens 9−7=2; hundreds 4−2=2.

+
+
+
+ Work out \(23 \times 14\). +
+

Answer: 322

+
+
+ +

Difficult

+
+ Work out \(156 \div 12\). +
+

Answer: 13

+
+
+
+ Work out \(157 \div 12\) and state the remainder. +
+

Answer: Quotient 13, remainder 1.

+
+
+ +

Hardcore

+
+ A book has 864 pages. You read 379 pages. How many pages are left? +
+

Answer: \(864 - 379 = 485\) pages.

+
+
+
+ A classroom orders 24 packs of 15 pencils. How many pencils is that? +
+

Answer: \(24 \times 15 = 360\) pencils.

+
+
+ +

References

+
    +
  • Wikipedia — Carry (arithmetic) and Subtraction.
  • +
  • Cajori, Florian — A History of Mathematical Notations.
  • +
  • Other school methods (Austrian/additions, partial differences) exist; this chapter uses column borrow as in most HK primary texts.
  • +
+
diff --git a/content/math/written-calculation/chapter.scss b/content/math/written-calculation/chapter.scss new file mode 100644 index 0000000..291cf7d --- /dev/null +++ b/content/math/written-calculation/chapter.scss @@ -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; +} diff --git a/content/math/written-calculation/core.html b/content/math/written-calculation/core.html new file mode 100644 index 0000000..55f0704 --- /dev/null +++ b/content/math/written-calculation/core.html @@ -0,0 +1,152 @@ +
+

+ You can read number sentences from basic math notation and use the + properties of operations to see why reordering or splitting sums can still give + the same answer. This chapter teaches + written calculation — how to add, subtract, multiply, and divide on paper by + stacking digits in vertical form: 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. +

+ +
+

Vertical form

+

+ 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. +

+
+ +

Why columns?

+

+ In counting and numbers you learned place value: 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. +

+

+ Always line up the ones under the ones, then tens under tens, and so on. Draw a + horizontal line under the stack before you write the answer. +

+ +

Carry and borrow

+

+ In the four operations, a column sometimes gives a digit from 0 to 9 and sometimes + gives 10 or more (or too small to subtract). Then you adjust the next column to the + left. +

+
    +
  • + Carry — 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. +
  • +
  • + Borrow — 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. +
  • +
+

Work from the right (ones) toward the left, one column at a time.

+ +

Carry: 27 + 59 = 86

+
¹
+  27
++ 59
+────
+  86
+

+ In the ones column: \(7 + 9 = 16\). Write 6 in the ones place. Carry + 1 to the tens column (written small above the tens). In the tens column: + \(2 + 5 + 1 = 8\). +

+ +

Borrow: 47 − 19 = 28

+
−1
+  47
+− 19
+────
+  28
+

+ In the ones column: \(7 - 9\) does not work. Borrow 10 from the tens: the 4 becomes + 3, and the ones digit becomes 17. Then \(17 - 9 = 8\). In the tens + column: \(3 - 1 = 2\). +

+ +

Addition

+

+ 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. +

+ +

Subtraction

+

+ Put the larger 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. +

+ +

Multiplication

+

+ Write the larger number on top (often). Multiply the top number by each digit of the bottom number, + one partial product per digit, shifted so ones line up with ones. Add the partial + products for the final answer. +

+
+   23
+ × 14
+ ────
+   92
+  230
+ ────
+  322
+

Here \(23 \times 4 = 92\) and \(23 \times 10 = 230\); \(92 + 230 = 322\).

+ +

Division

+

+ Long division 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 remainder. +

+ +

Names of the parts

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OperationPartsAnswer
Additionaddend + addendsum
Subtractionminuend − subtrahenddifference
Multiplicationfactor × factorproduct
Divisiondividend ÷ divisorquotient (and remainder if any)
+
+ +

Try it yourself

+

+ Use the visualizer below: enter two or more whole numbers (one per line), choose an operation, and + click Generate visualization. Step through with Play or the arrow + buttons to see each carry, borrow, and column update. +

+
+
diff --git a/content/math/written-calculation/supplement.html b/content/math/written-calculation/supplement.html new file mode 100644 index 0000000..96656ba --- /dev/null +++ b/content/math/written-calculation/supplement.html @@ -0,0 +1,129 @@ +
+

History

+

+ 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. +

+

+ Mechanical calculators (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 carry cycle to update higher digits. Later designs passed + the carry instantly along the whole machine. That idea appears again inside modern chips: a + carry flag records whether the last step overflowed a column. +

+ +

Derivation

+

Why column addition works

+

+ 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. +

+

Subtraction and addition

+

+ 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. +

+ +

Checkpoints

+
+

Checkpoint

+

When you add 7 and 9 in the ones column, what do you write in the ones place? What do you carry?

+
+
+

Checkpoint

+

In \(47 - 19\), why does the 4 in the tens column become 3 before you subtract the ones?

+
+
+

Checkpoint

+

In \(23 \times 14\), why are there two rows under the line before the final sum?

+
+ +

Real life

+
    +
  • Shopping — adding prices on a receipt column by column.
  • +
  • Scores — finding the difference between two team totals.
  • +
  • Sharing — dividing a total equally (e.g. \(157\) stickers among \(12\) children: quotient 13, remainder 1).
  • +
+ +

Question bank — Easy

+
+ Work out \(34 + 25\). +
+
  34
++ 25
+────
+  59
+

Answer: 59

+
+
+
+ Work out \(61 - 28\). +
+

Answer: 33 (borrow from tens in the ones column).

+
+
+ +

Intermediate

+
+ Work out \(27 + 59\). +
+
¹
+  27
++ 59
+────
+  86
+

Answer: 86

+
+
+
+ Work out \(503 - 278\). +
+

Answer: 225

+

Steps: Borrow across the tens (0) from hundreds; ones become 13−8=5; tens 9−7=2; hundreds 4−2=2.

+
+
+
+ Work out \(23 \times 14\). +
+

Answer: 322

+
+
+ +

Difficult

+
+ Work out \(156 \div 12\). +
+

Answer: 13

+
+
+
+ Work out \(157 \div 12\) and state the remainder. +
+

Answer: Quotient 13, remainder 1.

+
+
+ +

Hardcore

+
+ A book has 864 pages. You read 379 pages. How many pages are left? +
+

Answer: \(864 - 379 = 485\) pages.

+
+
+
+ A classroom orders 24 packs of 15 pencils. How many pencils is that? +
+

Answer: \(24 \times 15 = 360\) pencils.

+
+
+ +

References

+
    +
  • Wikipedia — Carry (arithmetic) and Subtraction.
  • +
  • Cajori, Florian — A History of Mathematical Notations.
  • +
  • Other school methods (Austrian/additions, partial differences) exist; this chapter uses column borrow as in most HK primary texts.
  • +
+
diff --git a/content/math/written-calculation/widgets/lib/math-visualizer.ts b/content/math/written-calculation/widgets/lib/math-visualizer.ts new file mode 100644 index 0000000..fe40572 --- /dev/null +++ b/content/math/written-calculation/widgets/lib/math-visualizer.ts @@ -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; + 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 | 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(); + } +} diff --git a/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx b/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx new file mode 100644 index 0000000..482c148 --- /dev/null +++ b/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx @@ -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("+"); + 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 ( +
+

+ Carry & borrow arithmetic visualizer +

+ +
+