import { useState, useRef, useEffect } from 'react' import { math2svg } from '../utils/math2svg' export function EquationRenderer() { const [isOpen, setIsOpen] = useState(false) const [type, setType] = useState<'function' | 'parametric'>('function') const [expression, setExpression] = useState('sin(x)') const [xExpression, setXExpression] = useState('cos(t)') const [yExpression, setYExpression] = useState('sin(t)') const [xMin, setXMin] = useState(-10) const [xMax, setXMax] = useState(10) const [tMin, setTMin] = useState(0) const [tMax, setTMax] = useState(6.28) const [scale, setScale] = useState(20) const [svgPath, setSvgPath] = useState('') const [zoom, setZoom] = useState(1) const svgRef = useRef(null) // Pinch to zoom useEffect(() => { const svg = svgRef.current if (!svg) return let initialDistance = 0 const getTouchDistance = (touches: TouchList) => { const dx = touches[0].clientX - touches[1].clientX const dy = touches[0].clientY - touches[1].clientY return Math.sqrt(dx * dx + dy * dy) } const handleTouchStart = (e: TouchEvent) => { if (e.touches.length === 2) { e.preventDefault() initialDistance = getTouchDistance(e.touches) } } const handleTouchMove = (e: TouchEvent) => { if (e.touches.length === 2 && initialDistance > 0) { e.preventDefault() const currentDistance = getTouchDistance(e.touches) const scale = currentDistance / initialDistance setZoom(prev => Math.max(0.5, Math.min(3, prev * scale))) initialDistance = currentDistance } } svg.addEventListener('touchstart', handleTouchStart, { passive: false }) svg.addEventListener('touchmove', handleTouchMove, { passive: false }) return () => { svg.removeEventListener('touchstart', handleTouchStart) svg.removeEventListener('touchmove', handleTouchMove) } }, [svgPath]) const generate = () => { if (type === 'function') { const path = math2svg({ type: 'function', expression, xMin, xMax, scaleX: scale, scaleY: scale, offsetX: 250, offsetY: 250, samples: 200 }) setSvgPath(path) } else { const path = math2svg({ type: 'parametric', xExpression, yExpression, tMin, tMax, scaleX: scale, scaleY: scale, offsetX: 250, offsetY: 250, samples: 200 }) setSvgPath(path) } } const insert = () => { if (!svgPath) return // For now, just copy to clipboard navigator.clipboard.writeText(svgPath) alert('SVG path copied to clipboard!') } if (!isOpen) { return ( ) } return (
{/* Header */}
ƒ Function Plotter
{/* Content */}
{/* Type Selector */}
{/* Expression Input */} {type === 'function' ? ( <>
setExpression(e.target.value)} placeholder="e.g., sin(x), x^2, cos(x)*x" className="tlui-input" style={{ width: '100%', height: '40px', fontSize: '14px' }} />
{/* Range */}
setXMin(Number(e.target.value))} className="tlui-input" style={{ width: '100%', height: '36px' }} />
setXMax(Number(e.target.value))} className="tlui-input" style={{ width: '100%', height: '36px' }} />
) : ( <>
setXExpression(e.target.value)} placeholder="e.g., cos(t)" className="tlui-input" style={{ width: '100%', height: '40px', fontSize: '14px' }} />
setYExpression(e.target.value)} placeholder="e.g., sin(t)" className="tlui-input" style={{ width: '100%', height: '40px', fontSize: '14px' }} />
setTMin(Number(e.target.value))} className="tlui-input" style={{ width: '100%', height: '36px' }} step="0.1" />
setTMax(Number(e.target.value))} className="tlui-input" style={{ width: '100%', height: '36px' }} step="0.1" />
)} {/* Scale */}
{scale}
setScale(Number(e.target.value))} style={{ width: '100%' }} />
{/* Generate Button */} {/* Preview */} {svgPath && (
{/* Axes */} {/* Function */} {/* Zoom indicator */} {zoom !== 1 && (
{zoom.toFixed(1)}x
)} {/* Reset zoom button */} {zoom !== 1 && ( )}
{/* Insert Button */}
)}
) }