UNPKG

@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
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;