@sirhc77/canvas-math-kit
Version:
A lightweight, interactive canvas-based vector visualizer for math, linear algebra, and ML education. Built with React + TypeScript.
112 lines (111 loc) • 5.59 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { useEffect, useRef, useState } from 'react';
import { usePointerDrag } from "../hooks/usePointerDrag";
import { drawGrid, drawAxes, drawLine, drawArrowhead, drawCircle, drawParallelogram, toCanvas, } from '../utils/canvasGraphUtils';
const GraphCanvas = ({ width, height, scale, vectors, parallelograms, snap, locked, onVectorsChange, customDragTargets, onCustomDragTargetsChange, customDraw }) => {
const canvasRef = useRef(null);
const [dragging, setDragging] = useState(false);
const shouldLock = locked !== null && locked !== void 0 ? locked : false;
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const resize = () => {
const screenWidth = window.innerWidth;
if (screenWidth < 600) {
// Mobile-ish: use full width, cap height to maintain square
const size = Math.min(screenWidth - 20, 300); // give some margin
setCanvasSize({ width: size, height: size });
}
else {
setCanvasSize({ width: width, height: height });
}
};
resize();
window.addEventListener('resize', resize);
return () => window.removeEventListener('resize', resize);
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas)
return;
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
const logicalWidth = rect.width;
const logicalHeight = rect.height;
canvas.width = canvasSize.width * dpr;
canvas.height = canvasSize.height * dpr;
const ctx = canvas.getContext('2d');
if (!ctx)
return;
ctx.scale(dpr, dpr); // this affects only the rendering, not the logical units
const origin = {
x: canvasSize.width / 2,
y: canvasSize.height / 2,
};
ctx.clearRect(0, 0, logicalWidth, logicalHeight);
drawGrid(ctx, logicalWidth, logicalHeight, scale);
drawAxes(ctx, logicalWidth, logicalHeight, origin);
vectors === null || vectors === void 0 ? void 0 : vectors.forEach((vec) => {
var _a, _b;
const from = toCanvas(0, 0, origin, scale);
const to = toCanvas(vec.x, vec.y, origin, scale);
const color = vec.color || 'blue';
const style = (_a = vec.headStyle) !== null && _a !== void 0 ? _a : 'arrow';
const lineWidth = (_b = vec.width) !== null && _b !== void 0 ? _b : 2;
drawLine(ctx, from, to, color, lineWidth);
if (style === 'arrow' || style === 'both')
drawArrowhead(ctx, from, to, color);
if (style === 'circle' || style === 'both')
drawCircle(ctx, to, 4, color); // 4 is already in logical units
if (vec.label) {
const label = typeof vec.label === 'string' ? vec.label : vec.label(vec.x, vec.y);
ctx.save();
ctx.font = '12px sans-serif';
ctx.fillStyle = color;
ctx.fillText(label, to.x + 5, to.y - 5);
ctx.restore();
}
});
parallelograms === null || parallelograms === void 0 ? void 0 : parallelograms.forEach(p => {
const vA = p.vectorA;
const vB = p.vectorB;
const p0 = toCanvas(0, 0, origin, scale);
const p1 = toCanvas(vA.x, vA.y, origin, scale);
const p2 = toCanvas(vA.x + vB.x, vA.y + vB.y, origin, scale);
const p3 = toCanvas(vB.x, vB.y, origin, scale);
drawParallelogram(ctx, p0, p1, p2, p3, p.fillColor, p.strokeColor);
});
if (customDraw) {
try {
customDraw(ctx, origin, scale);
}
catch (e) {
console.error('customDraw threw error: ', e);
}
}
}, [vectors, scale, parallelograms, customDraw, canvasSize]);
const vectorItems = vectors !== null && vectors !== void 0 ? vectors : [];
const customDragTargetItems = customDragTargets !== null && customDragTargets !== void 0 ? customDragTargets : [];
const allDragTargets = [...vectorItems.map((v, i) => (Object.assign(Object.assign({}, v), { type: 'vector', index: i }))),
...customDragTargetItems.map((v, i) => (Object.assign(Object.assign({}, v), { type: 'dragTarget', index: i })))];
const onAllDragTargetsChange = (updatedItems) => {
const vectorItems = updatedItems.filter(i => i.type === 'vector');
const customDragTargetItems = updatedItems.filter(i => i.type === 'dragTarget');
onVectorsChange === null || onVectorsChange === void 0 ? void 0 : onVectorsChange(vectorItems);
onCustomDragTargetsChange === null || onCustomDragTargetsChange === void 0 ? void 0 : onCustomDragTargetsChange(customDragTargetItems);
};
usePointerDrag(canvasRef, allDragTargets, onAllDragTargetsChange, {
origin: { x: canvasSize.width / 2, y: canvasSize.height / 2 },
scale: scale,
snap,
isLocked: shouldLock,
onDragStart: () => setDragging(true),
onDragEnd: () => setDragging(false),
});
return (_jsx("canvas", { ref: canvasRef, className: "border", style: {
width: canvasSize.width + 'px',
height: canvasSize.height + 'px',
touchAction: 'none',
cursor: dragging ? 'grabbing' : 'grab',
} }));
};
export default GraphCanvas;