From fb6089aff0893601fa238b5df51da16a43712f98 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Fri, 15 May 2026 20:35:02 +0700 Subject: [PATCH] docs: brainstorm + plan for cool demo pages round 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brainstorm survey of 6 candidates with brutal-honesty trade-off matrix; top-3 ranked (Pythagoras động, Sàng Eratosthenes, đường thẳng y=ax+b). Plan with 8 phases — 3 engines + 3 lessons + glue + verify. --- .../phase-01-engine-transforms.md | 71 +++++ .../phase-02-engine-sieve.md | 64 +++++ .../phase-03-engine-linear.md | 62 ++++ .../phase-04-lesson-pythagoras.md | 101 +++++++ .../phase-05-lesson-sieve.md | 95 +++++++ .../phase-06-lesson-linear.md | 100 +++++++ .../phase-07-registry-tiles.md | 65 +++++ .../phase-08-tests-a11y-verify.md | 125 ++++++++ .../plan.md | 80 ++++++ ...de-review-260515-cool-demo-pages-round2.md | 269 ++++++++++++++++++ ...torm-260515-1959-cool-demo-pages-round2.md | 162 +++++++++++ 11 files changed, 1194 insertions(+) create mode 100644 plans/260515-1959-cool-demo-pages-round2/phase-01-engine-transforms.md create mode 100644 plans/260515-1959-cool-demo-pages-round2/phase-02-engine-sieve.md create mode 100644 plans/260515-1959-cool-demo-pages-round2/phase-03-engine-linear.md create mode 100644 plans/260515-1959-cool-demo-pages-round2/phase-04-lesson-pythagoras.md create mode 100644 plans/260515-1959-cool-demo-pages-round2/phase-05-lesson-sieve.md create mode 100644 plans/260515-1959-cool-demo-pages-round2/phase-06-lesson-linear.md create mode 100644 plans/260515-1959-cool-demo-pages-round2/phase-07-registry-tiles.md create mode 100644 plans/260515-1959-cool-demo-pages-round2/phase-08-tests-a11y-verify.md create mode 100644 plans/260515-1959-cool-demo-pages-round2/plan.md create mode 100644 plans/260515-1959-cool-demo-pages-round2/reports/code-review-260515-cool-demo-pages-round2.md create mode 100644 plans/reports/brainstorm-260515-1959-cool-demo-pages-round2.md diff --git a/plans/260515-1959-cool-demo-pages-round2/phase-01-engine-transforms.md b/plans/260515-1959-cool-demo-pages-round2/phase-01-engine-transforms.md new file mode 100644 index 0000000..e3ebfd5 --- /dev/null +++ b/plans/260515-1959-cool-demo-pages-round2/phase-01-engine-transforms.md @@ -0,0 +1,71 @@ +--- +phase: 1 +title: "Engine: geom-engine/transforms.js" +status: pending +priority: P1 +effort: "4h" +dependencies: [] +--- + +# Phase 01: Engine — `geom-engine/transforms.js` + +## Overview +Add a pure 2D affine-transform module to `geom-engine/`. Required by Phase 04 (Pythagoras dissection-shear) and re-usable for a future `[8][later]` transformations-suite lesson. + +## Requirements +- **Functional**: + - `translate(dx, dy)` → returns a 3×3 matrix + - `rotate(thetaRad, center?)` → matrix; `center` defaults to origin + - `shear(kx, ky)` → matrix + - `compose(...matrices)` → matrix product + - `applyToPoint(m, p)` → `Vec2` + - `applyToPolygon(m, points)` → `Vec2[]` +- **Non-functional**: + - Pure JS, no DOM, no Svelte + - JSDoc strict (validated via `pnpm check`) + - Stateless: every function returns new data + - Use 3×3 matrices stored as length-9 `Array` (row-major), to interop with future CSS matrix exports + +## Architecture +Affine 2D transforms via homogeneous coordinates. Matrix is `[a b c | d e f | 0 0 1]`. Apply to `Vec2 = {x, y}` by treating it as `(x, y, 1)`. + +Exports added to `geom-engine/index.js` barrel. + +## Related Code Files +- Create: `src/lib/geom-engine/transforms.js` (~80 LOC including JSDoc) +- Create: `src/lib/geom-engine/transforms.test.js` (~12 tests, ~120 LOC) +- Modify: `src/lib/geom-engine/index.js` — add re-exports + +## Implementation Steps +1. Define matrix type via JSDoc: `Mat3 = number[]` (length 9, row-major) +2. Implement `translate(dx, dy)` — identity with `c=dx, f=dy` +3. Implement `rotate(theta, center = {x:0, y:0})` — compose translate(-c) → rotate-origin → translate(+c) +4. Implement `shear(kx, ky)` — `[1 kx 0 | ky 1 0 | 0 0 1]` +5. Implement `compose(...ms)` — left-to-right reduce via 3×3 multiply helper (private) +6. Implement `applyToPoint(m, p)` — return `{x: m[0]*p.x + m[1]*p.y + m[2], y: m[3]*p.x + m[4]*p.y + m[5]}` +7. Implement `applyToPolygon(m, pts)` — `pts.map(p => applyToPoint(m, p))` +8. Re-export from `geom-engine/index.js` +9. Write 12 unit tests (Vitest): + - identity translate (0,0) + - translate accumulates + - rotate 90° around origin: (1,0) → (0,1) + - rotate 90° around custom center + - shear unit (1,0): (0,1) → (1,1) + - compose order: translate(2,0) ∘ rotate(90°) at origin → applied to (1,0) → (2,1) + - applyToPolygon preserves count + - applyToPolygon doesn't mutate input + - identity round-trip (rotate θ + rotate -θ) + - matrix length = 9 invariant + - EPSILON_LEN tolerance reused for float comparisons + - degenerate compose() with zero args returns identity + +## Success Criteria +- [ ] `pnpm test src/lib/geom-engine/transforms.test.js` → 12/12 pass +- [ ] `pnpm check` clean (no JSDoc errors) +- [ ] No new dependencies in `package.json` +- [ ] `geom-engine/index.js` re-exports all public functions + +## Risk Assessment +- **R1**: Float drift in compose chains → use `EPSILON_LEN` from `vec.js` for tests; document precision limit in JSDoc +- **R2**: Row-major vs column-major confusion when later exporting to CSS `matrix()` → JSDoc the layout explicitly; add one test that verifies CSS-format string output is row-major +- **R3**: Rotate-around-center bug (signs flip) → test with both (0,0) and (5,5) centers diff --git a/plans/260515-1959-cool-demo-pages-round2/phase-02-engine-sieve.md b/plans/260515-1959-cool-demo-pages-round2/phase-02-engine-sieve.md new file mode 100644 index 0000000..b69ac82 --- /dev/null +++ b/plans/260515-1959-cool-demo-pages-round2/phase-02-engine-sieve.md @@ -0,0 +1,64 @@ +--- +phase: 2 +title: "Engine: numtheory-engine/sieve.js" +status: pending +priority: P1 +effort: "2h" +dependencies: [] +--- + +# Phase 02: Engine — `numtheory-engine/sieve.js` + +## Overview +Add Sieve of Eratosthenes + multiples helper to `numtheory-engine/`. Powers the Sàng Eratosthenes lesson (Phase 05). + +## Requirements +- **Functional**: + - `sieveUpTo(n)` → `{ primes: number[], composite: Set }` for 2..n + - `multiplesOf(p, n)` → `number[]` of multiples of `p` in `[2p, n]` (excludes p itself; useful for the "cross out multiples" interaction) + - `isPrime(k, n?)` → boolean (convenience; can reuse sieve if `n ≥ k`) +- **Non-functional**: + - Pure, deterministic, no DOM + - JSDoc strict + - `sieveUpTo(100)` must run in < 1ms (trivially true; documented as perf budget) + +## Architecture +Classic O(n log log n) sieve over a `Uint8Array` of size `n+1` (0 = prime, 1 = composite). Convert to outputs at the end. No state outside the function. + +## Related Code Files +- Create: `src/lib/numtheory-engine/sieve.js` (~50 LOC) +- Create: `src/lib/numtheory-engine/sieve.test.js` (~80 LOC, 8 tests) +- Modify: `src/lib/numtheory-engine/index.js` — re-export + +## Implementation Steps +1. `sieveUpTo(n)`: + - Guard `n < 2` → return `{primes: [], composite: new Set()}` + - Allocate `Uint8Array(n+1)`, mark 0 and 1 + - Outer loop `i = 2; i*i ≤ n` — if not marked, mark all multiples `i*i, i*i+i, ...` + - Collect primes (`marks[k] === 0` for `k ≥ 2`) and composites into outputs +2. `multiplesOf(p, n)`: + - Guard `p < 2 || n < 2*p` → return `[]` + - Loop `2p, 3p, ...` up to `n`; collect +3. `isPrime(k)`: + - For `k < 2` → false; for `k = 2` → true; even → false; trial-divide odd up to `√k` + - (Avoid building a sieve here; this is the cheap path) +4. Re-export from `numtheory-engine/index.js` +5. Write 8 tests: + - `sieveUpTo(1)` → empty + - `sieveUpTo(2)` → `[2]` + - `sieveUpTo(10)` → `[2, 3, 5, 7]` + - `sieveUpTo(100)` → length 25, contains 97, excludes 91 (= 7·13) + - `multiplesOf(2, 10)` → `[4, 6, 8, 10]` (excludes 2 itself) + - `multiplesOf(7, 100)` → starts at 14, ends at 98 + - `isPrime(1) === false`, `isPrime(2) === true`, `isPrime(91) === false` + - `sieveUpTo(0)` and `sieveUpTo(-5)` → empty (guard test) + +## Success Criteria +- [ ] `pnpm test src/lib/numtheory-engine/sieve.test.js` → 8/8 pass +- [ ] `pnpm check` clean +- [ ] No new dependencies +- [ ] `numtheory-engine/index.js` re-exports public surface + +## Risk Assessment +- **R1**: Off-by-one at `i*i > n` boundary → covered by `sieveUpTo(100)` test (97 is the largest prime; 11² = 121 > 100 so loop stops) +- **R2**: `multiplesOf` returning `p` itself when caller doesn't want it → spec'd "exclude p"; documented in JSDoc diff --git a/plans/260515-1959-cool-demo-pages-round2/phase-03-engine-linear.md b/plans/260515-1959-cool-demo-pages-round2/phase-03-engine-linear.md new file mode 100644 index 0000000..f0320e4 --- /dev/null +++ b/plans/260515-1959-cool-demo-pages-round2/phase-03-engine-linear.md @@ -0,0 +1,62 @@ +--- +phase: 3 +title: "Engine: algebra-engine/linear.js" +status: pending +priority: P1 +effort: "2h" +dependencies: [] +--- + +# Phase 03: Engine — `algebra-engine/linear.js` + +## Overview +**Officially open the `algebra-engine/` module** (2nd algebra consumer triggers it, per curriculum survey §3.1). First file: linear-function helpers powering Phase 06 (Đồ thị y=ax+b). + +## Requirements +- **Functional**: + - `lineFromPoints(p1, p2)` → `{a, b} | null` (null when `p1.x === p2.x`, i.e. vertical line) + - `yAt(line, x)` → number + - `linePoints(line, xMin, xMax)` → `[{x:xMin, y:y(xMin)}, {x:xMax, y:y(xMax)}]` (convenience for SVG ``) + - `lineFromSlope(a, b)` → `{a, b}` (trivial; included for API symmetry) +- **Non-functional**: + - Pure JS, JSDoc strict + - Mirrors `numtheory-engine/`/`geom-engine/` module shape + - No DOM + +## Architecture +`Line = { a: number, b: number }` represents `y = a·x + b`. Vertical lines are out-of-scope (Đồ thị lesson constrains x-domain such that any two anchors with distinct x produce a defined line). `lineFromPoints` returns `null` for the degenerate case so the lesson UI can clamp. + +New module directory mirrors existing engines exactly. + +## Related Code Files +- Create: `src/lib/algebra-engine/linear.js` (~30 LOC) +- Create: `src/lib/algebra-engine/linear.test.js` (~60 LOC, 6 tests) +- Create: `src/lib/algebra-engine/index.js` (barrel) + +## Implementation Steps +1. Define `Line` type in JSDoc (`@typedef`) +2. `lineFromPoints(p1, p2)`: + - If `Math.abs(p2.x - p1.x) < EPSILON_LEN` → return `null` + - `a = (p2.y - p1.y) / (p2.x - p1.x)` + - `b = p1.y - a * p1.x` +3. `yAt(line, x)` → `line.a * x + line.b` +4. `linePoints(line, xMin, xMax)` → `[{x: xMin, y: yAt(line, xMin)}, {x: xMax, y: yAt(line, xMax)}]` +5. `lineFromSlope(a, b)` → `{a, b}` +6. `algebra-engine/index.js` re-exports all +7. Write 6 tests: + - `lineFromPoints({0,1}, {1,3})` → `{a:2, b:1}` + - `lineFromPoints({2,5}, {2,7})` → `null` (vertical) + - `yAt({a:2, b:1}, 3)` → 7 + - `linePoints({a:1, b:0}, -5, 5)` → `[{-5,-5}, {5,5}]` + - `lineFromSlope(0, 4)` → `{a:0, b:4}` (horizontal) + - Round-trip: `lineFromPoints(p1, p2)` then `yAt` at `p1.x`/`p2.x` recovers `p1.y`/`p2.y` + +## Success Criteria +- [ ] `pnpm test src/lib/algebra-engine/linear.test.js` → 6/6 pass +- [ ] `pnpm check` clean +- [ ] `src/lib/algebra-engine/index.js` barrel exports all public functions +- [ ] No new dependencies + +## Risk Assessment +- **R1**: `EPSILON_LEN` import from `geom-engine` creates cross-engine dep → acceptable; both engines are siblings under `lib/`. Documented in JSDoc. +- **R2**: API too small to be a "module"? → fine. Survey §3.1 has 5+ more algebra files queued (`polynomial`, `factor`, `linear-eq-solver`, …); this is foundation. diff --git a/plans/260515-1959-cool-demo-pages-round2/phase-04-lesson-pythagoras.md b/plans/260515-1959-cool-demo-pages-round2/phase-04-lesson-pythagoras.md new file mode 100644 index 0000000..24da2a7 --- /dev/null +++ b/plans/260515-1959-cool-demo-pages-round2/phase-04-lesson-pythagoras.md @@ -0,0 +1,101 @@ +--- +phase: 4 +title: "Lesson: /hinh-hoc/dinh-ly-pythagoras/" +status: pending +priority: P1 +effort: "1.5d" +dependencies: [1] +--- + +# Phase 04: Lesson — Định lý Pythagoras động + +## Overview +Interactive proof of `a² + b² = c²`. User drags the right-angle vertex; three squares on the sides resize live. "Chứng minh" button plays a 2-second dissection-shear animation: the two leg-squares slide-and-shear into the hypotenuse-square. + +## Requirements +- **Functional**: + - Right triangle with one vertex at origin (the right angle), legs along axes + - Three labeled squares: side `a` (vertical leg), side `b` (horizontal leg), side `c` (hypotenuse) + - Side lengths + areas update live in KaTeX as the right-angle-vertex moves + - "Chứng minh" button: tween two leg-squares into the c-square; on completion both leg-areas fill the c-area exactly + - "Đặt lại" button: reset to initial config + - `prefers-reduced-motion`: replace tween with instant snap; keep "before/after" labels +- **Non-functional**: + - Single `+page.svelte` ≤ 200 LOC (split if exceeds) + - JSDoc strict, no TS + - Uses existing `` component for math + - Uses `geom-engine/transforms.js` from Phase 01 for the shear + +## Architecture + +``` +SVG viewBox (0 0 400 400) +├── triangle — three vertices, one draggable (right-angle vertex) +├── square-a — on side a (left edge), area filled +├── square-b — on side b (bottom edge), area filled +├── square-c — on hypotenuse, area filled (semi-transparent during tween) +├── shear-a — only visible during "Chứng minh" tween; starts at square-a, tweens to half of square-c +├── shear-b — only visible during tween; starts at square-b, tweens to other half of square-c +└── ARIA layer: vertex has aria-label, aria-live region announces a/b/c +``` + +State (Svelte 5 runes): +- `$state` for `vertex = {x, y}` (the right-angle vertex; other two vertices derive) +- `$state` for `phase: 'idle' | 'animating' | 'proven'` +- `$derived` for a, b, c, areas, KaTeX strings + +Animation: +- Use Svelte `tweened` store with `cubicOut` easing, 2000ms +- Compute target transforms via `geom-engine/transforms.js`: each leg-square needs a `compose(translate, rotate, shear)` chain that lands it on the corresponding half of the c-square +- For shear axis: use the classic Euclidean shear — translate leg-square along its base, then shear it to align with the c-square's edge + +Reduced-motion: +- Wrap the tween in `prefers-reduced-motion` check +- If reduced: directly set "after" coords; tween duration = 0 + +## Related Code Files +- Create: `src/routes/hinh-hoc/dinh-ly-pythagoras/+page.svelte` +- Create: `src/lib/lessons/dinh-ly-pythagoras/copy.vi.js` +- Modify: `src/lib/lessons/registry.js` — add `pythagorasCopy` import + entry + +## Implementation Steps +1. Create `copy.vi.js` with: `slug`, `topic: 'hinh-hoc'`, `grade: 'lop-7'`, `gradeLabel: 'Lớp 7'`, `title`, `intro`, theorem statement, instructions, dissection-shear narration text +2. Scaffold `+page.svelte` skeleton with `` (SEO via copy) +3. Lay out SVG: triangle + three squares as derived `$derived` polygons from `vertex` +4. Wire `use:draggable` on the right-angle vertex (keyboard + pointer + ARIA) +5. KaTeX strings: `a = ${a.toFixed(1)}`, `a^2 = ${...}`, full `a^2 + b^2 = c^2` +6. Add "Chứng minh" button + "Đặt lại" button (Tailwind-styled, focus-visible ring) +7. Implement dissection-shear via `tweened` + `transforms.js`: + - Phase A (0-50%): shear-a slides from square-a position toward c-square's left half via shear matrix + - Phase B (50-100%): shear-b slides from square-b position toward c-square's right half + - On completion: hide originals, leave shear-a and shear-b filling c-square +8. Wire `prefers-reduced-motion` check (`window.matchMedia`) +9. Add `aria-live="polite"` region announcing "Cạnh a = …, cạnh b = …, cạnh c = …; a² + b² = c²" +10. Register in `registry.js` + +## Success Criteria +- [ ] Route `/hinh-hoc/dinh-ly-pythagoras/` live in `pnpm dev` +- [ ] Drag vertex with mouse + touch + keyboard arrow → squares + KaTeX update +- [ ] Click "Chứng minh" → animation plays smoothly to completion +- [ ] After animation, the two leg-square pieces visibly fill the c-square area +- [ ] Click "Đặt lại" → returns to initial config +- [ ] `prefers-reduced-motion: reduce` set in DevTools → button snaps to final state, no tween +- [ ] Keyboard-only walkthrough completes the lesson +- [ ] `aria-live` announces dimension changes when vertex moves (debounced) +- [ ] `pnpm check` clean, `pnpm test` green +- [ ] File ≤ 200 LOC OR cleanly split into helpers under `src/lib/lessons/dinh-ly-pythagoras/` + +## A11y Verify Steps +1. Tab to vertex → focus ring visible +2. Arrow keys move vertex by 1px; Shift+Arrow by 10px +3. `aria-label` announces position +4. Tab to "Chứng minh" button → space/enter triggers +5. Reduced-motion: macOS System Settings → Accessibility → Reduce Motion ON; reload → button snaps +6. Screen reader (VoiceOver / NVDA) reads `aria-live` updates +7. Color-contrast: square fills must hit AA against white background + +## Risk Assessment +- **R1 — Animation jank**: cubicOut + 2s should feel right; budget 0.5d of polish time +- **R2 — Dissection geometry off**: leg-area total must exactly equal c-area; verify with `applyToPolygon` math at the destination state — write a console assertion gated by `import.meta.env.DEV` +- **R3 — Mobile touch on vertex**: existing `draggable` handles pointer events; verify on real mobile +- **R4 — Bundle delta**: SVG-only, Svelte `tweened` (already in core); ~1KB delta budget diff --git a/plans/260515-1959-cool-demo-pages-round2/phase-05-lesson-sieve.md b/plans/260515-1959-cool-demo-pages-round2/phase-05-lesson-sieve.md new file mode 100644 index 0000000..a3ee33a --- /dev/null +++ b/plans/260515-1959-cool-demo-pages-round2/phase-05-lesson-sieve.md @@ -0,0 +1,95 @@ +--- +phase: 5 +title: "Lesson: /so-hoc/sang-eratosthenes/" +status: pending +priority: P1 +effort: "1d" +dependencies: [2] +--- + +# Phase 05: Lesson — Sàng Eratosthenes + +## Overview +10×10 grid of numbers 1..100. Click any prime → cross-out ripple cascades through its multiples in that prime's color. Stack 2/3/5/7 to reveal all primes ≤ 100 visually. + +## Requirements +- **Functional**: + - Static 10×10 grid (numbers 1..100, row-major) + - Cell visual states: untouched, marked-as-prime (clicked + colored border), crossed-as-multiple (faded + strike) + - Clicking a prime triggers a ripple: multiples cross out in sequence with ~30ms stagger + - Cells that are multiples of multiple primes show stacked indicators (small dots in each color) + - "Đặt lại" button clears all state + - Color palette: extend Tailwind `colors.pair.{1,2,3}` to `pair.{1,2,3,4}` (one per small prime 2,3,5,7); see open question + - `prefers-reduced-motion`: no stagger; cross-outs apply instantly +- **Non-functional**: + - Single `+page.svelte` ≤ 200 LOC + - Uses `numtheory-engine/sieve.js` for `multiplesOf` + - Grid is `role="grid"`, cells are `role="gridcell"` + +## Architecture + +``` +Page +├── Header (KaTeX intro) +├── Grid (10×10 buttons, role="grid") +│ └── Cells: 1..100 — each is