diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ecd5b2f..f1c3c95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -158,10 +158,29 @@ Add edges in `data/{subject}-curriculum.json`: ## Add a new subject -1. `data/catalog.json` — add to `subjects`. -2. Create `data/{id}-curriculum.json` (copy structure from `math-curriculum.json`). -3. Wire the subject in `themes/openfreebooks/templates/catalog.html` (until auto-wire lands). -4. Add `content/{subject}/` when you have chapters. +1. `data/catalog.json` — add the subject to `subjects`. +2. Create `data/{id}-curriculum.json` (same schema as `math-curriculum.json`; `strands` + optional `graph.edges`). +3. Add `content/{subject}/` when you have live chapters. +4. Run `bun run build:js` (this syncs chapter content and auto-registers subject CSS/JS modules). + +### Subject scaffold (copyable) + +```text +data/ + {subject}-curriculum.json +content/{subject}/ + subject.scss # optional shared styles + subject.ts # optional shared JS init + {chapter-slug}/ + _index.md # template = "chapter.html", extra.subject = "{subject}" + core.html # required + supplement.html # optional + chapter.scss # optional chapter-only styles + assets/ # optional + widgets/*.tsx # optional +``` + +No catalog/chapter template edits are required for new subjects when the subject is present in `data/catalog.json` and `data/{subject}-curriculum.json` exists. --- diff --git a/README.md b/README.md index 41f5905..180e7b7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Open Free Books +> Work in Progress. I am looking for contributors to help build this project. Educators welcome. + Free, open-source textbooks for every learner — from elementary school through university. The site is fully static, open on GitHub, and deployed to Cloudflare. **Live site:** [openfreebooks.org](https://openfreebooks.org) diff --git a/content/english/_index.md b/content/english/_index.md new file mode 100644 index 0000000..06a4475 --- /dev/null +++ b/content/english/_index.md @@ -0,0 +1,5 @@ ++++ +title = "English" +description = "Browse English chapters in the curriculum catalog." +sort_by = "weight" ++++ diff --git a/content/english/reading-comprehension-foundations/_index.md b/content/english/reading-comprehension-foundations/_index.md new file mode 100644 index 0000000..8c70a27 --- /dev/null +++ b/content/english/reading-comprehension-foundations/_index.md @@ -0,0 +1,60 @@ ++++ +title = "Reading comprehension foundations" +description = "Find the main idea, key details, and evidence in short texts." +template = "chapter.html" +weight = 1 +[extra] +subject = "english" +chapter_id = "reading-comprehension-foundations" +strand = "Reading & Writing Foundations" ++++ + +
+

+ Reading comprehension means understanding what a text says directly and what it suggests + indirectly. In this chapter you will practice spotting the + main idea, finding + supporting details, and selecting + text evidence. +

+ +

Main idea and key details

+

+ The main idea is the most important message in a paragraph or passage. + Key details are specific facts or examples that support that message. +

+ +
+

Quick strategy

+

+ Ask: “If I had to summarize this paragraph in one sentence, what would I say?” Then check + which details in the text prove your sentence. +

+
+ +

Use evidence, not guesses

+

+ Good answers point to exact words or phrases in the passage. You can quote directly or + paraphrase, but your answer should always connect back to the text. +

+ +
+ Checkpoint: main idea +
+

+ Read this sentence: “After weeks of practice, the choir finally sang in harmony during the + final rehearsal.” What is the main idea, and which words are the strongest evidence? +

+
+
+ +
+ Checkpoint: evidence +
+

+ A student writes: “The character is brave.” Improve this answer by adding one short quote + from the text and explaining how that quote proves bravery. +

+
+
+
diff --git a/content/english/reading-comprehension-foundations/core.html b/content/english/reading-comprehension-foundations/core.html new file mode 100644 index 0000000..9f76039 --- /dev/null +++ b/content/english/reading-comprehension-foundations/core.html @@ -0,0 +1,49 @@ +
+

+ Reading comprehension means understanding what a text says directly and what it suggests + indirectly. In this chapter you will practice spotting the + main idea, finding + supporting details, and selecting + text evidence. +

+ +

Main idea and key details

+

+ The main idea is the most important message in a paragraph or passage. + Key details are specific facts or examples that support that message. +

+ +
+

Quick strategy

+

+ Ask: “If I had to summarize this paragraph in one sentence, what would I say?” Then check + which details in the text prove your sentence. +

+
+ +

Use evidence, not guesses

+

+ Good answers point to exact words or phrases in the passage. You can quote directly or + paraphrase, but your answer should always connect back to the text. +

+ +
+ Checkpoint: main idea +
+

+ Read this sentence: “After weeks of practice, the choir finally sang in harmony during the + final rehearsal.” What is the main idea, and which words are the strongest evidence? +

+
+
+ +
+ Checkpoint: evidence +
+

+ A student writes: “The character is brave.” Improve this answer by adding one short quote + from the text and explaining how that quote proves bravery. +

