website

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

commit 785d1821c6fff9735057c24944fdd5b36d943463
parent 92df0ad7c2e5af8e6dcfc41fd838354704929870
Author: hrryt <harry@mayesfield.uk>
Date:   Sun, 16 Aug 2026 12:14:53 +0100

Replace viewBox prop with PlotContext

Diffstat:
MTODO.md | 1+
Rfavicon.ico -> public/favicon.ico | 0
Msrc/components/plotting/Angle.jsx | 8++++----
Msrc/components/plotting/Cubic.jsx | 14+++++++-------
Msrc/components/plotting/Label.jsx | 4++--
Msrc/components/plotting/Line.jsx | 16++++++++--------
Msrc/components/plotting/Plot.jsx | 62+++++++++++++++++++++++++++++++++++---------------------------
Msrc/components/plotting/Quadratic.jsx | 11++++++-----
Msrc/components/plotting/Sinusoid.jsx | 12+++++++-----
Msrc/components/questions/DoubleReflectionQuestion.jsx | 15+++++++--------
Msrc/pages/Home.jsx | 13+++++--------
Msrc/scripts/contexts.js | 2+-
Msrc/scripts/graphUtils.js | 7++-----
13 files changed, 85 insertions(+), 80 deletions(-)

diff --git a/TODO.md b/TODO.md @@ -1,5 +1,6 @@ # Fixes - Use KaTeX_Math font outside MathML +- Match SVG IDs in production - Prevent components rendering outside graph bounds (with mask?) due to preserveAspectRatio # Questions diff --git a/favicon.ico b/public/favicon.ico Binary files differ. diff --git a/src/components/plotting/Angle.jsx b/src/components/plotting/Angle.jsx @@ -1,6 +1,6 @@ import Label from './Label.jsx'; import { useContext } from 'preact/hooks'; -import { StrokeWidthContext } from '../../scripts/contexts.js'; +import { PlotContext } from '../../scripts/contexts.js'; import { getColourClass, getMidpoint, interpolateDistance, sumVectors, getVector } from '../../scripts/graphUtils.js'; function getArc(points, radius) { @@ -26,9 +26,9 @@ function AngleLabel({ points, radius, colour, children }) { } export default function Angle({ points, right = false, arcs = 1, label = null, colour = "auto" }) { - const strokeWidth = useContext(StrokeWidthContext); - const radius = 10 * strokeWidth; - const concentricGap = 1.5 * strokeWidth; + const p = useContext(PlotContext); + const radius = 10 * p.strokeWidth; + const concentricGap = 1.5 * p.strokeWidth; return ( <g> {right ? ( diff --git a/src/components/plotting/Cubic.jsx b/src/components/plotting/Cubic.jsx @@ -1,3 +1,5 @@ +import { useContext } from 'preact/hooks'; +import { PlotContext } from '../../scripts/contexts.js'; import { getColourClass, getSymmetricBounds } from '../../scripts/graphUtils.js'; function getDepressedCubicRoots(a, b, c, d) { @@ -27,23 +29,21 @@ function getDepressedCubicRoots(a, b, c, d) { return [0,1,2].map(k => cosCoef * Math.cos(arccosine - kCoef * k)); } -export default function Cubic({ a=1, b=0, c=0, d=0, colour="auto", viewBox }) { +export default function Cubic({ a=1, b=0, c=0, d=0, colour="auto" }) { + const p = useContext(PlotContext); // 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; } const midX = -b / (3*a); - const [minX, minY, width, height] = viewBox; - const maxY = minY + height; const intercepts = [ - ...getDepressedCubicRoots(a,b,c,d - minY), - ...getDepressedCubicRoots(a,b,c,d - maxY) + ...getDepressedCubicRoots(a,b,c,d - p.minY), + ...getDepressedCubicRoots(a,b,c,d - p.maxY) ]; - console.log(intercepts.map(x => x + midX)); const dx = Math.max(...intercepts.map(x => Math.abs(x))); - const [i, f] = getSymmetricBounds(midX, dx, viewBox); + const [i, f] = getSymmetricBounds(midX, dx, p); // https://math.stackexchange.com/questions/2414459/draw-cubic-polynomial-using-2d-cubic-bezier-curve const x1 = 2/3 * i + 1/3 * f; diff --git a/src/components/plotting/Label.jsx b/src/components/plotting/Label.jsx @@ -1,5 +1,5 @@ import { useContext } from 'preact/hooks'; -import { StrokeWidthContext } from '../../scripts/contexts.js'; +import { PlotContext } from '../../scripts/contexts.js'; import { getColourClass } from '../../scripts/graphUtils.js'; function getPosition(position) { @@ -22,7 +22,7 @@ function getPosition(position) { export default function Label({ point, position='central', spacing=1, weight="normal", colour='auto', children }) { const [x, y] = point; const [dominantBaseline, textAnchor, dx, dy] = getPosition(position); - const scaledSpacing = spacing * useContext(StrokeWidthContext); + const scaledSpacing = spacing * useContext(PlotContext).strokeWidth; return ( <text x={x} y={-y} dx={scaledSpacing * dx} dy={scaledSpacing * -dy} transform='scale(1 -1)' diff --git a/src/components/plotting/Line.jsx b/src/components/plotting/Line.jsx @@ -1,17 +1,17 @@ +import { useContext } from 'preact/hooks'; +import { PlotContext } from '../../scripts/contexts.js'; import { getColourClass } from "../../scripts/graphUtils" -export default function Line({ line, viewBox, label=null, colour='auto' }) { - const [ minX, minY, width, height ] = viewBox - const maxX = minX + width - const maxY = minY + height +export default function Line({ line, label=null, colour='auto' }) { + const p = useContext(PlotContext); - const xPairs = [minX, maxX].map((x) => [x, (-line.c - line.a*x)/line.b]) - const yPairs = [minY, maxY].map((y) => [(-line.c - line.b*y)/line.a, y]) + const xPairs = [p.minX, p.maxX].map((x) => [x, (-line.c - line.a*x)/line.b]) + const yPairs = [p.minY, p.maxY].map((y) => [(-line.c - line.b*y)/line.a, y]) // important to do this for straight (y = c or x = c) lines. let points = [] - xPairs.map((point) => { (point[1] >= minY && point[1] <= maxY) && points.push(point) }) - yPairs.map((point) => { (point[0] > minX && point[0] < maxX) && points.push(point) }) + xPairs.map((point) => { (point[1] >= p.minY && point[1] <= p.maxY) && points.push(point) }) + yPairs.map((point) => { (point[0] > p.minX && point[0] < p.maxX) && points.push(point) }) // points will always be two. const [i, f] = points diff --git a/src/components/plotting/Plot.jsx b/src/components/plotting/Plot.jsx @@ -1,40 +1,40 @@ import { useContext } from 'preact/hooks'; -import { StrokeWidthContext } from '../../scripts/contexts.js'; +import { PlotContext } from '../../scripts/contexts.js'; import Label from './Label.jsx'; -function Grid({ viewBox }) { - const [ minX, minY, width, height ] = viewBox; - const d = `M ${minX},${minY} h ${width}${` m -${width},1 h ${width}`.repeat(height)} v -${height}${` m -1,${height} v -${height}`.repeat(width)}`; +function Grid() { + const p = useContext(PlotContext); + const d = `M ${p.minX},${p.minY} h ${p.width}${` m -${p.width},1 h ${p.width}`.repeat(p.height)} v -${p.height}${` m -1,${p.height} v -${p.height}`.repeat(p.width)}`; return <path id="grid" d={d} />; } -function Axes({ viewBox }) { - const [ minX, minY, width, height ] = viewBox; - const arrowheadHeight = 4 * useContext(StrokeWidthContext); +function Axes() { + const p = useContext(PlotContext); + const arrowheadHeight = 4 * p.strokeWidth; return ( <g> - <path class="axis" d={`M ${minX},0 h ${ width - arrowheadHeight}`} marker-end="url(#arrowhead)"/> - <path class="axis" d={`M 0,${minY} v ${height - arrowheadHeight}`} marker-end="url(#arrowhead)"/> + <path class="axis" d={`M ${p.minX},0 h ${p.width - arrowheadHeight}`} marker-end="url(#arrowhead)"/> + <path class="axis" d={`M 0,${p.minY} v ${p.height - arrowheadHeight}`} marker-end="url(#arrowhead)"/> </g> ); } -function AxisTicks({ viewBox }) { - const every = Math.floor(20 * useContext(StrokeWidthContext)); - const [ minX, minY, width, height ] = viewBox; +function AxisTicks() { + const p = useContext(PlotContext); + const every = Math.floor(20 * p.strokeWidth); return ( <g> <g> <g> - {Array.from({ length: width - 1 }).map((_, i) => { - const x = i + minX + 1; + {Array.from({ length: p.width - 1 }).map((_, i) => { + const x = i + p.minX + 1; if (x == 0 || x % every) { return; } return <Label point={[x, 0]} position="bottom">{x}</Label>; })} </g> <g> - {Array.from({ length: height - 1 }).map((_, i) => { - const y = i + minY + 1; + {Array.from({ length: p.height - 1 }).map((_, i) => { + const y = i + p.minY + 1; if (y == 0 || y % every) { return; } return <Label point={[0, y]} position="left">{y}</Label>; })} @@ -42,8 +42,8 @@ function AxisTicks({ viewBox }) { <Label point={[0, 0]} position="bottom-left">0</Label> </g> <g> - <Label weight="bold" point={[minX + width, 0]} position="bottom-left" spacing={3}>x</Label> - <Label weight="bold" point={[0, minY + height]} position="bottom-left" spacing={3}>y</Label> + <Label weight="bold" point={[p.minX + p.width, 0]} position="bottom-left" spacing={3}>x</Label> + <Label weight="bold" point={[0, p.minY + p.height]} position="bottom-left" spacing={3}>y</Label> </g> </g> ) @@ -52,17 +52,25 @@ function AxisTicks({ viewBox }) { export default function Plot({ children, mode="graph", viewBox, width="300", height="300" }) { const id = `graph-${Math.floor(1000 * Math.random())}`; const scaleFactor = Math.max(viewBox[2] / width, viewBox[3] / height); - const strokeWidth = 2.5 * scaleFactor; + const p = { + minX: viewBox[0], + minY: viewBox[1], + width: viewBox[2], + height: viewBox[3], + maxX: viewBox[0] + viewBox[2], + maxY: viewBox[1] + viewBox[3], + strokeWidth: 2.5 * scaleFactor, + } const style = ` g#${id} text { font-size: ${scaleFactor}em; - stroke-width: ${mode === "graph" ? 1.6 * strokeWidth : 0}; + stroke-width: ${mode === "graph" ? 1.6 * p.strokeWidth : 0}; } g#${id} :is(path,polygon,line,polyline,rect,circle,ellipse) { - stroke-width: ${strokeWidth}; + stroke-width: ${p.strokeWidth}; } g#${id} path#grid { - stroke-width: ${0.2 * strokeWidth}; + stroke-width: ${0.2 * p.strokeWidth}; } g#${id} polygon { fill-opacity: ${mode === "graph" ? 0.5 : 0}; @@ -136,18 +144,18 @@ export default function Plot({ children, mode="graph", viewBox, width="300", hei {style} </style> </defs> - <StrokeWidthContext value={strokeWidth}> + <PlotContext value={p}> <g id={id}> {mode === "graph" && ( <g> - <Grid viewBox={viewBox} /> - <Axes viewBox={viewBox} /> - <AxisTicks viewBox={viewBox} /> + <Grid /> + <Axes /> + <AxisTicks /> </g> )} {children} </g> - </StrokeWidthContext> + </PlotContext> </svg> ); } diff --git a/src/components/plotting/Quadratic.jsx b/src/components/plotting/Quadratic.jsx @@ -1,17 +1,18 @@ +import { useContext } from 'preact/hooks'; +import { PlotContext } from '../../scripts/contexts.js'; import { getColourClass, getSymmetricBounds } from "../../scripts/graphUtils"; -export default function Quadratic({ a=1, b=0, c=0, colour="auto", viewBox }) { +export default function Quadratic({ a=1, b=0, c=0, colour="auto" }) { + const p = useContext(PlotContext); function F(x) { return a*x*x + b*x + c; } const midX = -b / (2*a); - const [minX, minY, width, height] = viewBox; - const maxY = minY + height; const midY = F(midX); - const curveHeight = a < 0 ? midY - minY : maxY - midY; + const curveHeight = a < 0 ? midY - p.minY : p.maxY - midY; const dx = Math.sqrt(curveHeight / Math.abs(a)); - const [i, f] = getSymmetricBounds(midX, dx, viewBox); + const [i, f] = getSymmetricBounds(midX, dx, p); const y0 = F(i); const y1 = 2*midY - y0; diff --git a/src/components/plotting/Sinusoid.jsx b/src/components/plotting/Sinusoid.jsx @@ -1,13 +1,15 @@ +import { useContext } from 'preact/hooks'; +import { PlotContext } from '../../scripts/contexts.js'; import { getColourClass } from '../../scripts/graphUtils.js'; import { trueMod } from '../../scripts/mathsUtils.js'; import Point from './Point.jsx'; -export default function Sinusoid({ period=1, amp=1, dx=0, dy=0, colour="auto", viewBox }) { - const [minX, minY, width, height] = viewBox; +export default function Sinusoid({ period=1, amp=1, dx=0, dy=0, colour="auto" }) { + const p = useContext(PlotContext); - const offset = trueMod(minX - dx, period); - const x0 = minX - offset; - const periods = Math.ceil((width + offset) / period); + const offset = trueMod(p.minX - dx, period); + const x0 = p.minX - offset; + const periods = Math.ceil((p.width + offset) / period); const magicNumber = 0.3642124232; const halfPeriod = period / 2; diff --git a/src/components/questions/DoubleReflectionQuestion.jsx b/src/components/questions/DoubleReflectionQuestion.jsx @@ -41,7 +41,7 @@ function randomLine(seed, lineType, excludedLine = {a: 100, b: 100, c: 100}) { f = (seed) => {return {a: 1, b: getRandomChoice([-1, 1], seed), c: 0}} break } - + let output = excludedLine while (output.a == excludedLine.a && output.b == excludedLine.b && output.c == excludedLine.c) { output = f(seed++) @@ -59,18 +59,18 @@ export default function DoubleReflectionQuestion({ seed, showAnswer }) { const line1 = randomLine(seed++, line1Type) const line2 = randomLine(seed++, line2Type, line1) - + const question = (<> <p> Reflect P in <Equation equation={formatLine(line1)} /> then <Equation equation={formatLine(line2)} />. </p> - <p> + <p> What single transformation describes this? </p> <Plot viewBox={viewBox} width="300" height="300"> <Polygon points={originalShape} label="P" colour="1" /> - <Line line={line1} colour="2" viewBox={viewBox} /> - <Line line={line2} colour="3" viewBox={viewBox} /> + <Line line={line1} colour="2" /> + <Line line={line2} colour="3" /> </Plot> </>) @@ -95,12 +95,11 @@ export default function DoubleReflectionQuestion({ seed, showAnswer }) { <Polygon points={originalShape} label="P" colour="1" /> <Polygon points={midShape} label="Q" colour="2" /> <Polygon points={endShape} label="R" colour="3" /> - <Line line={line1} colour="2" viewBox={viewBox} /> - <Line line={line2} colour="3" viewBox={viewBox} /> + <Line line={line1} colour="2" /> + <Line line={line2} colour="3" /> {answerGraphElement} </Plot> </>) return <Question question={question} answer={answer} showAnswer={showAnswer} /> } - diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx @@ -11,22 +11,19 @@ import Label from '../components/plotting/Label.jsx'; import Sinusoid from '../components/plotting/Sinusoid.jsx'; export default function Home() { - const viewBox = [-5, -4, 8, 9]; const [A, B, C] = [[1,4], [3,2], [2,1]]; return ( <main> <Window title="Home"> <p>Hello World!</p> <div class="field-border kern"> - <Plot viewBox={viewBox}> - <Quadratic a={-2} b={1} c={-3} colour="1" viewBox={viewBox} /> - <Quadratic a={.04} b={.02} c={-.06} colour="1" viewBox={viewBox} /> - <Quadratic a={-.0001} b={2} c={-3} colour="1" viewBox={viewBox} /> + <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" /> - <Cubic a={2} b={1} c={-1} d={2} colour="3" viewBox={viewBox} /> + <Cubic a={2} b={1} c={-1} d={2} colour="3" /> <Point point={[1,0]} label="Q" colour="3" /> - <Line line={{a: -10, b: -9, c: -1}} colour="4" viewBox={viewBox} /> - <Sinusoid period={5} amp={0.5} dx={-11} dy={-2} colour="5" viewBox={viewBox} /> + <Line line={{a: -10, b: -9, c: -1}} colour="4" /> + <Sinusoid period={5} amp={0.5} dx={-11} dy={-2} colour="5" /> </Plot> <Plot mode="blank" viewBox={[0, 0, 5, 5]}> <Label point={A} position="top">A</Label> diff --git a/src/scripts/contexts.js b/src/scripts/contexts.js @@ -1,3 +1,3 @@ import { createContext } from 'preact'; -export const StrokeWidthContext = createContext(null); +export const PlotContext = createContext(null); diff --git a/src/scripts/graphUtils.js b/src/scripts/graphUtils.js @@ -31,11 +31,8 @@ export function getMidpoint(points) { return interpolate(points[0], points[1], 0.5); } -export function getSymmetricBounds(midX, dx, viewBox) { - const [minX, minY, width, height] = viewBox; - const maxX = minX + width; - - const maxDx = Math.max(midX - minX, maxX - midX); +export function getSymmetricBounds(midX, dx, p) { + const maxDx = Math.max(midX - p.minX, p.maxX - midX); const dx0 = Math.min(maxDx, dx); return [midX - dx0, midX + dx0]; }