@shohojdhara/atomix 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atomix.css +70 -33
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +2 -2
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +93 -109
- package/dist/charts.js +273 -371
- package/dist/charts.js.map +1 -1
- package/dist/core.js +183 -184
- package/dist/core.js.map +1 -1
- package/dist/forms.js +183 -184
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js +183 -184
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +7 -51
- package/dist/index.esm.js +281 -470
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +287 -476
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AtomixGlass/AtomixGlass.tsx +60 -38
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +6 -35
- package/src/components/AtomixGlass/glass-utils.ts +27 -14
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +19 -21
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +1162 -515
- package/src/components/AtomixGlass/stories/shared-components.tsx +11 -3
- package/src/components/Chart/BubbleChart.tsx +6 -2
- package/src/components/Chart/Chart.stories.tsx +108 -96
- package/src/components/Chart/ChartToolbar.tsx +6 -4
- package/src/components/Chart/ChartTooltip.tsx +5 -4
- package/src/components/Chart/GaugeChart.tsx +20 -12
- package/src/components/Chart/HeatmapChart.tsx +53 -23
- package/src/components/Chart/TreemapChart.tsx +44 -15
- package/src/components/Chart/index.ts +0 -2
- package/src/components/Chart/types.ts +4 -4
- package/src/components/Navigation/Navbar/Navbar.tsx +13 -5
- package/src/components/index.ts +0 -1
- package/src/lib/composables/index.ts +1 -2
- package/src/lib/composables/useAtomixGlass.ts +246 -222
- package/src/lib/composables/useAtomixGlassStyles.ts +46 -23
- package/src/lib/constants/components.ts +3 -1
- package/src/styles/01-settings/_settings.chart.scss +13 -13
- package/src/styles/06-components/_components.atomix-glass.scss +45 -20
- package/src/styles/06-components/_components.chart.scss +23 -5
- package/src/components/Chart/AnimatedChart.tsx +0 -230
- package/src/lib/composables/atomix-glass/useGlassBackgroundDetection.ts +0 -329
- package/src/lib/composables/atomix-glass/useGlassCornerRadius.ts +0 -82
- package/src/lib/composables/atomix-glass/useGlassMouseTracking.ts +0 -153
- package/src/lib/composables/atomix-glass/useGlassOverLight.ts +0 -198
- package/src/lib/composables/atomix-glass/useGlassState.ts +0 -112
- package/src/lib/composables/atomix-glass/useGlassTransforms.ts +0 -160
- package/src/lib/composables/glass-styles.ts +0 -302
- package/src/lib/composables/useGlassContainer.ts +0 -177
|
@@ -15,12 +15,12 @@ import type {
|
|
|
15
15
|
import { ATOMIX_GLASS } from '../constants/components';
|
|
16
16
|
import { globalMouseTracker } from './shared-mouse-tracker';
|
|
17
17
|
import {
|
|
18
|
-
calculateDistance,
|
|
19
18
|
calculateElementCenter,
|
|
20
19
|
calculateMouseInfluence,
|
|
21
20
|
extractBorderRadiusFromChildren,
|
|
22
21
|
extractBorderRadiusFromDOMElement,
|
|
23
22
|
validateGlassSize,
|
|
23
|
+
lerp,
|
|
24
24
|
} from '../../components/AtomixGlass/glass-utils';
|
|
25
25
|
import { updateAtomixGlassStyles } from './useAtomixGlassStyles';
|
|
26
26
|
|
|
@@ -179,8 +179,6 @@ interface UseAtomixGlassReturn {
|
|
|
179
179
|
};
|
|
180
180
|
|
|
181
181
|
// Transform calculations
|
|
182
|
-
elasticTranslation: { x: number; y: number };
|
|
183
|
-
directionalScale: string;
|
|
184
182
|
transformStyle: string;
|
|
185
183
|
|
|
186
184
|
// Event handlers
|
|
@@ -188,7 +186,6 @@ interface UseAtomixGlassReturn {
|
|
|
188
186
|
handleMouseLeave: () => void;
|
|
189
187
|
handleMouseDown: () => void;
|
|
190
188
|
handleMouseUp: () => void;
|
|
191
|
-
handleMouseMove: (e: MouseEvent) => void;
|
|
192
189
|
handleKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void;
|
|
193
190
|
}
|
|
194
191
|
|
|
@@ -230,6 +227,14 @@ export function useAtomixGlass({
|
|
|
230
227
|
const internalGlobalMousePositionRef = useRef<MousePosition>({ x: 0, y: 0 });
|
|
231
228
|
const internalMouseOffsetRef = useRef<MousePosition>({ x: 0, y: 0 });
|
|
232
229
|
|
|
230
|
+
// ── Lerp smoothing refs ───────────────────────────────────────────────
|
|
231
|
+
// Target positions that raw mouse events write to;
|
|
232
|
+
// the lerp loop continuously interpolates the "current" refs toward these.
|
|
233
|
+
const targetMouseOffsetRef = useRef<MousePosition>({ x: 0, y: 0 });
|
|
234
|
+
const targetGlobalMousePositionRef = useRef<MousePosition>({ x: 0, y: 0 });
|
|
235
|
+
const lerpRafRef = useRef<number | null>(null);
|
|
236
|
+
const lerpActiveRef = useRef(false);
|
|
237
|
+
|
|
233
238
|
const [dynamicBorderRadius, setDynamicCornerRadius] = useState<number>(
|
|
234
239
|
CONSTANTS.DEFAULT_CORNER_RADIUS
|
|
235
240
|
);
|
|
@@ -310,7 +315,36 @@ export function useAtomixGlass({
|
|
|
310
315
|
return () => clearTimeout(timeoutId);
|
|
311
316
|
}, [children, debugBorderRadius, contentRef]);
|
|
312
317
|
|
|
313
|
-
// Media query
|
|
318
|
+
// Media query detection for reduced motion and high contrast
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const mediaQueryReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
325
|
+
const mediaQueryHighContrast = window.matchMedia('(prefers-contrast: high)');
|
|
326
|
+
|
|
327
|
+
setUserPrefersReducedMotion(mediaQueryReducedMotion.matches);
|
|
328
|
+
setUserPrefersHighContrast(mediaQueryHighContrast.matches);
|
|
329
|
+
|
|
330
|
+
const handleReducedMotionChange = (e: MediaQueryListEvent) => {
|
|
331
|
+
setUserPrefersReducedMotion(e.matches);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const handleHighContrastChange = (e: MediaQueryListEvent) => {
|
|
335
|
+
setUserPrefersHighContrast(e.matches);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
mediaQueryReducedMotion.addEventListener('change', handleReducedMotionChange);
|
|
339
|
+
mediaQueryHighContrast.addEventListener('change', handleHighContrastChange);
|
|
340
|
+
|
|
341
|
+
return () => {
|
|
342
|
+
mediaQueryReducedMotion.removeEventListener('change', handleReducedMotionChange);
|
|
343
|
+
mediaQueryHighContrast.removeEventListener('change', handleHighContrastChange);
|
|
344
|
+
};
|
|
345
|
+
}, []);
|
|
346
|
+
|
|
347
|
+
// Background detection for overLight auto-detect
|
|
314
348
|
useEffect(() => {
|
|
315
349
|
// Only run auto-detection for 'auto' mode or object config (which uses auto-detection)
|
|
316
350
|
const shouldDetect = (overLight === 'auto' || (typeof overLight === 'object' && overLight !== null));
|
|
@@ -442,44 +476,8 @@ export function useAtomixGlass({
|
|
|
442
476
|
setDetectedOverLight(false);
|
|
443
477
|
}
|
|
444
478
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
try {
|
|
450
|
-
const mediaQueryReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
451
|
-
const mediaQueryHighContrast = window.matchMedia('(prefers-contrast: high)');
|
|
452
|
-
|
|
453
|
-
setUserPrefersReducedMotion(mediaQueryReducedMotion.matches);
|
|
454
|
-
setUserPrefersHighContrast(mediaQueryHighContrast.matches);
|
|
455
|
-
|
|
456
|
-
const handleReducedMotionChange = (e: MediaQueryListEvent) => {
|
|
457
|
-
setUserPrefersReducedMotion(e.matches);
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
const handleHighContrastChange = (e: MediaQueryListEvent) => {
|
|
461
|
-
setUserPrefersHighContrast(e.matches);
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
if (mediaQueryReducedMotion.addEventListener) {
|
|
465
|
-
mediaQueryReducedMotion.addEventListener('change', handleReducedMotionChange);
|
|
466
|
-
mediaQueryHighContrast.addEventListener('change', handleHighContrastChange);
|
|
467
|
-
} else if (mediaQueryReducedMotion.addListener) {
|
|
468
|
-
mediaQueryReducedMotion.addListener(handleReducedMotionChange);
|
|
469
|
-
mediaQueryHighContrast.addListener(handleHighContrastChange);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return () => {
|
|
473
|
-
try {
|
|
474
|
-
// cleanup
|
|
475
|
-
} catch (cleanupError) {
|
|
476
|
-
// ignore
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
} catch (error) {
|
|
480
|
-
return undefined;
|
|
481
|
-
}
|
|
482
|
-
}, [overLight, glassRef, debugOverLight]);
|
|
479
|
+
return undefined;
|
|
480
|
+
}, [overLight, glassRef]);
|
|
483
481
|
|
|
484
482
|
/**
|
|
485
483
|
* Get effective overLight value based on configuration
|
|
@@ -510,21 +508,23 @@ export function useAtomixGlass({
|
|
|
510
508
|
[]
|
|
511
509
|
);
|
|
512
510
|
|
|
513
|
-
|
|
514
|
-
const baseOverLightConfig = useMemo(() => {
|
|
511
|
+
const overLightConfig = useMemo(() => {
|
|
515
512
|
const isOverLight = getEffectiveOverLight();
|
|
516
|
-
|
|
517
|
-
const
|
|
513
|
+
const hoverIntensity = isHovered ? 1.4 : 1;
|
|
514
|
+
const activeIntensity = isActive ? 1.6 : 1;
|
|
518
515
|
|
|
519
|
-
|
|
516
|
+
// More robust overlight configuration with better defaults and clamping
|
|
517
|
+
const baseOpacity = isOverLight
|
|
518
|
+
? Math.min(0.6, Math.max(0.2, 0.5 * hoverIntensity * activeIntensity))
|
|
519
|
+
: 0;
|
|
520
520
|
|
|
521
521
|
const baseConfig = {
|
|
522
522
|
isOverLight,
|
|
523
523
|
threshold: 0.7,
|
|
524
524
|
opacity: baseOpacity,
|
|
525
|
-
contrast: 1,
|
|
526
|
-
brightness:
|
|
527
|
-
saturationBoost: 1.3,
|
|
525
|
+
contrast: 1.4,
|
|
526
|
+
brightness: 0.9,
|
|
527
|
+
saturationBoost: 1.3, // Fixed value — dynamic saturation amplifies perceived displacement
|
|
528
528
|
shadowIntensity: 0.9,
|
|
529
529
|
borderOpacity: 0.7,
|
|
530
530
|
};
|
|
@@ -532,42 +532,105 @@ export function useAtomixGlass({
|
|
|
532
532
|
if (typeof overLight === 'object' && overLight !== null) {
|
|
533
533
|
const objConfig = overLight as OverLightObjectConfig;
|
|
534
534
|
|
|
535
|
-
const validatedThreshold = validateConfigValue(
|
|
535
|
+
const validatedThreshold = validateConfigValue(
|
|
536
|
+
objConfig.threshold,
|
|
537
|
+
0.1,
|
|
538
|
+
1.0,
|
|
539
|
+
baseConfig.threshold
|
|
540
|
+
);
|
|
536
541
|
const validatedOpacity = validateConfigValue(objConfig.opacity, 0.1, 1.0, baseConfig.opacity);
|
|
537
|
-
const validatedContrast = validateConfigValue(
|
|
538
|
-
|
|
539
|
-
|
|
542
|
+
const validatedContrast = validateConfigValue(
|
|
543
|
+
objConfig.contrast,
|
|
544
|
+
0.5,
|
|
545
|
+
2.5,
|
|
546
|
+
baseConfig.contrast
|
|
547
|
+
);
|
|
548
|
+
const validatedBrightness = validateConfigValue(
|
|
549
|
+
objConfig.brightness,
|
|
550
|
+
0.5,
|
|
551
|
+
2.0,
|
|
552
|
+
baseConfig.brightness
|
|
553
|
+
);
|
|
554
|
+
const validatedSaturationBoost = validateConfigValue(
|
|
555
|
+
objConfig.saturationBoost,
|
|
556
|
+
0.5,
|
|
557
|
+
3.0,
|
|
558
|
+
baseConfig.saturationBoost
|
|
559
|
+
);
|
|
540
560
|
|
|
541
|
-
|
|
561
|
+
const finalConfig = {
|
|
542
562
|
...baseConfig,
|
|
543
563
|
threshold: validatedThreshold,
|
|
544
|
-
opacity: validatedOpacity,
|
|
564
|
+
opacity: validatedOpacity * hoverIntensity * activeIntensity,
|
|
545
565
|
contrast: validatedContrast,
|
|
546
566
|
brightness: validatedBrightness,
|
|
547
567
|
saturationBoost: validatedSaturationBoost,
|
|
548
568
|
};
|
|
569
|
+
|
|
570
|
+
if (
|
|
571
|
+
(typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
|
|
572
|
+
debugOverLight
|
|
573
|
+
) {
|
|
574
|
+
console.log('[AtomixGlass] OverLight Config:', {
|
|
575
|
+
isOverLight,
|
|
576
|
+
config: {
|
|
577
|
+
threshold: finalConfig.threshold.toFixed(3),
|
|
578
|
+
opacity: finalConfig.opacity.toFixed(3),
|
|
579
|
+
contrast: finalConfig.contrast.toFixed(3),
|
|
580
|
+
brightness: finalConfig.brightness.toFixed(3),
|
|
581
|
+
saturationBoost: finalConfig.saturationBoost.toFixed(3),
|
|
582
|
+
shadowIntensity: finalConfig.shadowIntensity.toFixed(3),
|
|
583
|
+
borderOpacity: finalConfig.borderOpacity.toFixed(3),
|
|
584
|
+
},
|
|
585
|
+
input: {
|
|
586
|
+
threshold: objConfig.threshold,
|
|
587
|
+
opacity: objConfig.opacity,
|
|
588
|
+
contrast: objConfig.contrast,
|
|
589
|
+
brightness: objConfig.brightness,
|
|
590
|
+
saturationBoost: objConfig.saturationBoost,
|
|
591
|
+
},
|
|
592
|
+
timestamp: new Date().toISOString(),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return finalConfig;
|
|
549
597
|
}
|
|
550
598
|
|
|
551
|
-
|
|
552
|
-
|
|
599
|
+
if (
|
|
600
|
+
(typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
|
|
601
|
+
debugOverLight
|
|
602
|
+
) {
|
|
603
|
+
console.log('[AtomixGlass] OverLight Config:', {
|
|
604
|
+
isOverLight,
|
|
605
|
+
configType: typeof overLight === 'boolean' ? (overLight ? 'true' : 'false') : overLight,
|
|
606
|
+
config: {
|
|
607
|
+
threshold: baseConfig.threshold.toFixed(3),
|
|
608
|
+
opacity: baseConfig.opacity.toFixed(3),
|
|
609
|
+
contrast: baseConfig.contrast.toFixed(3),
|
|
610
|
+
brightness: baseConfig.brightness.toFixed(3),
|
|
611
|
+
saturationBoost: baseConfig.saturationBoost.toFixed(3),
|
|
612
|
+
shadowIntensity: baseConfig.shadowIntensity.toFixed(3),
|
|
613
|
+
borderOpacity: baseConfig.borderOpacity.toFixed(3),
|
|
614
|
+
},
|
|
615
|
+
timestamp: new Date().toISOString(),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
553
618
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
619
|
+
return baseConfig;
|
|
620
|
+
}, [
|
|
621
|
+
overLight,
|
|
622
|
+
getEffectiveOverLight,
|
|
623
|
+
isHovered,
|
|
624
|
+
isActive,
|
|
625
|
+
validateConfigValue,
|
|
626
|
+
debugOverLight,
|
|
627
|
+
]);
|
|
559
628
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
brightness: Math.min(1.1, baseOverLightConfig.brightness + mouseInfluence * 0.05),
|
|
566
|
-
saturationBoost: baseOverLightConfig.saturationBoost,
|
|
567
|
-
shadowIntensity: Math.min(1.2, Math.max(0.5, baseOverLightConfig.shadowIntensity + mouseInfluence * 0.2)),
|
|
568
|
-
borderOpacity: Math.min(1.0, Math.max(0.3, baseOverLightConfig.borderOpacity + mouseInfluence * 0.1)),
|
|
569
|
-
};
|
|
570
|
-
}, [baseOverLightConfig, mouseOffset, isHovered, isActive]);
|
|
629
|
+
// Transform calculation (static base for React render)
|
|
630
|
+
// Mouse interactions are purely handled by imperative updates in the RAF lerp loop to prevent re-renders
|
|
631
|
+
const transformStyle = useMemo(() => {
|
|
632
|
+
return effectiveWithoutEffects || (isActive && Boolean(onClick)) ? 'scale(0.98)' : 'scale(1)';
|
|
633
|
+
}, [effectiveWithoutEffects, isActive, onClick]);
|
|
571
634
|
|
|
572
635
|
// Mouse tracking
|
|
573
636
|
const updateRectRef = useRef<number | null>(null);
|
|
@@ -575,100 +638,11 @@ export function useAtomixGlass({
|
|
|
575
638
|
// Derived values for imperative updates (we can use memoized ones or re-calculate)
|
|
576
639
|
// Since updateAtomixGlassStyles is called imperatively, we pass current refs and state
|
|
577
640
|
|
|
578
|
-
// Transform calculations for initial render
|
|
579
|
-
const calculateDirectionalScale = useCallback(() => {
|
|
580
|
-
const isOverLightActive = baseOverLightConfig.isOverLight;
|
|
581
|
-
|
|
582
|
-
if (isOverLightActive) {
|
|
583
|
-
return 'scale(1)';
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (!globalMousePosition.x || !globalMousePosition.y || !glassRef.current || !validateGlassSize(glassSize)) {
|
|
587
|
-
return 'scale(1)';
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const rect = glassRef.current.getBoundingClientRect();
|
|
591
|
-
const center = calculateElementCenter(rect);
|
|
592
|
-
const deltaX = globalMousePosition.x - center.x;
|
|
593
|
-
const deltaY = globalMousePosition.y - center.y;
|
|
594
|
-
|
|
595
|
-
const edgeDistanceX = Math.max(0, Math.abs(deltaX) - glassSize.width / 2);
|
|
596
|
-
const edgeDistanceY = Math.max(0, Math.abs(deltaY) - glassSize.height / 2);
|
|
597
|
-
const edgeDistance = calculateDistance({ x: edgeDistanceX, y: edgeDistanceY }, { x: 0, y: 0 });
|
|
598
|
-
|
|
599
|
-
if (edgeDistance > CONSTANTS.ACTIVATION_ZONE) {
|
|
600
|
-
return 'scale(1)';
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const fadeInFactor = 1 - edgeDistance / CONSTANTS.ACTIVATION_ZONE;
|
|
604
|
-
const centerDistance = calculateDistance(globalMousePosition, center);
|
|
605
|
-
|
|
606
|
-
if (centerDistance === 0) {
|
|
607
|
-
return 'scale(1)';
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const normalizedX = deltaX / centerDistance;
|
|
611
|
-
const normalizedY = deltaY / centerDistance;
|
|
612
|
-
const stretchIntensity = Math.min(centerDistance / 300, 1) * elasticity * fadeInFactor;
|
|
613
|
-
|
|
614
|
-
const scaleX = 1 + Math.abs(normalizedX) * stretchIntensity * 0.3 - Math.abs(normalizedY) * stretchIntensity * 0.15;
|
|
615
|
-
const scaleY = 1 + Math.abs(normalizedY) * stretchIntensity * 0.3 - Math.abs(normalizedX) * stretchIntensity * 0.15;
|
|
616
|
-
|
|
617
|
-
return `scaleX(${Math.max(0.8, scaleX)}) scaleY(${Math.max(0.8, scaleY)})`;
|
|
618
|
-
}, [globalMousePosition, elasticity, glassSize, glassRef, baseOverLightConfig]);
|
|
619
|
-
|
|
620
|
-
const calculateFadeInFactor = useCallback(() => {
|
|
621
|
-
if (!globalMousePosition.x || !globalMousePosition.y || !glassRef.current || !validateGlassSize(glassSize)) {
|
|
622
|
-
return 0;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const rect = glassRef.current.getBoundingClientRect();
|
|
626
|
-
const center = calculateElementCenter(rect);
|
|
627
|
-
|
|
628
|
-
const edgeDistanceX = Math.max(0, Math.abs(globalMousePosition.x - center.x) - glassSize.width / 2);
|
|
629
|
-
const edgeDistanceY = Math.max(0, Math.abs(globalMousePosition.y - center.y) - glassSize.height / 2);
|
|
630
|
-
const edgeDistance = calculateDistance({ x: edgeDistanceX, y: edgeDistanceY }, { x: 0, y: 0 });
|
|
631
|
-
|
|
632
|
-
return edgeDistance > CONSTANTS.ACTIVATION_ZONE ? 0 : 1 - edgeDistance / CONSTANTS.ACTIVATION_ZONE;
|
|
633
|
-
}, [globalMousePosition, glassSize, glassRef]);
|
|
634
|
-
|
|
635
|
-
const calculateElasticTranslation = useCallback(() => {
|
|
636
|
-
if (!glassRef.current) {
|
|
637
|
-
return { x: 0, y: 0 };
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const fadeInFactor = calculateFadeInFactor();
|
|
641
|
-
const rect = glassRef.current.getBoundingClientRect();
|
|
642
|
-
const center = calculateElementCenter(rect);
|
|
643
|
-
|
|
644
|
-
return {
|
|
645
|
-
x: (globalMousePosition.x - center.x) * elasticity * 0.1 * fadeInFactor,
|
|
646
|
-
y: (globalMousePosition.y - center.y) * elasticity * 0.1 * fadeInFactor,
|
|
647
|
-
};
|
|
648
|
-
}, [globalMousePosition, elasticity, calculateFadeInFactor, glassRef]);
|
|
649
|
-
|
|
650
|
-
const elasticTranslation = useMemo(() => {
|
|
651
|
-
if (effectiveWithoutEffects) {
|
|
652
|
-
return { x: 0, y: 0 };
|
|
653
|
-
}
|
|
654
|
-
return calculateElasticTranslation();
|
|
655
|
-
}, [calculateElasticTranslation, effectiveWithoutEffects]);
|
|
656
|
-
|
|
657
|
-
const directionalScale = useMemo(() => {
|
|
658
|
-
if (effectiveWithoutEffects) {
|
|
659
|
-
return 'scale(1)';
|
|
660
|
-
}
|
|
661
|
-
return calculateDirectionalScale();
|
|
662
|
-
}, [calculateDirectionalScale, effectiveWithoutEffects]);
|
|
663
|
-
|
|
664
|
-
const transformStyle = useMemo(() => {
|
|
665
|
-
if (effectiveWithoutEffects) {
|
|
666
|
-
return isActive && Boolean(onClick) ? 'scale(0.98)' : 'scale(1)';
|
|
667
|
-
}
|
|
668
|
-
return `translate(${elasticTranslation.x}px, ${elasticTranslation.y}px) ${isActive && Boolean(onClick) ? 'scale(0.96)' : directionalScale}`;
|
|
669
|
-
}, [elasticTranslation, isActive, onClick, directionalScale, effectiveWithoutEffects]);
|
|
670
641
|
|
|
671
642
|
// Handle mouse position updates
|
|
643
|
+
// ── Raw mouse handler — writes to TARGET refs only ──────────────────
|
|
644
|
+
// The lerp loop (below) reads the targets and incrementally
|
|
645
|
+
// moves the "current" refs toward them for liquid smoothing.
|
|
672
646
|
const handleGlobalMousePosition = useCallback(
|
|
673
647
|
(globalPos: MousePosition) => {
|
|
674
648
|
if (externalGlobalMousePosition && externalMouseOffset) {
|
|
@@ -697,63 +671,113 @@ export function useAtomixGlass({
|
|
|
697
671
|
|
|
698
672
|
const center = calculateElementCenter(rect);
|
|
699
673
|
|
|
700
|
-
//
|
|
701
|
-
|
|
674
|
+
// Write raw target — the lerp loop will smoothly pursue it
|
|
675
|
+
targetMouseOffsetRef.current = {
|
|
702
676
|
x: ((globalPos.x - center.x) / rect.width) * 100,
|
|
703
677
|
y: ((globalPos.y - center.y) / rect.height) * 100,
|
|
704
678
|
};
|
|
705
|
-
|
|
706
|
-
// Store in refs instead of state
|
|
707
|
-
internalMouseOffsetRef.current = newOffset;
|
|
708
|
-
internalGlobalMousePositionRef.current = globalPos;
|
|
709
|
-
|
|
710
|
-
// Imperative style update
|
|
711
|
-
updateAtomixGlassStyles(
|
|
712
|
-
wrapperRef?.current || null,
|
|
713
|
-
glassRef.current,
|
|
714
|
-
{
|
|
715
|
-
mouseOffset: newOffset,
|
|
716
|
-
globalMousePosition: globalPos,
|
|
717
|
-
glassSize,
|
|
718
|
-
isHovered,
|
|
719
|
-
isActive,
|
|
720
|
-
isOverLight: baseOverLightConfig.isOverLight,
|
|
721
|
-
baseOverLightConfig,
|
|
722
|
-
effectiveBorderRadius,
|
|
723
|
-
effectiveWithoutEffects,
|
|
724
|
-
effectiveReducedMotion,
|
|
725
|
-
elasticity,
|
|
726
|
-
directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)', // Simplified directional scale for fast path
|
|
727
|
-
onClick,
|
|
728
|
-
withLiquidBlur,
|
|
729
|
-
blurAmount,
|
|
730
|
-
saturation,
|
|
731
|
-
padding,
|
|
732
|
-
}
|
|
733
|
-
);
|
|
679
|
+
targetGlobalMousePositionRef.current = globalPos;
|
|
734
680
|
},
|
|
735
681
|
[
|
|
736
682
|
mouseContainer,
|
|
737
683
|
glassRef,
|
|
738
|
-
wrapperRef,
|
|
739
684
|
externalGlobalMousePosition,
|
|
740
685
|
externalMouseOffset,
|
|
741
686
|
effectiveWithoutEffects,
|
|
742
|
-
glassSize,
|
|
743
|
-
isHovered,
|
|
744
|
-
isActive,
|
|
745
|
-
baseOverLightConfig,
|
|
746
|
-
effectiveBorderRadius,
|
|
747
|
-
effectiveReducedMotion,
|
|
748
|
-
elasticity,
|
|
749
|
-
onClick,
|
|
750
|
-
withLiquidBlur,
|
|
751
|
-
blurAmount,
|
|
752
|
-
saturation,
|
|
753
|
-
padding
|
|
754
687
|
]
|
|
755
688
|
);
|
|
756
689
|
|
|
690
|
+
// ── Lerp animation loop ─────────────────────────────────────────────
|
|
691
|
+
// Continuously interpolates the current offset toward the target.
|
|
692
|
+
// Produces the signature liquid / viscous feel.
|
|
693
|
+
const startLerpLoop = useCallback(() => {
|
|
694
|
+
if (lerpActiveRef.current) return;
|
|
695
|
+
lerpActiveRef.current = true;
|
|
696
|
+
|
|
697
|
+
const LERP_T = CONSTANTS.LERP_FACTOR; // 0.08 – lower = more viscous
|
|
698
|
+
const EPSILON = 0.05; // Stop iterating when close enough
|
|
699
|
+
|
|
700
|
+
const tick = () => {
|
|
701
|
+
if (!lerpActiveRef.current) return;
|
|
702
|
+
|
|
703
|
+
const cur = internalMouseOffsetRef.current;
|
|
704
|
+
const tgt = targetMouseOffsetRef.current;
|
|
705
|
+
|
|
706
|
+
const dx = tgt.x - cur.x;
|
|
707
|
+
const dy = tgt.y - cur.y;
|
|
708
|
+
|
|
709
|
+
// If we're close enough, snap and park
|
|
710
|
+
if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
|
|
711
|
+
internalMouseOffsetRef.current = { ...tgt };
|
|
712
|
+
internalGlobalMousePositionRef.current = { ...targetGlobalMousePositionRef.current };
|
|
713
|
+
} else {
|
|
714
|
+
internalMouseOffsetRef.current = {
|
|
715
|
+
x: lerp(cur.x, tgt.x, LERP_T),
|
|
716
|
+
y: lerp(cur.y, tgt.y, LERP_T),
|
|
717
|
+
};
|
|
718
|
+
const curG = internalGlobalMousePositionRef.current;
|
|
719
|
+
const tgtG = targetGlobalMousePositionRef.current;
|
|
720
|
+
internalGlobalMousePositionRef.current = {
|
|
721
|
+
x: lerp(curG.x, tgtG.x, LERP_T),
|
|
722
|
+
y: lerp(curG.y, tgtG.y, LERP_T),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Imperative style update with the smoothed values
|
|
727
|
+
updateAtomixGlassStyles(
|
|
728
|
+
wrapperRef?.current || null,
|
|
729
|
+
glassRef.current,
|
|
730
|
+
{
|
|
731
|
+
mouseOffset: internalMouseOffsetRef.current,
|
|
732
|
+
globalMousePosition: internalGlobalMousePositionRef.current,
|
|
733
|
+
glassSize,
|
|
734
|
+
isHovered,
|
|
735
|
+
isActive,
|
|
736
|
+
isOverLight: overLightConfig.isOverLight,
|
|
737
|
+
baseOverLightConfig: overLightConfig,
|
|
738
|
+
effectiveBorderRadius,
|
|
739
|
+
effectiveWithoutEffects,
|
|
740
|
+
effectiveReducedMotion,
|
|
741
|
+
elasticity,
|
|
742
|
+
directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
|
|
743
|
+
onClick,
|
|
744
|
+
withLiquidBlur,
|
|
745
|
+
blurAmount,
|
|
746
|
+
saturation,
|
|
747
|
+
padding,
|
|
748
|
+
}
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
lerpRafRef.current = requestAnimationFrame(tick);
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
lerpRafRef.current = requestAnimationFrame(tick);
|
|
755
|
+
}, [
|
|
756
|
+
glassRef,
|
|
757
|
+
wrapperRef,
|
|
758
|
+
glassSize,
|
|
759
|
+
isHovered,
|
|
760
|
+
isActive,
|
|
761
|
+
overLightConfig,
|
|
762
|
+
effectiveBorderRadius,
|
|
763
|
+
effectiveWithoutEffects,
|
|
764
|
+
effectiveReducedMotion,
|
|
765
|
+
elasticity,
|
|
766
|
+
onClick,
|
|
767
|
+
withLiquidBlur,
|
|
768
|
+
blurAmount,
|
|
769
|
+
saturation,
|
|
770
|
+
padding,
|
|
771
|
+
]);
|
|
772
|
+
|
|
773
|
+
const stopLerpLoop = useCallback(() => {
|
|
774
|
+
lerpActiveRef.current = false;
|
|
775
|
+
if (lerpRafRef.current !== null) {
|
|
776
|
+
cancelAnimationFrame(lerpRafRef.current);
|
|
777
|
+
lerpRafRef.current = null;
|
|
778
|
+
}
|
|
779
|
+
}, []);
|
|
780
|
+
|
|
757
781
|
// Subscribe to shared mouse tracker
|
|
758
782
|
useEffect(() => {
|
|
759
783
|
if (externalGlobalMousePosition && externalMouseOffset) {
|
|
@@ -766,6 +790,9 @@ export function useAtomixGlass({
|
|
|
766
790
|
|
|
767
791
|
const unsubscribe = globalMouseTracker.subscribe(handleGlobalMousePosition);
|
|
768
792
|
|
|
793
|
+
// Start the lerp loop — it will smoothly chase the target
|
|
794
|
+
startLerpLoop();
|
|
795
|
+
|
|
769
796
|
const updateRect = () => {
|
|
770
797
|
if (updateRectRef.current !== null) {
|
|
771
798
|
cancelAnimationFrame(updateRectRef.current);
|
|
@@ -789,6 +816,7 @@ export function useAtomixGlass({
|
|
|
789
816
|
|
|
790
817
|
return () => {
|
|
791
818
|
unsubscribe();
|
|
819
|
+
stopLerpLoop();
|
|
792
820
|
if (updateRectRef.current !== null) {
|
|
793
821
|
cancelAnimationFrame(updateRectRef.current);
|
|
794
822
|
updateRectRef.current = null;
|
|
@@ -799,6 +827,8 @@ export function useAtomixGlass({
|
|
|
799
827
|
};
|
|
800
828
|
}, [
|
|
801
829
|
handleGlobalMousePosition,
|
|
830
|
+
startLerpLoop,
|
|
831
|
+
stopLerpLoop,
|
|
802
832
|
mouseContainer,
|
|
803
833
|
glassRef,
|
|
804
834
|
externalGlobalMousePosition,
|
|
@@ -817,13 +847,13 @@ export function useAtomixGlass({
|
|
|
817
847
|
glassSize,
|
|
818
848
|
isHovered,
|
|
819
849
|
isActive,
|
|
820
|
-
isOverLight:
|
|
821
|
-
baseOverLightConfig,
|
|
850
|
+
isOverLight: overLightConfig.isOverLight,
|
|
851
|
+
baseOverLightConfig: overLightConfig,
|
|
822
852
|
effectiveBorderRadius,
|
|
823
853
|
effectiveWithoutEffects,
|
|
824
854
|
effectiveReducedMotion,
|
|
825
855
|
elasticity,
|
|
826
|
-
directionalScale,
|
|
856
|
+
directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
|
|
827
857
|
onClick,
|
|
828
858
|
withLiquidBlur,
|
|
829
859
|
blurAmount,
|
|
@@ -835,12 +865,11 @@ export function useAtomixGlass({
|
|
|
835
865
|
isHovered,
|
|
836
866
|
isActive,
|
|
837
867
|
glassSize,
|
|
838
|
-
|
|
868
|
+
overLightConfig,
|
|
839
869
|
effectiveBorderRadius,
|
|
840
870
|
effectiveWithoutEffects,
|
|
841
871
|
effectiveReducedMotion,
|
|
842
872
|
elasticity,
|
|
843
|
-
directionalScale,
|
|
844
873
|
wrapperRef,
|
|
845
874
|
glassRef,
|
|
846
875
|
externalMouseOffset,
|
|
@@ -858,9 +887,7 @@ export function useAtomixGlass({
|
|
|
858
887
|
const handleMouseDown = useCallback(() => setIsActive(true), []);
|
|
859
888
|
const handleMouseUp = useCallback(() => setIsActive(false), []);
|
|
860
889
|
|
|
861
|
-
|
|
862
|
-
// Mouse tracking handled by shared global tracker
|
|
863
|
-
}, []);
|
|
890
|
+
|
|
864
891
|
|
|
865
892
|
const handleKeyDown = useCallback(
|
|
866
893
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
@@ -885,14 +912,11 @@ export function useAtomixGlass({
|
|
|
885
912
|
globalMousePosition, // This is now static (refs or props) unless prop changes
|
|
886
913
|
mouseOffset, // This is now static (refs or props) unless prop changes
|
|
887
914
|
overLightConfig,
|
|
888
|
-
elasticTranslation,
|
|
889
|
-
directionalScale,
|
|
890
915
|
transformStyle,
|
|
891
916
|
handleMouseEnter,
|
|
892
917
|
handleMouseLeave,
|
|
893
918
|
handleMouseDown,
|
|
894
919
|
handleMouseUp,
|
|
895
|
-
handleMouseMove,
|
|
896
920
|
handleKeyDown,
|
|
897
921
|
};
|
|
898
922
|
}
|