+
+
+
diff --git a/content/english/subject.scss b/content/english/subject.scss new file mode 100644 index 0000000..a9b8239 --- /dev/null +++ b/content/english/subject.scss @@ -0,0 +1,10 @@ +// Shared styles for English chapters. +// Keep chapter-specific rules in each chapter folder's chapter.scss. + +.english-key-term { + padding: 0.05rem 0.32rem; + border-radius: 4px; + background: color-mix(in srgb, var(--accent) 12%, transparent); + font-family: var(--mono); + font-size: 0.85em; +} diff --git a/content/faq.md b/content/faq.md index cc04d88..5ca7dd3 100644 --- a/content/faq.md +++ b/content/faq.md @@ -2,3 +2,5 @@ title = "FAQ" template = "faq.html" +++ + +We use AI to help draft and improve material, but we publish durable HTML chapters so learning stays accessible even if token pricing or model access changes in the future. diff --git a/content/physics/_index.md b/content/physics/_index.md new file mode 100644 index 0000000..617657c --- /dev/null +++ b/content/physics/_index.md @@ -0,0 +1,5 @@ ++++ +title = "Physics" +description = "Browse physics chapters in the curriculum catalog." +sort_by = "weight" ++++ diff --git a/content/physics/force-newtons-laws/_index.md b/content/physics/force-newtons-laws/_index.md new file mode 100644 index 0000000..2c83f0d --- /dev/null +++ b/content/physics/force-newtons-laws/_index.md @@ -0,0 +1,122 @@ ++++ +title = "Force and Newton's laws" +description = "Use free-body diagrams, F = ma, and motion graphs to predict how objects move." +template = "chapter.html" +weight = 1 +[extra] +subject = "physics" +chapter_id = "force-newtons-laws" +strand = "Force and Motion" ++++ + +
+

+ In HKDSE Physics, force and motion are linked by three core ideas: + inertia, + net force, and + acceleration. This chapter gives you one practical flow: + draw forces, find the net force, then predict acceleration with Newton's second law. +

+ +

Newton's three laws

+
    +
  1. + First law: if net force is zero, velocity stays constant (including rest). +
  2. +
  3. + Second law: acceleration is proportional to net force and inversely proportional + to mass. +
  4. +
  5. + Third law: forces come in equal and opposite interaction pairs. +
  6. +
+ +
+

Core equation

+

\[ \vec{F}_{\text{net}} = m\vec{a} \]

+
+ +

Problem-solving workflow

+
    +
  1. Choose one object as your system.
  2. +
  3. Sketch all external forces in a free-body diagram.
  4. +
  5. Resolve forces by direction and compute net force.
  6. +
  7. Use \(a = F_{\text{net}} / m\), then connect to motion equations.
  8. +
+ +

Interactive force lab

+

+ Adjust mass, pulling force, and surface friction. The demo computes friction, net force, + acceleration, and the motion state for a 3-second interval from rest. +

+
+ +
+ +
+

Interactive motion graphs

+

+ Change initial velocity, acceleration, and elapsed time to inspect + v-t and s-t behavior. + This connects Newton's second law to kinematics directly. +

+
+ +
+ +
+

Worked checkpoints

+

+ These quick prompts are designed for class discussion or self-check before moving to momentum + and energy. +

+ +
+ + A 4 kg trolley is pulled right with 18 N while friction is 10 N left. What is its acceleration? + +
+

+ Net force \(= 18 - 10 = 8\text{ N}\) to the right, so + \(a = F_{\text{net}}/m = 8/4 = 2\text{ m s}^{-2}\) to the right. +

+
+
+ +
+ + Why can a book stay at rest on a table even though gravity is acting? + +
+

+ Gravity pulls down, but the table exerts an equal normal force upward. Net force is zero, so + acceleration is zero. +

+
+
+ +
+ + A rocket pushes exhaust gas backward. Which Newton's law explains forward thrust? + +
+

+ Newton's third law. The rocket pushes gas backward; the gas pushes the rocket forward with an + equal and opposite force. +

+
+
+ +
+ + In the motion lab, what happens to the velocity-time line when acceleration is set to zero? + +
+

+ The line becomes horizontal, showing constant velocity. Position then changes linearly with + time. +

