commit 39283bf94f7bab05a72ca0351a6e26aa0e1c7d36
parent 785d1821c6fff9735057c24944fdd5b36d943463
Author: hrryt <harry@mayesfield.uk>
Date: Sun, 16 Aug 2026 19:09:17 +0100
Use KaTeX, allow question window caching
Diffstat:
14 files changed, 54 insertions(+), 51 deletions(-)
diff --git a/TODO.md b/TODO.md
@@ -1,7 +1,8 @@
# Fixes
-- Use KaTeX_Math font outside MathML
- Match SVG IDs in production
-- Prevent components rendering outside graph bounds (with mask?) due to preserveAspectRatio
+- Hide components outside graph bounds (with mask?) due to preserveAspectRatio
+- Stop question windows rerendering when following links to different question windows
+- Adjust spacing for adding fractions
# Questions
- Improve adding fractions
@@ -9,7 +10,6 @@
- Add graphing questions
- Find all roots of this quintic (or something) polynomial (repeated root knowledge)
- Transformations of shapes and graphs (curves)
- - Refactor plotting context to add viewBox elements
- Drop-down for finer controls
- tricky parallel lines / internal angles etc questions
- automate allQuestions using file exploration
diff --git a/package.json b/package.json
@@ -8,7 +8,7 @@
},
"dependencies": {
"98.css": "^0.1.21",
- "kern-typ": "^0.2.0",
+ "katex": "^0.18.4",
"preact": "^10.26.9",
"preact-iso": "^2.12.1"
},
diff --git a/src/components/Equation.jsx b/src/components/Equation.jsx
@@ -1,12 +1,10 @@
-import { useRef, useEffect } from 'preact/hooks';
-import { render } from 'kern-typ';
+import { render } from 'katex';
export default function Equation({ equation, displayMode = false }) {
- const ref = useRef(null);
- useEffect(() => {
- render(equation, ref.current, { displayMode: displayMode });
- }, [equation]);
+ function renderEquation(dom) {
+ dom === null || render(equation, dom, { displayMode: displayMode });
+ }
- return <span ref={ref}>{equation}</span>;
+ return <span ref={renderEquation}></span>;
}
diff --git a/src/components/QuestionWindow.jsx b/src/components/QuestionWindow.jsx
@@ -84,7 +84,7 @@ export default function QuestionWindow({ data, title }) {
Refresh Questions
</button>
</fieldset>
- <div class="field-border question-field kern">
+ <div class="field-border">
<ol class="master-question-list">
{questions}
</ol>
diff --git a/src/components/RotationTextElement.jsx b/src/components/RotationTextElement.jsx
@@ -9,5 +9,5 @@ export default function RotationTextElement({ angle }) {
if (Math.abs(angle) == 180) { directionText = "" }
else { directionText = angle < 180 ? 'clockwise' : 'anticlockwise' }
angle = angle > 180 ? 360 - angle : angle
- return <span><Equation equation={angle + 'degree'} /> {directionText}</span>
+ return <span><Equation equation={angle + '\\degree'} /> {directionText}</span>
}
diff --git a/src/components/plotting/Plot.jsx b/src/components/plotting/Plot.jsx
@@ -28,14 +28,14 @@ function AxisTicks() {
<g>
{Array.from({ length: p.width - 1 }).map((_, i) => {
const x = i + p.minX + 1;
- if (x == 0 || x % every) { return; }
+ if (x % every || x === 0) { return; }
return <Label point={[x, 0]} position="bottom">{x}</Label>;
})}
</g>
<g>
{Array.from({ length: p.height - 1 }).map((_, i) => {
const y = i + p.minY + 1;
- if (y == 0 || y % every) { return; }
+ if (y % every || y === 0) { return; }
return <Label point={[0, y]} position="left">{y}</Label>;
})}
</g>
@@ -50,7 +50,7 @@ function AxisTicks() {
}
export default function Plot({ children, mode="graph", viewBox, width="300", height="300" }) {
- const id = `graph-${Math.floor(1000 * Math.random())}`;
+ const id = `plot-${Math.floor(10000 * Math.random())}`;
const scaleFactor = Math.max(viewBox[2] / width, viewBox[3] / height);
const p = {
minX: viewBox[0],
@@ -60,7 +60,7 @@ export default function Plot({ children, mode="graph", viewBox, width="300", hei
maxX: viewBox[0] + viewBox[2],
maxY: viewBox[1] + viewBox[3],
strokeWidth: 2.5 * scaleFactor,
- }
+ };
const style = `
g#${id} text {
font-size: ${scaleFactor}em;
diff --git a/src/components/questions/DoubleReflectionQuestion.jsx b/src/components/questions/DoubleReflectionQuestion.jsx
@@ -6,7 +6,7 @@ import Point from "../plotting/Point";
import Equation from "../Equation";
import RotationTextElement from "../RotationTextElement";
import { getRandomChoice, getRandomInt } from "../../scripts/randomUtils";
-import { formatLine } from "../../scripts/formatUtils";
+import { formatLine, formatVec } from "../../scripts/formatUtils";
function reflectPoint(point, line = {a: 1, b: 1, c: 0}) {
const [px, py] = point
@@ -62,9 +62,7 @@ export default function DoubleReflectionQuestion({ seed, showAnswer }) {
const question = (<>
<p>
- Reflect P in <Equation equation={formatLine(line1)} /> then <Equation equation={formatLine(line2)} />.
- </p>
- <p>
+ Reflect P in <Equation equation={formatLine(line1)} /> then <Equation equation={formatLine(line2)} />.<br />
What single transformation describes this?
</p>
<Plot viewBox={viewBox} width="300" height="300">
@@ -83,10 +81,10 @@ export default function DoubleReflectionQuestion({ seed, showAnswer }) {
if (intersect === null) { // translation, lines parallel
const xTrans = 2*(line1.c*line1.a - line2.c*line2.a)
const yTrans = 2*(line1.c*line1.b - line2.c*line2.b)
- answerTextElement = <p>Translation by vector <Equation equation={`vec(${xTrans}, ${yTrans})`} /></p>
+ answerTextElement = <p>Translation by vector <Equation equation={formatVec([xTrans, yTrans])} /></p>
} else { // rotation
const angle = 2*(getLineAngle(line2) - getLineAngle(line1))
- answerTextElement = <p>Rotation <RotationTextElement angle={angle} /> about <Equation equation={`(${intersect[0]}, ${intersect[1]})`} /></p>
+ answerTextElement = <p>Rotation <RotationTextElement angle={angle} /> about <Equation equation={`(${intersect[0]}, ${intersect[1]}).`} /></p>
answerGraphElement = <Point point={intersect} />
}
const answer = (<>
diff --git a/src/components/questions/QuadraticEquationQuestion.jsx b/src/components/questions/QuadraticEquationQuestion.jsx
@@ -12,9 +12,9 @@ function formatQuadraticExpression(a, b, c) {
function formatRoots(l, m, n, o) {
if (m / l == o / n) { return `x = ${formatFraction(-m, l)}`; }
- if (-m / l == o / n) { return `x = plus.minus ${formatFraction(Math.abs(-m), l)}`; }
+ if (-m / l == o / n) { return `x = \\pm ${formatFraction(Math.abs(-m), l)}`; }
- return `x = ${formatFraction(-m, l)} "or" x = ${formatFraction(-o, n)}`;
+ return `x = ${formatFraction(-m, l)} \\text{ or } x = ${formatFraction(-o, n)}`;
}
function formatFactor(x_coeff, const_coeff) {
@@ -73,7 +73,7 @@ export default function QuadraticEquationQuestion({ seed, showAnswer, aPlurality
const c = m * o
const quadratic = formatQuadraticExpression(a, b, c);
- const question = factorise ? `"Factorise" ${quadratic}` : `"Solve" ${quadratic} = 0`;
+ const question = factorise ? `\\text{Factorise } ${quadratic}` : `\\text{Solve } ${quadratic} = 0`;
const answer = factorise ? formatFactors(l, m, n, o) : formatRoots(l, m, n, o);
diff --git a/src/components/questions/RotationTranslationQuestion.jsx b/src/components/questions/RotationTranslationQuestion.jsx
@@ -5,6 +5,7 @@ import Question from "../Question";
import Equation from "../Equation";
import RotationTextElement from "../RotationTextElement";
import { getRandomChoice, getRandomInt } from "../../scripts/randomUtils";
+import { formatVec } from "../../scripts/formatUtils";
function rotatePoint(point, angle, axis=[0, 0]) {
const diff = [point[0] - axis[0], point[1] - axis[1]]
@@ -33,9 +34,11 @@ export default function RotationTranslationQuestion({ seed, showAnswer }) {
const endShape = midShape.map((point) => point.map((value, i) => value + translationVector[i]))
const question = (<>
- <p>Shape P is rotated <RotationTextElement angle={angle} /> about point Q to form shape R.</p>
- <p>Shape R is translated by <Equation equation={`vec(${translationVector[0]}, ${translationVector[1]})`} /> to form shape S.</p>
- <p>What single transformation maps shape P onto shape S?</p>
+ <p>
+ Shape P is rotated <RotationTextElement angle={angle} /> about point Q to form shape R.<br />
+ Shape R is translated by <Equation equation={formatVec(translationVector)} /> to form shape S.<br />
+ What single transformation maps shape P onto shape S?
+ </p>
<Plot viewBox={viewBox}>
<Polygon points={originalShape} label="P" colour="1" />
<Point point={ axis1} label="Q" colour="2" />
@@ -43,13 +46,15 @@ export default function RotationTranslationQuestion({ seed, showAnswer }) {
</>)
const answer = (<>
- <p>Rotation <RotationTextElement angle={angle} /> about <Equation equation={`(${axis2[0]}, ${axis2[1]})`} />.</p>
+ <p>
+ Rotation <RotationTextElement angle={angle} /> about <Equation equation={`(${axis2[0]}, ${axis2[1]})`} />.
+ </p>
<Plot viewBox={viewBox}>
<Polygon points={originalShape} label="P" colour="1" />
<Point point={ axis1} label="Q" colour="2" />
<Polygon points={ midShape} label="R" colour="2" />
<Polygon points={ endShape} label="S" colour="3" />
- <Point point={ axis2} colour="3" />
+ <Point point={ axis2} colour="3" />
</Plot>
</>)
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
@@ -16,7 +16,7 @@ export default function Home() {
<main>
<Window title="Home">
<p>Hello World!</p>
- <div class="field-border kern">
+ <div class="field-border">
<Plot viewBox={[-5, -4, 8, 9]}>
<Quadratic a={2} b={2} c={-2} colour="1" />
<Polygon points={[[1,2], [-1,1], [-4,3]]} label="P" colour="2" />
diff --git a/src/pages/QuestionIndex.jsx b/src/pages/QuestionIndex.jsx
@@ -45,20 +45,20 @@ function QuestionsItem({ id, label, questions }) {
);
}
-export default function QuestionIndex() {
+const items = allQuestions.map(({ id, label, questions }) =>
+ <QuestionsItem id={id} label={label} questions={questions} />
+);
- const items = allQuestions.map(({ id, label, questions }) =>
- <QuestionsItem id={id} label={label} questions={questions} />
+const routes = allQuestions.flatMap(({ id, questions }) => questions.map(question => {
+ const Component = lazy(() =>
+ import(`../data/questions/${id}/${question.id}.js`).then(m =>
+ () => <QuestionWindow data={m.data} title={question.label} />
+ )
);
- const routes = allQuestions.flatMap(({ id, label, questions }) => questions.map(question => {
- const Component = lazy(() =>
- import(`../data/questions/${id}/${question.id}.js`).then(
- ({ data }) => () => <QuestionWindow data={data} title={question.label} />
- )
- );
- return <Route path={`/${id}/${question.id}`} component={Component} />;
- }));
+ return <Route path={`/${id}/${question.id}`} component={Component} />;
+}));
+export default function QuestionIndex() {
return (
<main>
<nav class="sidebar">
diff --git a/src/scripts/formatUtils.js b/src/scripts/formatUtils.js
@@ -1,20 +1,22 @@
import { getHCF, simplifyFraction } from './mathsUtils'
+export function formatVec(arr) {
+ return `\\bigl(\\begin{smallmatrix}${arr.join('\\\\')}\\end{smallmatrix}\\bigr)`
+}
+
export function formatCoefficient(x, parameters = { showPlus: false, showOne: true, showZero: true, suffix: '' }) {
if (!parameters.showZero && x == 0) { return ''; }
const plus = parameters.showPlus ? '+' : '';
const sign = x < 0 ? '-' : plus;
x = Math.abs(x);
parameters.showOne || x == 1 && (x = '');
- parameters.suffix || (parameters.suffix = '') // if suffix undefined
- return sign + x + parameters.suffix;
+ return sign + x + (parameters.suffix ?? '');
}
// this thing is probably begging for bugs from simplification and zeroes.
export function formatFraction(num, denom, parameters = { showPlus: false, showOne: true, showZero: true, simplify: false, suffix: '' }) {
if (!parameters.showZero && num == 0) { return ''; }
if (getHCF(num, denom) == denom && parameters.simplify) { return formatCoefficient(simplifyFraction(num, denom)[0], parameters)}
- const [numOriginal, denomOriginal] = [num, denom]
const plus = parameters.showPlus ? '+' : ''
const sign = num / denom < 0 ? '-' : plus
@@ -23,7 +25,7 @@ export function formatFraction(num, denom, parameters = { showPlus: false, showO
[num, denom] = parameters.simplify ? simplifyFraction(num, denom) : [num, denom]
parameters.suffix || (parameters.suffix = '')
- let x = `frac(${num}, ${denom})`
+ let x = `\\frac{${num}}{${denom}}`
parameters.showOne || num / denom == 1 && (x = '');
return sign + x + parameters.suffix
}
diff --git a/src/styles/index.css b/src/styles/index.css
@@ -1,4 +1,4 @@
-@import 'kern-typ/kern.css';
+@import 'katex/dist/katex.min.css';
@import '98.css';
@import './main.css';
@import './svg.css';
diff --git a/src/styles/main.css b/src/styles/main.css
@@ -16,7 +16,7 @@ body {
}
nav.taskbar {
- max-width: 100vw;
+ width: 100vw;
}
button.taskbar-element.active {
@@ -24,8 +24,7 @@ button.taskbar-element.active {
}
.window {
- min-width: min-content;
- max-width: max-content;
+ width: fit-content;
}
fieldset {
@@ -38,6 +37,7 @@ fieldset {
}
ol.master-question-list {
+ font-family: KaTeX_Main;
margin: 0;
padding-left: 20px;
display: flex;