mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-13 04:55:51 +00:00
Refactor math notation and integer foundations content: Update basic math notation to replace half-bar representation with a dot-based notation for clarity. Enhance integer foundations by revising definitions, adding historical context, and improving explanations of operations, absolute value, and percentages. Include new examples and applications to better illustrate concepts and their relevance in real-world scenarios.
This commit is contained in:
parent
057f2bebf2
commit
f74867fe4c
9 changed files with 1073 additions and 80 deletions
|
|
@ -0,0 +1,403 @@
|
|||
import { onCleanup, onMount } from "solid-js";
|
||||
|
||||
const MIN_VALUE = -10;
|
||||
const MAX_VALUE = 10;
|
||||
const STEP = 0.5;
|
||||
|
||||
function clampToStep(value: number) {
|
||||
const clamped = Math.max(MIN_VALUE, Math.min(MAX_VALUE, value));
|
||||
return Math.round(clamped / STEP) * STEP;
|
||||
}
|
||||
|
||||
function relationSymbol(a: number, b: number) {
|
||||
if (a > b) return ">";
|
||||
if (a < b) return "<";
|
||||
return "=";
|
||||
}
|
||||
|
||||
type DemoPalette = {
|
||||
canvasFill: string;
|
||||
distanceBand: string;
|
||||
positiveRay: string;
|
||||
negativeRay: string;
|
||||
axisLine: string;
|
||||
arrow: string;
|
||||
majorTick: string;
|
||||
minorTick: string;
|
||||
tickLabel: string;
|
||||
axisLabel: string;
|
||||
guideLine: string;
|
||||
absoluteLabelBg: string;
|
||||
absoluteLabelText: string;
|
||||
pointValueText: string;
|
||||
pointRing: string;
|
||||
pointA: string;
|
||||
pointB: string;
|
||||
};
|
||||
|
||||
function resolvePalette(): DemoPalette {
|
||||
const explicitTheme = document.documentElement.getAttribute("data-theme");
|
||||
const isDark =
|
||||
explicitTheme === "dark" ||
|
||||
(explicitTheme !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
|
||||
if (isDark) {
|
||||
return {
|
||||
canvasFill: "#0f172a",
|
||||
distanceBand: "rgba(56, 189, 248, 0.8)",
|
||||
positiveRay: "#22d3ee",
|
||||
negativeRay: "#fda4af",
|
||||
axisLine: "#e2e8f0",
|
||||
arrow: "#f1f5f9",
|
||||
majorTick: "#e2e8f0",
|
||||
minorTick: "#94a3b8",
|
||||
tickLabel: "#f8fafc",
|
||||
axisLabel: "#cbd5e1",
|
||||
guideLine: "rgba(226, 232, 240, 0.65)",
|
||||
absoluteLabelBg: "rgba(15, 23, 42, 0.82)",
|
||||
absoluteLabelText: "#f8fafc",
|
||||
pointValueText: "#f8fafc",
|
||||
pointRing: "#0f172a",
|
||||
pointA: "#2dd4bf",
|
||||
pointB: "#f59e0b",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
canvasFill: "rgba(148, 163, 184, 0.08)",
|
||||
distanceBand: "rgba(14, 116, 144, 0.75)",
|
||||
positiveRay: "#0f766e",
|
||||
negativeRay: "#b91c1c",
|
||||
axisLine: "#334155",
|
||||
arrow: "#64748b",
|
||||
majorTick: "#475569",
|
||||
minorTick: "#94a3b8",
|
||||
tickLabel: "#475569",
|
||||
axisLabel: "#64748b",
|
||||
guideLine: "rgba(15, 23, 42, 0.25)",
|
||||
absoluteLabelBg: "rgba(255, 255, 255, 0.92)",
|
||||
absoluteLabelText: "#0f172a",
|
||||
pointValueText: "#0f172a",
|
||||
pointRing: "#ffffff",
|
||||
pointA: "#0f766e",
|
||||
pointB: "#b45309",
|
||||
};
|
||||
}
|
||||
|
||||
function IntegerCompareDemo() {
|
||||
let rootRef: HTMLDivElement | undefined;
|
||||
let canvasRef: HTMLCanvasElement | undefined;
|
||||
let valueARef: HTMLSpanElement | undefined;
|
||||
let valueBRef: HTMLSpanElement | undefined;
|
||||
let relationRef: HTMLSpanElement | undefined;
|
||||
let distanceRef: HTMLSpanElement | undefined;
|
||||
|
||||
onMount(() => {
|
||||
const canvas = canvasRef;
|
||||
if (!canvas) return;
|
||||
const ctxCandidate = canvas.getContext("2d");
|
||||
if (!(ctxCandidate instanceof CanvasRenderingContext2D)) return;
|
||||
const ctx: CanvasRenderingContext2D = ctxCandidate;
|
||||
const canvasEl: HTMLCanvasElement = canvas;
|
||||
|
||||
let valueA = -2;
|
||||
let valueB = -5;
|
||||
let dragging: "a" | "b" | null = null;
|
||||
let dpr = window.devicePixelRatio || 1;
|
||||
let logicalWidth = 760;
|
||||
let logicalHeight = 260;
|
||||
|
||||
const marginX = 40;
|
||||
const axisY = 164;
|
||||
const handleY = 98;
|
||||
|
||||
const pxPerUnit = () => (logicalWidth - marginX * 2) / (MAX_VALUE - MIN_VALUE);
|
||||
const toScreenX = (value: number) => marginX + (value - MIN_VALUE) * pxPerUnit();
|
||||
const fromScreenX = (screenX: number) => MIN_VALUE + (screenX - marginX) / pxPerUnit();
|
||||
|
||||
function drawArrowHead(x: number, y: number, direction: 1 | -1, color: string) {
|
||||
const size = 7;
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x - direction * size, y - size * 0.75);
|
||||
ctx.lineTo(x - direction * size, y + size * 0.75);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawPoint(x: number, y: number, color: string, ringColor: string) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 10, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = ringColor;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function updateText() {
|
||||
if (valueARef) valueARef.textContent = valueA.toFixed(1);
|
||||
if (valueBRef) valueBRef.textContent = valueB.toFixed(1);
|
||||
if (relationRef) relationRef.textContent = relationSymbol(valueA, valueB);
|
||||
if (distanceRef) distanceRef.textContent = Math.abs(valueA - valueB).toFixed(1);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const palette = resolvePalette();
|
||||
ctx.clearRect(0, 0, logicalWidth, logicalHeight);
|
||||
|
||||
ctx.fillStyle = palette.canvasFill;
|
||||
ctx.fillRect(0, 0, logicalWidth, logicalHeight);
|
||||
|
||||
const xA = toScreenX(valueA);
|
||||
const xB = toScreenX(valueB);
|
||||
const xZero = toScreenX(0);
|
||||
const minX = Math.min(xA, xB);
|
||||
const maxX = Math.max(xA, xB);
|
||||
|
||||
ctx.strokeStyle = palette.distanceBand;
|
||||
ctx.lineWidth = 9;
|
||||
ctx.lineCap = "round";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(minX, axisY);
|
||||
ctx.lineTo(maxX, axisY);
|
||||
ctx.stroke();
|
||||
|
||||
const drawZeroRay = (value: number, x: number, y: number) => {
|
||||
if (value === 0) return;
|
||||
ctx.strokeStyle = value > 0 ? palette.positiveRay : palette.negativeRay;
|
||||
ctx.lineWidth = 5;
|
||||
ctx.lineCap = "round";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xZero, y);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
drawZeroRay(valueA, xA, axisY - 12);
|
||||
drawZeroRay(valueB, xB, axisY + 12);
|
||||
|
||||
ctx.strokeStyle = palette.axisLine;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = "round";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(marginX - 12, axisY);
|
||||
ctx.lineTo(logicalWidth - marginX + 12, axisY);
|
||||
ctx.stroke();
|
||||
|
||||
drawArrowHead(logicalWidth - marginX + 12, axisY, 1, palette.arrow);
|
||||
drawArrowHead(marginX - 12, axisY, -1, palette.arrow);
|
||||
|
||||
for (let tick = MIN_VALUE; tick <= MAX_VALUE; tick += 1) {
|
||||
const x = toScreenX(tick);
|
||||
const major = tick % 2 === 0;
|
||||
|
||||
ctx.strokeStyle = major ? palette.majorTick : palette.minorTick;
|
||||
ctx.lineWidth = major ? 1.5 : 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, axisY - (major ? 9 : 6));
|
||||
ctx.lineTo(x, axisY + (major ? 9 : 6));
|
||||
ctx.stroke();
|
||||
|
||||
if (major) {
|
||||
ctx.font = "12px var(--mono, ui-monospace, monospace)";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "top";
|
||||
ctx.fillStyle = palette.tickLabel;
|
||||
ctx.fillText(String(tick), x, axisY + 11);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.font = "600 12px var(--font-sans, system-ui, sans-serif)";
|
||||
ctx.textAlign = "right";
|
||||
ctx.textBaseline = "bottom";
|
||||
ctx.fillStyle = palette.axisLabel;
|
||||
ctx.fillText("Number line", logicalWidth - marginX + 12, axisY - 12);
|
||||
|
||||
ctx.strokeStyle = palette.guideLine;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.setLineDash([5, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xA, handleY + 12);
|
||||
ctx.lineTo(xA, axisY - 12);
|
||||
ctx.moveTo(xB, handleY + 12);
|
||||
ctx.lineTo(xB, axisY - 12);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
drawPoint(xA, handleY, palette.pointA, palette.pointRing);
|
||||
drawPoint(xB, handleY, palette.pointB, palette.pointRing);
|
||||
|
||||
const drawPointValue = (x: number, value: number) => {
|
||||
const text = value.toFixed(1);
|
||||
const y = handleY - 20;
|
||||
ctx.font = "700 12px var(--mono, ui-monospace, monospace)";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillStyle = palette.pointValueText;
|
||||
ctx.fillText(text, x, y);
|
||||
};
|
||||
|
||||
drawPointValue(xA, valueA);
|
||||
drawPointValue(xB, valueB);
|
||||
|
||||
const absText = `|A - B| = ${Math.abs(valueA - valueB).toFixed(1)}`;
|
||||
const absX = (xA + xB) / 2;
|
||||
const absY = axisY - 30;
|
||||
ctx.font = "700 12px var(--mono, ui-monospace, monospace)";
|
||||
const absTextWidth = ctx.measureText(absText).width;
|
||||
ctx.fillStyle = palette.absoluteLabelBg;
|
||||
ctx.fillRect(absX - absTextWidth / 2 - 6, absY - 9, absTextWidth + 12, 18);
|
||||
ctx.fillStyle = palette.absoluteLabelText;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(absText, absX, absY);
|
||||
updateText();
|
||||
}
|
||||
|
||||
function resize() {
|
||||
dpr = window.devicePixelRatio || 1;
|
||||
const rect = canvasEl.getBoundingClientRect();
|
||||
logicalWidth = Math.max(320, rect.width);
|
||||
logicalHeight = 260;
|
||||
|
||||
canvasEl.width = Math.round(logicalWidth * dpr);
|
||||
canvasEl.height = Math.round(logicalHeight * dpr);
|
||||
canvasEl.style.height = `${logicalHeight}px`;
|
||||
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.scale(dpr, dpr);
|
||||
draw();
|
||||
}
|
||||
|
||||
function pickHandle(pointerX: number, pointerY: number) {
|
||||
const xA = toScreenX(valueA);
|
||||
const xB = toScreenX(valueB);
|
||||
const distA = Math.hypot(pointerX - xA, pointerY - handleY);
|
||||
const distB = Math.hypot(pointerX - xB, pointerY - handleY);
|
||||
if (distA <= 18 && distA <= distB) return "a";
|
||||
if (distB <= 18) return "b";
|
||||
return null;
|
||||
}
|
||||
|
||||
function onPointerDown(event: PointerEvent) {
|
||||
const rect = canvasEl.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
dragging = pickHandle(x, y);
|
||||
if (!dragging) return;
|
||||
canvasEl.setPointerCapture(event.pointerId);
|
||||
canvasEl.style.cursor = "grabbing";
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent) {
|
||||
if (!dragging) {
|
||||
const rect = canvasEl.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
canvasEl.style.cursor = pickHandle(x, y) ? "grab" : "default";
|
||||
return;
|
||||
}
|
||||
const rect = canvasEl.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const next = clampToStep(fromScreenX(x));
|
||||
if (dragging === "a") {
|
||||
valueA = next;
|
||||
} else {
|
||||
valueB = next;
|
||||
}
|
||||
draw();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function onPointerEnd(event: PointerEvent) {
|
||||
if (dragging) {
|
||||
canvasEl.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
dragging = null;
|
||||
canvasEl.style.cursor = "default";
|
||||
}
|
||||
|
||||
function onResetClick() {
|
||||
valueA = -2;
|
||||
valueB = -5;
|
||||
draw();
|
||||
}
|
||||
|
||||
const resetBtn = rootRef?.querySelector<HTMLButtonElement>("[data-compare-reset]");
|
||||
resetBtn?.addEventListener("click", onResetClick);
|
||||
|
||||
canvasEl.addEventListener("pointerdown", onPointerDown);
|
||||
canvasEl.addEventListener("pointermove", onPointerMove);
|
||||
canvasEl.addEventListener("pointerup", onPointerEnd);
|
||||
canvasEl.addEventListener("pointercancel", onPointerEnd);
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
const themeObserver = new MutationObserver(() => {
|
||||
draw();
|
||||
});
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme"],
|
||||
});
|
||||
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const onSystemThemeChange = () => draw();
|
||||
darkQuery.addEventListener("change", onSystemThemeChange);
|
||||
|
||||
resize();
|
||||
|
||||
onCleanup(() => {
|
||||
resetBtn?.removeEventListener("click", onResetClick);
|
||||
canvasEl.removeEventListener("pointerdown", onPointerDown);
|
||||
canvasEl.removeEventListener("pointermove", onPointerMove);
|
||||
canvasEl.removeEventListener("pointerup", onPointerEnd);
|
||||
canvasEl.removeEventListener("pointercancel", onPointerEnd);
|
||||
window.removeEventListener("resize", resize);
|
||||
themeObserver.disconnect();
|
||||
darkQuery.removeEventListener("change", onSystemThemeChange);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<section class="math-widget integer-compare-demo" aria-labelledby="integer-compare-demo-title">
|
||||
<h3 id="integer-compare-demo-title" class="math-widget__title">
|
||||
Comparison demo: drag A and B on the number line
|
||||
</h3>
|
||||
<div class="math-widget__body" ref={rootRef}>
|
||||
<p class="math-widget__caption">
|
||||
Drag each point horizontally. The point farther right is greater. The highlighted segment
|
||||
shows absolute distance.
|
||||
</p>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
class="integer-compare-demo__canvas"
|
||||
aria-label="Interactive number line for comparing integers"
|
||||
/>
|
||||
<p class="integer-compare-demo__readout" aria-live="polite">
|
||||
A = <span ref={valueARef}>-2.0</span>, B = <span ref={valueBRef}>-5.0</span>, so A{" "}
|
||||
<span ref={relationRef}>{">"}</span> B and |A - B| ={" "}
|
||||
<span ref={distanceRef}>3.0</span>
|
||||
</p>
|
||||
<div class="integer-compare-demo__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="scenario-demo__btn integer-compare-demo__reset-btn"
|
||||
data-compare-reset
|
||||
aria-label="Reset example"
|
||||
title="Reset example"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-reload">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntegerCompareDemo;
|
||||
Loading…
Add table
Add a link
Reference in a new issue