UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

251 lines (248 loc) 7.67 kB
import { useState, useRef, useCallback, useEffect } from 'react'; const isRecord = value => typeof value === 'object' && value !== null; const getScaleAxis = (scale, scaleId) => { if (isRecord(scale)) { const { axis } = scale; if (axis === 'x' || axis === 'y') { return axis; } } const fallback = scaleId.charAt(0).toLowerCase(); return fallback === 'x' || fallback === 'y' ? fallback : null; }; const hasNumericRange = scale => { if (!isRecord(scale)) { return false; } const { min, max } = scale; return typeof min === 'number' && typeof max === 'number' && Number.isFinite(min) && Number.isFinite(max); }; const axisMatchesMode = (axis, mode) => mode === 'xy' || mode === axis; /** * Hook for physics-based chart interactions (zoom/pan) * * Provides smooth, physics-based zoom and pan interactions for Chart.js charts * with configurable spring physics and constraints. * * @param chartRef - Reference to the Chart.js instance * @param wrapperRef - Reference to the chart wrapper element * @param options - Configuration options for interactions * * @returns Object containing interaction state and control functions * * @example * ```tsx * function PhysicsChart() { * const chartRef = useRef<ChartJS>(null); * const wrapperRef = useRef<HTMLDivElement>(null); * * const { isPanning, zoomLevel, applyZoom, resetZoom } = useChartPhysicsInteraction( * chartRef, * wrapperRef, * { * enabled: true, * mode: 'xy', * minZoom: 0.5, * maxZoom: 5, * } * ); * * return ( * <div ref={wrapperRef}> * <Chart ref={chartRef} {...chartProps} /> * <button onClick={resetZoom} className="glass-focus glass-touch-target glass-contrast-guard">Reset</button> * </div> * ); * } * ``` */ function useChartPhysicsInteraction(chartRef, wrapperRef, options = {}) { const { enabled = false, mode = 'xy', physics = { tension: 300, friction: 30, mass: 1 }, minZoom = 0.5, maxZoom = 5, wheelSensitivity = 0.1, inertiaDuration = 500, respectReducedMotion = true } = options; const [isPanning, setIsPanning] = useState(false); const [zoomLevel, setZoomLevel] = useState(1); const panStartRef = useRef(null); const velocityRef = useRef({ x: 0, y: 0 }); const lastPosRef = useRef({ x: 0, y: 0 }); const animationFrameRef = useRef(null); /** * Apply zoom with physics constraints */ const applyZoom = useCallback(level => { if (!enabled || !chartRef.current) return; // Clamp zoom level const clampedLevel = Math.max(minZoom, Math.min(maxZoom, level)); setZoomLevel(clampedLevel); const chart = chartRef.current; // Apply zoom transformations to chart scales if (chart.options.scales) { Object.keys(chart.options.scales).forEach(scaleId => { const scale = chart.options.scales[scaleId]; if (!scale) return; const axis = getScaleAxis(scale, scaleId); if (!axis || !axisMatchesMode(axis, mode)) return; // Only apply zoom to enabled axes based on mode if (!hasNumericRange(scale)) return; const range = scale.max - scale.min; const center = (scale.max + scale.min) / 2; const newRange = range / clampedLevel; scale.min = center - newRange / 2; scale.max = center + newRange / 2; }); } chart.update('none'); }, [enabled, chartRef, minZoom, maxZoom, mode]); /** * Reset zoom to default level */ const resetZoom = useCallback(() => { applyZoom(1); }, [applyZoom]); /** * Handle wheel events for zooming */ const handleWheel = useCallback(event => { if (!enabled || !chartRef.current) return; event.preventDefault(); const delta = -event.deltaY * wheelSensitivity; const newZoom = zoomLevel * (1 + delta); applyZoom(newZoom); }, [enabled, chartRef, wheelSensitivity, zoomLevel, applyZoom]); /** * Handle mouse down for panning */ const handleMouseDown = useCallback(event => { if (!enabled || !chartRef.current) return; setIsPanning(true); panStartRef.current = { x: event.clientX, y: event.clientY }; lastPosRef.current = { x: event.clientX, y: event.clientY }; velocityRef.current = { x: 0, y: 0 }; }, [enabled, chartRef]); /** * Handle mouse move for panning */ const handleMouseMove = useCallback(event => { if (!isPanning || !panStartRef.current || !chartRef.current) return; const deltaX = event.clientX - lastPosRef.current.x; const deltaY = event.clientY - lastPosRef.current.y; // Update velocity for inertia velocityRef.current = { x: deltaX, y: deltaY }; const chart = chartRef.current; // Apply pan transformations to chart scales if (chart.options.scales) { Object.keys(chart.options.scales).forEach(scaleId => { const scale = chart.options.scales[scaleId]; if (!scale) return; const axis = getScaleAxis(scale, scaleId); if (!axis || !axisMatchesMode(axis, mode)) return; // Only pan enabled axes based on mode if (!hasNumericRange(scale)) return; const range = scale.max - scale.min; const panAmount = axis === 'x' ? -(deltaX / (wrapperRef.current?.offsetWidth || 1)) * range : deltaY / (wrapperRef.current?.offsetHeight || 1) * range; scale.min += panAmount; scale.max += panAmount; }); chart.update('none'); } lastPosRef.current = { x: event.clientX, y: event.clientY }; }, [isPanning, chartRef, mode, wrapperRef]); /** * Handle mouse up to end panning with inertia */ const handleMouseUp = useCallback(() => { if (!isPanning) return; setIsPanning(false); panStartRef.current = null; // Apply inertia if enabled if (!respectReducedMotion) { const applyInertia = () => { const { x, y } = velocityRef.current; if (Math.abs(x) < 0.1 && Math.abs(y) < 0.1) { // Inertia complete if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } return; } // Apply friction velocityRef.current.x *= 0.95; velocityRef.current.y *= 0.95; // Continue animation animationFrameRef.current = requestAnimationFrame(applyInertia); }; applyInertia(); } }, [isPanning, respectReducedMotion]); /** * Setup event listeners */ useEffect(() => { if (!enabled || !wrapperRef.current) return; const wrapper = wrapperRef.current; wrapper.addEventListener('wheel', handleWheel, { passive: false }); wrapper.addEventListener('mousedown', handleMouseDown); window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return () => { wrapper.removeEventListener('wheel', handleWheel); wrapper.removeEventListener('mousedown', handleMouseDown); window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, [enabled, wrapperRef, handleWheel, handleMouseDown, handleMouseMove, handleMouseUp]); return { isPanning, zoomLevel, applyZoom, resetZoom }; } export { useChartPhysicsInteraction }; //# sourceMappingURL=useChartPhysicsInteraction.js.map