mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-13 13:05:51 +00:00
Enhance curriculum structure and add new subjects: Update CONTRIBUTING.md with detailed guidelines for adding new subjects and their associated assets. Introduce English and Physics subjects in the catalog, along with their respective curriculum files and chapter structures. Implement shared styles for English and Physics chapters, and add interactive widgets for physics concepts. Update FAQ to clarify the use of AI in content creation, ensuring accessibility and durability of educational materials.
This commit is contained in:
parent
a2679aa4de
commit
5f14020cfa
26 changed files with 1530 additions and 12 deletions
5
content/english/_index.md
Normal file
5
content/english/_index.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
+++
|
||||
title = "English"
|
||||
description = "Browse English chapters in the curriculum catalog."
|
||||
sort_by = "weight"
|
||||
+++
|
||||
60
content/english/reading-comprehension-foundations/_index.md
Normal file
60
content/english/reading-comprehension-foundations/_index.md
Normal file
|
|
@ -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"
|
||||
+++
|
||||
|
||||
<div class="book-prose">
|
||||
<p>
|
||||
Reading comprehension means understanding what a text says directly and what it suggests
|
||||
indirectly. In this chapter you will practice spotting the
|
||||
<span class="english-key-term">main idea</span>, finding
|
||||
<span class="english-key-term">supporting details</span>, and selecting
|
||||
<span class="english-key-term">text evidence</span>.
|
||||
</p>
|
||||
|
||||
<h2 id="main-idea" class="book-prose__heading">Main idea and key details</h2>
|
||||
<p>
|
||||
The <strong>main idea</strong> is the most important message in a paragraph or passage.
|
||||
<strong>Key details</strong> are specific facts or examples that support that message.
|
||||
</p>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Quick strategy</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 id="evidence" class="book-prose__heading">Use evidence, not guesses</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Checkpoint: main idea</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
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?
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Checkpoint: evidence</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
49
content/english/reading-comprehension-foundations/core.html
Normal file
49
content/english/reading-comprehension-foundations/core.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<div class="book-prose">
|
||||
<p>
|
||||
Reading comprehension means understanding what a text says directly and what it suggests
|
||||
indirectly. In this chapter you will practice spotting the
|
||||
<span class="english-key-term">main idea</span>, finding
|
||||
<span class="english-key-term">supporting details</span>, and selecting
|
||||
<span class="english-key-term">text evidence</span>.
|
||||
</p>
|
||||
|
||||
<h2 id="main-idea" class="book-prose__heading">Main idea and key details</h2>
|
||||
<p>
|
||||
The <strong>main idea</strong> is the most important message in a paragraph or passage.
|
||||
<strong>Key details</strong> are specific facts or examples that support that message.
|
||||
</p>
|
||||
|
||||
<div class="book-callout" role="note">
|
||||
<p class="book-callout__label">Quick strategy</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 id="evidence" class="book-prose__heading">Use evidence, not guesses</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Checkpoint: main idea</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
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?
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Checkpoint: evidence</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
10
content/english/subject.scss
Normal file
10
content/english/subject.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
5
content/physics/_index.md
Normal file
5
content/physics/_index.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
+++
|
||||
title = "Physics"
|
||||
description = "Browse physics chapters in the curriculum catalog."
|
||||
sort_by = "weight"
|
||||
+++
|
||||
122
content/physics/force-newtons-laws/_index.md
Normal file
122
content/physics/force-newtons-laws/_index.md
Normal file
|
|
@ -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"
|
||||
+++
|
||||
|
||||
<div class="book-prose">
|
||||
<p>
|
||||
In HKDSE Physics, force and motion are linked by three core ideas:
|
||||
<span class="physics-key-term">inertia</span>,
|
||||
<span class="physics-key-term">net force</span>, and
|
||||
<span class="physics-key-term">acceleration</span>. This chapter gives you one practical flow:
|
||||
draw forces, find the net force, then predict acceleration with Newton's second law.
|
||||
</p>
|
||||
|
||||
<h2 id="newtons-laws" class="book-prose__heading">Newton's three laws</h2>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>First law:</strong> if net force is zero, velocity stays constant (including rest).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Second law:</strong> acceleration is proportional to net force and inversely proportional
|
||||
to mass.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Third law:</strong> forces come in equal and opposite interaction pairs.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="book-callout book-callout--formula" role="note">
|
||||
<p class="book-callout__label">Core equation</p>
|
||||
<p class="book-formula">\[ \vec{F}_{\text{net}} = m\vec{a} \]</p>
|
||||
</div>
|
||||
|
||||
<h2 id="workflow" class="book-prose__heading">Problem-solving workflow</h2>
|
||||
<ol>
|
||||
<li>Choose one object as your system.</li>
|
||||
<li>Sketch all external forces in a free-body diagram.</li>
|
||||
<li>Resolve forces by direction and compute net force.</li>
|
||||
<li>Use \(a = F_{\text{net}} / m\), then connect to motion equations.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="interactive-force-lab" class="book-prose__heading">Interactive force lab</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="physics-widget-mount" data-widget="force-lab" data-pagefind-ignore></div>
|
||||
|
||||
<div class="book-prose">
|
||||
<h2 id="interactive-motion-lab" class="book-prose__heading">Interactive motion graphs</h2>
|
||||
<p>
|
||||
Change initial velocity, acceleration, and elapsed time to inspect
|
||||
<span class="physics-key-term">v-t</span> and <span class="physics-key-term">s-t</span> behavior.
|
||||
This connects Newton's second law to kinematics directly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="physics-widget-mount" data-widget="motion-lab" data-pagefind-ignore></div>
|
||||
|
||||
<div class="book-prose">
|
||||
<h2 id="examples" class="book-prose__heading">Worked checkpoints</h2>
|
||||
<p>
|
||||
These quick prompts are designed for class discussion or self-check before moving to momentum
|
||||
and energy.
|
||||
</p>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
A 4 kg trolley is pulled right with 18 N while friction is 10 N left. What is its acceleration?
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
Why can a book stay at rest on a table even though gravity is acting?
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
Gravity pulls down, but the table exerts an equal normal force upward. Net force is zero, so
|
||||
acceleration is zero.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
A rocket pushes exhaust gas backward. Which Newton's law explains forward thrust?
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
Newton's third law. The rocket pushes gas backward; the gas pushes the rocket forward with an
|
||||
equal and opposite force.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
In the motion lab, what happens to the velocity-time line when acceleration is set to zero?
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
The line becomes horizontal, showing constant velocity. Position then changes linearly with
|
||||
time.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
201
content/physics/force-newtons-laws/chapter.scss
Normal file
201
content/physics/force-newtons-laws/chapter.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
55
content/physics/force-newtons-laws/core.html
Normal file
55
content/physics/force-newtons-laws/core.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<div class="book-prose">
|
||||
<p>
|
||||
In HKDSE Physics, force and motion are linked by three core ideas:
|
||||
<span class="physics-key-term">inertia</span>,
|
||||
<span class="physics-key-term">net force</span>, and
|
||||
<span class="physics-key-term">acceleration</span>. This chapter gives you one practical flow:
|
||||
draw forces, find the net force, then predict acceleration with Newton's second law.
|
||||
</p>
|
||||
|
||||
<h2 id="newtons-laws" class="book-prose__heading">Newton's three laws</h2>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>First law:</strong> if net force is zero, velocity stays constant (including rest).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Second law:</strong> acceleration is proportional to net force and inversely proportional
|
||||
to mass.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Third law:</strong> forces come in equal and opposite interaction pairs.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="book-callout book-callout--formula" role="note">
|
||||
<p class="book-callout__label">Core equation</p>
|
||||
<p class="book-formula">\[ \vec{F}_{\text{net}} = m\vec{a} \]</p>
|
||||
</div>
|
||||
|
||||
<h2 id="workflow" class="book-prose__heading">Problem-solving workflow</h2>
|
||||
<ol>
|
||||
<li>Choose one object as your system.</li>
|
||||
<li>Sketch all external forces in a free-body diagram.</li>
|
||||
<li>Resolve forces by direction and compute net force.</li>
|
||||
<li>Use \(a = F_{\text{net}} / m\), then connect to motion equations.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="interactive-force-lab" class="book-prose__heading">Interactive force lab</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="physics-widget-mount" data-widget="force-lab" data-pagefind-ignore></div>
|
||||
|
||||
<div class="book-prose">
|
||||
<h2 id="interactive-motion-lab" class="book-prose__heading">Interactive motion graphs</h2>
|
||||
<p>
|
||||
Change initial velocity, acceleration, and elapsed time to inspect
|
||||
<span class="physics-key-term">v-t</span> and <span class="physics-key-term">s-t</span> behavior.
|
||||
This connects Newton's second law to kinematics directly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="physics-widget-mount" data-widget="motion-lab" data-pagefind-ignore></div>
|
||||
55
content/physics/force-newtons-laws/supplement.html
Normal file
55
content/physics/force-newtons-laws/supplement.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<div class="book-prose">
|
||||
<h2 id="examples" class="book-prose__heading">Worked checkpoints</h2>
|
||||
<p>
|
||||
These quick prompts are designed for class discussion or self-check before moving to momentum
|
||||
and energy.
|
||||
</p>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
A 4 kg trolley is pulled right with 18 N while friction is 10 N left. What is its acceleration?
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
Why can a book stay at rest on a table even though gravity is acting?
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
Gravity pulls down, but the table exerts an equal normal force upward. Net force is zero, so
|
||||
acceleration is zero.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
A rocket pushes exhaust gas backward. Which Newton's law explains forward thrust?
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
Newton's third law. The rocket pushes gas backward; the gas pushes the rocket forward with an
|
||||
equal and opposite force.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="book-question">
|
||||
<summary class="book-question__prompt">
|
||||
In the motion lab, what happens to the velocity-time line when acceleration is set to zero?
|
||||
</summary>
|
||||
<div class="book-question__solution">
|
||||
<p>
|
||||
The line becomes horizontal, showing constant velocity. Position then changes linearly with
|
||||
time.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
441
content/physics/force-newtons-laws/widgets/force-lab.tsx
Normal file
441
content/physics/force-newtons-laws/widgets/force-lab.tsx
Normal file
|
|
@ -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 (
|
||||
<section class="physics-widget force-lab" aria-labelledby="force-lab-title">
|
||||
<h2 id="force-lab-title" class="physics-widget__title">
|
||||
Force lab: animated friction simulation
|
||||
</h2>
|
||||
|
||||
<div class="physics-widget__layout">
|
||||
<div class="physics-widget__section">
|
||||
<div class="physics-slider-row">
|
||||
<label for="force-lab-mass">
|
||||
<span>Mass</span>
|
||||
<span class="physics-value">{format(massKg(), " kg")}</span>
|
||||
</label>
|
||||
<input
|
||||
id="force-lab-mass"
|
||||
type="range"
|
||||
min={1}
|
||||
max={40}
|
||||
step={0.5}
|
||||
value={massKg()}
|
||||
onInput={(e) => {
|
||||
reset();
|
||||
setMassKg(Number(e.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="physics-slider-row">
|
||||
<label for="force-lab-applied">
|
||||
<span>Pull force</span>
|
||||
<span class="physics-value">{format(appliedForceN(), " N")}</span>
|
||||
</label>
|
||||
<input
|
||||
id="force-lab-applied"
|
||||
type="range"
|
||||
min={0}
|
||||
max={200}
|
||||
step={1}
|
||||
value={appliedForceN()}
|
||||
onInput={(e) => {
|
||||
reset();
|
||||
setAppliedForceN(Number(e.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="physics-slider-row">
|
||||
<label for="force-lab-us">
|
||||
<span>Static friction μs</span>
|
||||
<span class="physics-value">{format(muStatic())}</span>
|
||||
</label>
|
||||
<input
|
||||
id="force-lab-us"
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.02}
|
||||
value={muStatic()}
|
||||
onInput={(e) => {
|
||||
reset();
|
||||
setMuStatic(Number(e.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="physics-slider-row">
|
||||
<label for="force-lab-uk">
|
||||
<span>Kinetic friction μk</span>
|
||||
<span class="physics-value">{format(muKinetic())}</span>
|
||||
</label>
|
||||
<input
|
||||
id="force-lab-uk"
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.02}
|
||||
value={muKinetic()}
|
||||
onInput={(e) => {
|
||||
reset();
|
||||
setMuKinetic(Number(e.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="physics-widget__section">
|
||||
<div class="force-lab__status-row">
|
||||
<span
|
||||
class="force-lab__state-pill"
|
||||
classList={{ "force-lab__state-pill--moving": current().state === "Moving" }}
|
||||
>
|
||||
{current().state === "Moving" ? "Moving / Sliding" : "Static State"}
|
||||
</span>
|
||||
<span class="force-lab__time">{format(time(), "s")} / 3.00s</span>
|
||||
</div>
|
||||
|
||||
<div class="force-lab__diagram-wrap">
|
||||
<canvas
|
||||
ref={(el) => {
|
||||
simCanvasRef = el;
|
||||
}}
|
||||
class="force-lab__canvas"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="force-lab__timeline">
|
||||
<button
|
||||
type="button"
|
||||
class="force-lab__btn force-lab__btn--play"
|
||||
onClick={() => (playing() ? pause() : play())}
|
||||
>
|
||||
{playing() ? "Pause" : "Play"}
|
||||
</button>
|
||||
<button type="button" class="force-lab__btn" onClick={reset}>
|
||||
Reset
|
||||
</button>
|
||||
<input
|
||||
class="force-lab__scrubber"
|
||||
type="range"
|
||||
min={0}
|
||||
max={DURATION}
|
||||
step={0.01}
|
||||
value={time()}
|
||||
onInput={(e) => {
|
||||
pause();
|
||||
setTime(Number(e.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="force-lab__summary">
|
||||
<strong>Friction threshold:</strong> {format(current().maxStaticFriction, " N")}
|
||||
<br />
|
||||
<strong>Current friction:</strong> {format(current().friction, " N")}
|
||||
<br />
|
||||
<strong>Net force:</strong> {format(current().netForce, " N")}
|
||||
<br />
|
||||
<strong>Acceleration:</strong> {format(current().acceleration, " m s⁻²")}
|
||||
<br />
|
||||
<strong>Velocity:</strong> {format(current().velocity, " m s⁻¹")} · <strong>Displacement:</strong>{" "}
|
||||
{format(current().displacement, " m")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
196
content/physics/force-newtons-laws/widgets/motion-lab.tsx
Normal file
196
content/physics/force-newtons-laws/widgets/motion-lab.tsx
Normal file
|
|
@ -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 (
|
||||
<section class="physics-widget motion-lab" aria-labelledby="motion-lab-title">
|
||||
<h2 id="motion-lab-title" class="physics-widget__title">
|
||||
Motion lab: velocity-time and displacement-time
|
||||
</h2>
|
||||
|
||||
<div class="physics-widget__layout">
|
||||
<div class="physics-widget__section">
|
||||
<div class="physics-slider-row">
|
||||
<label for="motion-lab-u">
|
||||
<span>Initial velocity u</span>
|
||||
<span class="physics-value">{format(initialVelocity(), " m s⁻¹")}</span>
|
||||
</label>
|
||||
<input
|
||||
id="motion-lab-u"
|
||||
type="range"
|
||||
min={-8}
|
||||
max={16}
|
||||
step={0.2}
|
||||
value={initialVelocity()}
|
||||
onInput={(e) => setInitialVelocity(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="physics-slider-row">
|
||||
<label for="motion-lab-a">
|
||||
<span>Acceleration a</span>
|
||||
<span class="physics-value">{format(acceleration(), " m s⁻²")}</span>
|
||||
</label>
|
||||
<input
|
||||
id="motion-lab-a"
|
||||
type="range"
|
||||
min={-6}
|
||||
max={6}
|
||||
step={0.1}
|
||||
value={acceleration()}
|
||||
onInput={(e) => setAcceleration(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="physics-slider-row">
|
||||
<label for="motion-lab-t">
|
||||
<span>Elapsed time t</span>
|
||||
<span class="physics-value">{format(time(), " s")}</span>
|
||||
</label>
|
||||
<input
|
||||
id="motion-lab-t"
|
||||
type="range"
|
||||
min={0}
|
||||
max={MAX_TIME}
|
||||
step={0.1}
|
||||
value={time()}
|
||||
onInput={(e) => setTime(Number(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="motion-lab__stats">
|
||||
<div class="motion-lab__stat">
|
||||
<span class="motion-lab__stat-label">v = u + at</span>
|
||||
<span class="motion-lab__stat-value">{format(finalVelocity(), " m s⁻¹")}</span>
|
||||
</div>
|
||||
<div class="motion-lab__stat">
|
||||
<span class="motion-lab__stat-label">s = ut + 1/2at²</span>
|
||||
<span class="motion-lab__stat-value">{format(displacement(), " m")}</span>
|
||||
</div>
|
||||
<div class="motion-lab__stat">
|
||||
<span class="motion-lab__stat-label">Direction now</span>
|
||||
<span class="motion-lab__stat-value">
|
||||
{finalVelocity() > 0 ? "right" : finalVelocity() < 0 ? "left" : "instant rest"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="physics-widget__section">
|
||||
<svg
|
||||
class="motion-lab__plot"
|
||||
viewBox={`0 0 ${PLOT_W} ${PLOT_H}`}
|
||||
role="img"
|
||||
aria-label="Velocity-time and displacement-time graph"
|
||||
>
|
||||
<rect x="0" y="0" width={PLOT_W} height={PLOT_H} fill="transparent" />
|
||||
<line
|
||||
x1={PAD_X}
|
||||
y1={PLOT_H - PAD_Y}
|
||||
x2={PLOT_W - PAD_X}
|
||||
y2={PLOT_H - PAD_Y}
|
||||
stroke="var(--line)"
|
||||
/>
|
||||
<line
|
||||
x1={PAD_X}
|
||||
y1={PAD_Y}
|
||||
x2={PAD_X}
|
||||
y2={PLOT_H - PAD_Y}
|
||||
stroke="var(--line)"
|
||||
/>
|
||||
<line
|
||||
x1={toX(time())}
|
||||
y1={PAD_Y}
|
||||
x2={toX(time())}
|
||||
y2={PLOT_H - PAD_Y}
|
||||
stroke="color-mix(in srgb, var(--soft-ink) 55%, transparent)"
|
||||
stroke-dasharray="4 4"
|
||||
/>
|
||||
<path d={velocityPath()} fill="none" stroke="#0ea5e9" stroke-width="3" />
|
||||
<path d={displacementPath()} fill="none" stroke="#f59e0b" stroke-width="3" />
|
||||
<circle cx={toX(time())} cy={toY(finalVelocity())} r="4" fill="#0ea5e9" />
|
||||
<circle cx={toX(time())} cy={toY(displacement())} r="4" fill="#f59e0b" />
|
||||
<text x={PLOT_W - PAD_X + 4} y={PLOT_H - PAD_Y + 4} font-size="12" fill="var(--soft-ink)">
|
||||
t
|
||||
</text>
|
||||
<text x={PAD_X - 18} y={PAD_Y - 4} font-size="12" fill="var(--soft-ink)">
|
||||
y
|
||||
</text>
|
||||
</svg>
|
||||
<p class="motion-lab__legend" aria-hidden="true">
|
||||
<span class="motion-lab__legend-line motion-lab__legend-line--velocity">velocity</span>
|
||||
<span class="motion-lab__legend-line motion-lab__legend-line--position">displacement</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
12
content/physics/subject.scss
Normal file
12
content/physics/subject.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue