website

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

commit 25690356695b993ea05ed3e2d64479d9daf996f6
parent ff67737a89305a0c837cbdc855c299b15536a6a3
Author: fen123456 <fenwthompson@gmail.com>
Date:   Thu, 13 Aug 2026 12:22:54 +0100

Merge branch 'main' of https://github.com/hrryt/website

Diffstat:
Msrc/components/Equation.jsx | 2+-
Msrc/components/QuestionWindow.jsx | 8+++++---
Dsrc/components/graphing/CoordinateLabel.jsx | 5-----
Msrc/components/graphing/Cubic.jsx | 7++++---
Msrc/components/graphing/Graph.jsx | 34++++++++++++++++++++++++++--------
Msrc/components/graphing/Label.jsx | 9+++++++--
Asrc/components/graphing/Point.jsx | 11+++++++++++
Asrc/components/graphing/Polygon.jsx | 27+++++++++++++++++++++++++++
Msrc/components/graphing/Quadratic.jsx | 7++++---
Msrc/components/questions/AddingFractionsQuestion.jsx | 12++++++------
Msrc/components/questions/DoubleTransformationQuestion.jsx | 64++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/components/questions/QuadraticEquationQuestion.jsx | 25+++++++++----------------
Msrc/pages/Home.jsx | 10++++------
Msrc/scripts/utils.js | 5+++++
Msrc/styles/svg.css | 27+++++++++------------------
15 files changed, 162 insertions(+), 91 deletions(-)

diff --git a/src/components/Equation.jsx b/src/components/Equation.jsx @@ -8,5 +8,5 @@ export default function Equation({ equation, displayMode = false }) { render(equation, ref.current, { displayMode: displayMode }); }, [equation]); - return <div ref={ref}>{equation}</div>; + return <span ref={ref}>{equation}</span>; } diff --git a/src/components/QuestionWindow.jsx b/src/components/QuestionWindow.jsx @@ -3,6 +3,8 @@ import QuestionList from './QuestionList.jsx'; import { getRandomInt, getRandomNumber, getRandomChoice } from '../scripts/utils.js'; import Window from './Window.jsx'; +const seedDiff = 100; + function resolveParameters(question, variables) { const parameters = question.constants ?? { }; const variableKeys = question.variables ?? { }; @@ -16,7 +18,7 @@ function renderQuestion(seed, showAnswer, variables) { return (question, i) => { const QuestionComponent = question.type; const parameters = resolveParameters(question, variables); - return <QuestionComponent key={i} seed={seed+i} showAnswer={showAnswer} {...parameters} />; + return <QuestionComponent key={i} seed={seed+seedDiff*i} showAnswer={showAnswer} {...parameters} />; }; } @@ -61,7 +63,7 @@ export default function QuestionWindow({ data, title }) { const { id: id, n: n, questions: unique_questions } = questionList; const questions = shuffleArray(unique_questions, n, current_seed); const randomQuestionList = getQuestionList(id, questions, showAnswers, current_seed, variables); - current_seed += n; + current_seed += seedDiff * n; return randomQuestionList; }); @@ -76,7 +78,7 @@ export default function QuestionWindow({ data, title }) { <fieldset> <legend>Options</legend> <button onClick={revealAnswers}> - {showAnswers ? "Hide" : "Show"} Answers + {showAnswers ? "Show Questions" : "Show Answers"} </button> <button onClick={triggerRefresh}> Refresh Questions diff --git a/src/components/graphing/CoordinateLabel.jsx b/src/components/graphing/CoordinateLabel.jsx @@ -1,5 +0,0 @@ -import Label from './Label.jsx'; - -export default function CoordinateLabel({x, y, position}) { - return <Label x={x} y={y} position={position}>{`(${x},${y})`}</Label>; -} diff --git a/src/components/graphing/Cubic.jsx b/src/components/graphing/Cubic.jsx @@ -1,6 +1,6 @@ -import { getSymmetricBounds } from '/src/scripts/utils.js'; +import { getSymmetricBounds, getColourClass } from '../../scripts/utils.js'; -export default function Cubic({ a=1, b=0, c=0, d=0, viewBox }) { +export default function Cubic({ a=1, b=0, c=0, d=0, colour="auto", viewBox }) { // https://math.stackexchange.com/questions/3356084/to-construct-a-polynomial-using-b%C3%A9zier-curves // http://graphics.stanford.edu/courses/cs164-09-spring/Handouts/handout19.pdf function g(u, v, w) { return a*u*v*w + b*(v*w + w*u + u*v)/3 + c*(u + v + w)/3 + d; } @@ -10,5 +10,6 @@ export default function Cubic({ a=1, b=0, c=0, d=0, viewBox }) { const x1 = 2/3 * i + 1/3 * f; const x2 = 1/3 * i + 2/3 * f; - return <path d={`M ${i},${g(i,i,i)} C ${x1},${g(i,i,f)} ${x2},${g(i,f,f)} ${f},${g(f,f,f)}`} /> + d = `M ${i},${g(i,i,i)} C ${x1},${g(i,i,f)} ${x2},${g(i,f,f)} ${f},${g(f,f,f)}`; + return <path d={d} class={getColourClass(colour)} /> } diff --git a/src/components/graphing/Graph.jsx b/src/components/graphing/Graph.jsx @@ -10,27 +10,27 @@ function Axes({ viewBox }) { const [ minX, minY, width, height ] = viewBox; return ( <g> - <path id="axes" d={`M ${minX},0 h ${width-.1}`} marker-end="url(#arrow)"/> - <path id="axes" d={`M 0,${minY} v ${height-.1}`} marker-end="url(#arrow)"/> + <path class="axis" d={`M ${minX},0 h ${width-.1}`} marker-end="url(#arrow)"/> + <path class="axis" d={`M 0,${minY} v ${height-.1}`} marker-end="url(#arrow)"/> </g> ); } -function AxisTicks({ viewBox }) { +function AxisTicks({ every = 1, viewBox }) { const [ minX, minY, width, height ] = viewBox; return ( <g> <g> {Array.from({ length: width - 1 }).map((_, i) => { const x = i + minX + 1; - if (x == 0) { return; } + if (x == 0 || x % every) { return; } return <Label x={x} y="0" position="bottom" d={.1}>{x}</Label>; })} </g> <g> {Array.from({ length: height - 1 }).map((_, i) => { const y = i + minY + 1; - if (y == 0) { return; } + if (y == 0 || y % every) { return; } return <Label x="0" y={y} position="left" d={.1}>{y}</Label>; })} </g> @@ -39,7 +39,13 @@ function AxisTicks({ viewBox }) { ) } -export default function Graph({ children, viewBox, width="500", height="500" }) { +export default function Graph({ children, viewBox, width="300", height="300" }) { + const scaleFactor = Math.max(viewBox[2] / width, viewBox[3] / height); + const style = ` + text { + font-size: ${scaleFactor}em; + } + `; return ( <svg width={width} height={height} viewBox={viewBox.join(' ')} @@ -54,13 +60,25 @@ export default function Graph({ children, viewBox, width="500", height="500" }) markerUnits="strokeWidth" orient="auto" > - <path d="M 0,0 L 4,2 L 0,4 z" id='arrowhead' /> + <polygon class='marker' points="0,0 4,2 0,4" /> </marker> + <marker + id="cross" + viewBox="0 0 6 6" + refX="3" refY="3" + markerWidth="6" markerHeight="6" + markerUnits="strokeWidth" + > + <polygon class='marker' points="0,1 1,0 3,2 5,0 6,1 4,3 6,5 5,6 3,4 1,6 0,5 2,3" /> + </marker> + <style> + {style} + </style> </defs> <Grid viewBox={viewBox} /> <Axes viewBox={viewBox} /> + <AxisTicks every={Math.floor(50 * scaleFactor)} viewBox={viewBox} /> {children} - <AxisTicks viewBox={viewBox} /> </svg> ); } diff --git a/src/components/graphing/Label.jsx b/src/components/graphing/Label.jsx @@ -1,5 +1,9 @@ +import { getColourClass } from '../../scripts/utils.js'; + function getPosition(position) { switch (position) { + case 'central': + return ['central', 'middle', 0, 0]; case 'left': return ['central', 'end', -1, 0]; case 'right': @@ -13,13 +17,14 @@ function getPosition(position) { } } -export default function Label({ x, y, position, d=.15, children }) { +export default function Label({ x, y, position='central', colour='auto', d=.15, children }) { const [dominantBaseline, textAnchor, dx, dy] = getPosition(position); return ( <text - x={x} y={-y} dx={d*dx} dy={-d*dy} transform="scale(1 -1)" + x={x} y={-y} dx={d*dx} dy={-d*dy} transform='scale(1 -1)' dominant-baseline={dominantBaseline} text-anchor={textAnchor} + class={getColourClass(colour)} > {children} </text> diff --git a/src/components/graphing/Point.jsx b/src/components/graphing/Point.jsx @@ -0,0 +1,11 @@ +import Label from './Label.jsx'; +import { getColourClass } from '../../scripts/utils.js'; + +export default function Point({ x, y, label=null, colour="auto", position="top" }) { + return ( + <g> + <path marker-start="url(#cross)" d={`M ${x},${y}`} class={getColourClass(colour)}/> + {label && <Label x={x} y={y} position={position} d={.3} colour={colour}>{label}</Label>} + </g> + ); +} diff --git a/src/components/graphing/Polygon.jsx b/src/components/graphing/Polygon.jsx @@ -0,0 +1,27 @@ +import Label from './Label.jsx'; +import { getColourClass } from '../../scripts/utils.js'; + +function getMean(arr) { + return arr.reduce((a, b) => a + b) / arr.length; +} + +function getMiddle(points) { + const x = points.map(point => point[0]); + const y = points.map(point => point[1]); + return [getMean(x), getMean(y)]; +} + +function PolygonLabel({ children, points, colour }) { + const [x, y] = getMiddle(points); + return <Label x={x} y={y} colour={colour}>{children}</Label>; +} + +export default function Polygon({ points, label=null, colour="auto" }) { + const pointsString = points.map((point) => point.join(",")).join(" "); + return ( + <g> + <polygon points={pointsString} class={getColourClass(colour)}/> + {label && <PolygonLabel points={points} colour={colour}>{label}</PolygonLabel>} + </g> + ); +} diff --git a/src/components/graphing/Quadratic.jsx b/src/components/graphing/Quadratic.jsx @@ -1,6 +1,6 @@ -import { getSymmetricBounds } from '/src/scripts/utils.js'; +import { getSymmetricBounds, getColourClass } from '../../scripts/utils.js'; -export default function Quadratic({ a=1, b=0, c=0, viewBox }) { +export default function Quadratic({ a=1, b=0, c=0, colour="auto", viewBox }) { function f(x) { return a*x*x + b*x + c; } const turnX = -b / (2*a); @@ -9,5 +9,6 @@ export default function Quadratic({ a=1, b=0, c=0, viewBox }) { const endY = f(endX); const controlY = 2*f(turnX) - endY; - return <path d={`M ${startX},${endY} Q ${turnX},${controlY} ${endX},${endY}`} />; + const d = `M ${startX},${endY} Q ${turnX},${controlY} ${endX},${endY}`; + return <path d={d} class={getColourClass(colour)} />; } diff --git a/src/components/questions/AddingFractionsQuestion.jsx b/src/components/questions/AddingFractionsQuestion.jsx @@ -12,21 +12,21 @@ function simplifyFraction(num, denom) { } function getRandomFraction(seed) { - const num0 = getRandomInt(1, 5, seed); - const denom0 = getRandomInt(2, 10, seed + .05); + const num0 = getRandomInt(1, 5, seed++); + const denom0 = getRandomInt(2, 10, seed++); const [num, denom] = simplifyFraction(num0, denom0); - if (denom == 1) { return getRandomFraction(seed + .1); } + if (denom == 1) { return getRandomFraction(seed++); } return [num, denom]; } function getRandomFractions(seed) { - const [num1, denom1] = getRandomFraction(seed); - const [num2, denom2] = getRandomFraction(seed + .5); + const [num1, denom1] = getRandomFraction(seed++); + const [num2, denom2] = getRandomFraction(seed++); return [num1, denom1, num2, denom2]; } export default function AddingFractionsQuestion({ seed, showAnswer }) { - const [num1, denom1, num2, denom2] = getRandomFractions(seed); + const [num1, denom1, num2, denom2] = getRandomFractions(seed++); const question = formatFraction(num1, denom1) + ' + ' + formatFraction(num2, denom2); diff --git a/src/components/questions/DoubleTransformationQuestion.jsx b/src/components/questions/DoubleTransformationQuestion.jsx @@ -1,11 +1,9 @@ import { getRandomChoice, getRandomInt } from "../../scripts/utils"; -import CoordinateLabel from "../graphing/CoordinateLabel"; +import Point from "../graphing/Point"; +import Polygon from "../graphing/Polygon"; import Graph from "../graphing/Graph"; import Question from "../Question"; - -function shapeToString(shape) { - return shape.map((point) => point.join(",")).join(" ") -} +import Equation from "../Equation"; function rotatePoint(point, angle, axis=[0, 0]) { const diff = [point[0] - axis[0], point[1] - axis[1]] @@ -14,34 +12,60 @@ function rotatePoint(point, angle, axis=[0, 0]) { return [axis[0] + c*diff[0] + s*diff[1], axis[1] + c*diff[1] - s*diff[0]] } - export default function DoubleTransformationQuestion({ seed, showAnswer }) { const viewBox = [-7, -7, 14, 14] const originalShape = [[0, 1], [2, 1], [0, 2]] - const axis1 = [getRandomInt(-3, 3, seed), getRandomInt(-3, 3, seed + .1)] - const axis2 = [getRandomInt(-3, 3, seed + .2), getRandomInt(-3, 3, seed + .3)] - const angle = getRandomChoice([Math.PI/2, Math.PI, 3*Math.PI/2], seed + .4) + const axis1 = [getRandomInt(-3, 3, seed++), getRandomInt(-3, 3, seed++)] + let axis2 = axis1; + while (axis2.every((x, i) => x === axis1[i])) { + axis2 = [getRandomInt(-3, 3, seed++), getRandomInt(-3, 3, seed++)] + } + const angleDegrees = getRandomChoice([90, 180, -90], seed++) + const angle = angleDegrees / 180 * Math.PI // Rotation then translation!. could do other way around if really needed. const midShape = originalShape.map((point) => rotatePoint(point, angle, axis1)) const rotatedAxis = rotatePoint(axis2, angle, axis1) - const translationVector = axis2.map((value, i) => value - rotatedAxis[i]) + const translationVector = axis2.map((value, i) => Math.round(value - rotatedAxis[i])) const endShape = midShape.map((point) => point.map((value, i) => value + translationVector[i])) + let angleDirection = "" + if (angleDegrees !== 180) { + if (angleDegrees < 0) { + angleDirection = "counterclockwise" + } else { + angleDirection = "clockwise" + } + } + + const angleExpression = ( + <span> + <Equation equation={`${Math.abs(angleDegrees)} degree`} /> {angleDirection} + </span> + ) + const question = (<> - <p> - What single transformation..? - </p> - <Graph viewBox={viewBox} width="300" height="300"> - <polygon className="color-1" points={shapeToString(originalShape)} /> - <polygon className="color-2" points={shapeToString(midShape)} /> - <polygon className="color-3" points={shapeToString(endShape)} /> - <CoordinateLabel x={axis2[0]} y={axis2[1]} position="top" /> - <CoordinateLabel x={axis1[0]} y={axis1[1]} position="top" /> + <p>Shape P is rotated {angleExpression} 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> + <Graph viewBox={viewBox}> + <Polygon points={originalShape} label="P" colour="1" /> + <Point x={axis1[0]} y={axis1[1]} label="Q" colour="2" /> + </Graph> + </>) + + const answer = (<> + <p>Rotation {angleExpression} about <Equation equation={`(${axis2[0]}, ${axis2[1]})`} />.</p> + <Graph viewBox={viewBox}> + <Polygon points={originalShape} label="P" colour="1" /> + <Point x={axis1[0]} y={axis1[1]} label="Q" colour="2" /> + <Polygon points={ midShape} label="R" colour="3" /> + <Polygon points={ endShape} label="S" colour="4" /> + <Point x={axis2[0]} y={axis2[1]} /> </Graph> </>) - return <Question question={question} answer="" showAnswer={showAnswer} /> + return <Question question={question} answer={answer} showAnswer={showAnswer} /> } diff --git a/src/components/questions/QuadraticEquationQuestion.jsx b/src/components/questions/QuadraticEquationQuestion.jsx @@ -10,7 +10,7 @@ function formatQuadraticExpression(a, b, c) { function formatFraction(num, denom) { const hcf = getHCF(Math.abs(num), Math.abs(denom)) - const prefix = num / denom > 0 ? "" : "-" + const prefix = num / denom > 0 ? "" : "-" num = Math.abs(num / hcf) denom = Math.abs(denom / hcf) @@ -22,7 +22,7 @@ function formatFraction(num, denom) { 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)}`; } - + return `x = ${formatFraction(-m, l)} "or" x = ${formatFraction(-o, n)}`; } @@ -38,36 +38,29 @@ function formatFactors(l, m, n, o) { return `${formatFactor(l, m)}${formatFactor(n, o)}` } -function getRandomRoots(seed) { - return [ - getRandomInt(-9, 9, seed), - getRandomInt(-9, 9, seed + .1), - ].sort((a, b) => a - b); -} - export default function QuadraticEquationQuestion({ seed, showAnswer, aPlurality, framing, cSigns }) { // (lx + m)(nx + o) const aChoices = Array(); aPlurality.includes("a=1") && aChoices.push(1); aPlurality.includes("a>1") && aChoices.push(2, 3); - let l = getRandomChoice(aChoices, seed); - let n = getRandomChoice(aChoices, seed + .1); + let l = getRandomChoice(aChoices, seed++); + let n = getRandomChoice(aChoices, seed++); const factoriseChoices = Array(); framing.includes("Solve") && factoriseChoices.push(false); framing.includes("Factorise") && factoriseChoices.push(true); - const factorise = getRandomChoice(factoriseChoices, seed + .2); + const factorise = getRandomChoice(factoriseChoices, seed++); const cSignChoices = Array(); cSigns.includes("Positive") && cSignChoices.push(1) cSigns.includes("Negative") && cSignChoices.push(-1) - const mSign = getRandomChoice([-1, 1], seed + .3) - const cSign = getRandomChoice(cSignChoices, seed + .4) + const mSign = getRandomChoice([-1, 1], seed++) + const cSign = getRandomChoice(cSignChoices, seed++) - let m = getRandomInt(1, 9, seed + .5) * mSign - let o = getRandomInt(1, 9, seed + .6) * mSign * cSign + let m = getRandomInt(1, 9, seed++) * mSign + let o = getRandomInt(1, 9, seed++) * mSign * cSign if (-m/l > -o/n) { [l, m, n, o] = [n, o, l, m] diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx @@ -2,7 +2,7 @@ import Window from '../components/Window.jsx'; import Graph from '../components/graphing/Graph.jsx'; import Quadratic from '../components/graphing/Quadratic.jsx'; import Cubic from '../components/graphing/Cubic.jsx'; -import CoordinateLabel from '../components/graphing/CoordinateLabel.jsx'; +import Polygon from '../components/graphing/Polygon.jsx'; export default function Home() { const viewBox = [-5, -4, 8, 8]; @@ -12,11 +12,9 @@ export default function Home() { <p>Hello World!</p> <div class="field-border kern"> <Graph viewBox={viewBox}> - <Quadratic a={2} b={1} c={-3} viewBox={viewBox} /> - <CoordinateLabel x={-.25} y={-3.125} position="bottom" /> - <polygon points="1,2 -1,1, -4,3" /> - <CoordinateLabel x={-4} y={3} position="top" /> - <Cubic a={1} b={-3} c={3} d={-1} viewBox={viewBox}/> + <Quadratic a={2} b={1} c={-3} colour="1" viewBox={viewBox} /> + <Polygon points={[[1,2], [-1,1], [-4,3]]} label="P" colour="2" /> + <Cubic a={1} b={-3} c={3} d={-1} colour="3" viewBox={viewBox}/> </Graph> </div> </Window> diff --git a/src/scripts/utils.js b/src/scripts/utils.js @@ -35,3 +35,8 @@ export function getSymmetricBounds(midX, viewBox) { const halfWidth = Math.max(midX - minX, minX + width - midX); return [midX - halfWidth, midX + halfWidth]; } + +export function getColourClass(colour) { + if (colour == 'auto') { return ''; } + return `color-${colour}`; +} diff --git a/src/styles/svg.css b/src/styles/svg.css @@ -1,13 +1,12 @@ -path,polygon,circle,ellipse,line,polyline { - stroke: #066; +path,polygon,line,polyline,circle,ellipse { stroke-width: 0.06; stroke-linejoin: round; stroke-linecap: round; - fill: transparent; + fill-opacity: 0; } polygon { - fill: #0668; + fill-opacity: 0.5; } path#grid { @@ -15,46 +14,39 @@ path#grid { stroke-width: 0.01; } -path#axes { +path.axis { stroke: black; stroke-width: 0.08; stroke-linecap: butt; } -path#arrowhead { - stroke: black; - fill: black; +polygon.marker { + fill-opacity: 1; } text { - font-size: 0.02em; - stroke: white; - stroke-width: 0.03; + stroke: white !important; + stroke-width: 0.1; paint-order: stroke; } .color-1 { fill: var(--graph-1); stroke: var(--graph-1); - fill-opacity: 0.5; } .color-2 { fill: var(--graph-2); stroke: var(--graph-2); - fill-opacity: 0.5; } .color-3 { fill: var(--graph-3); stroke: var(--graph-3); - fill-opacity: 0.5; } .color-4 { fill: var(--graph-4); stroke: var(--graph-4); - fill-opacity: 0.5; } .color-5 { fill: var(--graph-5); stroke: var(--graph-5); - fill-opacity: 0.5; -} -\ No newline at end of file +}