+
+
+
diff --git a/content/physics/force-newtons-laws/chapter.scss b/content/physics/force-newtons-laws/chapter.scss new file mode 100644 index 0000000..5c7ab7a --- /dev/null +++ b/content/physics/force-newtons-laws/chapter.scss @@ -0,0 +1,201 @@ +.physics-widget { + margin: 20px 0; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--card-bg); + padding: 14px; +} + +.physics-widget__layout { + display: grid; + gap: 12px; +} + +.physics-widget__section { + border: 1px solid var(--line); + border-radius: 10px; + background: color-mix(in srgb, var(--paper) 72%, transparent); + padding: 10px; +} + +.physics-widget__title { + margin: 0 0 8px; + font-size: 1rem; +} + +.physics-slider-row { + display: grid; + gap: 6px; + margin-bottom: 8px; +} + +.physics-slider-row label { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 10px; + font-size: 0.86rem; +} + +.physics-slider-row input[type="range"] { + width: 100%; +} + +.physics-value { + font-family: var(--mono); + font-weight: 700; +} + +.force-lab__summary { + margin: 8px 0 0; + font-size: 0.9rem; +} + +.force-lab__status-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; +} + +.force-lab__state-pill { + display: inline-flex; + align-items: center; + padding: 0.28rem 0.55rem; + border-radius: 999px; + border: 1px solid color-mix(in srgb, #f59e0b 40%, var(--line)); + background: color-mix(in srgb, #f59e0b 16%, transparent); + color: color-mix(in srgb, #b45309 70%, var(--ink)); + font-size: 0.73rem; + font-weight: 700; + letter-spacing: 0.02em; +} + +.force-lab__state-pill--moving { + border-color: color-mix(in srgb, #10b981 45%, var(--line)); + background: color-mix(in srgb, #10b981 18%, transparent); + color: color-mix(in srgb, #065f46 74%, var(--ink)); +} + +.force-lab__time { + font-family: var(--mono); + font-size: 0.76rem; + color: var(--soft-ink); +} + +.force-lab__diagram-wrap { + margin: 0; + padding: 4px; + border: 1px solid var(--line); + border-radius: 10px; + background: color-mix(in srgb, var(--paper) 86%, transparent); +} + +.force-lab__canvas { + --force-canvas-bg: color-mix(in srgb, var(--paper) 90%, #e2e8f0); + --force-canvas-grid: color-mix(in srgb, var(--line) 72%, transparent); + --force-canvas-ink: color-mix(in srgb, var(--ink) 90%, #111827); + + width: 100%; + display: block; + border-radius: 8px; + background: var(--force-canvas-bg); +} + +.force-lab__timeline { + margin-top: 10px; + display: grid; + grid-template-columns: auto auto minmax(0, 1fr); + gap: 8px; + align-items: center; +} + +.force-lab__btn { + border: 1px solid var(--line); + border-radius: 8px; + background: var(--card-bg); + color: var(--ink); + font-family: var(--mono); + font-size: 0.75rem; + font-weight: 700; + padding: 0.42rem 0.62rem; + cursor: pointer; +} + +.force-lab__btn--play { + border-color: color-mix(in srgb, #2563eb 50%, var(--line)); + background: color-mix(in srgb, #2563eb 14%, transparent); +} + +.force-lab__scrubber { + width: 100%; +} + +.motion-lab__stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.motion-lab__stat { + border: 1px solid var(--line); + border-radius: 8px; + padding: 8px; + background: color-mix(in srgb, var(--paper) 70%, transparent); +} + +.motion-lab__stat-label { + display: block; + font-size: 0.72rem; + color: var(--soft-ink); +} + +.motion-lab__stat-value { + display: block; + margin-top: 2px; + font-family: var(--mono); + font-weight: 700; +} + +.motion-lab__plot { + width: 100%; + max-width: 520px; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--paper); +} + +.motion-lab__legend { + margin: 8px 0 0; + font-size: 0.76rem; + color: var(--soft-ink); +} + +.motion-lab__legend-line { + display: inline-flex; + align-items: center; + margin-right: 12px; +} + +.motion-lab__legend-line::before { + content: ""; + width: 18px; + height: 2px; + margin-right: 6px; +} + +.motion-lab__legend-line--velocity::before { + background: #0ea5e9; +} + +.motion-lab__legend-line--position::before { + background: #f59e0b; +} + +@media (min-width: 900px) { + .physics-widget__layout { + grid-template-columns: minmax(260px, 1fr) minmax(0, 1.2fr); + align-items: start; + } +} diff --git a/content/physics/force-newtons-laws/core.html b/content/physics/force-newtons-laws/core.html new file mode 100644 index 0000000..508e612 --- /dev/null +++ b/content/physics/force-newtons-laws/core.html @@ -0,0 +1,55 @@ +
+

+ In HKDSE Physics, force and motion are linked by three core ideas: + inertia, + net force, and + acceleration. This chapter gives you one practical flow: + draw forces, find the net force, then predict acceleration with Newton's second law. +

+ +

Newton's three laws

+
    +
  1. + First law: if net force is zero, velocity stays constant (including rest). +
  2. +
  3. + Second law: acceleration is proportional to net force and inversely proportional + to mass. +
  4. +
  5. + Third law: forces come in equal and opposite interaction pairs. +
  6. +
+ +
+

Core equation

+

\[ \vec{F}_{\text{net}} = m\vec{a} \]

+
+ +

Problem-solving workflow

+
    +
  1. Choose one object as your system.
  2. +
  3. Sketch all external forces in a free-body diagram.
  4. +
  5. Resolve forces by direction and compute net force.
  6. +
  7. Use \(a = F_{\text{net}} / m\), then connect to motion equations.
  8. +
+ +

Interactive force lab

+

+ Adjust mass, pulling force, and surface friction. The demo computes friction, net force, + acceleration, and the motion state for a 3-second interval from rest. +

+
+ +
+ +
+

Interactive motion graphs

+

+ Change initial velocity, acceleration, and elapsed time to inspect + v-t and s-t behavior. + This connects Newton's second law to kinematics directly. +

+
+ +
diff --git a/content/physics/force-newtons-laws/supplement.html b/content/physics/force-newtons-laws/supplement.html new file mode 100644 index 0000000..e7e5256 --- /dev/null +++ b/content/physics/force-newtons-laws/supplement.html @@ -0,0 +1,55 @@ +
+

Worked checkpoints

+

+ These quick prompts are designed for class discussion or self-check before moving to momentum + and energy. +

+ +
+ + A 4 kg trolley is pulled right with 18 N while friction is 10 N left. What is its acceleration? + +
+

+ Net force \(= 18 - 10 = 8\text{ N}\) to the right, so + \(a = F_{\text{net}}/m = 8/4 = 2\text{ m s}^{-2}\) to the right. +

+
+
+ +
+ + Why can a book stay at rest on a table even though gravity is acting? + +
+

+ Gravity pulls down, but the table exerts an equal normal force upward. Net force is zero, so + acceleration is zero. +

+
+
+ +
+ + A rocket pushes exhaust gas backward. Which Newton's law explains forward thrust? + +
+

+ Newton's third law. The rocket pushes gas backward; the gas pushes the rocket forward with an + equal and opposite force. +

+
+
+ +
+ + In the motion lab, what happens to the velocity-time line when acceleration is set to zero? + +
+

+ The line becomes horizontal, showing constant velocity. Position then changes linearly with + time. +

+
+
+
diff --git a/content/physics/force-newtons-laws/widgets/force-lab.tsx b/content/physics/force-newtons-laws/widgets/force-lab.tsx new file mode 100644 index 0000000..4780dee --- /dev/null +++ b/content/physics/force-newtons-laws/widgets/force-lab.tsx @@ -0,0 +1,441 @@ +import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"; + +const G = 9.81; +const DURATION = 3; + +type PhysicsState = { + state: "Static" | "Moving"; + normalForce: number; + maxStaticFriction: number; + friction: number; + netForce: number; + acceleration: number; + velocity: number; + displacement: number; +}; + +function round(value: number, digits = 2) { + const p = 10 ** digits; + return Math.round(value * p) / p; +} + +function format(value: number, unit = "") { + const n = Math.abs(value) < 1e-9 ? 0 : round(value, 2); + return `${Number.isInteger(n) ? n : n.toFixed(2)}${unit}`; +} + +function drawArrow( + ctx: CanvasRenderingContext2D, + fromX: number, + fromY: number, + toX: number, + toY: number, + color: string, + width: number +) { + ctx.strokeStyle = color; + ctx.lineWidth = width; + ctx.beginPath(); + ctx.moveTo(fromX, fromY); + ctx.lineTo(toX, toY); + ctx.stroke(); + + const angle = Math.atan2(toY - fromY, toX - fromX); + const head = 10; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(toX, toY); + ctx.lineTo( + toX - head * Math.cos(angle - Math.PI / 6), + toY - head * Math.sin(angle - Math.PI / 6) + ); + ctx.lineTo( + toX - head * Math.cos(angle + Math.PI / 6), + toY - head * Math.sin(angle + Math.PI / 6) + ); + ctx.closePath(); + ctx.fill(); +} + +export default function ForceLab() { + const [massKg, setMassKg] = createSignal(10); + const [appliedForceN, setAppliedForceN] = createSignal(60); + const [muStatic, setMuStatic] = createSignal(0.5); + const [muKinetic, setMuKinetic] = createSignal(0.3); + const [time, setTime] = createSignal(0); + const [playing, setPlaying] = createSignal(false); + + let simCanvasRef: HTMLCanvasElement | undefined; + let raf = 0; + let lastTime = 0; + let lastCanvasWidth = 0; + let lastCanvasHeight = 0; + + const physicsAt = (t: number): PhysicsState => { + const normalForce = massKg() * G; + const maxStaticFriction = muStatic() * normalForce; + const startsMoving = appliedForceN() > maxStaticFriction; + + if (!startsMoving) { + return { + state: "Static", + normalForce, + maxStaticFriction, + friction: appliedForceN(), + netForce: 0, + acceleration: 0, + velocity: 0, + displacement: 0, + }; + } + + const friction = muKinetic() * normalForce; + const netForce = appliedForceN() - friction; + const acceleration = netForce / massKg(); + return { + state: "Moving", + normalForce, + maxStaticFriction, + friction, + netForce, + acceleration, + velocity: acceleration * t, + displacement: 0.5 * acceleration * t * t, + }; + }; + + const current = createMemo(() => physicsAt(time())); + + const pause = () => { + setPlaying(false); + if (raf) cancelAnimationFrame(raf); + raf = 0; + }; + + const play = () => { + if (time() >= DURATION) setTime(0); + setPlaying(true); + lastTime = 0; + const step = (stamp: number) => { + if (!playing()) return; + if (!lastTime) lastTime = stamp; + const dt = (stamp - lastTime) / 1000; + lastTime = stamp; + setTime((prev) => { + const next = Math.min(DURATION, prev + dt); + if (next >= DURATION) { + setPlaying(false); + } + return next; + }); + if (playing()) raf = requestAnimationFrame(step); + }; + raf = requestAnimationFrame(step); + }; + + const reset = () => { + pause(); + setTime(0); + }; + + createEffect(() => { + const uk = muKinetic(); + const us = muStatic(); + if (uk > us) { + setMuStatic(uk); + } + }); + + createEffect(() => { + const canvas = simCanvasRef; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + const width = Math.max(420, Math.floor(rect.width || 640)); + const height = 250; + + if (width !== lastCanvasWidth || height !== lastCanvasHeight) { + lastCanvasWidth = width; + lastCanvasHeight = height; + canvas.width = Math.floor(width * dpr); + canvas.height = Math.floor(height * dpr); + canvas.style.height = `${height}px`; + } + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + const p = current(); + const style = getComputedStyle(canvas); + const bg = style.getPropertyValue("--force-canvas-bg").trim() || "#f8fafc"; + const grid = style.getPropertyValue("--force-canvas-grid").trim() || "#d7dde6"; + const ink = style.getPropertyValue("--force-canvas-ink").trim() || "#111827"; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, width, height); + + const groundY = height - 58; + const scalePxPerM = 26; + const visualShift = p.displacement * scalePxPerM * 0.9; + const gridSize = 28; + const gridPhase = ((visualShift % gridSize) + gridSize) % gridSize; + + ctx.strokeStyle = grid; + ctx.lineWidth = 1; + for (let x = -gridPhase; x < width; x += gridSize) { + const px = Math.round(x) + 0.5; + ctx.beginPath(); + ctx.moveTo(px, 0); + ctx.lineTo(px, groundY); + ctx.stroke(); + } + + ctx.strokeStyle = ink; + ctx.lineWidth = 2; + ctx.lineJoin = "round"; + const toothWidth = 12; + const toothHeight = 8; + const toothPeriod = toothWidth * 2; + const toothPhase = ((visualShift % toothPeriod) + toothPeriod) % toothPeriod; + + ctx.beginPath(); + let started = false; + for (let i = -4; ; i++) { + const x = i * toothWidth - toothPhase; + if (x > width + toothWidth * 4) break; + const y = i % 2 === 0 ? groundY : groundY + toothHeight; + if (!started) { + ctx.moveTo(x, y); + started = true; + } else { + ctx.lineTo(x, y); + } + } + ctx.stroke(); + + const blockSize = Math.max(44, Math.min(86, 52 * Math.sqrt(massKg() / 10))); + const blockX = width * 0.33; + const blockY = groundY - blockSize; + + const grad = ctx.createLinearGradient(blockX, blockY, blockX + blockSize, blockY + blockSize); + grad.addColorStop(0, "#ef4444"); + grad.addColorStop(1, "#b91c1c"); + ctx.fillStyle = grad; + ctx.fillRect(blockX, blockY, blockSize, blockSize); + ctx.strokeStyle = ink; + ctx.lineWidth = 3; + ctx.strokeRect(blockX, blockY, blockSize, blockSize); + + const centerY = blockY + blockSize / 2; + const maxForce = Math.max(30, appliedForceN(), p.friction); + const pullLen = 40 + (appliedForceN() / maxForce) * 130; + const fricLen = 40 + (p.friction / maxForce) * 130; + + drawArrow( + ctx, + blockX + blockSize, + centerY - 12, + blockX + blockSize + pullLen, + centerY - 12, + "#2563eb", + 4 + ); + drawArrow(ctx, blockX, centerY + 12, blockX - fricLen, centerY + 12, "#ef4444", 4); + + const vertLen = Math.max(24, Math.min(72, (massKg() * G) / 2.8)); + drawArrow( + ctx, + blockX + blockSize / 2, + blockY, + blockX + blockSize / 2, + blockY - vertLen, + "#06b6d4", + 2.5 + ); + drawArrow( + ctx, + blockX + blockSize / 2, + blockY + blockSize, + blockX + blockSize / 2, + blockY + blockSize + vertLen, + "#f97316", + 2.5 + ); + + ctx.fillStyle = "#1e293b"; + ctx.font = "600 14px Inter, sans-serif"; + ctx.textAlign = "left"; + ctx.fillText("F", blockX + blockSize + pullLen + 8, centerY - 14); + ctx.fillStyle = "#7f1d1d"; + ctx.fillText("f_fric", blockX - fricLen - 56, centerY + 8); + + ctx.fillStyle = "#164e63"; + ctx.font = "500 11px Inter, sans-serif"; + ctx.textAlign = "center"; + ctx.fillText(`F_N ${p.normalForce.toFixed(0)} N`, blockX + blockSize / 2, blockY - vertLen - 8); + ctx.fillStyle = "#7c2d12"; + ctx.fillText( + `W ${p.normalForce.toFixed(0)} N`, + blockX + blockSize / 2, + blockY + blockSize + vertLen + 14 + ); + + ctx.fillStyle = "#0f172a"; + ctx.font = "700 11px Inter, sans-serif"; + ctx.textAlign = "center"; + ctx.fillText(`${massKg().toFixed(1)} kg`, blockX + blockSize / 2, blockY + blockSize / 2 + 3); + }); + + onCleanup(() => { + if (raf) cancelAnimationFrame(raf); + }); + + return ( +
+

+ Force lab: animated friction simulation +

+ +
+
+
+ + { + reset(); + setMassKg(Number(e.currentTarget.value)); + }} + /> +
+ +
+ + { + reset(); + setAppliedForceN(Number(e.currentTarget.value)); + }} + /> +
+ +
+ + { + reset(); + setMuStatic(Number(e.currentTarget.value)); + }} + /> +
+ +
+ + { + reset(); + setMuKinetic(Number(e.currentTarget.value)); + }} + /> +
+
+ +
+
+ + {current().state === "Moving" ? "Moving / Sliding" : "Static State"} + + {format(time(), "s")} / 3.00s +
+ +
+ { + simCanvasRef = el; + }} + class="force-lab__canvas" + /> +
+ +
+ + + { + pause(); + setTime(Number(e.currentTarget.value)); + }} + /> +
+ +

