mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
Enhance math content presentation and widget styles: Update SKILL.md to specify the use of shared modal patterns for chapter widgets. Revise algebraic fractions examples in _index.md, core.html, and supplement.html to utilize aligned multi-line KaTeX for improved readability. Introduce responsive styles in chapter.scss for better mobile layout in addition groups and big numbers scale controls.
This commit is contained in:
parent
3f666cf1b5
commit
669fbd59fa
8 changed files with 436 additions and 122 deletions
|
|
@ -137,6 +137,8 @@ Live chapters live under `content/{subject}/{slug}/`:
|
|||
|
||||
**Math equation layout (all subjects):** for chained calculations, **always prefer multi-line KaTeX** and **always align at the equal sign** using `aligned` (e.g. `\begin{aligned} ... &= ... \\ ... \end{aligned}`). Do not write long one-line chains; aligned multi-line steps are the default for readability and mobile layout.
|
||||
|
||||
**Dialog/modals in widgets:** when adding a `<dialog>` in chapter widgets, use the shared modal pattern and classes from `themes/openfreebooks/sass/_contributors.scss` (`contributor-dialog`, `contributor-dialog-panel`, `contributor-dialog-close`) as the base style, keep the dialog centered in modal mode, and only layer minimal widget-specific overrides on top.
|
||||
|
||||
### Interactive demo technology choices
|
||||
|
||||
Interactive demos can use different technologies based on the learning goal and interaction type:
|
||||
|
|
|
|||
|
|
@ -59,13 +59,26 @@ strand = "Number & Algebra"
|
|||
<p>
|
||||
Factorise: \(x^2 - 1 = (x-1)(x+1)\) and \(x^2 + x = x(x+1)\). Cancel the common factor
|
||||
\((x+1)\):
|
||||
\[ \frac{(x-1)(x+1)}{x(x+1)} = \frac{x-1}{x}, \quad x \neq 0,\; x \neq -1. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{(x-1)(x+1)}{x(x+1)}
|
||||
&= \frac{x-1}{x}, \\
|
||||
x &\neq 0,\; x \neq -1
|
||||
\end{aligned}
|
||||
\]
|
||||
The restrictions matter: at \(x = -1\) the original denominator is zero.
|
||||
</p>
|
||||
|
||||
<p><strong>Example.</strong> Simplify \(\dfrac{2x^2 - 8}{x^2 - 4}\).</p>
|
||||
<p>
|
||||
\[ \frac{2(x^2-4)}{x^2-4} = \frac{2(x-2)(x+2)}{(x-2)(x+2)} = 2, \quad x \neq 2,\; x \neq -2. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{2(x^2-4)}{x^2-4}
|
||||
&= \frac{2(x-2)(x+2)}{(x-2)(x+2)} \\
|
||||
&= 2, \\
|
||||
x &\neq 2,\; x \neq -2
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<h2 id="add-subtract" class="book-prose__heading">Adding and subtracting</h2>
|
||||
|
|
@ -77,13 +90,26 @@ strand = "Number & Algebra"
|
|||
<p><strong>Example.</strong> \(\dfrac{2}{x} + \dfrac{3}{x+1}\).</p>
|
||||
<p>
|
||||
LCD is \(x(x+1)\):
|
||||
\[ \frac{2}{x} + \frac{3}{x+1} = \frac{2(x+1) + 3x}{x(x+1)} = \frac{5x+2}{x(x+1)}, \quad x \neq 0,\; x \neq -1. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{2}{x} + \frac{3}{x+1}
|
||||
&= \frac{2(x+1) + 3x}{x(x+1)} \\
|
||||
&= \frac{5x+2}{x(x+1)}, \\
|
||||
x &\neq 0,\; x \neq -1
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<p><strong>Example.</strong> \(\dfrac{1}{x-2} - \dfrac{3}{x^2 - 4}\).</p>
|
||||
<p>
|
||||
Note \(x^2 - 4 = (x-2)(x+2)\). With LCD \((x-2)(x+2)\):
|
||||
\[ \frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)} = \frac{x-1}{(x-2)(x+2)}, \quad x \neq 2,\; x \neq -2. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)}
|
||||
&= \frac{x-1}{(x-2)(x+2)}, \\
|
||||
x &\neq 2,\; x \neq -2
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<h2 id="multiply-divide" class="book-prose__heading">Multiplying and dividing</h2>
|
||||
|
|
@ -101,12 +127,25 @@ strand = "Number & Algebra"
|
|||
|
||||
<p><strong>Example.</strong> \(\dfrac{x^2 - 9}{2x} \times \dfrac{4}{x+3}\).</p>
|
||||
<p>
|
||||
\[ \frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3} = \frac{4(x-3)}{2x} = \frac{2(x-3)}{x}, \quad x \neq 0,\; x \neq -3. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3}
|
||||
&= \frac{4(x-3)}{2x} \\
|
||||
&= \frac{2(x-3)}{x}, \\
|
||||
x &\neq 0,\; x \neq -3
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<p><strong>Example.</strong> \(\dfrac{x^2 - 1}{x+1} \div \dfrac{x-1}{2}\).</p>
|
||||
<p>
|
||||
\[ \frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1} = 2, \quad x \neq -1,\; x \neq 1. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1}
|
||||
&= 2, \\
|
||||
x &\neq -1,\; x \neq 1
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<h2 id="harder-cases" class="book-prose__heading">Harder examples</h2>
|
||||
|
|
@ -273,8 +312,16 @@ strand = "Number & Algebra"
|
|||
<h3 id="uniform-motion" class="book-prose__heading">Uniform motion</h3>
|
||||
<p>
|
||||
For constant speed, distance \(d\), speed \(v\), and time \(t\) are related by \(v = d/t\). If a
|
||||
train travels \(240\) km in \(3\) h, then \(v = 240/3 = 80\) km/h. If you know \(v\) and \(t\)
|
||||
but need distance, rearrange to \(d = vt\).
|
||||
train travels \(240\) km in \(3\) h, then:
|
||||
</p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
v &= \frac{240}{3} \\
|
||||
&= 80\ \text{km/h}
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p>
|
||||
If you know \(v\) and \(t\) but need distance, rearrange to \(d = vt\).
|
||||
</p>
|
||||
|
||||
<div class="math-widget-mount" data-widget="formula-rearrange-demo" data-pagefind-ignore></div>
|
||||
|
|
@ -282,8 +329,16 @@ strand = "Number & Algebra"
|
|||
<h3 id="density" class="book-prose__heading">Density</h3>
|
||||
<p>
|
||||
Density \(\rho\) (rho) is mass per unit volume: \(\rho = m/V\). A block of mass \(2.7\) kg with
|
||||
volume \(0.001\) m³ has \(\rho = 2.7/0.001 = 2700\) kg/m³. To find mass from \(\rho\) and \(V\),
|
||||
use \(m = \rho V\).
|
||||
volume \(0.001\) m³ has:
|
||||
</p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
\rho &= \frac{2.7}{0.001} \\
|
||||
&= 2700\ \text{kg/m}^3
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p>
|
||||
To find mass from \(\rho\) and \(V\), use \(m = \rho V\).
|
||||
</p>
|
||||
|
||||
<h3 id="pressure-circuits" class="book-prose__heading">Pressure and electric circuits</h3>
|
||||
|
|
@ -408,7 +463,14 @@ strand = "Number & Algebra"
|
|||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Make \(t\) the subject of \(v = \dfrac{d}{t}\) and then find \(t\) when \(d = 150\) km and \(v = 75\) km/h.</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(t = d/v = 2\) h.</p>
|
||||
<p><strong>Answer:</strong></p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
t &= \frac{d}{v} \\
|
||||
&= \frac{150}{75} \\
|
||||
&= 2\ \text{h}
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p><strong>Steps:</strong> \(vt = d \Rightarrow t = d/v\); substitute values.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
|
@ -424,7 +486,20 @@ strand = "Number & Algebra"
|
|||
<details class="book-question">
|
||||
<summary class="book-question__prompt">A formula is \(P = \dfrac{F}{A}\). The force is \(120\) N and the area is \(0.05\) m². Find \(P\), then make \(F\) the subject and verify your force value.</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(P = 120/0.05 = 2400\) Pa; \(F = PA = 2400 \times 0.05 = 120\) N.</p>
|
||||
<p><strong>Answer:</strong></p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
P &= \frac{120}{0.05} \\
|
||||
&= 2400\ \text{Pa}
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
F &= PA \\
|
||||
&= 2400 \times 0.05 \\
|
||||
&= 120\ \text{N}
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p><strong>Explanation:</strong> Rearranging checks that the formula is consistent.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -48,13 +48,26 @@
|
|||
<p>
|
||||
Factorise: \(x^2 - 1 = (x-1)(x+1)\) and \(x^2 + x = x(x+1)\). Cancel the common factor
|
||||
\((x+1)\):
|
||||
\[ \frac{(x-1)(x+1)}{x(x+1)} = \frac{x-1}{x}, \quad x \neq 0,\; x \neq -1. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{(x-1)(x+1)}{x(x+1)}
|
||||
&= \frac{x-1}{x}, \\
|
||||
x &\neq 0,\; x \neq -1
|
||||
\end{aligned}
|
||||
\]
|
||||
The restrictions matter: at \(x = -1\) the original denominator is zero.
|
||||
</p>
|
||||
|
||||
<p><strong>Example.</strong> Simplify \(\dfrac{2x^2 - 8}{x^2 - 4}\).</p>
|
||||
<p>
|
||||
\[ \frac{2(x^2-4)}{x^2-4} = \frac{2(x-2)(x+2)}{(x-2)(x+2)} = 2, \quad x \neq 2,\; x \neq -2. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{2(x^2-4)}{x^2-4}
|
||||
&= \frac{2(x-2)(x+2)}{(x-2)(x+2)} \\
|
||||
&= 2, \\
|
||||
x &\neq 2,\; x \neq -2
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<h2 id="add-subtract" class="book-prose__heading">Adding and subtracting</h2>
|
||||
|
|
@ -66,13 +79,26 @@
|
|||
<p><strong>Example.</strong> \(\dfrac{2}{x} + \dfrac{3}{x+1}\).</p>
|
||||
<p>
|
||||
LCD is \(x(x+1)\):
|
||||
\[ \frac{2}{x} + \frac{3}{x+1} = \frac{2(x+1) + 3x}{x(x+1)} = \frac{5x+2}{x(x+1)}, \quad x \neq 0,\; x \neq -1. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{2}{x} + \frac{3}{x+1}
|
||||
&= \frac{2(x+1) + 3x}{x(x+1)} \\
|
||||
&= \frac{5x+2}{x(x+1)}, \\
|
||||
x &\neq 0,\; x \neq -1
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<p><strong>Example.</strong> \(\dfrac{1}{x-2} - \dfrac{3}{x^2 - 4}\).</p>
|
||||
<p>
|
||||
Note \(x^2 - 4 = (x-2)(x+2)\). With LCD \((x-2)(x+2)\):
|
||||
\[ \frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)} = \frac{x-1}{(x-2)(x+2)}, \quad x \neq 2,\; x \neq -2. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)}
|
||||
&= \frac{x-1}{(x-2)(x+2)}, \\
|
||||
x &\neq 2,\; x \neq -2
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<h2 id="multiply-divide" class="book-prose__heading">Multiplying and dividing</h2>
|
||||
|
|
@ -90,12 +116,25 @@
|
|||
|
||||
<p><strong>Example.</strong> \(\dfrac{x^2 - 9}{2x} \times \dfrac{4}{x+3}\).</p>
|
||||
<p>
|
||||
\[ \frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3} = \frac{4(x-3)}{2x} = \frac{2(x-3)}{x}, \quad x \neq 0,\; x \neq -3. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3}
|
||||
&= \frac{4(x-3)}{2x} \\
|
||||
&= \frac{2(x-3)}{x}, \\
|
||||
x &\neq 0,\; x \neq -3
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<p><strong>Example.</strong> \(\dfrac{x^2 - 1}{x+1} \div \dfrac{x-1}{2}\).</p>
|
||||
<p>
|
||||
\[ \frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1} = 2, \quad x \neq -1,\; x \neq 1. \]
|
||||
\[
|
||||
\begin{aligned}
|
||||
\frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1}
|
||||
&= 2, \\
|
||||
x &\neq -1,\; x \neq 1
|
||||
\end{aligned}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<h2 id="harder-cases" class="book-prose__heading">Harder examples</h2>
|
||||
|
|
|
|||
|
|
@ -90,8 +90,16 @@
|
|||
<h3 id="uniform-motion" class="book-prose__heading">Uniform motion</h3>
|
||||
<p>
|
||||
For constant speed, distance \(d\), speed \(v\), and time \(t\) are related by \(v = d/t\). If a
|
||||
train travels \(240\) km in \(3\) h, then \(v = 240/3 = 80\) km/h. If you know \(v\) and \(t\)
|
||||
but need distance, rearrange to \(d = vt\).
|
||||
train travels \(240\) km in \(3\) h, then:
|
||||
</p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
v &= \frac{240}{3} \\
|
||||
&= 80\ \text{km/h}
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p>
|
||||
If you know \(v\) and \(t\) but need distance, rearrange to \(d = vt\).
|
||||
</p>
|
||||
|
||||
<div class="math-widget-mount" data-widget="formula-rearrange-demo" data-pagefind-ignore></div>
|
||||
|
|
@ -99,8 +107,16 @@
|
|||
<h3 id="density" class="book-prose__heading">Density</h3>
|
||||
<p>
|
||||
Density \(\rho\) (rho) is mass per unit volume: \(\rho = m/V\). A block of mass \(2.7\) kg with
|
||||
volume \(0.001\) m³ has \(\rho = 2.7/0.001 = 2700\) kg/m³. To find mass from \(\rho\) and \(V\),
|
||||
use \(m = \rho V\).
|
||||
volume \(0.001\) m³ has:
|
||||
</p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
\rho &= \frac{2.7}{0.001} \\
|
||||
&= 2700\ \text{kg/m}^3
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p>
|
||||
To find mass from \(\rho\) and \(V\), use \(m = \rho V\).
|
||||
</p>
|
||||
|
||||
<h3 id="pressure-circuits" class="book-prose__heading">Pressure and electric circuits</h3>
|
||||
|
|
@ -225,7 +241,14 @@
|
|||
<details class="book-question">
|
||||
<summary class="book-question__prompt">Make \(t\) the subject of \(v = \dfrac{d}{t}\) and then find \(t\) when \(d = 150\) km and \(v = 75\) km/h.</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(t = d/v = 2\) h.</p>
|
||||
<p><strong>Answer:</strong></p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
t &= \frac{d}{v} \\
|
||||
&= \frac{150}{75} \\
|
||||
&= 2\ \text{h}
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p><strong>Steps:</strong> \(vt = d \Rightarrow t = d/v\); substitute values.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
|
@ -241,7 +264,20 @@
|
|||
<details class="book-question">
|
||||
<summary class="book-question__prompt">A formula is \(P = \dfrac{F}{A}\). The force is \(120\) N and the area is \(0.05\) m². Find \(P\), then make \(F\) the subject and verify your force value.</summary>
|
||||
<div class="book-question__solution">
|
||||
<p><strong>Answer:</strong> \(P = 120/0.05 = 2400\) Pa; \(F = PA = 2400 \times 0.05 = 120\) N.</p>
|
||||
<p><strong>Answer:</strong></p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
P &= \frac{120}{0.05} \\
|
||||
&= 2400\ \text{Pa}
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p class="book-formula">\[
|
||||
\begin{aligned}
|
||||
F &= PA \\
|
||||
&= 2400 \times 0.05 \\
|
||||
&= 120\ \text{N}
|
||||
\end{aligned}
|
||||
\]</p>
|
||||
<p><strong>Explanation:</strong> Rearranging checks that the formula is consistent.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -235,6 +235,33 @@
|
|||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.addition-groups__piles {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.addition-groups__pile {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.addition-groups__dots {
|
||||
width: 96px;
|
||||
min-height: 78px;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.addition-groups__dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.addition-groups__plus,
|
||||
.addition-groups__equals {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.multiplication-array__grid {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
|
@ -587,11 +614,6 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.big-numbers-scale__canvas:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.big-numbers-scale__status {
|
||||
|
|
@ -619,6 +641,36 @@
|
|||
gap: 10px;
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.1rem;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls-dialog {
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls-dialog__sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls-dialog__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls-dialog__title {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--soft-ink);
|
||||
}
|
||||
|
||||
.big-numbers-scale__section {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
|
|
@ -710,4 +762,63 @@
|
|||
.big-numbers-scale__hud {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.big-numbers-scale__controls-toggle {
|
||||
display: none;
|
||||
}
|
||||
.big-numbers-scale__controls-dialog {
|
||||
display: block;
|
||||
position: static;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.big-numbers-scale__controls-dialog__sheet {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 959px) {
|
||||
.big-numbers-scale__layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls-dialog {
|
||||
width: min(92vw, 420px);
|
||||
max-width: none;
|
||||
margin: auto;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls-dialog__sheet {
|
||||
padding: 18px 18px 14px;
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
transition: opacity 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls-dialog[open] .big-numbers-scale__controls-dialog__sheet {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls-dialog__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.big-numbers-scale__controls {
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ function BigNumbersScale() {
|
|||
const soundToggle = byId<HTMLButtonElement>("soundToggle");
|
||||
const soundIcon = byId<HTMLSpanElement>("soundIcon");
|
||||
const soundText = byId<HTMLSpanElement>("soundText");
|
||||
const controlsToggleBtn = byId<HTMLButtonElement>("controlsToggleBtn");
|
||||
const controlsDialog = byId<HTMLDialogElement>("controlsDialog");
|
||||
const presetStepsContainer = byId<HTMLDivElement>("presetStepsContainer");
|
||||
|
||||
if (
|
||||
|
|
@ -49,6 +51,8 @@ function BigNumbersScale() {
|
|||
!soundToggle ||
|
||||
!soundIcon ||
|
||||
!soundText ||
|
||||
!controlsToggleBtn ||
|
||||
!controlsDialog ||
|
||||
!presetStepsContainer
|
||||
) {
|
||||
return;
|
||||
|
|
@ -121,11 +125,6 @@ function BigNumbersScale() {
|
|||
currentTierIdx: 0,
|
||||
visualMode: "NORMAL" as VisualMode,
|
||||
transitionProgress: 0,
|
||||
panX: 0,
|
||||
panY: 0,
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
particles: [] as Particle[],
|
||||
ambientStars: [] as CosmicStar[],
|
||||
shockwaves: [] as Shockwave[],
|
||||
|
|
@ -555,8 +554,8 @@ function BigNumbersScale() {
|
|||
ctx.fillStyle = canvasBg;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
const cx = width / 2 + state.panX;
|
||||
const cy = height / 2 + state.panY;
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
|
||||
state.ambientStars.forEach((s) => s.draw(cx, cy, starColor));
|
||||
|
||||
|
|
@ -566,8 +565,8 @@ function BigNumbersScale() {
|
|||
const gridSpacing = 40;
|
||||
const gridCols = Math.ceil(width / gridSpacing);
|
||||
const gridRows = Math.ceil(height / gridSpacing);
|
||||
const startX = state.panX % gridSpacing;
|
||||
const startY = state.panY % gridSpacing;
|
||||
const startX = 0;
|
||||
const startY = 0;
|
||||
for (let i = 0; i < gridCols; i += 1) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX + i * gridSpacing, 0);
|
||||
|
|
@ -711,47 +710,33 @@ function BigNumbersScale() {
|
|||
}
|
||||
});
|
||||
|
||||
const toLocal = (evtX: number, evtY: number) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return { x: evtX - rect.left, y: evtY - rect.top };
|
||||
const isMobileControlsMode = () => window.matchMedia("(max-width: 959px)").matches;
|
||||
|
||||
const onControlsToggleClick = () => {
|
||||
if (!isMobileControlsMode()) return;
|
||||
if (!controlsDialog.open) {
|
||||
controlsDialog.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
canvas.addEventListener("mousedown", (e) => {
|
||||
state.isDragging = true;
|
||||
const p = toLocal(e.clientX, e.clientY);
|
||||
state.startX = p.x - state.panX;
|
||||
state.startY = p.y - state.panY;
|
||||
});
|
||||
const onControlsDialogClick = (event: MouseEvent) => {
|
||||
if (event.target === controlsDialog) {
|
||||
controlsDialog.close();
|
||||
return;
|
||||
}
|
||||
if (!isMobileControlsMode()) return;
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
const clickedButton = target.closest("button");
|
||||
if (!clickedButton) return;
|
||||
if (clickedButton.id === "controlsToggleBtn") return;
|
||||
requestAnimationFrame(() => {
|
||||
if (controlsDialog.open) controlsDialog.close();
|
||||
});
|
||||
};
|
||||
|
||||
canvas.addEventListener("mousemove", (e) => {
|
||||
if (!state.isDragging) return;
|
||||
const p = toLocal(e.clientX, e.clientY);
|
||||
state.panX = p.x - state.startX;
|
||||
state.panY = p.y - state.startY;
|
||||
});
|
||||
|
||||
window.addEventListener("mouseup", () => {
|
||||
state.isDragging = false;
|
||||
});
|
||||
|
||||
canvas.addEventListener("touchstart", (e) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
state.isDragging = true;
|
||||
const p = toLocal(e.touches[0]!.clientX, e.touches[0]!.clientY);
|
||||
state.startX = p.x - state.panX;
|
||||
state.startY = p.y - state.panY;
|
||||
});
|
||||
|
||||
canvas.addEventListener("touchmove", (e) => {
|
||||
if (!state.isDragging || e.touches.length !== 1) return;
|
||||
const p = toLocal(e.touches[0]!.clientX, e.touches[0]!.clientY);
|
||||
state.panX = p.x - state.startX;
|
||||
state.panY = p.y - state.startY;
|
||||
});
|
||||
|
||||
canvas.addEventListener("touchend", () => {
|
||||
state.isDragging = false;
|
||||
});
|
||||
controlsToggleBtn.addEventListener("click", onControlsToggleClick);
|
||||
controlsDialog.addEventListener("click", onControlsDialogClick);
|
||||
|
||||
let rafId = 0;
|
||||
let lastTime = performance.now();
|
||||
|
|
@ -786,6 +771,8 @@ function BigNumbersScale() {
|
|||
onCleanup(() => {
|
||||
cancelAnimationFrame(rafId);
|
||||
window.removeEventListener("resize", resizeCanvas);
|
||||
controlsToggleBtn.removeEventListener("click", onControlsToggleClick);
|
||||
controlsDialog.removeEventListener("click", onControlsDialogClick);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -801,6 +788,16 @@ function BigNumbersScale() {
|
|||
<span id="soundIcon">🔇</span>
|
||||
<span id="soundText">Muted</span>
|
||||
</button>
|
||||
<button
|
||||
id="controlsToggleBtn"
|
||||
type="button"
|
||||
class="big-numbers-scale__tool-btn big-numbers-scale__controls-toggle"
|
||||
aria-label="Open controls panel"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="controlsDialog"
|
||||
>
|
||||
⚙
|
||||
</button>
|
||||
<span id="speedIndicatorText" class="big-numbers-scale__badge">
|
||||
Speed: Normal
|
||||
</span>
|
||||
|
|
@ -840,49 +837,64 @@ function BigNumbersScale() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="big-numbers-scale__controls">
|
||||
<div class="big-numbers-scale__section">
|
||||
<h3 class="big-numbers-scale__section-title">Mathematical milestones</h3>
|
||||
<div id="presetStepsContainer" class="big-numbers-scale__preset-grid" />
|
||||
</div>
|
||||
<div class="big-numbers-scale__section">
|
||||
<h3 class="big-numbers-scale__section-title">Motion control</h3>
|
||||
<div class="big-numbers-scale__speed-grid">
|
||||
<button type="button" class="speed-btn" data-speed="slow">
|
||||
Slow
|
||||
</button>
|
||||
<button type="button" class="speed-btn speed-btn--active" data-speed="normal">
|
||||
Normal
|
||||
</button>
|
||||
<button type="button" class="speed-btn" data-speed="fast">
|
||||
Fast
|
||||
<dialog id="controlsDialog" class="big-numbers-scale__controls-dialog contributor-dialog">
|
||||
<div class="big-numbers-scale__controls-dialog__sheet contributor-dialog-panel">
|
||||
<div class="big-numbers-scale__controls-dialog__header">
|
||||
<h3 class="big-numbers-scale__controls-dialog__title">Controls</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="contributor-dialog-close"
|
||||
aria-label="Close controls panel"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="big-numbers-scale__nav-row">
|
||||
<button id="btnPrevStep" type="button" class="big-numbers-scale__tool-btn">
|
||||
Down step
|
||||
</button>
|
||||
<button id="btnPlayPause" type="button" class="big-numbers-scale__autoplay-btn">
|
||||
Auto Increment
|
||||
</button>
|
||||
<button id="btnNextStep" type="button" class="big-numbers-scale__tool-btn">
|
||||
Up step
|
||||
</button>
|
||||
|
||||
<div class="big-numbers-scale__controls">
|
||||
<div class="big-numbers-scale__section">
|
||||
<h3 class="big-numbers-scale__section-title">Mathematical milestones</h3>
|
||||
<div id="presetStepsContainer" class="big-numbers-scale__preset-grid" />
|
||||
</div>
|
||||
<div class="big-numbers-scale__section">
|
||||
<h3 class="big-numbers-scale__section-title">Motion control</h3>
|
||||
<div class="big-numbers-scale__speed-grid">
|
||||
<button type="button" class="speed-btn" data-speed="slow">
|
||||
Slow
|
||||
</button>
|
||||
<button type="button" class="speed-btn speed-btn--active" data-speed="normal">
|
||||
Normal
|
||||
</button>
|
||||
<button type="button" class="speed-btn" data-speed="fast">
|
||||
Fast
|
||||
</button>
|
||||
</div>
|
||||
<div class="big-numbers-scale__nav-row">
|
||||
<button id="btnPrevStep" type="button" class="big-numbers-scale__tool-btn">
|
||||
Down step
|
||||
</button>
|
||||
<button id="btnPlayPause" type="button" class="big-numbers-scale__autoplay-btn">
|
||||
Auto Increment
|
||||
</button>
|
||||
<button id="btnNextStep" type="button" class="big-numbers-scale__tool-btn">
|
||||
Up step
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="big-numbers-scale__section big-numbers-scale__stats">
|
||||
<h3 class="big-numbers-scale__section-title">Time to count individually</h3>
|
||||
<p class="big-numbers-scale__stat-text">
|
||||
Counting to <span id="analogyValueText">1</span> at one number per second takes:
|
||||
</p>
|
||||
<div id="timeStatValue" class="big-numbers-scale__stat-value">
|
||||
1 Second
|
||||
</div>
|
||||
<p id="analogyText" class="big-numbers-scale__analogy">
|
||||
"A single heartbeat. The primary unit of all structures."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="big-numbers-scale__section big-numbers-scale__stats">
|
||||
<h3 class="big-numbers-scale__section-title">Time to count individually</h3>
|
||||
<p class="big-numbers-scale__stat-text">
|
||||
Counting to <span id="analogyValueText">1</span> at one number per second takes:
|
||||
</p>
|
||||
<div id="timeStatValue" class="big-numbers-scale__stat-value">
|
||||
1 Second
|
||||
</div>
|
||||
<p id="analogyText" class="big-numbers-scale__analogy">
|
||||
"A single heartbeat. The primary unit of all structures."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -102,14 +102,45 @@ export function observeMathAutoRender(root: ParentNode = document) {
|
|||
|
||||
let scheduled = false;
|
||||
let rendering = false;
|
||||
const pendingTargets = new Set<HTMLElement>();
|
||||
|
||||
const queueTargetFromNode = (node: Node) => {
|
||||
if (nodeInsideKatex(node)) return;
|
||||
|
||||
if (node instanceof HTMLElement) {
|
||||
if (node.matches(AUTO_RENDER_TARGETS)) {
|
||||
pendingTargets.add(node);
|
||||
}
|
||||
node.querySelectorAll<HTMLElement>(AUTO_RENDER_TARGETS).forEach((el) => {
|
||||
pendingTargets.add(el);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (node instanceof DocumentFragment) {
|
||||
node.querySelectorAll<HTMLElement>(AUTO_RENDER_TARGETS).forEach((el) => {
|
||||
pendingTargets.add(el);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = node.parentElement;
|
||||
if (!parent || nodeInsideKatex(parent)) return;
|
||||
const host = parent.closest<HTMLElement>(AUTO_RENDER_TARGETS);
|
||||
if (host) pendingTargets.add(host);
|
||||
};
|
||||
|
||||
const scheduleRender = () => {
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
scheduled = false;
|
||||
if (pendingTargets.size === 0) return;
|
||||
|
||||
rendering = true;
|
||||
renderBookMath(root, { force: true });
|
||||
const targets = Array.from(pendingTargets);
|
||||
pendingTargets.clear();
|
||||
targets.forEach((el) => renderMathInContainer(el));
|
||||
rendering = false;
|
||||
});
|
||||
};
|
||||
|
|
@ -118,11 +149,19 @@ export function observeMathAutoRender(root: ParentNode = document) {
|
|||
if (rendering) return;
|
||||
const hasExternalChange = mutations.some((mutation) => {
|
||||
if (nodeInsideKatex(mutation.target)) return false;
|
||||
if (mutation.type === "characterData") return true;
|
||||
if (mutation.addedNodes.length === 0 && mutation.removedNodes.length === 0) return false;
|
||||
const addedInsideKatex = Array.from(mutation.addedNodes).every(nodeInsideKatex);
|
||||
const removedInsideKatex = Array.from(mutation.removedNodes).every(nodeInsideKatex);
|
||||
return !(addedInsideKatex && removedInsideKatex);
|
||||
|
||||
if (mutation.type === "characterData") {
|
||||
const parent = mutation.target.parentElement;
|
||||
if (!parent || nodeInsideKatex(parent)) return false;
|
||||
const host = parent.closest<HTMLElement>(AUTO_RENDER_TARGETS);
|
||||
if (!host) return false;
|
||||
delete host.dataset.katexRendered;
|
||||
pendingTargets.add(host);
|
||||
return true;
|
||||
}
|
||||
|
||||
mutation.addedNodes.forEach((node) => queueTargetFromNode(node));
|
||||
return mutation.addedNodes.length > 0;
|
||||
});
|
||||
if (hasExternalChange) scheduleRender();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ let stopMathObserver: (() => void) | null = null;
|
|||
function scheduleBookMath() {
|
||||
void import("./lib/katex").then(({ observeMathAutoRender, renderBookMath }) => {
|
||||
requestAnimationFrame(() => {
|
||||
renderBookMath(document, { force: true });
|
||||
renderBookMath(document);
|
||||
if (!stopMathObserver) {
|
||||
stopMathObserver = observeMathAutoRender(document);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue