aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
251 lines (248 loc) • 7.67 kB
JavaScript
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