website

source code for the ttmaths.uk website
Log | Files | Refs | README

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:
MTODO.md | 6+++---
Mpackage.json | 2+-
Msrc/components/Equation.jsx | 12+++++-------
Msrc/components/QuestionWindow.jsx | 2+-
Msrc/components/RotationTextElement.jsx | 2+-
Msrc/components/plotting/Plot.jsx | 8++++----
Msrc/components/questions/DoubleReflectionQuestion.jsx | 10++++------
Msrc/components/questions/QuadraticEquationQuestion.jsx | 6+++---
Msrc/components/questions/RotationTranslationQuestion.jsx | 15++++++++++-----
Msrc/pages/Home.jsx | 2+-
Msrc/pages/QuestionIndex.jsx | 22+++++++++++-----------
Msrc/scripts/formatUtils.js | 10++++++----
Msrc/styles/index.css | 2+-
Msrc/styles/main.css | 6+++---
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;