From 7636bb4a86ffd7b490fccc843ded61bae17effc7 Mon Sep 17 00:00:00 2001 From: Hananoshika Yomaru Date: Wed, 20 May 2026 12:40:51 -0700 Subject: [PATCH] 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. --- .agents/skills/zola/reference.md | 1 + CONTRIBUTING.md | 31 + content/faq.md | 4 + content/math/directed-numbers-rates/_index.md | 5 +- content/math/directed-numbers-rates/core.html | 5 +- .../math/properties-of-operations/_index.md | 271 ++++++ .../properties-of-operations/chapter.scss | 469 ++++++++++ .../math/properties-of-operations/core.html | 137 +++ .../properties-of-operations/supplement.html | 122 +++ .../widgets/associative-demo.tsx | 205 +++++ .../widgets/commutative-demo.tsx | 170 ++++ .../widgets/distributive-area-demo.tsx | 158 ++++ content/math/written-calculation/_index.md | 293 ++++++ content/math/written-calculation/chapter.scss | 371 ++++++++ content/math/written-calculation/core.html | 152 ++++ .../math/written-calculation/supplement.html | 129 +++ .../widgets/lib/math-visualizer.ts | 841 ++++++++++++++++++ .../widgets/vertical-arithmetic-workbench.tsx | 231 +++++ data/catalog.json | 2 +- data/catalog.types.ts | 2 + data/faq.json | 142 +++ data/math-curriculum.json | 18 +- frontend/src/components/catalog-app.tsx | 16 +- frontend/src/main.tsx | 3 + math-banner.jpg | Bin 0 -> 369002 bytes static/catalog/banners/math.webp | Bin 0 -> 139706 bytes themes/openfreebooks/sass/_catalog.scss | 83 ++ themes/openfreebooks/sass/_faq.scss | 164 ++++ themes/openfreebooks/sass/main.scss | 1 + themes/openfreebooks/templates/base.html | 2 + themes/openfreebooks/templates/catalog.html | 3 +- themes/openfreebooks/templates/faq.html | 87 ++ .../partials/contributing-content.html | 27 + tsconfig.json | 4 +- zola.toml | 1 + 35 files changed, 4140 insertions(+), 10 deletions(-) create mode 100644 content/faq.md create mode 100644 content/math/properties-of-operations/_index.md create mode 100644 content/math/properties-of-operations/chapter.scss create mode 100644 content/math/properties-of-operations/core.html create mode 100644 content/math/properties-of-operations/supplement.html create mode 100644 content/math/properties-of-operations/widgets/associative-demo.tsx create mode 100644 content/math/properties-of-operations/widgets/commutative-demo.tsx create mode 100644 content/math/properties-of-operations/widgets/distributive-area-demo.tsx create mode 100644 content/math/written-calculation/_index.md create mode 100644 content/math/written-calculation/chapter.scss create mode 100644 content/math/written-calculation/core.html create mode 100644 content/math/written-calculation/supplement.html create mode 100644 content/math/written-calculation/widgets/lib/math-visualizer.ts create mode 100644 content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx create mode 100644 data/faq.json create mode 100644 math-banner.jpg create mode 100644 static/catalog/banners/math.webp create mode 100644 themes/openfreebooks/sass/_faq.scss create mode 100644 themes/openfreebooks/templates/faq.html 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 +

+ +
+