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("[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 (

Comparison demo: drag A and B on the number line

Drag each point horizontally. The point farther right is greater. The highlighted segment shows absolute distance.

A = -2.0, B = -5.0, so A{" "} {">"} B and |A - B| ={" "} 3.0

); } export default IntegerCompareDemo;