+ Friction threshold: {format(current().maxStaticFriction, " N")} +
+ Current friction: {format(current().friction, " N")} +
+ Net force: {format(current().netForce, " N")} +
+ Acceleration: {format(current().acceleration, " m s⁻²")} +
+ Velocity: {format(current().velocity, " m s⁻¹")} · Displacement:{" "} + {format(current().displacement, " m")} +

+
+
+
+ ); +} diff --git a/content/physics/force-newtons-laws/widgets/motion-lab.tsx b/content/physics/force-newtons-laws/widgets/motion-lab.tsx new file mode 100644 index 0000000..23ae01e --- /dev/null +++ b/content/physics/force-newtons-laws/widgets/motion-lab.tsx @@ -0,0 +1,196 @@ +import { createMemo, createSignal } from "solid-js"; + +const PLOT_W = 520; +const PLOT_H = 280; +const PAD_X = 48; +const PAD_Y = 24; +const MAX_TIME = 12; + +function round(value: number, digits = 2) { + const p = 10 ** digits; + return Math.round(value * p) / p; +} + +function format(value: number, unit = "") { + const n = round(value, 2); + return `${Number.isInteger(n) ? n : n.toFixed(2)}${unit}`; +} + +function normalize(value: number, min: number, max: number) { + if (Math.abs(max - min) < 1e-9) return 0.5; + return (value - min) / (max - min); +} + +export default function MotionLab() { + const [initialVelocity, setInitialVelocity] = createSignal(2); + const [acceleration, setAcceleration] = createSignal(1.2); + const [time, setTime] = createSignal(6); + + const finalVelocity = createMemo( + () => initialVelocity() + acceleration() * time() + ); + const displacement = createMemo( + () => initialVelocity() * time() + 0.5 * acceleration() * time() * time() + ); + + const samples = createMemo(() => { + const points = []; + for (let t = 0; t <= MAX_TIME; t += 0.2) { + const v = initialVelocity() + acceleration() * t; + const s = initialVelocity() * t + 0.5 * acceleration() * t * t; + points.push({ t, v, s }); + } + return points; + }); + + const yRange = createMemo(() => { + const allV = samples().map((p) => p.v); + const allS = samples().map((p) => p.s); + const min = Math.min(...allV, ...allS, 0); + const max = Math.max(...allV, ...allS, 0); + return { min, max }; + }); + + const toX = (t: number) => + PAD_X + (t / MAX_TIME) * (PLOT_W - PAD_X * 2); + const toY = (y: number) => + PAD_Y + (1 - normalize(y, yRange().min, yRange().max)) * (PLOT_H - PAD_Y * 2); + + const velocityPath = createMemo( + () => + samples() + .map((p, i) => `${i === 0 ? "M" : "L"} ${toX(p.t)} ${toY(p.v)}`) + .join(" ") + ); + + const displacementPath = createMemo( + () => + samples() + .map((p, i) => `${i === 0 ? "M" : "L"} ${toX(p.t)} ${toY(p.s)}`) + .join(" ") + ); + + return ( +
+

+ Motion lab: velocity-time and displacement-time +

+ +
+
+
+ + setInitialVelocity(Number(e.currentTarget.value))} + /> +
+ +
+ + setAcceleration(Number(e.currentTarget.value))} + /> +
+ +
+ + setTime(Number(e.currentTarget.value))} + /> +
+ +
+
+ v = u + at + {format(finalVelocity(), " m s⁻¹")} +
+
+ s = ut + 1/2at² + {format(displacement(), " m")} +
+
+ Direction now + + {finalVelocity() > 0 ? "right" : finalVelocity() < 0 ? "left" : "instant rest"} + +
+
+
+ +
+ + + + + + + + + + + t + + + y + + + +
+
+
+ ); +} diff --git a/content/physics/subject.scss b/content/physics/subject.scss new file mode 100644 index 0000000..bb698e3 --- /dev/null +++ b/content/physics/subject.scss @@ -0,0 +1,12 @@ +.physics-key-term { + padding: 0.05rem 0.34rem; + border-radius: 4px; + background: color-mix(in srgb, var(--accent) 14%, transparent); + font-family: var(--mono); + font-size: 0.85em; +} + +.physics-unit { + font-family: var(--mono); + font-size: 0.9em; +} diff --git a/data/catalog.json b/data/catalog.json index 7dae402..e27515d 100644 --- a/data/catalog.json +++ b/data/catalog.json @@ -4,6 +4,8 @@ "curriculums": ["Foundation", "DSE", "IB", "A-Level", "AP", "IGCSE", "Common Core"], "subjects": [ { "id": "math", "name": "Mathematics", "banner": "/catalog/banners/math.webp" }, + { "id": "english", "name": "English" }, + { "id": "physics", "name": "Physics", "banner": "/catalog/banners/physics.jpg" }, { "id": "science", "name": "Science" }, { "id": "humanities", "name": "Humanities" }, { "id": "computing", "name": "Computing" } diff --git a/data/computing-curriculum.json b/data/computing-curriculum.json new file mode 100644 index 0000000..075cf57 --- /dev/null +++ b/data/computing-curriculum.json @@ -0,0 +1,8 @@ +{ + "title": "Computing", + "intro": "Planned computing pathways will appear here.", + "graph": { + "edges": [] + }, + "strands": [] +} diff --git a/data/english-curriculum.json b/data/english-curriculum.json new file mode 100644 index 0000000..32c50b9 --- /dev/null +++ b/data/english-curriculum.json @@ -0,0 +1,39 @@ +{ + "title": "English", + "intro": "Build reading, writing, speaking, and analysis skills through progressive chapter pathways.", + "graph": { + "edges": [] + }, + "strands": [ + { + "id": "reading-writing-foundations", + "title": "Reading & Writing Foundations", + "chapters": [ + { + "slug": "reading-comprehension-foundations", + "title": "Reading comprehension foundations", + "description": "Find main ideas, supporting details, and text evidence in short passages.", + "status": "live", + "curriculumCoverage": { + "Foundation": "core", + "IGCSE": "core", + "Common Core": "core", + "IB": "related" + } + }, + { + "slug": "paragraph-writing-basics", + "title": "Paragraph writing basics", + "description": "Write focused paragraphs with a clear topic sentence, evidence, and explanation.", + "status": "planned", + "curriculumCoverage": { + "Foundation": "core", + "IGCSE": "core", + "Common Core": "core", + "IB": "related" + } + } + ] + } + ] +} diff --git a/data/faq.json b/data/faq.json index d61d401..9528b7a 100644 --- a/data/faq.json +++ b/data/faq.json @@ -97,8 +97,9 @@ "type": "standard", "question": "Is the content AI-generated?", "answer": [ - "No. Chapters are authored HTML with care: figures, interactives, and maps where they help understanding — not bulk-generated slop or print PDFs pasted online.", - "Core curriculum changes slowly; one carefully reviewed book, shared openly, beats regenerating the same chapter thousands of times in a data center." + "We use AI as a drafting and editing tool, but we do not rely on AI chat tokens as the final product layer. We publish reviewed, hand-curated chapters as open HTML so learning materials stay readable without paid AI access.", + "Token costs matter. Today some companies appear to offer \"free\" AI access, but that is often market-share strategy in an early stage. If prices rise later, learners with less money or weaker infrastructure could lose access.", + "The open web is more durable: HTML and JavaScript are stable foundations likely to remain usable for decades. Building high-quality, future-proof educational materials now — with AI assistance but web-native outputs — is the practical long-term approach." ] }, { diff --git a/data/humanities-curriculum.json b/data/humanities-curriculum.json new file mode 100644 index 0000000..f1fb9d2 --- /dev/null +++ b/data/humanities-curriculum.json @@ -0,0 +1,8 @@ +{ + "title": "Humanities", + "intro": "Planned humanities pathways will appear here.", + "graph": { + "edges": [] + }, + "strands": [] +} diff --git a/data/physics-curriculum.json b/data/physics-curriculum.json new file mode 100644 index 0000000..ea35e6c --- /dev/null +++ b/data/physics-curriculum.json @@ -0,0 +1,215 @@ +{ + "title": "Physics", + "intro": "HKDSE-aligned physics pathways across compulsory and elective modules.", + "graph": { + "edges": [ + { "from": "thermal-energy-temperature", "to": "heat-transfer-and-insulation" }, + { "from": "heat-transfer-and-insulation", "to": "change-of-state-latent-heat" }, + { "from": "change-of-state-latent-heat", "to": "gas-laws-kinetic-theory" }, + { "from": "kinematics-motion-graphs", "to": "force-newtons-laws" }, + { "from": "force-newtons-laws", "to": "momentum-impulse-collisions" }, + { "from": "force-newtons-laws", "to": "work-energy-power" }, + { "from": "force-newtons-laws", "to": "uniform-circular-motion-gravitation" }, + { "from": "wave-basics-superposition", "to": "light-optics" }, + { "from": "wave-basics-superposition", "to": "sound-doppler-effect" }, + { "from": "electrostatics-electric-fields", "to": "circuits-domestic-electricity" }, + { "from": "circuits-domestic-electricity", "to": "electromagnetism-induction" }, + { "from": "radioactivity-half-life", "to": "nuclear-energy-fission-fusion" }, + { "from": "uniform-circular-motion-gravitation", "to": "astronomy-and-space-science" }, + { "from": "work-energy-power", "to": "energy-and-use-of-energy" }, + { "from": "gas-laws-kinetic-theory", "to": "atomic-world-quantum-intro" }, + { "from": "nuclear-energy-fission-fusion", "to": "atomic-world-quantum-intro" }, + { "from": "electromagnetism-induction", "to": "medical-physics" }, + { "from": "nuclear-energy-fission-fusion", "to": "medical-physics" } + ] + }, + "strands": [ + { + "id": "heat-and-gases", + "title": "I. Heat and Gases", + "chapters": [ + { + "slug": "thermal-energy-temperature", + "title": "Thermal energy, temperature, and internal energy", + "description": "Distinguish heat, temperature, and internal energy using particle and energy models.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IGCSE": "core", "A-Level": "core", "AP": "related" } + }, + { + "slug": "heat-transfer-and-insulation", + "title": "Heat transfer: conduction, convection, and radiation", + "description": "Compare thermal transfer processes and evaluate insulation strategies in real systems.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IGCSE": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "change-of-state-latent-heat", + "title": "Change of state and latent heat", + "description": "Model phase changes with heating curves and latent heat calculations.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IGCSE": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "gas-laws-kinetic-theory", + "title": "Gas laws and kinetic theory", + "description": "Connect Boyle's and Charles's laws to particle motion and absolute temperature.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + } + ] + }, + { + "id": "force-and-motion", + "title": "II. Force and Motion", + "chapters": [ + { + "slug": "kinematics-motion-graphs", + "title": "Position, velocity, acceleration, and motion graphs", + "description": "Interpret and connect displacement-time, velocity-time, and acceleration-time graphs.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "force-newtons-laws", + "title": "Force and Newton's laws", + "description": "Use free-body diagrams and Newton's laws to predict acceleration and motion changes.", + "status": "live", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "momentum-impulse-collisions", + "title": "Momentum, impulse, and collisions", + "description": "Apply conservation of momentum to collisions and recoil situations.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "work-energy-power", + "title": "Work, energy, and power", + "description": "Relate force to energy transfer and analyze efficiency and power in systems.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "uniform-circular-motion-gravitation", + "title": "Uniform circular motion and gravitation", + "description": "Model circular motion and gravitational attraction in satellites and planetary systems.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "extension", "A-Level": "core", "AP": "related" } + } + ] + }, + { + "id": "wave-motion", + "title": "III. Wave Motion", + "chapters": [ + { + "slug": "wave-basics-superposition", + "title": "Wave properties and superposition", + "description": "Describe wave quantities and apply reflection, refraction, diffraction, and interference.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "light-optics", + "title": "Light, lenses, and optical models", + "description": "Use ray and wave models to analyze image formation and optical instruments.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "sound-doppler-effect", + "title": "Sound, pitch, loudness, and Doppler effect", + "description": "Connect sound wave behavior to frequency, intensity, and moving-source effects.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "related" } + } + ] + }, + { + "id": "electricity-and-magnetism", + "title": "IV. Electricity and Magnetism", + "chapters": [ + { + "slug": "electrostatics-electric-fields", + "title": "Electrostatics, electric fields, and potential", + "description": "Model electric forces and potential differences in static-charge systems.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "circuits-domestic-electricity", + "title": "Circuits and domestic electricity", + "description": "Analyze circuit behavior, power consumption, and safety in household systems.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IGCSE": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "electromagnetism-induction", + "title": "Magnetism and electromagnetic induction", + "description": "Explain magnetic effects of current and induction in generators and transformers.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + } + ] + }, + { + "id": "radioactivity-and-nuclear-energy", + "title": "V. Radioactivity and Nuclear Energy", + "chapters": [ + { + "slug": "radioactivity-half-life", + "title": "Radiation, radioactivity, and half-life", + "description": "Classify ionizing radiation and model radioactive decay quantitatively.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "nuclear-energy-fission-fusion", + "title": "Nuclear models, fission, and fusion", + "description": "Compare fission and fusion through binding energy and mass-energy conversion.", + "status": "planned", + "curriculumCoverage": { "DSE": "core", "IB": "extension", "A-Level": "core", "AP": "core" } + } + ] + }, + { + "id": "elective-part", + "title": "Elective Part (choose two)", + "chapters": [ + { + "slug": "astronomy-and-space-science", + "title": "Astronomy and space science", + "description": "Study celestial coordinates, orbital motion, and cosmology foundations.", + "status": "planned", + "tier": "non-foundation", + "curriculumCoverage": { "DSE": "extension", "IB": "core", "A-Level": "related", "AP": "related" } + }, + { + "slug": "atomic-world-quantum-intro", + "title": "Atomic world and quantum foundations", + "description": "Bridge atomic models, photoelectric effect, and wave-particle duality.", + "status": "planned", + "tier": "non-foundation", + "curriculumCoverage": { "DSE": "extension", "IB": "core", "A-Level": "core", "AP": "core" } + }, + { + "slug": "energy-and-use-of-energy", + "title": "Energy and use of energy", + "description": "Evaluate energy resources, efficiency, and sustainability trade-offs.", + "status": "planned", + "tier": "non-foundation", + "curriculumCoverage": { "DSE": "extension", "IB": "related", "A-Level": "core", "AP": "related" } + }, + { + "slug": "medical-physics", + "title": "Medical physics", + "description": "Connect imaging technologies and radiation safety to physics principles.", + "status": "planned", + "tier": "non-foundation", + "curriculumCoverage": { "DSE": "extension", "IB": "related", "A-Level": "related", "AP": "related" } + } + ] + } + ] +} diff --git a/data/science-curriculum.json b/data/science-curriculum.json new file mode 100644 index 0000000..06645ec --- /dev/null +++ b/data/science-curriculum.json @@ -0,0 +1,8 @@ +{ + "title": "Science", + "intro": "Planned science pathways will appear here.", + "graph": { + "edges": [] + }, + "strands": [] +} diff --git a/static/catalog/banners/physics.jpg b/static/catalog/banners/physics.jpg new file mode 100644 index 0000000..ff1fb5b Binary files /dev/null and b/static/catalog/banners/physics.jpg differ diff --git a/themes/openfreebooks/templates/catalog.html b/themes/openfreebooks/templates/catalog.html index 92a800d..5885d75 100644 --- a/themes/openfreebooks/templates/catalog.html +++ b/themes/openfreebooks/templates/catalog.html @@ -15,12 +15,13 @@ "curriculums": {{ catalog.curriculums | json_encode() | safe }}, "subjects": [ {% for subject in catalog.subjects %} + {% set subject_curriculum = load_data(path="data/" ~ subject.id ~ "-curriculum.json", format="json") %} { "id": {{ subject.id | json_encode() | safe }}, "name": {{ subject.name | json_encode() | safe }}{% if subject.banner %}, "banner": {{ subject.banner | json_encode() | safe }}{% endif %}, - "strands": {% if subject.id == "math" %}{{ math_catalog.strands | json_encode() | safe }}{% else %}[]{% endif %}{% if subject.id == "math" and math_catalog.graph %}, - "graph": {{ math_catalog.graph | json_encode() | safe }}{% endif %} + "strands": {{ subject_curriculum.strands | default(value=[]) | json_encode() | safe }}{% if subject_curriculum.graph %}, + "graph": {{ subject_curriculum.graph | json_encode() | safe }}{% endif %} }{% if not loop.last %},{% endif %} {% endfor %} ] diff --git a/themes/openfreebooks/templates/chapter.html b/themes/openfreebooks/templates/chapter.html index 536924e..879cd24 100644 --- a/themes/openfreebooks/templates/chapter.html +++ b/themes/openfreebooks/templates/chapter.html @@ -1,7 +1,8 @@ {% extends "base.html" %} {% block title %} -{% set subject_catalog = load_data(path="data/math-curriculum.json", format="json") %} +{% set subject = section.extra.subject | default(value="math") %} +{% set subject_catalog = load_data(path="data/" ~ subject ~ "-curriculum.json", format="json") %} {{ section.title }} | {{ subject_catalog.title }} | {{ config.extra.site_title }} {% endblock title %} @@ -26,10 +27,10 @@ {% endblock extra_head %} {% block content %} -{% set subject = section.extra.subject %} +{% set subject = section.extra.subject | default(value="math") %} {% set slug = section.extra.chapter_id | default(value=section.slug) %} {% set chapter_key = subject ~ "/" ~ slug %} -{% set subject_catalog = load_data(path="data/math-curriculum.json", format="json") %} +{% set subject_catalog = load_data(path="data/" ~ subject ~ "-curriculum.json", format="json") %} {% set catalog_subject_url = get_url(path="/catalog/") ~ "?subject=" ~ subject %}
diff --git a/themes/openfreebooks/templates/partials/contributing-content.html b/themes/openfreebooks/templates/partials/contributing-content.html index d222bbe..bb637ec 100644 --- a/themes/openfreebooks/templates/partials/contributing-content.html +++ b/themes/openfreebooks/templates/partials/contributing-content.html @@ -41,6 +41,7 @@ content/{subject}/{slug}/ → live page (one folder)
  • Compare (?view=compare, Mathematics only) — curricula comparison doc (HTML partial, not JSON).
  • Curriculum names (DSE, IB, …) appear only in the catalog, never inside lesson prose.
  • Every chapter needs a description (1–2 sentences; map clamps to three lines).
  • +
  • Adding a subject should only require updates in data/ and content/; templates stay unchanged.
  • @@ -77,7 +78,7 @@ content/{subject}/{slug}/ → live page (one folder)

    2. Chapter folder

    Add _index.md, core.html, optional supplement.html, assets/, widgets/*.tsx. Copy content/math/quadratic-equations/.

    3. Build

    -

    bun run build:js syncs HTML and bundles widgets — no theme or main.tsx edits.

    +

    bun run build:js syncs HTML and bundles widgets — no theme template or main.tsx edits.