commit 785d1821c6fff9735057c24944fdd5b36d943463
parent 92df0ad7c2e5af8e6dcfc41fd838354704929870
Author: hrryt <harry@mayesfield.uk>
Date: Sun, 16 Aug 2026 12:14:53 +0100
Replace viewBox prop with PlotContext
Diffstat:
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];
}